# Pyzed > 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. ## Getting started ### Introduction > What Pyzed is: a Python desktop GUI framework rendered natively by Rust (Iced), with ready-made modern components. How it works and when to use it. Source: https://albakore.github.io/pyzed/getting-started/introduction/ **Pyzed** is a library for building desktop interfaces in Python with the performance of [Iced](https://iced.rs), a GUI toolkit written in Rust. It includes a set of components inspired by the [Zed](https://zed.dev) editor: tabs, file tree, command palette, code editor, terminal, menus, notifications and a workspace with dockable panels. (Image: A code editor built with Pyzed (One Light theme)) :::note[Where Pyzed is today] Pyzed 1.x is stable for desktop apps on Windows, macOS and Linux: its public API follows semantic versioning. `pyzed.agents`, `pyzed.mcp` and animations are still provisional, and mobile and web are planned but not available yet. See [Stability and known limitations](https://albakore.github.io/pyzed/guides/stability/). ::: The interface style takes after the Zed editor, and Pyzed Design, the visual designer, takes after the form and scene editors of Godot, Unreal Engine, Visual Basic and Qt Designer. #### How it works Your Python code describes **what** is shown. Rust takes care of **how** it is drawn. ``` 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) ``` - `view()` returns a tree of `Widget`s. They are immutable Rust objects, so building it is cheap. - When the user interacts, Pyzed calls your `update(message)` or directly the callable you passed (`on_press=self.save`). Then it calls `view()` again. - Layout, text, syntax highlighting and drawing happen in Rust. - The GIL is released while the event loop runs, so your Python threads keep working. #### When to use Pyzed - Desktop tools, internal dashboards, editors and utilities that should feel fast. - Dense IDE-style interfaces with many panels, lists and keyboard shortcuts. - Apps you want to test or document with screenshots generated headlessly. #### What you need to know Basic Python: classes, functions and lambdas. No Rust or Iced knowledge required. Continue with the [installation](https://albakore.github.io/pyzed/getting-started/installation/) or jump straight to the [quickstart](https://albakore.github.io/pyzed/getting-started/quickstart/). Questions about licensing, platforms or how Pyzed compares with tkinter, Kivy, PyQt or Flet? See the [FAQ](https://albakore.github.io/pyzed/faq/). ### Installation > Install Pyzed with pip or uv on Windows, macOS and Linux. Prebuilt wheels include the Rust engine, so Python 3.10+ is all you need to start. Source: https://albakore.github.io/pyzed/getting-started/installation/ Pyzed requires **Python 3.10 or later**. #### From PyPI - **uv** manages a project for you: [Create a project](https://albakore.github.io/pyzed/getting-started/new-project/) starts a new app this way, and it is what the rest of these docs use. Install it from [docs.astral.sh/uv](https://docs.astral.sh/uv/). - **pip** installs into a *virtual environment* (`.venv`), a folder that keeps this project's packages apart from the rest of your computer. Activate it in each new terminal before running your app. - **python** installs into the Python on your computer, with nothing else to learn. On Windows, `py` is the Python launcher that comes with the installer from [python.org](https://www.python.org/downloads/); on macOS and Linux the command is usually `python3`. The package is called `pyzed-ui` on PyPI (the name `pyzed` was already taken), but it is imported as `pyzed`. The prebuilt wheels for Linux, macOS and Windows include the Rust engine, so you don't need to compile anything. **Only prebuilt wheels:** PyPI only has wheels, built and tested by the release pipeline for Linux (x86_64 and ARM, glibc and musl), macOS (Intel and Apple Silicon) and Windows (x64 and ARM). One wheel covers every Python from 3.10. On any other platform the install stops with an error instead of compiling; install from source (below) there. #### From source This compiles the Rust extension, so you need [Rust 1.88 or newer](https://rustup.rs) (`rustup update` upgrades an older one) and your platform's system dependencies: **Linux:** ```bash # Debian / Ubuntu sudo apt install build-essential libxkbcommon-x11-0 curl https://sh.rustup.rs -sSf | sh ``` **macOS:** ```bash xcode-select --install curl https://sh.rustup.rs -sSf | sh ``` **Windows:** Install [Rust with rustup](https://rustup.rs) and the *Build Tools for Visual Studio* ("Desktop development with C++" workload). #### Developing Pyzed To work on Pyzed itself, clone the repository. You need [Rust 1.88 or newer](https://rustup.rs) (`rustup update` upgrades an older one) and the system dependencies above. - `uv sync` also installs the `test` group (maturin and pytest). The first build takes a few minutes. - Python changes under `python/pyzed/` are picked up directly. - With uv, the next `uv run` or `uv sync` after changing Rust code (`src/`) rebuilds the core by itself; with pip, run `maturin develop --release` again. Use `--release`: the debug build is much slower when using the window. - `uv sync --extra designer` adds ty and ruff for the designer's code editor. - On Linux a build from source plays no sound by default, so it builds without extra system packages: `pyzed.audio` renders offline and on its silent clock. For audio devices and MIDI, install ALSA's headers (`sudo apt install libasound2-dev pkg-config`; Fedora: `sudo dnf install alsa-lib-devel`) and build with `maturin develop --release --features alsa` (with uv, keep `MATURIN_PEP517_ARGS="--features alsa"` exported and run `uv sync --reinstall-package pyzed-ui`). #### Linux and WSL Pyzed sets a few defaults before the first window opens: - **GPU driver warnings are silenced.** Mesa prints messages such as `libEGL warning: failed to get driver name for fd -1` or `MESA: error: ZINK: failed to choose pdev` while the renderer looks for a working backend. They are harmless, so Pyzed sets `EGL_LOG_LEVEL=fatal` and `MESA_LOG=null`. Set `PYZED_GPU_LOGS=1` to see them again. - **WSL uses X11.** The Wayland compositor of WSLg drops the connection when a window is maximized or resized, which prints `Io error: Broken pipe (os error 32)`. Under WSL, Pyzed uses X11 (through XWayland) instead. Set `PYZED_WAYLAND=1` to keep Wayland. - **WSL opens pages in Windows' browser.** WSL usually has no browser of its own, so sign-ins (the AI agents'), the docs and other links open in your default Windows browser. Pages that call back to Pyzed on `127.0.0.1` still reach it through WSL's localhost forwarding. Programs that need a terminal, such as the Claude Code sign-in, open in Windows Terminal (or a console) back in your WSL distribution. Set `BROWSER` to pick a browser yourself. Variables you set yourself (`EGL_LOG_LEVEL`, `MESA_LOG`, `WAYLAND_DISPLAY`, `BROWSER`...) are never changed. #### WSL: native Windows windows Under WSL, Pyzed opens every window **as a native Windows program**, not through WSLg: `pyzed design`, `pyzed run`, `pyzed dev`, your scripts that call `pz.run`, Run in Pyzed Design and the AI agents' live previews. The window is a real Windows window, drawn with Windows' own GPU drivers, so it is faster and snaps, scales and shows in the taskbar like any other app. Your code and your project stay in WSL: you keep editing in Linux (in Zed, for example), and hot reload picks up every save. All it needs is [uv on Windows](https://docs.astral.sh/uv/getting-started/installation/). Pyzed can install it for you, from WSL: ```bash uv run pyzed windows setup # installs uv on Windows if it is missing and prepares the project uv run pyzed windows # shows what is used and where ``` How it works: - Windows reads the project through `\\wsl.localhost\\...` and starts in the same folder. - Windows gets **its own environment**, in `%LOCALAPPDATA%\pyzed\wsl\`, with the same Python version, the project's dependencies at the versions of `uv.lock`, what the editor needs (AI agents, MCP, language servers) and the `pyzed-ui` wheel of the same version (always a prebuilt wheel: nothing is compiled on Windows). The project's `.venv` stays the Linux one. It is made the first time (a few seconds) and again only when the dependencies change. - The command in WSL waits for the window: its output, the console of Pyzed Design and the exit code are the app's, and stopping it (Stop, Ctrl+C) closes the window. Without uv (or Python's `py` launcher) on Windows, windows open through WSLg as before, with a note on how to set it up. To always use WSLg, set `PYZED_WINDOWS=0` or add `--wslg` to `pyzed design`, `run` or `dev`. | Variable | What it does | | --- | --- | | `PYZED_WINDOWS=0` | Opens windows through WSLg. | | `PYZED_WINDOWS_ENV=NAME,NAME` | More variables for the app on Windows (API keys...). `PYZED_*` ones always cross. | | `PYZED_WINDOWS_WHEEL` | Path or URL of the `pyzed-ui` wheel for Windows (see below). | | `PYZED_WINDOWS_TARGET` | Rust target for a checkout's core (`x86_64-pc-windows-gnu` or `-msvc`). | :::note[Working on Pyzed itself] From a checkout of Pyzed (`uv sync` in the repository), Windows runs **your checkout**: its Python files are imported from WSL, so every change shows up, and its Rust core is cross-compiled for Windows in WSL, once per change to the Rust sources (the first build takes a few minutes; nothing is compiled on Windows). It needs a cross compiler, installed once: ```bash sudo apt install mingw-w64 # or clang and lld, for the MSVC target of the released wheels ``` Without one, windows open through WSLg with a note saying so. `PYZED_WINDOWS_WHEEL` (the path or URL of a Windows wheel) skips the build, and `PYZED_WINDOWS_TARGET` picks the Rust target. ::: When Pyzed Design runs on Windows this way, everything it starts runs on Windows too: Run, the project's script, previews and audio use Windows' devices. Creating or cloning a project in a WSL folder uses Linux's uv and git, so the project and its `.venv` stay Linux ones. #### Verify the installation The first line prints the version; the second lists the icon names that contain "folder". ### Quickstart > Your first native Python desktop window with Pyzed in five minutes: install the package, write a small counter app and run it anywhere. Source: https://albakore.github.io/pyzed/getting-started/quickstart/ #### 1. A window ```python title="hello.py" import pyzed as pz class Hello(pz.App): title = "Hello" size = (400, 240) def view(self): return pz.center(pz.text("Hello, Pyzed", size=24)) pz.run(Hello) ``` An app is a class that inherits from `pz.App`. Its `view()` method returns the widgets that are shown. `pz.run()` opens the window and blocks until it is closed. #### 2. State and messages State lives in your object. Interactive widgets take a **message**: any Python value. When the user clicks the button, Pyzed calls `update(message)` and redraws. ```python title="counter.py" import pyzed as pz from pyzed import ui class Counter(pz.App): title = "Counter" size = (360, 220) def __init__(self): self.count = 0 def update(self, message): if message == "increment": self.count += 1 elif message == "decrement": self.count -= 1 def view(self): return pz.center( pz.column([ ui.heading(str(self.count)), pz.row([ ui.button("1", "decrement", icon="minus"), ui.button("1", "increment", icon="plus", variant="solid"), ui.icon_button("rotate-ccw", lambda: setattr(self, "count", 0), tooltip="Reset"), ], spacing=8), ], spacing=16, align="center"), bg=self.resolved_theme().editor, ) pz.run(Counter) ``` (Image: The counter) #### 3. Callables instead of messages If the message is a callable (a lambda or a method), Pyzed **calls it directly** and skips `update`. For small apps this is usually the most convenient: ```python ui.button("Save", self.save) # calls self.save() ui.button("Delete", lambda: self.delete(id)) # with arguments ``` Controls that produce a value (`on_input`, `on_toggle`, `on_change`…) call the callable with that value: ```python ui.input(self.name, lambda text: setattr(self, "name", text), placeholder="Your name") ``` #### 4. Components and themes `pyzed.ui` ships ready-made components. They all read the current theme: ```python class MyApp(pz.App): theme = pz.themes.ONE_LIGHT def toggle_theme(self): self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark" ``` #### Next - [Create a project with `pyzed new`](https://albakore.github.io/pyzed/getting-started/new-project/) - [Tutorial: to-do list](https://albakore.github.io/pyzed/tutorials/basic/todo-app/) - [Architecture guide](https://albakore.github.io/pyzed/guides/architecture/) ### Create a project > Generate a Pyzed desktop app as a standard uv project with one command: the UI in its own folder, plus ruff, ty and git ready to go. Source: https://albakore.github.io/pyzed/getting-started/new-project/ Pyzed installs the `pyzed` command. It creates a complete project with [uv](https://docs.astral.sh/uv/), a tool that installs Python packages and keeps each project in its own environment (the pip and python tabs install uv for you): This generates a standard uv project: ``` my-app/ ├── pyproject.toml # dependencies (pyzed-ui; ruff and ty for development) + [tool.pyzed] ├── uv.lock ├── .venv/ ├── .git/ ├── src/my_app/ # backend └── ui/ # forms (.pzform) and their event handlers (.py) ``` The UI folder keeps the interface apart from the rest of the code, and its handlers can import the backend, any module of the project and every dependency. Already have a project made with `uv init`? Run `pyzed init` inside it. See [Projects with uv](https://albakore.github.io/pyzed/guides/projects/) for the layout, dependency commands and the designer's Project tab, and [Build and distribute](https://albakore.github.io/pyzed/packaging/build/) to package the app for each platform. #### A single-file app To start from code instead of the designer, `--script` creates one `main.py` without uv: The template already includes: - A `ui.workspace` with a title bar, left panel and status bar. - Navigation between pages with `ui.list_item`. - A command palette that opens with Ctrl+Shift+P and closes with Esc. - A button to switch between light and dark themes. #### Find icons Pyzed includes the ~2000 [Lucide](https://lucide.dev) icons. To search for names: Use them with `pz.icon("folder")` or with the `icon=` parameter of components. #### Run as a module `python -m pyzed` is equivalent to the `pyzed` command: ## Tutorials ### Tutorials > Learn Pyzed by building real Python desktop apps step by step, with the visual designer and in code: basic, intermediate and advanced tutorials, each with runnable code. Source: https://albakore.github.io/pyzed/tutorials/ Every tutorial ends with a program you can run, and its full source is in the [`examples`](https://github.com/albakore/pyzed/tree/main/examples) folder of the repository. Each one says what you will build, how long it takes, what you need to know first and what you will learn. ```mermaid flowchart LR B["Basic
draw a window · a form · layout · themes · Python basics"] --> I["Intermediate
data binding · pages · tables · dialogs · components · threads · shaders"] I --> A["Advanced
editor · games · shader graphs · backends · packaging · AI agent"] ``` **Never programmed before?:** Start with **Your first window in Pyzed Design**: you draw it with the mouse and run it with F5, no code needed. Each tutorial after that adds a little Python, and explains every line. #### Basic From an empty canvas to your first apps, with the designer and in code. #### Intermediate Real data, several screens and your own reusable pieces. #### Advanced Whole applications, games and shipping them. #### Running the examples Clone the repository (or download it as a zip) and run any example from its folder with the tool you use. `uv` creates the environment for you; with `pip`, create and activate a virtual environment first. Every tutorial has a block like this one with its own file: Forms made with the designer (`.pzform`) run with `pyzed run` instead of `python`: **About this section:** Tutorials are tested: every example app in `examples/tutorial` is imported, drawn off-screen and exercised by the test suite, so the code on these pages runs with the current version of Pyzed. Short recordings of each tutorial are being added. **Want to make games?:** The [Make games](https://albakore.github.io/pyzed/games/) path in the Showcase goes from a first game loop to complete platformers, shooters and puzzles, with animated examples and tips. **Tip:** New to Python? Read [Installation](https://albakore.github.io/pyzed/getting-started/installation/) first, and the [Quickstart](https://albakore.github.io/pyzed/getting-started/quickstart/) to see a first window in five minutes. Looking for finished apps to study? See the [Showcase](https://albakore.github.io/pyzed/showcase/). ### A mini code editor > Tutorial: build a code editor in Python with Pyzed: workspace, file tree, tabs, syntax highlighting, saving and a command palette. Source: https://albakore.github.io/pyzed/tutorials/advanced/code-editor/ Let's build a code editor in about 150 lines: it opens a real folder, shows the file tree, opens tabs, edits with syntax highlighting, saves with Ctrl+S and has a command palette. The workspace pieces (docks, tabs, palette) are the same ones Pyzed Design is built with; their look is inspired by the Zed editor. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A working code editor for a folder | 45 minutes | Advanced · Python | [Lists and tables](https://albakore.github.io/pyzed/tutorials/intermediate/lists-tables/), [Navigation](https://albakore.github.io/pyzed/tutorials/intermediate/navigation/) | #### What you'll learn - Turn a folder into a file tree (`ui.TreeNode.from_paths`) and open files in tabs. - Edit with syntax highlighting (`ui.editor`) and track unsaved changes. - Lay out a whole workspace: title bar, docks, status bar. - Add a command palette with fuzzy search, and keyboard shortcuts. (Image: The mini editor) 1. **Reading the folder.** `TreeNode.from_paths` turns a list of paths into the project panel tree. We skip heavy folders such as `.git` or `node_modules`: ```python def scan(self): files = [] for folder, dirs, names in os.walk(self.root): dirs[:] = [d for d in dirs if d not in IGNORED] rel = Path(folder).relative_to(self.root) files += [(rel / n).as_posix() for n in names] return files self.nodes = ui.TreeNode.from_paths(self.scan()) self.expanded = set() # expanded folders ``` 2. **Opening and closing tabs.** The state is the open paths, the active one and the text of each. Each tab's id is its path: ```python def open_file(self, path): if path not in self.texts: self.texts[path] = (self.root / path).read_text(encoding="utf-8") if path not in self.open_tabs: self.open_tabs.append(path) self.active = path ``` 3. **The editor.** We use the path as the editor's `id`, so each file keeps its cursor and undo history when switching tabs. `on_change` marks the file as modified: ```python ui.editor(self.active, self.texts[self.active], path=self.active, on_change=self.edited, on_cursor=lambda line, col: setattr(self, "cursor", (line, col))) def edited(self, text): self.texts[self.active] = text self.dirty.add(self.active) # the tab shows a dot ``` 4. **The workspace.** `ui.workspace` lays out the title bar, the left dock, the center and the status bar. Passing `None` as `left` hides the panel: ```python ui.workspace( self.center(), title_bar=ui.title_bar([ui.title_button(self.root.name, chevron=True)]), left=project if self.show_left else None, status_bar=ui.status_bar([...], [ui.status_item(f"Ln {line}, Col {col}")]), ) ``` 5. **Shortcuts.** We map Cmd to Ctrl so it works the same on macOS: ```python def on_key(self, combo): combo = combo.replace("cmd-", "ctrl-") if combo == "ctrl-s": self.save() elif combo == "ctrl-b": self.toggle_left() ``` 6. **Command palette.** It opens with Ctrl+Shift+P, focuses its field and floats over the workspace with `ui.layer`: ```python if combo == "ctrl-shift-p": self.palette_open, self.query = True, "" return Command.focus("command-palette") # at the end of view(): palette = ui.command_palette(self.query, lambda q: setattr(self, "query", q), self.commands(), on_submit=self.run_first_match) if self.palette_open else None return ui.layer(layout, palette, on_dismiss=lambda: setattr(self, "palette_open", False)) ``` (Image: The command palette) #### Full code `examples/mini_editor.py`: ```py """A small Zed-like editor in ~150 lines: project tree, tabs, code editor, save, docks and command palette. python examples/mini_editor.py [folder] Walkthrough: https://albakore.github.io/pyzed/tutorials/advanced/code-editor/ For a fuller version see examples/zed_editor.py. """ import os import sys from pathlib import Path import pyzed as pz from pyzed import Command, icons, ui IGNORED = {".git", "target", "node_modules", "__pycache__", ".venv", "dist"} class MiniEditor(pz.App): size = (1100, 700) def __init__(self, root="."): self.root = Path(root).resolve() self.nodes = ui.TreeNode.from_paths(self.scan()[:2000]) self.expanded = set() self.open_tabs = [] # paths, in tab order self.active = None self.texts = {} # path -> current text self.dirty = set() self.cursor = (1, 1) self.show_left = True self.palette_open = False self.query = "" def scan(self): files = [] for folder, dirs, names in os.walk(self.root): dirs[:] = [d for d in dirs if d not in IGNORED] rel = Path(folder).relative_to(self.root) files += [(rel / n).as_posix() for n in names] return files def title(self): name = self.active or "sin archivos" return f"{name} — {self.root.name}" # -- files ------------------------------------------------------------------ def open_file(self, path): if path not in self.texts: try: self.texts[path] = (self.root / path).read_text(encoding="utf-8") except (UnicodeDecodeError, OSError): return if path not in self.open_tabs: self.open_tabs.append(path) self.active = path def close_tab(self, path): index = self.open_tabs.index(path) self.open_tabs.remove(path) if self.active == path: self.active = self.open_tabs[min(index, len(self.open_tabs) - 1)] if self.open_tabs else None def edited(self, text): self.texts[self.active] = text self.dirty.add(self.active) def save(self): if self.active in self.dirty: (self.root / self.active).write_text(self.texts[self.active], encoding="utf-8") self.dirty.discard(self.active) def toggle_folder(self, folder): self.expanded ^= {folder} # -- commands & keys ---------------------------------------------------------- def toggle_left(self): self.show_left = not self.show_left def toggle_theme(self): self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark" def commands(self): return [ ui.PaletteItem("workspace: save", self.save, keys="ctrl-s", icon="save"), ui.PaletteItem("workspace: toggle left dock", self.toggle_left, keys="ctrl-b", icon="panel-left"), ui.PaletteItem("theme: toggle light/dark", self.toggle_theme, icon="sun-moon"), ui.PaletteItem("pane: close active item", lambda: self.active and self.close_tab(self.active), keys="ctrl-w", icon="x"), ] def run_first_match(self): matches = ui.fuzzy_filter(self.query, self.commands(), key=lambda c: c.label) self.palette_open = False if matches: return matches[0][0].on_press() def on_key(self, combo): combo = combo.replace("cmd-", "ctrl-") if combo == "ctrl-shift-p": self.palette_open, self.query = True, "" return Command.focus("command-palette") if combo == "escape": self.palette_open = False elif combo == "ctrl-s": self.save() elif combo == "ctrl-b": self.toggle_left() elif combo == "ctrl-w" and self.active: self.close_tab(self.active) # -- view ----------------------------------------------------------------------- def center(self): if not self.active: return pz.center(pz.column([ui.heading("Mini editor", level=2), ui.muted("Abre un archivo del panel de proyecto."), ui.kbd("ctrl-shift-p")], spacing=8, align="center")) tabs = [ui.Tab.for_file(p, dirty=p in self.dirty) for p in self.open_tabs] return pz.column([ ui.tab_bar(tabs, self.active, on_select=lambda p: setattr(self, "active", p), on_close=self.close_tab), ui.toolbar([ui.breadcrumbs(self.active.split("/"))]), ui.editor(self.active, self.texts[self.active], path=self.active, on_change=self.edited, on_cursor=lambda line, col: setattr(self, "cursor", (line, col))), ]) def view(self): project = pz.column([ ui.panel_header(self.root.name, [ui.icon_button("chevrons-down-up", lambda: self.expanded.clear(), size="xs", tooltip="Plegar todo")]), pz.scrollable(ui.tree(self.nodes, expanded=self.expanded, selected=self.active, on_toggle=self.toggle_folder, on_select=self.open_file), height="fill"), ]) line, col = self.cursor layout = ui.workspace( self.center(), title_bar=ui.title_bar([ui.title_button(self.root.name, chevron=True)], [ui.icon_button("sun-moon", self.toggle_theme, size="sm", tooltip="Tema")]), left=project if self.show_left else None, status_bar=ui.status_bar( [ui.status_item(icon="panel-left", on_press=self.toggle_left, selected=self.show_left, tooltip="Panel de proyecto", keys="ctrl-b")], [ui.status_item(f"Ln {line}, Col {col}") if self.active else None, ui.status_item(icons.language_for(self.active)) if self.active else None], ), ) palette = None if self.palette_open: palette = ui.command_palette(self.query, lambda q: setattr(self, "query", q), self.commands(), on_submit=self.run_first_match) return ui.layer(layout, palette, on_dismiss=lambda: setattr(self, "palette_open", False)) if __name__ == "__main__": pz.run(MiniEditor(sys.argv[1] if len(sys.argv) > 1 else ".")) ``` **Tip:** Pass a folder to open it: `python examples/mini_editor.py path/to/a/folder`. Without one it opens the current folder. For a more complete version (file finder, terminal panel, outline, themes, git markers) see [`examples/zed_editor.py`](https://github.com/albakore/pyzed/blob/main/examples/zed_editor.py) and its page in the [Showcase](https://albakore.github.io/pyzed/showcase/apps/#workspace-editor). #### Events in this tutorial | Widget | Event | Handler gets | | --- | --- | --- | | `ui.tab_bar` | `on_select`, `on_close` | The tab's id (its path). | | `ui.editor` | `on_change`, `on_cursor` | The text; the line and column. | | `ui.tree` | `on_toggle`, `on_select` | The path of the folder or file. | | `ui.status_item` | `on_press` | Nothing. | | `ui.command_palette` | `on_query`, `on_submit` | The query; nothing. | | `ui.layer` | `on_dismiss` | Nothing (a click outside the palette). | | The app | `on_key` | The combo (`ctrl-shift-p`, `ctrl-b`…). | ##### Try more events ```python ui.editor(self.active, self.texts[self.active], path=self.active, font_size=self.font_size, on_change=self.edited, on_cursor=lambda line, col: setattr(self, "cursor", (line, col)), on_zoom=lambda step: setattr(self, "font_size", 14 if step == 0 else self.font_size + step)) ui.tree(self.nodes, expanded=self.expanded, selected=self.active, on_toggle=self.toggle_folder, on_select=self.open_file, on_open=self.open_file, # double click on_context=lambda path: setattr(self, "menu_for", path)) # right click: a menu ui.tab_bar(tabs, self.active, on_select=lambda p: setattr(self, "active", p), on_close=self.close_tab, on_new=self.new_file) # double click after the tabs ``` #### Next - [A game loop](https://albakore.github.io/pyzed/tutorials/advanced/game-loop/) with `pyzed.game`. - [Workspace](https://albakore.github.io/pyzed/library/workspace/), [Tree](https://albakore.github.io/pyzed/library/tree/) and [Command palette](https://albakore.github.io/pyzed/library/command-palette/) in the component library. ### Data binding and validation > Bind the controls of a Pyzed Design form to form.data, add validation rules without code, show a live expression in a label, and validate the whole form in one line. Source: https://albakore.github.io/pyzed/tutorials/intermediate/data-binding/ In [A form that does something](https://albakore.github.io/pyzed/tutorials/basic/first-form/) every field was read and checked by hand. Here the controls do that work themselves: each one is **bound** to a field of the form's data, carries its own **rules**, and shows its own error message. The Python left to write is a single `if`. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A sign-up form that validates itself | 25 minutes | Intermediate · a little Python | [A form that does something](https://albakore.github.io/pyzed/tutorials/basic/first-form/) | #### What you'll learn - **Bind** an input to a field of `form.data` (two-way: it shows the field and writes back what the user types). - Show a **live expression** in a label (`f"Nice to meet you, {name}!"`). - Add **rules**: required, a pattern, a minimum length, a minimum number, your own message. - Validate everything with `form.validate()`, and read `form.data` and `form.errors`. #### Steps 1. **Draw the form.** A new form named `signup` with a **Heading**, a **Label** named `greeting`, three **Text boxes** (`your_name`, `email`, `password`, the last with **Password** switched on), a **Number box** named `age`, a **Check box** named `terms` (text `I accept the terms`), a small label `summary`, a label `status` and a **Button** named `create`. 2. **Bind the inputs.** Select `your_name` and, in the **Data** section of Properties, type `name` in **Bind to**. Do the same for the others: | Control | Bind to | Rules (Data section) | | --- | --- | --- | | `your_name` | `name` | **Required** | | `email` | `email` | **Required**, **Pattern** `[^@\s]+@[^@\s]+\.[a-z]{2,}`, **Message** `Enter a valid email` | | `password` | `password` | **Required**, **Minimum length** 8 | | `age` | `age` | **Minimum** 13, **Message** `You must be 13 or older` | | `terms` | `terms` | **Required** (a check box passes only when checked), **Message** `Please accept the terms` | 3. **Show a live expression.** A display control (label, heading, badge...) can bind to a **Python expression** over the data, recomputed on every redraw. Select `greeting` and set **Bind to**: ```python f"Nice to meet you, {name}!" if name else "Fill in the form below." ``` And `summary`: ```python f"{len(password)} characters in your password" ``` 4. **Run it** (F5) and type. The greeting follows your name letter by letter, and each input checks its rules **as you type**: a red border and a message bubble under it. 5. **Validate on click.** Double-click `create` and write the handler: ```python def create_click(form, event): if not form.validate(): # checks every rule and shows every message form.status.color = "error" form.status.text = f"{len(form.errors)} field(s) need your attention." return data = form.data # {"name": ..., "email": ..., "age": 18, "terms": True, ...} form.status.color = "success" form.status.text = f"Welcome aboard, {data['name']}!" ``` `form.data` is a plain dict filled by the bound controls. Save it, send it to an API or pass it to another form. **Load existing data:** To edit a record instead of creating one, assign it: `form.data = {"name": "Ada", "email": "ada@example.com"}` in the form's `load` handler fills every bound control. Another form can pass it too: `form.open("signup", data=customer)`. **Server-side errors:** Rules catch typing mistakes. For checks only a server can make ("that email is taken"), set the message yourself: `form.email.error = "Already registered"`. `None` clears it. #### Full code `signup.pzform` holds the layout, the bindings and the rules; `signup.py` only the button: `examples/tutorial/designer/signup.py`: ```python """Event handlers for the sign-up form (tutorial: Data binding and validation). pyzed run examples/tutorial/designer/signup.pzform The controls are bound to fields of ``form.data`` (Properties → Data → Bind to) and carry their validation rules, so the handler only has to call ``form.validate()`` and read ``form.data``. """ def create_click(form, event): """Runs when create is clicked.""" if not form.validate(): # checks every rule and shows the messages under the controls form.status.color = "error" form.status.text = f"{len(form.errors)} field(s) need your attention." return data = form.data # {"name": ..., "email": ..., "password": ..., "age": ..., "terms": True} print("New account:", {k: v for k, v in data.items() if k != "password"}) form.status.color = "success" form.status.text = f"Welcome aboard, {data['name']}!" ``` #### Events in this tutorial Bound controls need almost no events: each change goes into `form.data` by itself. The form has one handler, `create_click`, which calls `form.validate()` and reads `form.data`. ##### Try more events ```python def form_load(form, event): # start in the first box return form.name.focus() def terms_change(form, event): # a check box: event.value is True or False form.create.enabled = event.value def email_change(form, event): # check one field as the user types form.email.validate() # shows (or clears) its message right away ``` #### Next - [Data binding and validation](https://albakore.github.io/pyzed/editor/data-binding/) in the designer manual: nested paths (`customer.address.city`), tables, translations with `@keys` and previewing other languages. - [Validation errors from Pydantic, Django or FastAPI](https://albakore.github.io/pyzed/integrations/overview/). - [A multi-view app](https://albakore.github.io/pyzed/tutorials/intermediate/navigation/). ### Your first window in Pyzed Design > Draw your first desktop window with Pyzed Design, the visual designer: drag controls, set their properties and run the form with F5, without writing code. Source: https://albakore.github.io/pyzed/tutorials/basic/first-window/ You will build a small window that greets you by name. You will **draw** it with the mouse, the way you would in Visual Basic or Qt Designer, and run it with one key. There is no code to write until the very last (optional) step, and that step is one line. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A window with a title, a text box and a button | 15 minutes | Basic · no code | Pyzed Design (below) | #### What you'll learn - Open Pyzed Design and create a **form** (a window of your app). - Place controls from the **toolbox** and change them in the **Properties** panel. - Make a control stretch when the window grows, with **anchors**. - Save and **run** the form with F5. - Wire a button to a Python **event handler** (optional). #### Before you start Pyzed Design is the visual editor of Pyzed. Install it in one of these ways: **uv:** ```bash title="Windows, macOS and Linux" uv tool install "pyzed-ui[designer]" # installs the pyzed command pyzed design hello.pzform ``` **pip:** ```bash title="macOS and Linux" python3 -m venv .venv source .venv/bin/activate python -m pip install "pyzed-ui[designer]" pyzed design hello.pzform ``` ```powershell title="Windows (PowerShell)" py -m venv .venv .venv\Scripts\Activate.ps1 python -m pip install "pyzed-ui[designer]" pyzed design hello.pzform ``` **python:** ```bash title="With Pyzed already installed" python -m pyzed.designer hello.pzform # the same, without the pyzed command on PATH ``` **No Python at all?:** Download the standalone **Pyzed Design** app from the [Download](https://albakore.github.io/pyzed/download/) page. It brings its own Python and `uv`, so there is nothing else to install. #### Steps 1. **Create the form.** In Pyzed Design press Ctrl+N (or **☰ → New**). Pick the **Form** card, type `hello` as the **Name**, `Hello` as the **Title**, leave the **Blank** template and press **Create**. If you started the editor with `pyzed design hello.pzform`, the empty form is already open. 2. **Add a heading.** In the **Toolbox** on the left, find **Heading** (type it in **Search components…**) and drag it onto the form. With it selected, look at the **Properties** panel on the right and change: - **Name**: `headline` (press Enter). This is how your code will call it later. - **Text**: `Hello, Pyzed!` 3. **Add a label and a text box.** Drag a **Label** under the heading and set its **Text** to `Type your name and press the button.` and its **Color** to `text_muted` (a softer gray that follows the theme). Then drag a **Text box** under it, name it `your_name` and set its **Placeholder** to `Your name` and its **Icon** to `user`. 4. **Add the button.** Drag a **Button** below the text box, name it `greet`, set its **Text** to `Greet`, its **Variant** to `solid` and its **Icon** to `hand`. 5. **Line things up.** Drag the controls until the red **smart guides** show that their left edges line up. Select several (drag a box around them) and use **Align left** in the toolbar for a perfect line. 6. **Make it stretch.** Select the text box and open the **Position** section of Properties. In the grid of anchor presets pick the one that stretches **left and right**. Do the same for the heading and the label. Pick **top · right** for the button, so it follows the right edge. 7. **Save and run.** Press Ctrl+S: the editor writes `hello.pzform` (the layout) and `hello.py` (its code, still empty). Press F5: your form opens as a real window. Resize it and watch the text box stretch. **Two names are taken:** A control cannot be called `name`, `title`, `size`, `data` or `window`: your code uses those words for the form itself (`form.title` is the window's title). That is why the text box is `your_name` and the heading `headline`. #### Bonus: make the button do something 1. **Double-click the button** on the canvas. The **Events** panel wires its `click` event to a new function, `greet_click`, and opens it in the **code editor** at the bottom. 2. **Write one line** (two, if you like) inside the function: ```python def greet_click(form, event): form.headline.text = f"Hello, {form.your_name.text}!" ``` `form` is the running window: every control is an attribute (`form.your_name`), and changing a property (`.text`) redraws it at once. Type `form.` and the editor lists your controls. 3. **Wire Enter too.** Select the text box, open **Events** and double-click its `submit` event (pressing Enter). In the new `your_name_submit` function, call the button's handler: ```python def your_name_submit(form, event): return greet_click(form, event) ``` 4. **Save** (Ctrl+S). If the form is still running it updates in place; otherwise press F5 again. Type your name and press **Greet**. #### The finished files The form is two files side by side: `hello.pzform` (JSON describing the window, made by the designer) and `hello.py` (your code). Both are in [`examples/tutorial/designer`](https://github.com/albakore/pyzed/tree/main/examples/tutorial/designer). `examples/tutorial/designer/hello.py`: ```python """Event handlers for the hello form (tutorial: Your first window in Pyzed Design). pyzed run examples/tutorial/designer/hello.pzform # run it pyzed design examples/tutorial/designer/hello.pzform # open it in the designer Each function receives the running ``form`` (controls are attributes: ``form.your_name.text``) and an ``event`` with ``event.sender``, ``event.name`` and ``event.value``. """ def greet_click(form, event): """Runs when greet is clicked.""" name = form.your_name.text.strip() or "stranger" form.headline.text = f"Hello, {name}!" form.subtitle.text = "Change the name and press the button again." def your_name_submit(form, event): """Runs when Enter is pressed in your_name: the same as clicking the button.""" return greet_click(form, event) ``` Run the finished example without opening the designer: **Where the ideas come from:** The canvas, toolbox and property grid follow the classic RAD designers (Visual Basic, the WinForms designer, Qt Designer); anchors work like those of WinForms, Godot and Unreal Engine. The editor's look is inspired by the Zed code editor. #### Events in this tutorial | Control | Event | Handler | What it does here | | --- | --- | --- | --- | | `greet` (Button) | `click` | `greet_click` | Writes the greeting in `headline`. | | `your_name` (Text box) | `submit` | `your_name_submit` | Enter does the same as the button. | ##### Try more events Double-click an event in the **Events** panel to create its handler, then paste the body: ```python def your_name_change(form, event): # every keystroke: event.value is the text form.greet.enabled = bool(event.value.strip()) def headline_double_click(form, event): # a label has click and double_click form.headline.text = "Hello!" form.your_name.text = "" def form_key(form, event): # any key in the window: event.value is the combo if event.value == "escape": return form.close() ``` Every control has `mouse_enter`, `mouse_leave` and `right_click` too. The full list of each control is in [Events and handlers](https://albakore.github.io/pyzed/editor/events/), and how forms, apps and components get their events in [Apps, windows, forms and components](https://albakore.github.io/pyzed/guides/classes/). #### Next - [A form that does something](https://albakore.github.io/pyzed/tutorials/basic/first-form/): several inputs, validation and the console. - [The Pyzed Design manual](https://albakore.github.io/pyzed/editor/): every panel of the editor in detail. - [Toolbox and canvas](https://albakore.github.io/pyzed/editor/toolbox-and-canvas/): all the ways to place, select and align controls. ### A form that does something > Build a contact form in Pyzed Design: text boxes, a drop-down and a text area, a Send button wired to Python, a live character counter and messages in the console. Source: https://albakore.github.io/pyzed/tutorials/basic/first-form/ A **contact form**: the user types a name, an email and a message, picks a topic and presses **Send**. The form checks what was typed, shows a friendly message, counts characters as you type, and prints what it "sent" in the console. It is the shape of most business apps: inputs, a button, and a few lines of Python that react. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A contact form with checks and feedback | 25 minutes | Basic · a little Python | [Your first window](https://albakore.github.io/pyzed/tutorials/basic/first-window/) | #### What you'll learn - Use several kinds of input: **Text box**, **Drop-down** and **Text area**. - Read what the user typed in code: `form.email.text`, `form.topic.selected`. - React **as the user types** with a `change` event, and to a click with `click`. - Give feedback: change a label's text and color, move the cursor with `focus()`. - Run code when the window opens (the form's `load` event) and read `print` output in the **console**. #### Steps 1. **Create the form.** Ctrl+N → **Form**, name it `contact`, title `Contact us`. Select an empty spot of the canvas: the Properties panel now shows the **form**. Set **Width** to 480 and **Height** to 500. 2. **Add the fields.** For each field drag a **Label** (its title) and the input under it: | Label text | Control | Name | Properties | | --- | --- | --- | --- | | Name | Text box | `your_name` | Placeholder `Ada Lovelace` | | Email | Text box | `email` | Placeholder `ada@example.com`, Icon `mail` | | Topic | Drop-down | `topic` | Options `Question`, `Bug report`, `Idea`; Selected `Question` | | (none) | Text area | `message` | Placeholder `How can we help?` | The **Options** of a drop-down are a list: each item is its own field, and **+** adds one. 3. **Add the button and two labels for feedback.** A **Button** named `send` (text `Send`, variant `solid`, icon `send`) in the bottom-right corner, a small **Label** named `counter` (text `0 / 500`, size `xs`) under the message, and an empty **Label** named `status` next to the button. 4. **Anchor everything.** Text boxes and labels: stretch **left and right**. The text area: stretch on **all four sides**, so it takes the extra room when the window grows. The button: **bottom · right**. `counter` and `status`: **bottom**, so they stay under the text area. 5. **Focus the first box when the window opens.** Click an empty spot (the form is selected), open **Events** and double-click `load`. Write: ```python def form_load(form, event): return form.your_name.focus() ``` Handlers can **return a command**: `focus()` puts the cursor in that box. 6. **Count characters as the user types.** Select the text area, double-click its `change` event. `event.value` is the new text: ```python LIMIT = 500 def message_change(form, event): count = len(event.value) form.counter.text = f"{count} / {LIMIT}" form.counter.color = "error" if count > LIMIT else "text_muted" ``` `"error"` and `"text_muted"` are **theme colors**: they look right in a dark and in a light theme. 7. **Check and send.** Double-click the **Send** button and write the checks. Each one shows a message, moves the cursor to the field and stops with `return`: ```python def send_click(form, event): if not form.your_name.text.strip(): form.status.color = "error" form.status.text = "Please tell us your name." return form.your_name.focus() if "@" not in form.email.text: form.status.color = "error" form.status.text = "That email does not look right." return form.email.focus() print(f"New {form.topic.selected.lower()} from {form.email.text}") form.status.color = "success" form.status.text = "Thanks! We got your message." ``` 8. **Run it** with F5. Press **Send** with empty fields, then fill them in. The `print` goes to the **Console** at the bottom of the editor, together with the events the form handled and any error. **Less code with data binding:** Checking fields by hand is fine for a few of them. For bigger forms, give each control **rules** (required, a pattern, a minimum length) in the Properties panel and call `form.validate()`: see [Data binding and validation](https://albakore.github.io/pyzed/tutorials/intermediate/data-binding/). **Errors do not close the window:** If a handler raises an exception, the running window shows a red **Event handler error** banner and the console gets the full traceback. Fix the code, save, and the window picks up the new version. #### The complete code `examples/tutorial/designer/contact.py`: ```python """Event handlers for the contact form (tutorial: A form with inputs and a button). pyzed run examples/tutorial/designer/contact.pzform Each function receives the running ``form`` and an ``event`` (``event.value`` holds the new value of a ``change`` event). """ LIMIT = 500 def form_load(form, event): """Runs once when contact opens: put the cursor in the first box.""" return form.your_name.focus() def message_change(form, event): """Runs on every keystroke in message: update the character counter.""" count = len(event.value) form.counter.text = f"{count} / {LIMIT}" form.counter.color = "error" if count > LIMIT else "text_muted" def send_click(form, event): """Runs when send is clicked: check the fields, then 'send' the message.""" if not form.your_name.text.strip(): form.status.color = "error" form.status.text = "Please tell us your name." return form.your_name.focus() if "@" not in form.email.text: form.status.color = "error" form.status.text = "That email does not look right." return form.email.focus() if len(form.message.text) > LIMIT: form.status.color = "error" form.status.text = f"Keep the message under {LIMIT} characters." return None # print() shows up in the designer's console (and in the terminal) print(f"New {form.topic.selected.lower()} from {form.email.text}: {form.message.text!r}") form.status.color = "success" form.status.text = f"Thanks, {form.your_name.text.split()[0]}! We got your message." form.message.text = "" form.counter.text = f"0 / {LIMIT}" return None ``` #### Events in this tutorial | Control | Event | Handler | `event.value` | | --- | --- | --- | --- | | The form | `load` | `form_load` | `None`: it puts the cursor in the first box. | | `message` (Text area) | `change` | `message_change` | The whole text, on every keystroke. | | `send` (Button) | `click` | `send_click` | `None`. | ##### Try more events ```python def email_submit(form, event): # Enter in the email box sends too return send_click(form, event) def topic_change(form, event): # a drop-down: event.value is the option picked form.status.color = "text_muted" form.status.text = f"Topic: {event.value}" def send_mouse_enter(form, event): # every control has mouse_enter and mouse_leave form.status.text = "Ready to send?" def form_close(form, event): # return False to keep the window open if form.message.text.strip(): form.status.text = "Send or clear the message before closing." return False ``` #### Next - [Layout basics](https://albakore.github.io/pyzed/tutorials/basic/layout-basics/): rows, columns and grids, in the designer and in code. - [Events and handlers](https://albakore.github.io/pyzed/editor/events/): every event of every control. - [Running and the console](https://albakore.github.io/pyzed/editor/console-and-running/). ### A game loop > Write a small real-time game with Pyzed: a frame loop with pz.frames, held keys, collisions, glow, particles and screen shake, and a HUD made of ordinary widgets. Source: https://albakore.github.io/pyzed/tutorials/advanced/game-loop/ Apps wait for the user; games do not. A game **updates the world many times per second**, whether or not you press a key. This tutorial builds *Dodge*: blocks fall, you slide left and right, you collect stars and the game speeds up. About 130 lines, and every piece is reusable. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A playable arcade game | 40 minutes | Advanced · Python | [A multi-view app](https://albakore.github.io/pyzed/tutorials/intermediate/navigation/) | #### What you'll learn - Run code **every frame** with `pz.frames(callback)` and move things by *speed × dt*. - Read the keys **held down** (`game.axis`) instead of single key presses. - Detect **collisions** with `game.Rect`. - Draw on a `pz.canvas` and add **glow**, **particles** and **screen shake**. - Stack a **HUD** and a *Game over* card, made of normal widgets, over the canvas. #### Steps 1. **The loop.** Return `pz.frames(self.tick)` from `subscriptions()` and Pyzed calls `tick(dt)` before every frame (60, 120 or 144 times a second, in sync with the screen). `dt` is the time since the last frame, in seconds. When the game is over, return nothing and the loop stops, so a paused game uses no CPU: ```python def subscriptions(self): return [] if self.over else [pz.frames(self.tick)] ``` 2. **Move by speed × dt.** Speeds are in pixels per second. Multiplying by `dt` makes the game run at the same speed on any screen: ```python self.x += game.axis(("left", "a"), ("right", "d")) * 380 * dt # -1, 0 or 1 × speed × time self.x = game.clamp(self.x, 0, W - PLAYER_W) ``` `game.axis` reads the keys held **right now**: hold the arrow and the paddle keeps moving. 3. **Spawn things on a timer.** A countdown in seconds, reset when it reaches zero. It shrinks over time, so the game gets harder: ```python self.spawn -= dt if self.spawn <= 0: self.spawn = max(0.25, 0.8 - self.time * 0.01) size = self.random.uniform(24, 60) self.blocks.append(game.Rect(self.random.uniform(0, W - size), -size, size, size)) ``` 4. **Collisions.** `game.Rect` is a box with `overlaps`, `contains` and `center`: ```python player = game.Rect(self.x, PLAYER_Y, PLAYER_W, PLAYER_H) if any(block.overlaps(player) for block in self.blocks): self.over = True self.shake.kick(10) # screen shake self.sparks.burst(*player.center, count=40, color=["#f87171", "#fca5a5"]) ``` 5. **Draw the world.** A list of shapes from `pyzed.draw` on a `pz.canvas`. `game.glow` adds soft light, `sparks.shapes()` draws the particles and `shake.shapes()` moves everything a little after a hit: ```python shapes = [draw.background("#0b0e14"), *self.shake.shapes()] shapes += [block.shape(fill="#ef4444", radius=6) for block in self.blocks] for x, y in self.stars: shapes += game.glow(x, y, 6, "#fbbf24", layers=3) shapes.append(draw.rect(self.x, PLAYER_Y, PLAYER_W, PLAYER_H, radius=7, fill="#22d3ee")) shapes += self.sparks.shapes(glow=True) return pz.canvas(shapes, width=W, height=H) ``` 6. **HUD and Game over.** These are not drawn on the canvas: they are widgets. `pz.stack` layers the *Game over* card (a glass panel with a button) over the canvas, and the score badges sit above it: ```python play = pz.stack([self.board(), self.overlay()], width=W, height=H) ``` 7. **Restart.** `on_key` gets single key presses; Enter resets the state and, since `over` is false again, `subscriptions()` returns the loop and the game starts over. **Make it yours:** Change one number at a time and see what happens: the paddle speed (380), the gravity of the sparks (`Particles(gravity=300)`), the spawn interval, or give the blocks a `rotation` with `draw.group`. Run it with `pyzed dev examples/tutorial/dodge.py` to see each change when you save. **A fixed step or a frame loop?:** Grid games such as Snake move one cell at a time at a steady pace: use `pz.every(seconds, callback)`. Action games that move smoothly use `pz.frames`. The [Games](https://albakore.github.io/pyzed/games/) section has one of each. #### Full code `examples/tutorial/dodge.py`: ```python """Dodge: a tiny real-time game built with the game loop of ``pyzed.game``. python examples/tutorial/dodge.py Move with the arrows or A/D, dodge the falling blocks, collect the stars. Enter starts again. - ``pz.frames(self.tick)`` in ``subscriptions()`` calls ``tick(dt)`` once per displayed frame. - ``game.axis("left", "right")`` reads the keys held down: -1, 0 or 1. - ``game.Rect.overlaps`` checks collisions; ``game.glow``, ``game.Particles`` and ``game.Shake`` add light, sparks and screen shake. - The score and the "Game over" card are ordinary widgets stacked over the canvas. Walkthrough: https://albakore.github.io/pyzed/tutorials/advanced/game-loop/ """ import random import pyzed as pz from pyzed import draw, game, ui W, H = 480, 560 PLAYER_W, PLAYER_H, PLAYER_Y = 56, 14, H - 48 class Dodge(pz.App): title = "Dodge" size = (W + 32, H + 88) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) self.sparks = game.Particles(gravity=300, seed=seed) self.shake = game.Shake(seed=seed) self.best = 0 self.reset() def reset(self): self.x = W / 2 - PLAYER_W / 2 self.blocks = [] # game.Rect objects falling down self.stars = [] # (x, y) of the stars to collect self.spawn = 0.0 # seconds until the next block self.time = 0.0 # seconds survived self.bonus = 0 # points from stars self.over = False @property def score(self): return int(self.time * 10) + self.bonus # -- the loop ------------------------------------------------------------------------- def subscriptions(self): return [] if self.over else [pz.frames(self.tick)] def tick(self, dt): dt = min(dt, 0.05) # never jump too far after a pause self.time += dt speed = 220 + self.time * 8 # blocks fall faster over time # 1. input: move the player by speed x dt, so it goes as fast at 60 Hz as at 144 Hz self.x = game.clamp(self.x + game.axis(("left", "a"), ("right", "d")) * 380 * dt, 0, W - PLAYER_W) player = game.Rect(self.x, PLAYER_Y, PLAYER_W, PLAYER_H) # 2. spawn new things now and then self.spawn -= dt if self.spawn <= 0: self.spawn = max(0.25, 0.8 - self.time * 0.01) size = self.random.uniform(24, 60) self.blocks.append(game.Rect(self.random.uniform(0, W - size), -size, size, size)) if self.random.random() < 0.3: self.stars.append((self.random.uniform(16, W - 16), -16.0)) # 3. move everything and check collisions for block in self.blocks: block.y += speed * dt self.blocks = [b for b in self.blocks if b.y < H] self.stars = [(x, y + speed * 0.8 * dt) for x, y in self.stars if y < H] for x, y in list(self.stars): if player.overlaps(game.Rect(x - 12, y - 12, 24, 24)): # a star is a 24 px box self.stars.remove((x, y)) self.bonus += 50 self.sparks.burst(x, y, count=18, color=["#fde68a", "#fbbf24"]) if any(block.overlaps(player) for block in self.blocks): self.over = True self.best = max(self.best, self.score) self.shake.kick(10) self.sparks.burst(*player.center, count=40, color=["#f87171", "#fca5a5"]) # 4. effects self.sparks.update(dt) self.shake.update(dt) def on_key(self, combo): if combo == "enter" and self.over: self.reset() # -- drawing -------------------------------------------------------------------------- def board(self): shapes = [draw.background("#0b0e14"), *self.shake.shapes()] for block in self.blocks: shapes.append(block.shape(fill="#ef4444", radius=6)) for x, y in self.stars: shapes += game.glow(x, y, 6, "#fbbf24", layers=3) shapes += game.glow(self.x + PLAYER_W / 2, PLAYER_Y + PLAYER_H / 2, 4, "#22d3ee", layers=4, core=False) shapes.append(draw.rect(self.x, PLAYER_Y, PLAYER_W, PLAYER_H, radius=7, fill="#22d3ee")) shapes += self.sparks.shapes(glow=True) return pz.canvas(shapes, width=W, height=H) def overlay(self): if not self.over: return None card = pz.container(pz.column([ ui.heading("Game over", level=2), ui.muted(f"Score {self.score} · Best {self.best}"), ui.button("Play again", self.reset, variant="solid", icon="rotate-ccw"), ], spacing=10, align="center"), padding=24).style(glass=True, radius=14) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def view(self): t = self.resolved_theme() hud = pz.row([ui.label("Dodge", weight="semibold"), pz.hspace(), ui.badge(f"Score {self.score}", icon="star"), ui.badge(f"Best {self.best}", tint="warning", icon="trophy")], spacing=8, align="center") play = pz.stack([self.board(), self.overlay()], width=W, height=H) return pz.container( pz.column([hud, pz.container(play, radius=10, clip=True), ui.muted("Arrows / A-D move · Enter restarts", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(Dodge) ``` #### Events in this tutorial | Source | Event | Handler | | --- | --- | --- | | `subscriptions()` | every displayed frame | `pz.frames(self.tick)`: `tick(dt)` gets the seconds since the last frame. | | The keyboard | keys held | `game.axis(("left", "a"), ("right", "d"))` reads them inside `tick`. | | The app | `on_key` | Enter starts again after a crash. | A game reads the keys that are *held* in every frame instead of reacting to each press; `on_key` is for one-off actions. ##### Try more events ```python def on_key(self, combo): if combo == "enter" and self.over: self.reset() elif combo == "p": # pause: stop returning the frame subscription self.paused = not self.paused def subscriptions(self): return [] if self.over or self.paused else [pz.frames(self.tick)] def board(self): ... return pz.canvas(shapes, width=W, height=H, on_press=lambda x, y: self.reset() if self.over else None) # click to restart ``` #### Next - [Making games](https://albakore.github.io/pyzed/games/): Snake, Breakout and Neon Invaders, step by step. - [A custom shader graph](https://albakore.github.io/pyzed/tutorials/advanced/shader-graph/) for a GPU background. ### A multi-view app > Build a Pyzed app with several pages: a sidebar that switches views, tabs inside a page, breadcrumbs and keyboard shortcuts, all driven by one attribute of state. Source: https://albakore.github.io/pyzed/tutorials/intermediate/navigation/ Real apps have more than one screen: a home page, an inbox, settings. In Pyzed, "which screen is showing" is just **a piece of state**, like any other. Change it and `view()` draws the other screen. There is no router to learn. | Goal | Time | Level | You need | | --- | --- | --- | --- | | An app with a sidebar, three pages and tabs | 25 minutes | Intermediate · Python | [Layout basics](https://albakore.github.io/pyzed/tutorials/basic/layout-basics/) | #### What you'll learn - Keep the current page in `self.page` and pick the view from it. - Build a **sidebar** of `ui.list_item` rows with a selected state and a badge. - Use `ui.segmented` as **tabs** inside a page. - Show **breadcrumbs** and jump between pages with **shortcuts** (`on_key`). #### Steps 1. **One attribute decides the page.** ```python PAGES = {"Home": "house", "Inbox": "inbox", "Settings": "settings"} # name → icon def __init__(self): self.page = "Home" def go(self, page): self.page = page ``` 2. **The sidebar.** One `ui.list_item` per page. `selected=` highlights the current one, `end=` puts a counter at the right. The `name=name` in the lambda captures each page's own name: ```python items = [ ui.list_item(name, lambda name=name: self.go(name), icon=icon, selected=name == self.page, height=34) for name, icon in PAGES.items() ] ``` 3. **Pick the page in `view()`.** Each page is a method that returns widgets; a dict maps names to methods: ```python page = {"Home": self.home, "Inbox": self.inbox, "Settings": self.settings}[self.page]() return pz.row([self.sidebar(), pz.column([breadcrumbs, page], padding=24, width="fill")]) ``` 4. **Tabs inside a page.** A second attribute, `self.tab`, and a `ui.segmented` control to change it. The body below depends on it: ```python tabs = ui.segmented(["General", "Notifications", "About"], self.tab, lambda tab: setattr(self, "tab", tab)) ``` 5. **Shortcuts.** `on_key` receives every key combination as text. Ctrl+1, Ctrl+2 and Ctrl+3 open the pages: ```python def on_key(self, combo): shortcuts = {f"ctrl-{i}": page for i, page in enumerate(PAGES, start=1)} if combo in shortcuts: self.go(shortcuts[combo]) ``` **State survives page changes:** Because pages are drawn from your state, nothing is lost when you leave one: the inbox remembers which messages you read, Settings remembers its tab. To reset a page, reset its attributes in `go()`. **In Pyzed Design:** In the designer, a **Tab bar** control raises `change` with the selected tab; show and hide panels in its handler (`form.general.visible = event.value == "General"`), or open another form in its own window with `form.open("settings")`. See [Events and handlers](https://albakore.github.io/pyzed/editor/events/). #### Full code `examples/tutorial/navigation.py`: ```python """A multi-view app: a sidebar that switches pages, and tabs inside a page. python examples/tutorial/navigation.py - The current page is one attribute (``self.page``); ``view()`` draws the page it names. - The sidebar is a column of ``ui.list_item`` rows; the selected one is highlighted. - Inside "Settings", ``ui.segmented`` works as a row of tabs (``self.tab``). - ``ui.breadcrumbs`` shows where you are; Ctrl+1, Ctrl+2 and Ctrl+3 jump between pages. Walkthrough: https://albakore.github.io/pyzed/tutorials/intermediate/navigation/ """ import pyzed as pz from pyzed import ui PAGES = {"Home": "house", "Inbox": "inbox", "Settings": "settings"} MESSAGES = [("Grace", "The compiler works!"), ("Alan", "Lunch on Friday?"), ("Linus", "Patch attached")] class Navigation(pz.App): title = "Navigation" size = (820, 520) def __init__(self): self.page = "Home" self.tab = "General" self.notifications = True self.read = set() # -- moving around ------------------------------------------------------------------ def go(self, page): self.page = page def on_key(self, combo): shortcuts = {f"ctrl-{i}": page for i, page in enumerate(PAGES, start=1)} if combo in shortcuts: self.go(shortcuts[combo]) # -- the pieces --------------------------------------------------------------------- def sidebar(self): t = self.resolved_theme() unread = len(MESSAGES) - len(self.read) items = [ ui.list_item(name, lambda name=name: self.go(name), icon=icon, selected=name == self.page, height=34, end=ui.count_badge(unread) if name == "Inbox" and unread else None) for name, icon in PAGES.items() ] return pz.container( pz.column([ui.label("My app", weight="semibold"), pz.vspace(8), *items], spacing=2), padding=12, width=200, height="fill", bg=t.panel, ) def home(self): return pz.column([ ui.heading("Welcome back", level=2), ui.muted("Pick a page on the left, or press Ctrl+1, Ctrl+2 or Ctrl+3."), pz.row([ui.button("Open the inbox", lambda: self.go("Inbox"), variant="solid", icon="inbox"), ui.button("Settings", lambda: self.go("Settings"), icon="settings")], spacing=8), ], spacing=12) def inbox(self): rows = [ ui.list_item(pz.row([ui.label(who, weight="medium" if i not in self.read else "normal"), ui.muted(text)], spacing=12), lambda i=i: self.read.add(i), icon="mail-open" if i in self.read else "mail", height=36) for i, (who, text) in enumerate(MESSAGES) ] return pz.column([ui.heading("Inbox", level=2), ui.muted("Click a message to mark it as read."), *rows], spacing=8) def settings(self): tabs = ui.segmented(["General", "Notifications", "About"], self.tab, lambda tab: setattr(self, "tab", tab)) if self.tab == "General": body = ui.field("Theme", ui.select(["One Dark", "One Light"], self.resolved_theme().name, lambda name: setattr(self, "theme", name), width=180)) elif self.tab == "Notifications": body = ui.field("Show notifications", ui.switch(self.notifications, lambda on: setattr(self, "notifications", on))) else: body = ui.label("Version 1.0 · made with Pyzed") return pz.column([ui.heading("Settings", level=2), tabs, body], spacing=16) def view(self): t = self.resolved_theme() page = {"Home": self.home, "Inbox": self.inbox, "Settings": self.settings}[self.page]() content = pz.column([ ui.breadcrumbs(["My app", self.page], on_press=lambda i: self.go("Home") if i == 0 else None), page, ], spacing=16, padding=24, width="fill") return pz.container(pz.row([self.sidebar(), content], height="fill"), width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(Navigation) ``` #### Events in this tutorial | Widget | Event | Handler | | --- | --- | --- | | Sidebar `ui.list_item` | `on_press` | `lambda name=name: self.go(name)` | | The app | `on_key` | `ctrl-1`, `ctrl-2`, `ctrl-3` go to a page | | `ui.breadcrumbs` | `on_press` | gets the index of the part: `0` goes home | | Inbox `ui.list_item` | `on_press` | marks the message as read | | `ui.segmented`, `ui.select`, `ui.switch` | `on_select`, `on_select`, `on_toggle` | the tab, the theme, on/off | ##### Try more events Back and forward with a history, like a browser: ```python def __init__(self): ... self.history, self.future = [], [] def go(self, page): if page != self.page: self.history.append(self.page) self.future.clear() self.page = page def on_key(self, combo): if combo == "alt-left" and self.history: self.future.append(self.page) self.page = self.history.pop() elif combo == "alt-right" and self.future: self.history.append(self.page) self.page = self.future.pop() ``` Mice with back and forward buttons send them as `mouse_down` with `event.button` set: add `.on(mouse_down=lambda e: ...)` to the page. #### Next - [Lists and tables with data](https://albakore.github.io/pyzed/tutorials/intermediate/lists-tables/). - [Multiple windows](https://albakore.github.io/pyzed/guides/windows/): several real windows that share state. - The [Dashboard](https://albakore.github.io/pyzed/showcase/dashboards/#dashboard) and [Settings](https://albakore.github.io/pyzed/showcase/forms/#settings-page) showcases use the same sidebar. ### A custom shader graph > Build a GPU shader graph node by node in Python with pyzed.shading: fractal noise, smoothstep bands, a gradient sky, mutable variables driven by sliders, and a .pzshad you can open in Pyzed Design. Source: https://albakore.github.io/pyzed/tutorials/advanced/shader-graph/ In [the intermediate shader tutorial](https://albakore.github.io/pyzed/tutorials/intermediate/shader-effect/) you edited a template in the Shader view. Here you build a shader from nothing, **in Python**, with the same graph the Shader view edits: an aurora, curtains of green light drifting over a night sky, with sliders for its speed, height and color. | Goal | Time | Level | You need | | --- | --- | --- | --- | | An animated aurora made of nodes, with live controls | 40 minutes | Advanced · Python | [A shader effect](https://albakore.github.io/pyzed/tutorials/intermediate/shader-effect/) | #### What you'll learn - Create a `shading.Graph`, add nodes by calling them, and pick their outputs. - Use arithmetic on outputs (`+`, `-`, `*`) to add Math nodes for you. - Combine **fractal noise**, **smoothstep** and a **gradient** into a picture. - Add **mutable variables** (uniforms) and change them every frame from sliders. - Save the graph as a `.pzshad` and keep editing it visually in Pyzed Design. #### How a shader thinks A shader answers one question, for every pixel at once: *what color is this pixel right now?* It knows where the pixel is (**UV**: from 0,0 at the top left to 1,1 at the bottom right) and the **time**. Everything else is math on those two things. The graph is that math, drawn as boxes and wires: ```mermaid flowchart LR UV[UV] --> P["Compose: x·3 + t, y·0.6 − t·0.3"] T["Time × speed"] --> P P --> N[Fractal noise] N --> S1["Smoothstep 0.35 → 0.8"] UV --> B["Band near the top (height)"] S1 --> L["light = curtains × band"] B --> L UV --> SKY["Gradient: night sky"] SKY --> M["Mix sky → tint by light"] L --> M M --> V["× Vignette"] --> O[Output] ``` #### Steps 1. **A graph and its variables.** `g.uniform` adds a *mutable* variable (the GPU reads it every frame, so changing it is free) and returns a node that reads it: ```python from pyzed import shading g = shading.Graph("aurora") speed = g.uniform("speed", "float", 0.3, min=0, max=2) height = g.uniform("height", "float", 0.55, min=0.2, max=0.9) tint = g.uniform("tint", "color", "#34d399") ``` 2. **Inputs.** Calling a node type on the graph adds that node. `node["name"]` picks an output, and arithmetic adds Math nodes: ```python uv = g.uv() t = g.time()["time"] * speed # a Multiply node ``` 3. **The curtains.** Stretch the coordinates sideways and move them with time, then sample **fractal noise** (layers of noise, each finer than the last; the classic look of clouds and smoke): ```python p = g.compose(uv["x"] * 3.0 + t, uv["y"] * 0.6 - t * 0.3)["vector"] curtains = g.fbm(p, scale=2.5, octaves=5) ``` 4. **Keep them in a band.** `smoothstep(a, b, x)` is 0 below `a`, 1 above `b` and a smooth ramp between: the shader version of "fade in". Two of them make a band near the top, controlled by `height`: ```python band = g.smoothstep(height - 0.35, height, 1.0 - uv["y"]) * g.smoothstep(1.0, height, 1.0 - uv["y"]) light = g.smoothstep(0.35, 0.8, curtains["value"]) * band ``` 5. **Color it.** A **Gradient** node makes the sky; **Mix** blends from the sky to the tint where there is light; **Vignette** darkens the corners; **Output** is the final color: ```python sky = g.gradient(uv["y"], stops=[[0.0, "#020617"], [1.0, "#0f172a"]]) glow = g.mix(sky["rgb"], tint * 1.4, light) g.output(g.rgba(glow * g.vignette(strength=0.6), 1.0)["color"]) g.arrange() # lay the nodes out in columns, like the Arrange button of the Shader view ``` 6. **Show it.** `pz.shader` takes the graph, compiles it to WGSL once and runs it on the GPU. `uniforms=` sets the mutable variables by name, so the sliders change the picture without recompiling anything: ```python pz.shader(AURORA, uniforms={"speed": self.speed, "height": self.height, "tint": TINTS[self.tint]}, width="fill", height="fill", fallback=pz.Gradient(["#020617", "#064e3b"], angle=180)) ``` 7. **Open it in the designer.** Run the example with `--save` to write `examples/tutorial/shaders/aurora.pzshad`, then `pyzed design examples/tutorial/shaders/aurora.pzshad`: every node you created in code is on the canvas, with its live preview, ready to tweak with the mouse. **When you need raw WGSL:** An **Expression** node holds your own WGSL (`g.add("expression", code="sin(a * 10.0) * b", a=..., b=...)`), so you never have to leave the graph for one special formula. For a whole shader in WGSL, see [Shaders](https://albakore.github.io/pyzed/shading/shaders/). **Compile errors point at the node:** `g.compile()` returns the WGSL and the problems by node (`.errors`); a type mismatch (a texture into a number) is refused with a `GraphError` the moment you wire it. In the Shader view the faulty node turns red. #### Full code `examples/tutorial/aurora_graph.py`: ```python """A custom shader graph built in Python: an aurora with sliders for its variables. python examples/tutorial/aurora_graph.py # show it python examples/tutorial/aurora_graph.py --save # also write shaders/aurora.pzshad - ``shading.Graph`` is the same graph the Shader view of Pyzed Design edits: calling a node type on it (``g.fbm(...)``, ``g.smoothstep(...)``) adds that node, ``node["output"]`` picks an output, and ``+``, ``-``, ``*`` and ``/`` on outputs add Math nodes. - ``g.uniform(...)`` adds a *mutable* variable: the sliders change it every frame without recompiling anything. - ``pz.shader(graph, uniforms=...)`` compiles the graph to WGSL and runs it on the GPU. Without a GPU it draws the ``fallback`` gradient. - ``--save`` writes the graph as a ``.pzshad`` file you can open in Pyzed Design to keep editing. Walkthrough: https://albakore.github.io/pyzed/tutorials/advanced/shader-graph/ """ import sys from pathlib import Path import pyzed as pz from pyzed import shading, ui HERE = Path(__file__).resolve().parent TINTS = {"Green": "#34d399", "Violet": "#a78bfa", "Blue": "#38bdf8"} def build_aurora() -> shading.Graph: g = shading.Graph("aurora") g.description = "Curtains of light drifting over a night sky." # Variables: the knobs your Python code (and the sliders) can turn. speed = g.uniform("speed", "float", 0.3, min=0, max=2) height = g.uniform("height", "float", 0.55, min=0.2, max=0.9) tint = g.uniform("tint", "color", "#34d399") # 1. Where is this pixel, and what time is it? uv = g.uv() t = g.time()["time"] * speed # 2. Drifting fractal noise, stretched sideways: the curtains. p = g.compose(uv["x"] * 3.0 + t, uv["y"] * 0.6 - t * 0.3)["vector"] curtains = g.fbm(p, scale=2.5, octaves=5) # 3. Keep them in a band near the top, fading toward the ground. band = g.smoothstep(height - 0.35, height, 1.0 - uv["y"]) * g.smoothstep(1.0, height, 1.0 - uv["y"]) light = g.smoothstep(0.35, 0.8, curtains["value"]) * band # 4. Colors: a night sky gradient, plus the tint where there is light. sky = g.gradient(uv["y"], stops=[[0.0, "#020617"], [1.0, "#0f172a"]]) glow = g.mix(sky["rgb"], tint * 1.4, light) g.output(g.rgba(glow * g.vignette(strength=0.6), 1.0)["color"]) g.arrange() # lay the nodes out in columns, as the Arrange button of the Shader view does return g AURORA = build_aurora() class Aurora(pz.App): title = "Aurora" size = (760, 480) theme = "One Dark" def __init__(self): self.speed = 0.3 self.height = 0.55 self.tint = "Green" def knob(self, label, value, low, high, field): return ui.field(label, pz.container(ui.slider(value, lambda v: setattr(self, field, v), min=low, max=high), width=180)) def view(self): sky = pz.shader(AURORA, uniforms={"speed": self.speed, "height": self.height, "tint": TINTS[self.tint]}, width="fill", height="fill", fallback=pz.Gradient(["#020617", "#064e3b"], angle=180)) panel = pz.container(pz.column([ ui.label("Aurora", weight="semibold"), self.knob("Speed", self.speed, 0, 2, "speed"), self.knob("Height", self.height, 0.2, 0.9, "height"), ui.field("Tint", ui.segmented(list(TINTS), self.tint, lambda c: setattr(self, "tint", c))), ], spacing=8), padding=14).style(glass=True, radius=12) return pz.stack([sky, pz.container(panel, padding=16, width="fill", height="fill", align_x="start", align_y="end")]) if __name__ == "__main__": if "--save" in sys.argv: print("wrote", AURORA.save(HERE / "shaders" / "aurora.pzshad")) pz.run(Aurora) ``` #### Next - [Shader graphs](https://albakore.github.io/pyzed/shading/shader-graphs/): the whole `pyzed.shading` API and the `.pzshad` format. - The [Shading](https://albakore.github.io/pyzed/shading/) section: every node, with examples. - [Connecting a backend](https://albakore.github.io/pyzed/tutorials/advanced/backend/). ### Layout basics > Arrange a Pyzed window with rows, columns, fill, spacing, padding and grids, in Python code and with the flex containers and anchors of Pyzed Design. Source: https://albakore.github.io/pyzed/tutorials/basic/layout-basics/ Every screen, from a login box to a full IDE, is built from four ideas: **stack** things vertically, put them **side by side**, decide **who takes the free space**, and leave **room** between them. Learn them once and you can read any Pyzed layout. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A profile page that resizes nicely | 20 minutes | Basic · Python | Pyzed installed ([Installation](https://albakore.github.io/pyzed/getting-started/installation/)) | #### What you'll learn - `pz.column` and `pz.row`: vertical and horizontal stacks. - `spacing` (the gap between children) and `padding` (the room inside the edge). - `width="fill"` / `height="fill"` and `pz.hspace()`: taking the free space. - `align`: lining children up across the stack. - `pz.grid`: equal cells. - The same ideas in the designer: **Flex row**, **Flex column**, **Grid**, **Spacer** and **anchors**. #### The four ideas ```text pz.column([...], spacing=16) pz.row([...], spacing=12, align="center") ┌──────────────┐ ┌───────────────────────────────────────┐ │ header │ │ (AL) Ada Lovelace ⟷fill⟷ [Follow]│ │ ── 16 px ── │ └───────────────────────────────────────┘ │ stats │ pz.hspace() pushes the button right │ ── 16 px ── │ │ about (fill) │ ← height="fill" takes what is left └──────────────┘ ``` | Size value | Means | | --- | --- | | a number (`width=200`) | Exactly that many pixels. | | `"shrink"` (the default for most widgets) | As small as the content. | | `"fill"` | All the free space; several `fill` children share it equally. | | `"fill:2"` | Twice the share of a plain `fill` sibling. | #### Steps 1. **The page is a column.** `view()` returns one widget; here a `pz.container` with `padding=24` around a column of three parts, `spacing=16` apart: ```python def view(self): t = self.resolved_theme() return pz.container( pz.column([self.header(), self.stats(), self.about()], spacing=16, height="fill"), padding=24, width="fill", height="fill", bg=t.background, ) ``` 2. **The header is a row.** An avatar, a column with the name and a muted line, then `pz.hspace()`, an empty piece with `width="fill"`, then the button. `align="center"` centers them vertically: ```python pz.row([ ui.avatar("AL", size=48), pz.column([ui.heading("Ada Lovelace", level=2), ui.muted("Analyst · London")], spacing=2), pz.hspace(), ui.button("Follow", self.toggle_follow, variant="solid", icon="user-plus"), ], spacing=12, align="center") ``` 3. **Three equal cards: a grid.** `columns=3` makes three cells of the same width; `height` is the height of each row: ```python pz.grid([ui.stat("Projects", "12", width="fill"), ...], columns=3, spacing=12, height=84) ``` 4. **The last part fills.** `height="fill"` on the About box makes it take every pixel the other parts leave, so it grows with the window. 5. **Run it and resize the window.** The header keeps the button at the right edge, the cards stay equal and the About box stretches. **See changes as you save:** Run any script with `pyzed dev examples/tutorial/layout_basics.py` (or `uv run pyzed dev ...`): the window updates every time you save the file and keeps its state. Perfect for playing with `spacing` and `padding`. #### The same thing in Pyzed Design The designer has the same building blocks in the **Layout** category of the toolbox: | In code | In the designer | | --- | --- | | `pz.row([...], spacing=8)` | **Flex row**, with **Gap** 8 | | `pz.column([...])` | **Flex column** | | `pz.hspace()` | **Spacer** (a child with grow 1) | | `width="fill"` on a child | The child's **Grow** field (Position section) set to 1 | | `pz.grid([...], columns=3)` | **Grid** with **Columns** 3 | | `padding=24` | The container's **Padding** | Controls placed freely on the form (not in a flex container) use **anchors** instead: anchored left and right, a control stretches; anchored only right, it follows the right edge. Select controls and use **Group in a row / column / grid** (right-click menu) to turn a free arrangement into a flex one. Details in [Layouts and containers](https://albakore.github.io/pyzed/editor/layouts/). #### Full code `examples/tutorial/layout_basics.py`: ```python """Layout basics: rows, columns, fill, spacing, padding, alignment and grids. python examples/tutorial/layout_basics.py A small profile page laid out with the four ideas behind every Pyzed layout: - ``pz.column`` stacks children top to bottom, ``pz.row`` puts them side by side. - ``spacing`` is the gap between children, ``padding`` the room inside the edge. - ``width="fill"`` / ``height="fill"`` take the space that is left; ``pz.hspace()`` is an empty piece that fills, so it pushes what follows to the end of the row. - ``pz.grid`` places children in cells of equal width. Walkthrough: https://albakore.github.io/pyzed/tutorials/basic/layout-basics/ """ import pyzed as pz from pyzed import ui class Profile(pz.App): title = "Layout basics" size = (720, 500) def __init__(self): self.follows = False def header(self): # A row: avatar, a column with two lines, a spacer that fills, then a button at the far right. return pz.row([ ui.avatar("AL", size=48), pz.column([ui.heading("Ada Lovelace", level=2), ui.muted("Analyst · London")], spacing=2), pz.hspace(), ui.button("Following" if self.follows else "Follow", self.toggle_follow, variant="outline" if self.follows else "solid", icon="user-plus"), ], spacing=12, align="center") def stats(self): # A grid: three cells of the same width, whatever the window size. return pz.grid([ ui.stat("Projects", "12", width="fill"), ui.stat("Followers", "1,205" if self.follows else "1,204", width="fill"), ui.stat("Stars", "86", width="fill"), ], columns=3, spacing=12, height=84) # height: the height of each row def about(self): # A container with height="fill" grows to take the rest of the window. t = self.resolved_theme() return pz.container( pz.column([ ui.label("About", weight="semibold"), ui.label("Writes the first programs for the Analytical Engine. " "Resize the window: this box grows, the button stays at the right."), ], spacing=6), padding=16, width="fill", height="fill", bg=t.surface, border_color=t.border, border_width=1, radius=t.radius + 2, ) def toggle_follow(self): self.follows = not self.follows def view(self): t = self.resolved_theme() return pz.container( pz.column([self.header(), self.stats(), self.about()], spacing=16, height="fill"), padding=24, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(Profile) ``` #### Events in this tutorial The page has one event: the **Follow** button's `on_press`, which calls `toggle_follow` (no argument). Layout widgets have no events of their own, but any widget takes the [common events](https://albakore.github.io/pyzed/guides/events/) with `.on()`. ##### Try more events ```python def __init__(self): self.follows = False self.narrow = False # new: the window is narrow self.opened = None # new: the stat that was clicked def stats(self): return pz.grid([ ui.stat(name, value, width="fill", on_press=lambda name=name: setattr(self, "opened", name)) for name, value in (("Projects", "12"), ("Followers", "1,204"), ("Stars", "86")) ], columns=1 if self.narrow else 3, spacing=12, height=84) def view(self): page = pz.column([self.header(), self.stats(), self.about()], spacing=20, padding=24) # resize: one column when the window is narrower than 560 px return page.on(resize=lambda e: setattr(self, "narrow", e.width < 560), double_click=lambda: setattr(self, "opened", None)) ``` #### Next - [Change the theme](https://albakore.github.io/pyzed/tutorials/basic/themes/) of this window. - [Layout guide](https://albakore.github.io/pyzed/guides/layout/): track grids (`"200 1fr auto"`), `pz.flex`, scrolling and anchored layouts. - [Row](https://albakore.github.io/pyzed/library/row/), [Column](https://albakore.github.io/pyzed/library/column/) and [Grid](https://albakore.github.io/pyzed/library/grid/) in the component library. ### Lists and tables with data > Show Python data in a Pyzed app: a searchable list, a sortable table with clickable rows and custom cells, and a details panel for the selected row. Source: https://albakore.github.io/pyzed/tutorials/intermediate/lists-tables/ Most apps show **data**: customers, files, orders, books. This tutorial takes a plain Python list and shows it two ways, as a sortable **table** and as a simple **list**, with a search box and a panel with the details of the row you click. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A book library with search, sorting and details | 30 minutes | Intermediate · Python | [A multi-view app](https://albakore.github.io/pyzed/tutorials/intermediate/navigation/) | #### What you'll learn - Filter data in `view()` from a search box. - Draw a `ui.table` with columns, **sorting** by clicking a header, and clickable rows. - Turn a cell into any widget (a colored badge) with `render=`. - Show the same data as a list of `ui.list_item` rows. - Keep the **selected** item and show it in a details panel; change it in place. #### Steps 1. **The data is just Python.** A list of dicts (objects or dataclasses work too): ```python BOOKS = [ {"title": "Dune", "author": "Frank Herbert", "year": 1965, "status": "Read"}, {"title": "Neuromancer", "author": "William Gibson", "year": 1984, "status": "Reading"}, # ... ] ``` 2. **Search.** Keep the text in `self.query` and filter on every redraw. No "refresh" is needed: typing changes the state and `view()` runs again. ```python def visible(self): q = self.query.lower() return [b for b in BOOKS if q in b["title"].lower() or q in b["author"].lower()] ``` 3. **The columns.** A `ui.Column` has a title, the key it reads and a width (`("fill", 3)` takes three shares of the free space). `render=` turns the value into a widget: ```python columns = [ ui.Column("Title", "title", width=("fill", 3)), ui.Column("Author", "author", width=("fill", 2)), ui.Column("Year", "year", width=70), ui.Column("Status", "status", width=100, render=lambda s: ui.badge(s, tint=TINTS[s])), ] ``` 4. **Sorting.** Give the table the column it sorts by and the direction; clicking a header calls `on_sort` with that column's title, and you flip the direction when it is the same one: ```python def sort(self, column): self.descending = not self.descending if column == self.sort_by else False self.sort_by = column ui.table(columns, books, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort, on_row_press=lambda book: setattr(self, "selected", book), selected=self.selected, striped=True, height="fill") ``` 5. **The details panel** shows `self.selected`, or an empty state. Changing the status writes into the same dict, so the table shows it too: ```python ui.segmented(list(TINTS), book["status"], lambda s: book.__setitem__("status", s)) ``` 6. **A list view.** The same data as rows with an icon and a trailing year, switched with a segmented control: ```python ui.list_item(book["title"], lambda book=book: setattr(self, "selected", book), icon="book", selected=book is self.selected, end=ui.muted(str(book["year"]))) ``` **Thousands of rows:** `ui.table` only builds the rows you can see, so a table of 100,000 rows scrolls as smoothly as one of ten. Keep the list sorted in your state (or in a `@pz.memo` function) instead of sorting it on every redraw. The [SQLAlchemy contacts](https://albakore.github.io/pyzed/integrations/sqlalchemy/) demo shows 20,000 rows searched in SQL. #### Full code `examples/tutorial/lists_tables.py`: ```python """Lists and tables with data: search, sort, select a row and show its details. python examples/tutorial/lists_tables.py - The data is a plain Python list of dicts; ``view()`` filters it with the search text. - ``ui.table`` draws it with sortable columns (``sort_by``/``descending``/``on_sort``), clickable rows (``on_row_press``) and a highlighted ``selected`` row. - ``ui.Column(..., render=...)`` turns a cell into any widget (here, a colored badge). - A segmented control switches between the table and a simple list of ``ui.list_item`` rows. Walkthrough: https://albakore.github.io/pyzed/tutorials/intermediate/lists-tables/ """ import pyzed as pz from pyzed import ui BOOKS = [ {"title": "Dune", "author": "Frank Herbert", "year": 1965, "status": "Read"}, {"title": "Neuromancer", "author": "William Gibson", "year": 1984, "status": "Reading"}, {"title": "The Left Hand of Darkness", "author": "Ursula K. Le Guin", "year": 1969, "status": "Read"}, {"title": "Foundation", "author": "Isaac Asimov", "year": 1951, "status": "Wishlist"}, {"title": "Hyperion", "author": "Dan Simmons", "year": 1989, "status": "Reading"}, {"title": "Snow Crash", "author": "Neal Stephenson", "year": 1992, "status": "Wishlist"}, {"title": "Solaris", "author": "Stanisław Lem", "year": 1961, "status": "Read"}, ] TINTS = {"Read": "success", "Reading": "accent", "Wishlist": "warning"} class Library(pz.App): title = "My books" size = (860, 540) def __init__(self): self.query = "" self.sort_by, self.descending = "Year", False self.selected = None # the book (a dict) shown in the details panel self.mode = "Table" def visible(self): q = self.query.lower() return [b for b in BOOKS if q in b["title"].lower() or q in b["author"].lower()] def sort(self, column): # Clicking the same header again flips the order. self.descending = not self.descending if column == self.sort_by else False self.sort_by = column def table(self, books): columns = [ ui.Column("Title", "title", width=("fill", 3)), ui.Column("Author", "author", width=("fill", 2)), ui.Column("Year", "year", width=70), ui.Column("Status", "status", width=100, render=lambda s: ui.badge(s, tint=TINTS[s])), ] return ui.table(columns, books, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort, on_row_press=lambda book: setattr(self, "selected", book), selected=self.selected, striped=True, row_height=34, height="fill") def simple_list(self, books): return pz.scrollable(pz.column([ ui.list_item(book["title"], lambda book=book: setattr(self, "selected", book), icon="book", selected=book is self.selected, end=ui.muted(str(book["year"])), height=32) for book in books ], spacing=2), height="fill") def details(self): book = self.selected if book is None: return ui.empty_state("Pick a book", "Click a row to see its details.", icon="book-open") return pz.column([ ui.heading(book["title"], level=3), ui.label(f"by {book['author']}, {book['year']}", color="text_muted"), ui.segmented(list(TINTS), book["status"], lambda s: book.__setitem__("status", s)), ], spacing=10) def view(self): t = self.resolved_theme() books = self.visible() left = pz.column([ pz.row([ pz.container(ui.search_input(self.query, lambda v: setattr(self, "query", v), placeholder="Search title or author…"), width="fill"), ui.segmented(["Table", "List"], self.mode, lambda m: setattr(self, "mode", m)), ], spacing=8, align="center"), ui.muted(f"{len(books)} of {len(BOOKS)} books", size="sm"), self.table(books) if self.mode == "Table" else self.simple_list(books), ], spacing=10, width="fill", height="fill") right = pz.container(self.details(), width=260, height="fill", padding=16, bg=t.surface, radius=t.radius) return pz.container(pz.row([left, right], spacing=16), padding=20, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(Library) ``` #### Events in this tutorial | Widget | Event | Handler gets | | --- | --- | --- | | `ui.table` | `on_sort` | The title of the clicked column. | | `ui.table` | `on_row_press` | The row: the book's dict. | | `ui.list_item` | `on_press` | Nothing (the book is bound with `book=book`). | | `ui.search_input`, `ui.segmented` | `on_input`, `on_select` | The query; the mode. | ##### Try more events The table has two more row events: a double click and a right click. ```python def table(self, books): return ui.table( columns, books, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort, selected=self.selected, on_row_press=lambda book: setattr(self, "selected", book), on_row_double_press=lambda book: setattr(self, "editing", book), # open an editor on_row_context=lambda book: setattr(self, "menu_for", book), # a context menu striped=True, row_height=34, height="fill", ) def on_key(self, combo): # arrows move the selection books = self.visible() if combo in ("up", "down") and books: i = books.index(self.selected) if self.selected in books else -1 self.selected = books[max(0, min(len(books) - 1, i + (1 if combo == "down" else -1)))] ``` #### Next - [Dialogs, notifications and validation](https://albakore.github.io/pyzed/tutorials/intermediate/dialogs-feedback/). - The [Data table](https://albakore.github.io/pyzed/showcase/dashboards/#data-table) showcase: filters, selection, paging and export. - [Table](https://albakore.github.io/pyzed/library/table/) and [List item](https://albakore.github.io/pyzed/library/list-item/) in the component library. ### Change the theme > Give a Pyzed app another look: pick a bundled theme, change the accent color, follow the system's light or dark mode, or generate your own theme from three colors, in code and in Pyzed Design. Source: https://albakore.github.io/pyzed/tutorials/basic/themes/ A **theme** is the set of colors (and a few sizes, like the corner radius) every component reads when it draws itself. Change the theme and the whole window changes at once: buttons, inputs, badges, the code editor... You never color components one by one. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A window with a theme picker | 15 minutes | Basic · Python, or no code in the designer | [Layout basics](https://albakore.github.io/pyzed/tutorials/basic/layout-basics/) | #### What you'll learn - Pick a theme in code (`theme = "One Light"`) and in the designer (**Properties → Look → Theme**). - Change only the **accent** color. - Follow the operating system's **light or dark** mode. - Make **your own theme** from three colors and give it a name. - Use **theme colors** (`"accent"`, `"text_muted"`) instead of fixed `#hex` values. #### Steps 1. **Pick a bundled theme.** `theme` is a class attribute of your app. Any name from `pz.themes.names()` works: the One, Ayu and Gruvbox families, light and dark. ```python class ThemeSwitcher(pz.App): theme = "One Dark" ``` 2. **Change it while the app runs.** Assign `self.theme`; the next redraw uses it. A drop-down of every theme is one line: ```python ui.select(pz.themes.names(), self.base, lambda name: setattr(self, "theme", name)) ``` 3. **Change only the accent.** The accent is the color of primary buttons, focus rings, selection and links: ```python self.theme = pz.themes.get("One Dark").with_accent("#e5c07b") ``` 4. **Follow the system.** `pz.themes.auto(light, dark)` switches by itself when the user changes their operating system between light and dark mode: ```python self.theme = pz.themes.auto("One Light", "One Dark") ``` 5. **Make your own.** `pz.Theme.generate` derives every surface, border, hover state and muted text from three colors; `pz.themes.register` adds it to the list under its name: ```python SUNSET = pz.themes.register( pz.Theme.generate("Sunset", background="#2b1d2f", text="#f5e6e8", accent="#ff8a65") ) ``` 6. **Read the theme in your view.** `self.resolved_theme()` returns the `Theme` in use, handy for a background or to say "dark" or "light": ```python t = self.resolved_theme() pz.container(..., bg=t.background) ui.muted(f"Using {t.name} ({'dark' if t.is_dark else 'light'})") ``` **Use theme colors, not #hex:** Wherever a component takes a color, prefer a **token** such as `"accent"`, `"text_muted"`, `"success"` or `"error"` over a fixed `"#3b82f6"`. Tokens follow the theme, so your app stays readable in light and dark themes. #### In Pyzed Design Select an empty spot of the canvas so the Properties panel shows the **form**, then open the **Look** section: - **Theme**: every registered theme. - **Accent**: seven preset colors, or a custom `#rrggbb`. - **Background**, **Corner radius** and **Font size** for the whole form. **View → Preview theme…** shows the canvas in another theme without changing the form, so you can check both looks. Each control can also override its own colors in its **Style** section. #### Full code `examples/tutorial/theme_switcher.py`: ```python """Changing the theme: a bundled theme, an accent color, follow the system, or your own. python examples/tutorial/theme_switcher.py - ``theme`` (a class attribute or ``self.theme``) takes the name of a bundled theme, a ``pz.Theme`` or ``pz.themes.auto(light, dark)``, which follows the operating system's light/dark setting. - ``theme.with_accent("#e5c07b")`` changes only the accent (primary buttons, focus, selection). - ``pz.Theme.generate(...)`` builds a whole theme from three colors; ``pz.themes.register`` adds it to ``pz.themes.names()``. Every component reads its colors from the active theme, so the window restyles at once. Walkthrough: https://albakore.github.io/pyzed/tutorials/basic/themes/ """ import pyzed as pz from pyzed import ui # A theme of your own, made from three colors and registered by name. SUNSET = pz.themes.register(pz.Theme.generate("Sunset", background="#2b1d2f", text="#f5e6e8", accent="#ff8a65")) ACCENTS = {"Theme": None, "Blue": "#74ade8", "Purple": "#b477cf", "Green": "#a1c181", "Gold": "#e5c07b"} class ThemeSwitcher(pz.App): title = "Themes" size = (760, 520) theme = "One Dark" # the theme the window opens with def __init__(self): self.base = "One Dark" self.accent = "Theme" self.follow_system = False self.name = "Ada" def apply(self): """Builds the theme from the choices and assigns it: the next view() uses it.""" if self.follow_system: self.theme = pz.themes.auto("One Light", "One Dark") return theme = pz.themes.get(self.base) color = ACCENTS[self.accent] self.theme = theme.with_accent(color) if color else theme def choose(self, field): def setter(value): setattr(self, field, value) self.apply() return setter def controls(self): return ui.settings_group("Appearance", [ ui.field("Follow the system", ui.switch(self.follow_system, self.choose("follow_system")), description="Light or dark, as your operating system"), ui.field("Theme", ui.select(pz.themes.names(), self.base, self.choose("base"), width=220)), ui.field("Accent", ui.segmented(list(ACCENTS), self.accent, self.choose("accent"))), ]) def preview(self): return ui.card(pz.column([ ui.input(self.name, lambda v: setattr(self, "name", v), placeholder="Your name", icon="user"), pz.row([ ui.button("Save", lambda: None, variant="solid", icon="check"), ui.button("Cancel", lambda: None, variant="ghost"), pz.hspace(), ui.badge("New"), ui.badge("Done", tint="success"), ui.badge("Late", tint="error"), ], spacing=8, align="center"), ui.progress(0.65), ui.callout("Hello " + (self.name or "there"), "Every color here comes from the theme.", tint="info"), ], spacing=12), title="Preview", width="fill") def view(self): t = self.resolved_theme() # the Theme in use right now return pz.container( pz.column([ ui.heading("Pick a look", level=2), ui.muted(f"Using {t.name} ({'dark' if t.is_dark else 'light'})"), self.controls(), self.preview(), ], spacing=16), padding=24, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(ThemeSwitcher) ``` **Going further:** This is the quick tour. The [Theming](https://albakore.github.io/pyzed/theming/) section explains every token, how to style single widgets with `.style(...)`, gradients, fonts and icons, and how to save a theme to a file. #### Events in this tutorial | Widget | Event | Handler gets | | --- | --- | --- | | `ui.switch` (Follow the system) | `on_toggle` | `True` or `False`. | | `ui.select` (Theme) | `on_select` | The theme's name. | | `ui.segmented` (Accent) | `on_select` | The accent's name. | | `ui.input` (Your name) | `on_input` | The text. | `self.choose("base")` returns the handler: a small function that stores what it receives in that field. One factory serves the three controls. ##### Try more events ```python def on_key(self, combo): # a shortcut for every window of the app if combo == "ctrl-t": # Ctrl+T: flip between light and dark self.base = "One Light" if self.resolved_theme().is_dark else "One Dark" self.follow_system = False def on_resize(self, width, height): # the window's own resize event self.compact = width < 600 # e.g. hide the preview on small windows ``` #### Next - [5-step tutorial](https://albakore.github.io/pyzed/tutorials/basic/step-by-step/): state, layout, async and a notes app in pure Python. - [Themes](https://albakore.github.io/pyzed/theming/themes/) and [Styles](https://albakore.github.io/pyzed/theming/styles/) in the Theming section. ### Connecting a backend > Connect a Pyzed desktop app to a database: a repository class with SQLite, queries in a background thread with Command.perform, a loading spinner and errors shown instead of crashes. Source: https://albakore.github.io/pyzed/tutorials/advanced/backend/ Pyzed is the **frontend**: it draws windows and reacts to the user. Your data lives somewhere else: a database, a REST API, files. This tutorial connects the two the way large apps do, with a thin **backend layer** the window talks to, and every slow call moved off the UI thread so the window never freezes. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A contacts app backed by SQLite | 40 minutes | Advanced · Python | [Background tasks](https://albakore.github.io/pyzed/tutorials/intermediate/background-tasks/) | #### What you'll learn - Put all data access in a **repository** class with no UI in it, easy to test and to replace. - Run queries in a **background thread** with `Command.perform(work, then)`. - Show a **spinner** while a query runs, and turn exceptions into a message instead of a crash. - Reload the view after a change (add, delete, search). #### The shape of the app ```mermaid flowchart LR UI["Contacts (pz.App)
state + view()"] -- "Command.perform(work)" --> T["background thread"] T -- "work(): repo.search(...)" --> R["ContactsRepository"] R --> DB[("SQLite
(or an API, an ORM...)")] T -- "then(result) on the UI thread" --> UI ``` #### Steps 1. **The repository.** Plain Python, no Pyzed import needed. It opens a connection per call, which makes it safe to use from any thread: ```python class ContactsRepository: def __init__(self, path=DEFAULT_DB): self.path = Path(path) with self.connect() as db: db.execute("CREATE TABLE IF NOT EXISTS contacts (id INTEGER PRIMARY KEY, name TEXT, email TEXT)") def connect(self): return sqlite3.connect(self.path) def search(self, text=""): with self.connect() as db: rows = db.execute("SELECT id, name, email FROM contacts WHERE name LIKE ? ORDER BY name", (f"%{text}%",)).fetchall() return [{"id": i, "name": n, "email": e} for i, n, e in rows] ``` 2. **Never query on the UI thread.** A query that takes 300 ms would freeze the window for 300 ms. `Command.perform(work, then)` runs `work()` in a thread and calls `then(result)` back on the UI thread, where it is safe to change state: ```python def reload(self): return pz.Command.perform(lambda: self.repo.search(self.query), self.loaded) def loaded(self, contacts): self.contacts = contacts ``` 3. **Busy and errors.** One small helper wraps every call: it sets `self.busy` (the view shows a spinner) and catches exceptions so they become a callout: ```python def background(self, work, then=None): self.busy, self.error = True, None def guarded(): try: return ("ok", work()) except Exception as exc: return ("error", str(exc)) def done(outcome): self.busy = False kind, value = outcome if kind == "error": self.error = value elif then is not None: return then(value) return pz.Command.perform(guarded, done) ``` 4. **Load on start, reload after changes.** `on_start` runs once when the window opens. Adding or deleting reloads the list when it is done: ```python def on_start(self): return self.reload() def add(self): name, email = self.name, self.email self.name = self.email = "" return self.background(lambda: self.repo.add(name, email), lambda _: self.reload()) ``` 5. **Test the backend alone.** Because the repository has no UI, a test is three lines: ```python repo = ContactsRepository(tmp_path / "contacts.db") repo.add("Grace Hopper", "grace@example.com") assert [c["name"] for c in repo.search("grace")] == ["Grace Hopper"] ``` **Async libraries:** If your client is async (`httpx.AsyncClient`, `aiosqlite`, SQLAlchemy's `AsyncSession`), pass an `async def` to `Command.perform`, or return a coroutine from a callback: it runs on Pyzed's asyncio loop. See [Async and subscriptions](https://albakore.github.io/pyzed/guides/async/). **Do not touch self from the thread:** `work()` runs in another thread: let it return a value and change `self` in `then`. Changing state from the worker thread can race with drawing. #### Full code `examples/tutorial/contacts_backend.py`: ```python """Connecting a backend: a contacts app whose data lives in SQLite, behind a small repository class. python examples/tutorial/contacts_backend.py - ``ContactsRepository`` is the backend: plain Python with no UI in it, easy to test and to swap for SQLAlchemy, an HTTP API or anything else. - The window never talks to the database directly: ``Command.perform(work, then)`` runs the query in a background thread and hands the result to ``then`` on the UI thread, so the window never freezes, even on a slow disk or network. - A spinner shows while a query runs; errors become a callout instead of a crash. Walkthrough: https://albakore.github.io/pyzed/tutorials/advanced/backend/ """ import sqlite3 import tempfile import time from pathlib import Path import pyzed as pz from pyzed import ui DEFAULT_DB = Path(tempfile.gettempdir()) / "pyzed-tutorial-contacts.db" class ContactsRepository: """The backend. One connection per call, so it is safe to use from any thread.""" def __init__(self, path=DEFAULT_DB, delay=0.3): self.path = Path(path) self.delay = delay # pretend the database is far away, to see the spinner with self.connect() as db: db.execute("CREATE TABLE IF NOT EXISTS contacts (id INTEGER PRIMARY KEY, name TEXT, email TEXT)") if not db.execute("SELECT COUNT(*) FROM contacts").fetchone()[0]: db.executemany("INSERT INTO contacts (name, email) VALUES (?, ?)", [("Ada Lovelace", "ada@example.com"), ("Alan Turing", "alan@example.com")]) def connect(self): return sqlite3.connect(self.path) def search(self, text=""): time.sleep(self.delay) with self.connect() as db: rows = db.execute("SELECT id, name, email FROM contacts WHERE name LIKE ? OR email LIKE ? ORDER BY name", (f"%{text}%", f"%{text}%")).fetchall() return [{"id": i, "name": n, "email": e} for i, n, e in rows] def add(self, name, email): if not name.strip() or "@" not in email: raise ValueError("A contact needs a name and a valid email.") time.sleep(self.delay) with self.connect() as db: db.execute("INSERT INTO contacts (name, email) VALUES (?, ?)", (name.strip(), email.strip())) def delete(self, contact_id): with self.connect() as db: db.execute("DELETE FROM contacts WHERE id = ?", (contact_id,)) class Contacts(pz.App): title = "Contacts" size = (640, 520) def __init__(self, repository=None): self.repo = repository or ContactsRepository() self.contacts = [] self.query = "" self.name = self.email = "" self.busy = False self.error = None # -- talking to the backend --------------------------------------------------------- def background(self, work, then=None): """Runs ``work()`` in a thread; ``then(result)`` gets the result on the UI thread.""" self.busy, self.error = True, None def guarded(): try: return ("ok", work()) except Exception as exc: # noqa: BLE001 - show the problem instead of crashing return ("error", str(exc)) def done(outcome): self.busy = False kind, value = outcome if kind == "error": self.error = value elif then is not None: return then(value) return pz.Command.perform(guarded, done) def on_start(self): return self.reload() def reload(self): return self.background(lambda: self.repo.search(self.query), self.loaded) def loaded(self, contacts): self.contacts = contacts def search(self, text): self.query = text return self.reload() def add(self): name, email = self.name, self.email self.name = self.email = "" return self.background(lambda: self.repo.add(name, email), lambda _: self.reload()) def delete(self, contact): return self.background(lambda: self.repo.delete(contact["id"]), lambda _: self.reload()) # -- view --------------------------------------------------------------------------- def row(self, contact): initials = "".join(part[0] for part in contact["name"].split()[:2]) return ui.list_item( pz.row([ui.label(contact["name"], weight="medium"), ui.muted(contact["email"])], spacing=10), None, start=ui.avatar(initials, size=26, index=contact["id"]), height=40, end=ui.icon_button("trash-2", lambda: self.delete(contact), size="sm", tooltip="Delete"), ) def view(self): t = self.resolved_theme() form = pz.row([ ui.input(self.name, lambda v: setattr(self, "name", v), placeholder="Name"), ui.input(self.email, lambda v: setattr(self, "email", v), placeholder="Email", on_submit=self.add), ui.button("Add", self.add, variant="solid", icon="plus"), ], spacing=8) body = (ui.empty_state("No contacts", "Add one above.", icon="users") if not self.contacts else pz.scrollable(pz.column([self.row(c) for c in self.contacts], spacing=2), height="fill")) return pz.container(pz.column([ pz.row([ui.heading("Contacts", level=2), pz.hspace(), ui.spinner() if self.busy else None], align="center"), ui.search_input(self.query, self.search, placeholder="Search by name or email…"), form, ui.callout("Could not save", self.error, tint="error") if self.error else None, body, ], spacing=12), padding=24, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(Contacts) ``` #### Real backends The same pattern with real libraries, each a complete project in [`examples/integrations`](https://github.com/albakore/pyzed/tree/main/examples/integrations): #### Events in this tutorial | Source | Event | Handler | | --- | --- | --- | | The app | `on_start` | Loads the contacts from the backend in the background. | | `ui.input` | `on_input` | Stores the text of each field. | | `ui.input` (email) | `on_submit` | `self.add`: sends the new contact to the backend. | ##### Try more events ```python def row(self, contact): return ui.list_item(contact.name, None, icon="user", on_double_press=lambda: setattr(self, "editing", contact), # edit it on_right_press=lambda: setattr(self, "deleting", contact)) # ask to delete it def on_key(self, combo): if combo == "f5": # reload from the backend return self.on_start() ``` #### Next - [Package your app](https://albakore.github.io/pyzed/tutorials/advanced/packaging/) for Windows, macOS and Linux. - [Integrations overview](https://albakore.github.io/pyzed/integrations/overview/): where your code runs and how forms show server errors. ### Dialogs, notifications and validation > Talk to the user in a Pyzed app: field errors computed from state, a confirmation dialog over the page and toast notifications that disappear on their own. Source: https://albakore.github.io/pyzed/tutorials/intermediate/dialogs-feedback/ A good app tells the user what is going on: what is wrong with the form, what is about to happen, what just happened. Pyzed has a ready-made piece for each: **field errors**, **dialogs** and **toasts** (small notifications in the corner). | Goal | Time | Level | You need | | --- | --- | --- | --- | | A sign-up form with errors, a confirmation and notifications | 30 minutes | Intermediate · Python | [Lists and tables](https://albakore.github.io/pyzed/tutorials/intermediate/lists-tables/) | #### What you'll learn - Compute validation errors from your state and show them with `ui.field(..., error=...)`. - Show them only after the first try, not while the user is still typing. - Open a **confirmation dialog** over the page with `ui.dialog_layer` and `ui.confirm`. - Show **toasts** with `ui.toasts` and remove them after a few seconds without freezing the window. #### Steps 1. **Errors are a function of the state.** One method returns `{field: message}` for whatever is wrong right now: ```python def errors(self): found = {} if not self.name.strip(): found["name"] = "Tell us your name." if "@" not in self.email: found["email"] = "Enter an email such as ada@example.com." if len(self.password) < 8: found["password"] = "At least 8 characters." return found ``` 2. **Show them under each field**, but only once the user has tried to submit (`self.tried`). Nobody likes red text before they had a chance: ```python errors = self.errors() if self.tried else {} ui.field("Email", ui.input(self.email, lambda v: setattr(self, "email", v)), error=errors.get("email")) ``` 3. **A confirmation dialog.** `ui.dialog_layer(page, dialog)` draws `dialog` centered over a dimmed page; `dialog=None` shows just the page. So "is the dialog open?" is one attribute: ```python dialog = None if self.confirming: dialog = ui.confirm(f"Create an account for {self.email}?", self.create, lambda: setattr(self, "confirming", False), title="Almost there", confirm_label="Create") page = ui.dialog_layer(page, dialog, on_dismiss=lambda: setattr(self, "confirming", False)) ``` 4. **Toasts.** Keep a list of notes, draw them with `ui.toasts`, and remove each one after 4 seconds. `Command.perform` waits in a background thread, so the window keeps responding: ```python def notify(self, kind, title, text=""): note_id = next(_ids) self.notes.append((note_id, kind, title, text)) return pz.Command.perform(lambda: time.sleep(4), lambda _: self.dismiss(note_id)) # in view(): return ui.toasts(page, [ui.toast(title, text, kind=kind, on_dismiss=lambda i=i: self.dismiss(i)) for i, kind, title, text in self.notes]) ``` 5. **Put it together.** The button sets `tried`, shows an error toast if something is wrong, or opens the dialog; confirming clears the form and shows a success toast. **Other ready-made dialogs:** `ui.alert` (one button), `ui.prompt` (asks for a line of text), `ui.choice` (your own buttons) and `ui.unsaved_changes` ("Save before closing?") work the same way. See them all in `examples/dialogs.py` and in the [Alert](https://albakore.github.io/pyzed/library/alert/) and [Confirm](https://albakore.github.io/pyzed/library/confirm/) pages of the library. **Validating with Pydantic:** If your data already has a Pydantic (or SQLModel, Django, FastAPI) model, let it validate and turn its errors into `{field: message}` with `pyzed.forms.validation_errors(error)`. See [Pydantic](https://albakore.github.io/pyzed/integrations/pydantic/). #### Full code `examples/tutorial/feedback.py`: ```python """Talking to the user: validation messages, a confirmation dialog and toast notifications. python examples/tutorial/feedback.py - ``ui.field(..., error=...)`` shows a message under a field; the errors are computed from the state. - ``ui.dialog_layer(page, dialog)`` puts a dialog (``ui.confirm``, ``ui.alert``...) over the page; ``dialog`` is ``None`` when nothing is open. - ``ui.toasts(page, [...])`` stacks notifications in the corner; ``Command.perform`` waits a few seconds in the background and then removes them. Walkthrough: https://albakore.github.io/pyzed/tutorials/intermediate/dialogs-feedback/ """ import itertools import time import pyzed as pz from pyzed import ui _ids = itertools.count() class Signup(pz.App): title = "Create an account" size = (640, 560) def __init__(self): self.name = "" self.email = "" self.password = "" self.tried = False # show errors only after the first try self.confirming = False self.notes = [] # (id, kind, title, text) of the visible toasts # -- validation ----------------------------------------------------------------------- def errors(self): found = {} if not self.name.strip(): found["name"] = "Tell us your name." if "@" not in self.email or "." not in self.email.split("@")[-1]: found["email"] = "Enter an email such as ada@example.com." if len(self.password) < 8: found["password"] = "At least 8 characters." return found # -- toasts --------------------------------------------------------------------------- def notify(self, kind, title, text=""): note_id = next(_ids) self.notes.append((note_id, kind, title, text)) # wait 4 seconds in a background thread, then remove it (the window never freezes) return pz.Command.perform(lambda: time.sleep(4), lambda _: self.dismiss(note_id)) def dismiss(self, note_id): self.notes = [n for n in self.notes if n[0] != note_id] # -- actions -------------------------------------------------------------------------- def submit(self): self.tried = True if self.errors(): return self.notify("error", "Check the form", "Some fields need your attention.") self.confirming = True # open the dialog def create(self): self.confirming = False name = self.name self.name = self.email = self.password = "" self.tried = False return self.notify("success", "Account created", f"Welcome, {name}!") # -- view ----------------------------------------------------------------------------- def view(self): t = self.resolved_theme() errors = self.errors() if self.tried else {} form = pz.column([ ui.heading("Create an account", level=2), ui.muted("Leave a field empty and press the button to see the messages."), ui.field("Name", ui.input(self.name, lambda v: setattr(self, "name", v), placeholder="Ada Lovelace"), error=errors.get("name")), ui.field("Email", ui.input(self.email, lambda v: setattr(self, "email", v), icon="mail", placeholder="ada@example.com"), error=errors.get("email")), ui.field("Password", ui.input(self.password, lambda v: setattr(self, "password", v), secure=True, on_submit=self.submit), error=errors.get("password"), description="8 characters or more"), pz.row([pz.hspace(), ui.button("Create account", self.submit, variant="solid", icon="user-plus")]), ], spacing=14, padding=28, width="fill") page = pz.container(form, width="fill", height="fill", bg=t.background) dialog = None if self.confirming: dialog = ui.confirm(f"Create an account for {self.email}?", self.create, lambda: setattr(self, "confirming", False), title="Almost there", confirm_label="Create") page = ui.dialog_layer(page, dialog, on_dismiss=lambda: setattr(self, "confirming", False)) return ui.toasts(page, [ui.toast(title, text, kind=kind, on_dismiss=lambda i=i: self.dismiss(i)) for i, kind, title, text in self.notes]) if __name__ == "__main__": pz.run(Signup) ``` #### Events in this tutorial | Widget | Event | Handler | | --- | --- | --- | | `ui.input` (three fields) | `on_input` | `lambda v: setattr(self, "name", v)`… | | `ui.input` (password) | `on_submit` | `self.submit` | | **Create account** | `on_press` | `self.submit` | | `ui.confirm` | `on_confirm`, `on_cancel` | `self.create`, close the dialog | | `ui.dialog_layer` | `on_dismiss` | close the dialog (a click on the backdrop) | | `ui.toast` | `on_dismiss` | `lambda i=i: self.dismiss(i)` | ##### Try more events ```python def on_key(self, combo): if combo == "escape" and self.confirming: # Escape closes the dialog self.confirming = False return [] # handled: the key goes no further def view(self): ... toasts = [ui.toast(title, text, kind=kind, on_dismiss=lambda i=i: self.dismiss(i), on_press=lambda i=i: self.dismiss(i)) # a click on the card closes it too for i, kind, title, text in self.notes] ``` #### Next - [A custom component](https://albakore.github.io/pyzed/tutorials/intermediate/custom-component/) in the designer's Component view. - [Background tasks](https://albakore.github.io/pyzed/tutorials/intermediate/background-tasks/): progress bars and toasts from threads. ### 5-step tutorial > Learn Pyzed in five steps: go from an empty window to a notes app with Markdown, async work and a second window, in pure Python. Source: https://albakore.github.io/pyzed/tutorials/basic/step-by-step/ Five short files in [`examples/tutorial/`](https://github.com/albakore/pyzed/tree/main/examples/tutorial). Each one runs on its own and adds one idea to the previous one, so you can stop after any step and still have a working program. | Goal | Time | Level | You need | | --- | --- | --- | --- | | From an empty window to a notes app, in pure Python | 45 minutes | Basic · Python | Pyzed installed ([Installation](https://albakore.github.io/pyzed/getting-started/installation/)) | #### What you'll learn - How a Pyzed app works: an `App` class, its state and a `view()` that describes the window. - Two ways to react to the user: messages sent to `update()` and plain callbacks. - Layout, ready-made components and themes. - Slow work with `async def` without freezing the window. - Keyboard shortcuts, a resizable split and a second window. **Edit and see:** Run a step with `pyzed dev examples/tutorial/step1_hello.py` and the window updates every time you save the file, without losing its state. #### 1. A window An app is a class that inherits from `pz.App` and defines `view()`. `view()` returns a tree of widgets (`pz.text`, `pz.column`, `pz.row`, `pz.container`…). Pyzed calls it again after every change, so you never modify widgets: you describe what the current state looks like. Class attributes such as `title` and `size` configure the window. (Image: Step 1) `examples/tutorial/step1_hello.py`: ```python """Tutorial, step 1: a window with some text. python examples/tutorial/step1_hello.py """ import pyzed as pz class Hello(pz.App): title = "Hello" size = (400, 200) def view(self): # view() returns a tree of widgets; Pyzed calls it again after every change. return pz.center(pz.text("Hello, Pyzed!", size=28)) if __name__ == "__main__": pz.run(Hello) ``` #### 2. State and messages State is plain attributes on your object. Widgets are told what to do in two ways: - **A value** (`ui.button("Greet", "greet")`): sent to `update(message)`. - **A callable** (`ui.button("Reset", self.reset)`): called directly. Callbacks that carry data receive the new value: `on_input=lambda text: ...`, `on_toggle=lambda checked: ...`, `on_change=lambda value: ...`. (Image: Step 2) `examples/tutorial/step2_state.py`: ```python """Tutorial, step 2: state, messages and callbacks. python examples/tutorial/step2_state.py """ import pyzed as pz from pyzed import ui class Greeter(pz.App): title = "Greeter" size = (420, 260) def __init__(self): # State is plain attributes on your object. self.name = "" self.greetings = 0 def update(self, message): # Widgets given a plain value (here the string "greet") send it to update(). if message == "greet" and self.name: self.greetings += 1 def view(self): return pz.container( pz.column([ # Callbacks that receive data get the new value: here the input's text. ui.input(self.name, lambda text: setattr(self, "name", text), placeholder="Your name", on_submit="greet"), ui.button("Greet", "greet", variant="solid"), # A callable is called directly, no update() needed. ui.button("Reset", self.reset, variant="ghost"), ui.label(f"Hello, {self.name}! ({self.greetings})" if self.greetings else "Nobody greeted yet."), ], spacing=12), padding=24, ) def reset(self): self.name, self.greetings = "", 0 if __name__ == "__main__": pz.run(Greeter) ``` #### 3. Layout, components and themes `pyzed.ui` ships ready-made components (`button`, `input`, `select`, `number_field`, `settings_group`, `callout`…) that take their colors from the active theme. `theme` can be a `Theme`, the name of a bundled theme (`pz.themes.names()`) or `pz.themes.auto(...)` to follow the system. `self.resolved_theme()` returns the `Theme` in use. More in [Theming](https://albakore.github.io/pyzed/theming/themes/). (Image: Step 3) `examples/tutorial/step3_layout.py`: ```python """Tutorial, step 3: layout, ready-made components and themes. python examples/tutorial/step3_layout.py """ import pyzed as pz from pyzed import ui class Settings(pz.App): title = "Settings" size = (640, 420) theme = "One Dark" # any name from pz.themes.names(), a Theme, or pz.themes.auto() def __init__(self): self.font_size = 14 self.wrap = True self.language = "Python" def view(self): t = self.resolved_theme() # the Theme in use, handy for colors return pz.container( pz.column([ pz.row([ui.heading("Settings", level=2), pz.hspace(), ui.select(pz.themes.names(), t.name, lambda n: setattr(self, "theme", n), width=200)], align="center"), ui.settings_group("Editor", [ ui.field("Font size", ui.number_field(self.font_size, lambda v: setattr(self, "font_size", v), min=8, max=32)), ui.field("Soft wrap", ui.switch(self.wrap, lambda v: setattr(self, "wrap", v)), description="Wrap long lines at the window edge"), ui.field("Language", ui.select(["Python", "Rust", "Go"], self.language, lambda v: setattr(self, "language", v), width=140)), ]), ui.callout("Tip", "Every component reads its colors from the theme.", tint="hint"), ], spacing=16), padding=24, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(Settings) ``` #### 4. Async Slow work must not block the window. `Command.perform` accepts `async def` functions, which run on Pyzed's asyncio loop; the result reaches your callback on the UI thread. While loading, a `pz.every` subscription redraws the window to animate the spinner. You can also return coroutines or generators from `update` and callbacks: see the [Async and subscriptions](https://albakore.github.io/pyzed/guides/async/) guide. (Image: Step 4) `examples/tutorial/step4_async.py`: ```python """Tutorial, step 4: async work without freezing the window. python examples/tutorial/step4_async.py """ import asyncio import random import pyzed as pz from pyzed import ui async def fetch_quote(): await asyncio.sleep(1.2) # pretend this is a network request (aiohttp, httpx...) return random.choice(["Simple is better than complex.", "Readability counts.", "Now is better than never."]) class Quotes(pz.App): title = "Quotes" size = (480, 260) def __init__(self): self.quote = "Press the button." self.loading = False def load(self): self.loading = True # Return a Command.perform: fetch_quote runs on Pyzed's asyncio loop and its # result is passed to self.loaded on the UI thread. return pz.Command.perform(fetch_quote, self.loaded) def loaded(self, quote): self.quote, self.loading = quote, False def subscriptions(self): # Redraw ~30 times per second while loading so the spinner turns. return [pz.every(1 / 30, lambda: None)] if self.loading else [] def view(self): return pz.container( pz.column([ ui.label(self.quote, size="lg"), pz.row([ui.button("New quote", self.load, disabled=self.loading, variant="solid"), ui.spinner() if self.loading else None], spacing=10, align="center"), ], spacing=16), padding=24, ) if __name__ == "__main__": pz.run(Quotes) ``` #### 5. Putting it all together A notes app: a sidebar list, an editor with a Markdown preview in a resizable `ui.split`, a shortcut (`on_key` receives combos such as `"ctrl-n"`) and a second window (`pz.Window`) opened with `return About().open()` that shares the app's state. More in [Multiple windows](https://albakore.github.io/pyzed/guides/windows/). (Image: Step 5) `examples/tutorial/step5_notes.py`: ```python """Tutorial, step 5: a small notes app that puts it all together. python examples/tutorial/step5_notes.py A list of notes, a code editor with a live Markdown preview in a resizable split, a command (ctrl-n) and a separate "About" window. """ import pyzed as pz from pyzed import ui class About(pz.Window): title = "About Notes" size = (320, 180) resizable = False def view(self): return pz.container(pz.column([ui.heading("Notes", level=3), ui.muted("Built with Pyzed."), ui.button("Close", self.close)], spacing=10), padding=20, width="fill", height="fill", bg=self.resolved_theme().background) class Notes(pz.App): title = "Notes" size = (1000, 640) def __init__(self): self.notes = {"Welcome": "# Welcome\n\nWrite **Markdown** on the left.", "Ideas": "- One\n- Two"} self.current = "Welcome" self.ratio = 0.5 def new_note(self): name = f"Note {len(self.notes) + 1}" self.notes[name] = f"# {name}\n" self.current = name def on_key(self, combo): if combo == "ctrl-n": self.new_note() def view(self): t = self.resolved_theme() sidebar = pz.container( pz.column([ pz.row([ui.label("Notes", weight="semibold"), pz.hspace(), ui.icon_button("plus", self.new_note, tooltip="New note (ctrl-n)"), ui.icon_button("info", lambda: About().open(), tooltip="About")], align="center"), *[ui.list_item(name, lambda n=name: setattr(self, "current", n), icon="file-text", selected=name == self.current) for name in self.notes], ], spacing=2), padding=8, width=220, height="fill", bg=t.panel, ) # The editor's id is the note name, so each note keeps its own cursor and undo history. editor = ui.editor(self.current, self.notes[self.current], language="md", wrap=True, on_change=lambda text: self.notes.__setitem__(self.current, text)) preview = pz.container(pz.scrollable(pz.container(ui.markdown(self.notes[self.current]), padding=20), height="fill"), width="fill", height="fill", bg=t.background) return pz.row([sidebar, ui.split(editor, preview, ratio=self.ratio, on_resize=lambda r: setattr(self, "ratio", r))]) if __name__ == "__main__": pz.run(Notes) ``` #### Events in this tutorial | Step | Widget | Event | Handler gets | | --- | --- | --- | --- | | 2 | `ui.input` | `on_input` | The new text, on every keystroke. | | 2 | `ui.input`, `ui.button` | `on_submit`, `on_press` | Nothing. Here they are the *message* `"greet"`, which goes to `update`. | | 3 | `ui.number_field`, `ui.switch`, `ui.select` | `on_change`, `on_toggle`, `on_select` | The number, `True`/`False`, the option. | | 4 | `subscriptions()` | a timer | `pz.every(seconds, message)` sends the message to `update`. | | 5 | `on_key(combo)` | every key | The combo, such as `"ctrl-n"`. | | 5 | `ui.editor`, `ui.split` | `on_change`, `on_resize` | The text; the share of the first pane. | Step 2 handles its events in two styles at once: the input calls a lambda that changes `self` (imperative) and the button sends the message `"greet"` to `update` (declarative). The same greeter with only messages: ```python from dataclasses import dataclass @dataclass class Typed: text: str class Greeter(pz.App): def __init__(self): self.name, self.greetings = "", 0 def update(self, message): match message: case Typed(text): self.name = text case "greet" if self.name: self.greetings += 1 def view(self): return pz.column([ ui.input(self.name, Typed, placeholder="Your name", on_submit="greet"), ui.button("Greet", "greet", variant="solid"), ], spacing=12, padding=24) ``` See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/) for the third way. #### Next steps - [To-do list](https://albakore.github.io/pyzed/tutorials/basic/todo-app/) and the [mini code editor](https://albakore.github.io/pyzed/tutorials/advanced/code-editor/), in more detail. - [Data binding in the designer](https://albakore.github.io/pyzed/tutorials/intermediate/data-binding/), if you prefer to draw your forms. - [Examples](https://albakore.github.io/pyzed/examples/): recreations of the Iced demos (game of life, clock, stopwatch, multi-window, images, QR codes, styling, pane grid…). ### A custom component > Draw a reusable star rating in the Component view of Pyzed Design: a public property, bindings, event handlers, a custom event, and using it in a form and in code. Source: https://albakore.github.io/pyzed/tutorials/intermediate/custom-component/ When the same group of controls shows up again and again (a star rating, a user card, a search bar), turn it into a **component**: draw it once, give it properties and events, and place it anywhere like a built-in control. This is the idea of Godot's scenes and Unreal's widget blueprints, and it is how large apps stay tidy. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A five-star `Rating` you can drop on any form | 35 minutes | Intermediate · Python | [Data binding](https://albakore.github.io/pyzed/tutorials/intermediate/data-binding/) | #### What you'll learn - Create a component in the **Component view** (a `.pzcomp` and its class in a `.py`). - Add a **public property** (`value`) and a **private state** (`clicks`). - **Bind** the controls' properties to the component's (`"solid" if value >= 3 else "ghost"`). - Write **methods** and raise a **custom event** with `self.emit("change", ...)`. - Try it in the **preview window**, place it on a form and use it from Python code. #### Steps 1. **Create the component.** Ctrl+N → **Component**, name it `rating` and press **Create component**. The designer creates `rating.pzcomp` (the drawing) and `rating.py` (the class `Rating`) and opens them in the **Component view**: an accent frame instead of window buttons, and a **Component** badge. 2. **Draw it.** With nothing selected, set the component's **Width** to 232 and **Height** to 28 (Layout section). Place five **Buttons** in a row named `star1` … `star5`, each with icon `star`, no text and variant `ghost`, and a **Label** named `caption` at the end. 3. **Add the properties.** In the **Properties** section of the Component panel press **Public** and create `value`, type `int`, default `3`; then `readonly`, type `bool`. Press **Private** for `clicks`, an `int` that only the component sees. The class in `rating.py` gets them as annotated attributes: ```python value: Annotated[int, pz.Range(0, 5)] = 3 readonly: bool = False clicks: int = pz.state(0, doc="Times the user picked a rating") ``` 4. **Bind the stars.** Select `star3`, open **Bindings**, pick **variant** and choose **Expression…**: ```python "solid" if value >= 3 else "ghost" ``` Do the same for the other stars (with their own number), and bind `caption.text` to `f"{value} / 5"`. Change `value` in the panel: the stars light up on the canvas. 5. **Add the custom event.** In **Custom events** press **Event**, name it `change` and describe it: *A star was picked (event.value is the rating)*. 6. **Write the logic.** Add a method `pick` (type the name and press **+ Method**), then double-click each star to write its click handler. In a component, handlers are methods and controls are `self.`: ```python def star1_click(self, event): return self.pick(1) def pick(self, stars): """Lights stars stars and tells the parent.""" if self.readonly: return None self.value = stars self.clicks += 1 return self.emit("change", stars) ``` 7. **Try it alone.** Press F5: the component opens in a **preview window** that lists every custom event it raises. Click the stars and watch `change` arrive. 8. **Use it in a form.** Save, open (or create) a form: `Rating` is now in the **toolbox**, in its category. Drop one, add a **Label** named `status`, select the rating and, in **Events**, double-click `change`: ```python def rating1_change(form, event): form.status.text = f"You gave it {event.value} stars" # event.value is what emit() sent ``` Double-click a placed component to open it in its own tab; edits show up in every form that uses it. **The same class works in code:** A designed component **is** a `pz.Component`. Import it and use it in any app: `Rating(value=self.stars, on_change=lambda e: setattr(self, "stars", e.value))`. You can also write components entirely by hand: see [Components](https://albakore.github.io/pyzed/guides/components/). #### Full code The finished components (including a `ProfileCard` that contains a `Rating`, and a `TeamCard` that inherits from it) are in [`examples/designer/components`](https://github.com/albakore/pyzed/tree/main/examples/designer/components). `examples/designer/components/rating.py`: ```python """Rating: a component designed in Pyzed Design. The layout is in rating.pzcomp; properties, events and logic are below. Use it like any component: ``Rating(...)`` in a view, or from the designer's toolbox. """ from typing import Annotated import pyzed as pz from pyzed.designer import SceneComponent class Rating(SceneComponent, scene="rating.pzcomp"): """Five stars the user can click.""" value: Annotated[int, pz.Range(0, 5)] = 3 # How many stars are lit readonly: bool = False clicks: int = pz.state(0, doc="Times the user picked a rating") events = {"change": "A star was picked (event.value is the rating)"} def star1_click(self, event): return self.pick(1) def star2_click(self, event): return self.pick(2) def star3_click(self, event): return self.pick(3) def star4_click(self, event): return self.pick(4) def star5_click(self, event): return self.pick(5) def pick(self, stars): """Lights ``stars`` stars and tells the parent.""" if self.readonly: return None self.value = stars self.clicks += 1 return self.emit("change", stars) ``` `examples/designer/components/reviews.py (a form that uses two cards)`: ```python """Event handlers for the Reviews form. Each function receives the running ``form`` (controls are attributes: ``form.button1.text``) and an ``event`` with ``event.sender``, ``event.name`` and ``event.value``. """ def ada_rated(form, event): """Runs when the card of ada is rated.""" event.sender.score = event.value form.status.text = f"{event.sender.person} now has {event.value} stars" def grace_rated(form, event): """Runs when the card of grace is rated.""" event.sender.score = event.value form.status.text = f"{event.sender.person} now has {event.value} stars" ``` Run the component's preview, or the form that uses it: #### Events in this tutorial `Rating` declares one custom event, `change`, in `events` and raises it with `self.emit("change", stars)`. The parent listens with `on_change=`, and its handler gets a `pz.Event` whose `value` is the rating. Like every component, `Rating` also raises `mount` and `unmount`, and takes the common events (`on_right_click`, `on_mouse_enter`…). ##### Try more events A second custom event, raised only if someone listens: ```python class Rating(pz.Component): value: int = 0 events = { "change": "A star was picked (event.value is the rating).", "clear": "The rating was cleared with a right click.", # new } def render(self): stars = pz.row([...]) # as in the tutorial if self.handles("clear"): # only when the parent passed on_clear stars = stars.on(right_click=lambda: self.emit("clear")) return stars # The parent, in its view: Rating(value=self.stars, on_change=lambda e: setattr(self, "stars", e.value), on_clear=lambda: setattr(self, "stars", 0), # no argument: called with nothing on_mount=lambda: print("the rating is on screen")) ``` More in [creating your own events](https://albakore.github.io/pyzed/guides/classes/#creating-your-own-events). #### Next - [Component designer](https://albakore.github.io/pyzed/editor/component-designer/): every section of the Component panel, property types, nesting, responsive components and inheritance. - [Background tasks](https://albakore.github.io/pyzed/tutorials/intermediate/background-tasks/). ### Package your app > From project to download: create a Pyzed project with uv, build a standalone executable with pyzed build on Windows, macOS and Linux, add an icon and data files, and build every platform in CI. Source: https://albakore.github.io/pyzed/tutorials/advanced/packaging/ Your app works on your machine. Now you want to send it to someone who does not have Python, does not know what `pip` is, and should be able to **double-click** it. This tutorial takes an app from a project folder to a file you can share, and then to an automatic build for all three systems. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A double-clickable app for Windows, macOS and Linux | 30 minutes (plus build time) | Advanced · command line | [uv](https://docs.astral.sh/uv/getting-started/installation/) installed, a GitHub account for the last part | #### What you'll learn - Create (or convert to) a **Pyzed project**: the layout `pyzed build` expects. - Build a **standalone executable** for the system you are on. - Add an **icon**, **data files** and a **name**, and read bundled files from your code. - Build **all platforms** at once with GitHub Actions, and why signing matters. #### Steps 1. **Start from a project.** `pyzed build` packages a [uv](https://docs.astral.sh/uv/) project whose main form or script is set in `pyproject.toml`. Create one (or open **New project** in Pyzed Design's welcome window): **uv:** ```bash title="Windows, macOS and Linux" uvx --from pyzed-ui pyzed new my-app cd my-app uv run pyzed run # try it ``` **pip:** ```bash title="macOS and Linux" python -m pip install pyzed-ui pyzed new my-app # uses uv under the hood: install it first cd my-app uv run pyzed run ``` ```powershell title="Windows (PowerShell)" python -m pip install pyzed-ui pyzed new my-app cd my-app uv run pyzed run ``` **python:** ```bash title="With Pyzed already installed" python -m pyzed new my-app ``` Already have a uv project? Run `pyzed init` inside it. The UI lives in `ui/` (forms and their handlers), the backend in `src/my_app/`. 2. **Build for this computer.** From the project folder: **uv:** ```bash title="Windows, macOS and Linux" uv run pyzed build ``` **pip:** ```bash title="macOS and Linux" source .venv/bin/activate pyzed build ``` ```powershell title="Windows (PowerShell)" .venv\Scripts\Activate.ps1 pyzed build ``` **python:** ```bash title="Inside the project's environment" python -m pyzed build ``` The first build takes a minute or two. The result goes to `dist/`: | Built on | You get | Share it as | | --- | --- | --- | | Windows | `dist\my-app\my-app.exe` | A zip of the folder, or an installer (Inno Setup, WiX) | | macOS | `dist/my-app.app` | A zip or a `.dmg`, signed and notarized | | Linux | `dist/my-app/my-app` | A tarball, AppImage, `.deb` or Flatpak | 3. **Try it like a user would.** Copy the folder somewhere else (or to another machine without Python) and open the executable. If it closes at once, build with `--console` to see the error. 4. **Name, icon and data files.** Add a `[tool.pyzed.build]` table to `pyproject.toml`: ```toml title="pyproject.toml" [tool.pyzed.build] name = "Notes" # the executable's name icon = "assets/icon.ico" # .ico on Windows, .icns on macOS include = ["assets", "data/help.md"] # files your app reads at run time ``` Inside the bundle, find those files from the bundle's root: ```python import sys from pathlib import Path ROOT = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parents[1])) help_text = (ROOT / "data" / "help.md").read_text() ``` 5. **One file or a folder?** `pyzed build --onefile` makes a single file, handy to send, but slower to start (it unpacks itself every time) and more often flagged by antivirus software. A folder plus an installer is the safer choice for a real release. 6. **Every platform from CI.** A build only runs on the system it was made on, so you need Windows, macOS and Linux machines. GitHub has them: ```bash uv run pyzed build --workflow # writes .github/workflows/build.yml git add .github && git commit -m "Build on every platform" && git push git tag v1.0.0 && git push --tags # a v* tag starts the build ``` Each run keeps the three `dist/` folders as downloadable artifacts. **Signing:** Unsigned apps run, but Windows SmartScreen and macOS Gatekeeper warn users (macOS may refuse to open it). For a public release sign the Windows `.exe` with a code signing certificate, and sign and notarize the macOS `.app` with an Apple Developer ID. The commands are in [Build and distribute](https://albakore.github.io/pyzed/packaging/build/). **Desktop today, more targets later:** Pyzed builds desktop apps for Windows, macOS and Linux today. Mobile and web targets are on the roadmap; the [Distribute](https://albakore.github.io/pyzed/packaging/) section will say when they are ready. #### Next - [Build and distribute](https://albakore.github.io/pyzed/packaging/build/): what gets bundled, every option, troubleshooting. - [Build an app with the AI agent](https://albakore.github.io/pyzed/tutorials/advanced/ai-agent/). ### Background tasks > Tutorial: run downloads with progress, blocking work and notifications in a Pyzed desktop app without freezing the Python user interface. Source: https://albakore.github.io/pyzed/tutorials/intermediate/background-tasks/ A UI must never block waiting on the network or the disk. Pyzed offers two tools: - **`Command.perform(fn, then)`** runs a blocking function on another thread and hands you the result. - **`pz.post(message)`** sends messages to the app from any thread (progress, events). | Goal | Time | Level | You need | | --- | --- | --- | --- | | Downloads with progress bars and notifications that never freeze the window | 30 minutes | Intermediate · Python | [Dialogs and notifications](https://albakore.github.io/pyzed/tutorials/intermediate/dialogs-feedback/) | #### What you'll learn - Why slow work must leave the UI thread, and the two ways to move it out. - Get one result back with `Command.perform`. - Report progress from a thread with `pz.post` and handle it in `update`. - Show notifications that dismiss themselves. **Tip:** If you prefer `asyncio`, `update` and callbacks can also be `async def` functions or `async` generators, and `pz.listen` turns a stream into a subscription. See the [Async and subscriptions](https://albakore.github.io/pyzed/guides/async/) guide. (Image: Downloads with progress and notifications) 1. **One result: `Command.perform`.** For "do this and tell me when it's done": ```python def check_updates(self): self.checking = True return Command.perform(self.fetch_latest_version, self.got_version) @staticmethod def fetch_latest_version(): time.sleep(1.5) # e.g. an HTTP request return "0.2.0" def got_version(self, version): # runs on the UI thread self.checking, self.latest = False, version ``` While `self.checking` is `True`, the button is drawn disabled (`on_press=None`). 2. **Many results: `pz.post`.** A thread of your own reports progress with messages that reach `update`: ```python def start(self, name): self.progress[name] = 0.0 def worker(): done = 0.0 while done < 1.0: time.sleep(0.05) done = min(1.0, done + 0.05) pz.post(("progress", name, done)) pz.post(("finished", name)) threading.Thread(target=worker, daemon=True).start() def update(self, message): kind, name, *rest = message if kind == "progress": self.progress[name] = rest[0] elif kind == "finished": self.notify(f"{name} downloaded", kind="success") ``` 3. **Self-dismissing notifications.** A `threading.Timer` posts the dismiss message: ```python def notify(self, title, message=None, kind="info"): toast_id = f"{time.monotonic()}" self.toasts.append((toast_id, title, message, kind)) threading.Timer(4, pz.post, args=(("dismiss", toast_id),)).start() # in view(): return ui.toasts(body, [ui.toast(t, m, kind=k, on_dismiss=("dismiss", i)) for i, t, m, k in self.toasts]) ``` **Note:** State is only modified on the UI thread (in `update` and in callbacks). Worker threads don't touch `self`: they send messages with `pz.post`. #### Full code `examples/downloads.py`: ```py """Background work without freezing the UI: Command.perform, pz.post from threads, progress bars and toasts. python examples/downloads.py The "downloads" are simulated with sleep so the example works offline. Walkthrough: https://albakore.github.io/pyzed/tutorials/intermediate/background-tasks/ """ import random import threading import time import pyzed as pz from pyzed import Command, ui FILES = ["iced-0.14.tar.gz", "zed-themes.json", "ibm-plex.zip", "lucide-icons.ttf"] class Downloads(pz.App): title = "Descargas" size = (560, 460) def __init__(self): self.progress = {} # name -> 0..1 self.toasts = [] # (id, title, message, kind) self.checking = False self.latest = None # -- pz.post: a worker thread reports progress through update() ----------------- def start(self, name): self.progress[name] = 0.0 def worker(): done = 0.0 while done < 1.0: time.sleep(random.uniform(0.03, 0.12)) done = min(1.0, done + random.uniform(0.02, 0.08)) pz.post(("progress", name, done)) pz.post(("finished", name)) threading.Thread(target=worker, daemon=True).start() def update(self, message): kind, name, *rest = message if kind == "progress": self.progress[name] = rest[0] elif kind == "finished": self.notify(f"{name} descargado", kind="success") elif kind == "dismiss": self.toasts = [t for t in self.toasts if t[0] != name] def notify(self, title, message=None, kind="info"): toast_id = f"{time.monotonic()}" self.toasts.append((toast_id, title, message, kind)) threading.Timer(4, pz.post, args=(("dismiss", toast_id),)).start() # -- Command.perform: run a blocking function and get its result back -------------- def check_updates(self): self.checking = True return Command.perform(self.fetch_latest_version, self.got_version) @staticmethod def fetch_latest_version(): time.sleep(1.5) # e.g. an HTTP request return "0.2.0" def got_version(self, version): self.checking, self.latest = False, version self.notify("Hay una versión nueva", f"Pyzed {version} está disponible.") # -- view ---------------------------------------------------------------------------- def row(self, name): value = self.progress.get(name) if value is None: action = ui.button("Descargar", lambda: self.start(name), size="sm", icon="download") elif value < 1: action = ui.muted(f"{value:.0%}", size="sm") else: action = ui.badge("listo", tint="success", icon="check") return pz.column([ pz.row([pz.icon("file-archive", size=16), ui.label(name), pz.hspace(), action], spacing=8, align="center"), ui.progress(value or 0, tint="success" if value == 1 else "accent") if value is not None else None, ], spacing=6) def view(self): body = pz.container( pz.column([ pz.row([ui.heading("Descargas", level=2), pz.hspace(), ui.button("Buscar actualizaciones", None if self.checking else self.check_updates, icon="loader" if self.checking else "refresh-cw", variant="outline")], align="center"), ui.card(pz.column([self.row(n) for n in FILES], spacing=14)), ui.muted(f"Última versión: {self.latest}" if self.latest else "Pulsa un botón: la interfaz sigue respondiendo.", size="sm"), ], spacing=16), padding=24, width="fill", height="fill", bg=self.resolved_theme().editor, ) items = [ui.toast(title, msg, kind=kind, on_dismiss=("dismiss", tid)) for tid, title, msg, kind in self.toasts] return ui.toasts(body, items) if __name__ == "__main__": pz.run(Downloads) ``` #### Events in this tutorial This app is **declarative**: the events of the threads are messages. `pz.post(("progress", name, done))` from a worker thread and `on_dismiss=("dismiss", tid)` of each toast both reach `update(message)`, which unpacks the tuple. | Source | Event | Message or handler | | --- | --- | --- | | **Download** button | `on_press` | `lambda: self.start(name)` | | The worker thread | progress, finished | `pz.post(("progress", name, done))`, `pz.post(("finished", name))` | | **Check for updates** | `on_press` | `self.check_updates`, which returns `Command.perform(work, done)` | | `Command.perform` | the work finished | `self.got_version(result)` | | A toast | `on_dismiss` | the message `("dismiss", tid)` | ##### Try more events ```python def row(self, name): ... return pz.column([...]).on(double_click=lambda: self.start(name)) # double click: download again def subscriptions(self): # an event every second while something downloads busy = any(v < 1 for v in self.progress.values()) return [pz.every(1, ("tick", "", None))] if busy else [] ``` `update` gets `("tick", "", None)` every second while a download runs, and nothing once they finish: a subscription you stop returning stops. #### Next - [A shader effect](https://albakore.github.io/pyzed/tutorials/intermediate/shader-effect/) from the Shader view. - [Connecting a backend](https://albakore.github.io/pyzed/tutorials/advanced/backend/): a database behind a repository class. - [Async and subscriptions](https://albakore.github.io/pyzed/guides/async/): coroutines, async generators and streams. ### Build an app with the AI agent > Use the AI agent of Pyzed Design to build a whole desktop app from a description: connect a model, write a good request, follow its plan, review its changes and keep improving the result. Source: https://albakore.github.io/pyzed/tutorials/advanced/ai-agent/ Pyzed Design has an **AI agent** built in. Describe an app in plain words and it plans the work, creates the project, draws the forms, writes the Python, looks at the result, runs it and fixes what breaks, using the same editor you use. You stay in charge: every change can be undone or rejected in one click. | Goal | Time | Level | You need | | --- | --- | --- | --- | | An expense tracker built from a description, then improved by hand | 30 minutes | Advanced · no code needed | Pyzed Design and an AI model (Claude, or any provider in the list) | #### What you'll learn - Open the **Agent** panel and connect a model. - Write a request that gets a good result the first time. - Follow the agent's **plan**, and **keep** or **reject** its changes. - Point at things with **@** (a control, the form, a file) and use **/** commands. - Keep improving the app by hand, with the agent at your side. **Check what it builds:** An agent is a fast helper, not a guarantee. Read the code it writes, run the app and test the important paths yourself before you ship it, especially anything that touches files, money or personal data. #### Steps 1. **Open the panel.** In Pyzed Design press Ctrl+Alt+A (or **View → Agent panel**). The first time it asks you to **connect a model**: click a provider to sign in with the browser, or **All providers…** for the rest. If you installed Pyzed with pip, add the agent's extras first: **uv:** ```bash title="Windows, macOS and Linux" uv tool install "pyzed-ui[designer,agents]" ``` **pip:** ```bash title="Windows, macOS and Linux (inside your environment)" python -m pip install "pyzed-ui[designer,agents]" ``` **python:** ```bash title="The standalone Pyzed Design app" # nothing to install: the agent is included ``` 2. **Ask for the app.** Type `/app` and describe it. Say **what** it is for, **what** is on screen and **what** should happen; leave the "how" to the agent: > Create an app to track my expenses. A form with date, amount, a category drop-down (Food, Transport, Home, > Fun) and a note. Save adds the expense to a table below and to a JSON file next to the app. Show the total > for the month at the top and validate that the amount is a positive number. 3. **Follow the plan.** The agent writes a **Plan** card above the message box and ticks the steps off: create the project, add dependencies, draw the forms, wire the events, write the handlers, take a screenshot of each form, run checks and finally run the app. Each tool it uses is a line in the thread; click one to see what it did. 4. **Review.** When it finishes, run the app with F5 and try it. The **Agent changes** bar offers **Reject** (put every file it touched back as it was) or **Keep**. Ctrl+Z still undoes one change at a time. 5. **Improve it, pointing at things.** Select a control and ask about *this*, or type **@** to attach the form, its code, a control or a file: > @amount should accept a comma as the decimal separator, and the table should show the newest expense first. 6. **Polish with commands.** `/tidy` aligns the controls and evens out the margins, `/look` takes a screenshot of the form and improves its design, `/fix` checks the code and fixes what it finds, `/explain` walks you through the form and its handlers. 7. **Take over.** It is your project: open the files, change the layout by hand, write code. The agent reads what you changed in the next message, so you can switch between doing it yourself and asking. **Requests that work:** Name the screens and the fields, the rules ("amount must be positive"), where data is saved, and one example of the result. Ask for one change at a time when you refine. More advice in [Prompting](https://albakore.github.io/pyzed/ai/prompting/). **Profiles:** **Write** (the default) edits forms, code and files, and asks before it creates files or runs the app unless you turn off **Ask before writing files**; **Ask** only reads and answers; **Minimal** is a plain conversation with no tools. Pick one in the profile menu at the bottom of the message editor. #### Next - [AI agents](https://albakore.github.io/pyzed/editor/ai-agents/) in the designer manual: providers, MCP servers, external agents, images. - The [AI](https://albakore.github.io/pyzed/ai/) section: the MCP server and `llms.txt` for agents outside the editor (Claude Code, Cursor...). - [Package your app](https://albakore.github.io/pyzed/tutorials/advanced/packaging/) once it is ready. ### To-do list > Build your first complete Python desktop app with Pyzed: a to-do list with state, forms, lists, filters and keyboard shortcuts. Source: https://albakore.github.io/pyzed/tutorials/basic/todo-app/ In this tutorial we build a to-do list: the classic first app, and a good one, because it has everything a real app has: data, a form to add to it, a list to show it and ways to change it. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A to-do list with filters and shortcuts | 30 minutes | Basic · Python | [5-step tutorial](https://albakore.github.io/pyzed/tutorials/basic/step-by-step/) (steps 1 and 2) | #### What you'll learn - Keep the app's data in plain Python objects (a dataclass). - Read a text field as the user types, and submit it with Enter. - Draw a list of rows with a checkbox and a delete button each. - Filter what is shown, and add keyboard shortcuts. (Image: The finished app) 1. **The model.** A task is a dataclass. The app state is the list of tasks, the text being typed and the active filter: ```python @dataclass class Task: title: str done: bool = False class Todo(pz.App): title = "Tasks" size = (520, 560) def __init__(self): self.tasks = [Task("Build my first app")] self.draft = "" self.filter = "All" ``` 2. **Adding tasks.** The field is *controlled*: it shows `self.draft` and every keystroke updates it. Enter (`on_submit`) and the button call the same method: ```python def add(self): if self.draft.strip(): self.tasks.append(Task(self.draft.strip())) self.draft = "" return Command.focus("new-task") # give focus back to the field # in view(): pz.row([ ui.input(self.draft, lambda text: setattr(self, "draft", text), id="new-task", placeholder="What needs to be done?", on_submit=self.add, icon="plus"), ui.button("Add", self.add, variant="solid"), ], spacing=8) ``` 3. **Drawing the list.** Each task is a `ui.list_item` with a checkbox at the start and a delete button at the end. The lambdas capture `task` because `task_row` receives the task as an argument: ```python def task_row(self, task): return ui.list_item( ui.label(task.title, color="text_muted" if task.done else "text"), lambda: self.toggle(task), start=ui.checkbox(task.done, lambda _: self.toggle(task)), end=ui.icon_button("trash-2", lambda: self.remove(task), size="sm", tooltip="Delete"), height=34, inset=True, ) ``` 4. **Filtering.** `ui.segmented` changes `self.filter`, and `view()` only shows the visible tasks. Since `view()` is called again after every change, there is nothing to "refresh": ```python ui.segmented(["All", "Pending", "Done"], self.filter, lambda f: setattr(self, "filter", f)) ``` 5. **Shortcuts.** `on_key` receives each key as text: ```python def on_key(self, combo): if combo == "ctrl-l": return self.clear_done() if combo == "ctrl-t": self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark" ``` 6. **Initial focus.** `on_start` runs once when the window opens: ```python def on_start(self): return Command.focus("new-task") ``` **Tip:** Try switching the theme with Ctrl+T: every component restyles itself because it reads its colors from the active theme. #### Full code `examples/todo.py`: ```py """A to-do list: state, text input, list items, filters and keyboard shortcuts. python examples/todo.py Walkthrough: https://albakore.github.io/pyzed/tutorials/basic/todo-app/ """ from dataclasses import dataclass from itertools import count import pyzed as pz from pyzed import Command, ui _ids = count(1) @dataclass class Task: title: str done: bool = False id: int = 0 def __post_init__(self): self.id = self.id or next(_ids) class Todo(pz.App): title = "Tareas" size = (520, 560) min_size = (400, 360) def __init__(self): self.tasks = [Task("Leer la guía de Pyzed", done=True), Task("Crear mi primera app"), Task("Probar los temas")] self.draft = "" self.filter = "Todas" # -- actions ----------------------------------------------------------- def add(self): if self.draft.strip(): self.tasks.append(Task(self.draft.strip())) self.draft = "" return Command.focus("new-task") def toggle(self, task): task.done = not task.done def remove(self, task): self.tasks.remove(task) def clear_done(self): self.tasks = [t for t in self.tasks if not t.done] def on_start(self): return Command.focus("new-task") def on_key(self, combo): if combo == "ctrl-l": return self.clear_done() if combo == "ctrl-t": self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark" # -- view ---------------------------------------------------------------- def visible(self): if self.filter == "Pendientes": return [t for t in self.tasks if not t.done] if self.filter == "Hechas": return [t for t in self.tasks if t.done] return self.tasks def task_row(self, task): return ui.list_item( ui.label(task.title, color="text_muted" if task.done else "text"), lambda: self.toggle(task), start=ui.checkbox(task.done, lambda _: self.toggle(task)), end=ui.icon_button("trash-2", lambda: self.remove(task), size="sm", tooltip="Eliminar"), height=34, inset=True, ) def view(self): pending = sum(not t.done for t in self.tasks) tasks = self.visible() return pz.container( pz.column( [ pz.row([ui.heading("Tareas", level=2), pz.hspace(), ui.badge(f"{pending} pendientes")], align="center"), pz.row( [ ui.input(self.draft, lambda text: setattr(self, "draft", text), id="new-task", placeholder="¿Qué hay que hacer?", on_submit=self.add, icon="plus"), ui.button("Añadir", self.add, variant="solid"), ], spacing=8, ), pz.row( [ ui.segmented(["Todas", "Pendientes", "Hechas"], self.filter, lambda f: setattr(self, "filter", f)), pz.hspace(), ui.button("Limpiar hechas", self.clear_done, variant="ghost", size="sm", keys="ctrl-l"), ], align="center", ), ui.card( pz.scrollable(pz.column([self.task_row(t) for t in tasks], spacing=2), height="fill") if tasks else pz.center(ui.muted("No hay tareas aquí."), height=120), padding=6, ), ], spacing=14, max_width=640, ), padding=24, width="fill", height="fill", align_x="center", bg=self.resolved_theme().editor, ) if __name__ == "__main__": pz.run(Todo) ``` #### Events in this tutorial | Widget | Event | Handler | Gets | | --- | --- | --- | --- | | `ui.input` | `on_input` | `lambda text: setattr(self, "draft", text)` | The text. | | `ui.input`, **Add** | `on_submit`, `on_press` | `self.add` | Nothing. | | `ui.segmented` | `on_select` | `lambda f: setattr(self, "filter", f)` | The filter. | | `ui.list_item` | `on_press` | `lambda: self.toggle(task)` | Nothing. | | `ui.checkbox` | `on_toggle` | `lambda _: self.toggle(task)` | `True`/`False` (ignored: `_`). | | `ui.icon_button` | `on_press` | `lambda: self.remove(task)` | Nothing. | | The app | `on_start`, `on_key` | focus the field; `ctrl-l`, `ctrl-t` | —, the combo. | ##### Try more events Rename a task with a double click, and open a menu with a right click: ```python def __init__(self): ... self.renaming = None # the task being renamed self.menu_for = None # the task whose menu is open def task_row(self, task): return ui.list_item( ui.label(task.title), lambda: self.toggle(task), start=ui.checkbox(task.done, lambda _: self.toggle(task)), on_double_press=lambda: setattr(self, "renaming", task), # double click on_right_press=lambda: setattr(self, "menu_for", task), # right click height=34, inset=True, ) def view(self): page = ... # the page of the tutorial dialog = None if self.renaming: task = self.renaming dialog = ui.prompt("Rename", task.title, lambda t: setattr(task, "title", t), lambda: setattr(self, "renaming", None), lambda: setattr(self, "renaming", None)) return ui.dialog_layer(page, dialog) ``` #### Next - [Data binding and validation](https://albakore.github.io/pyzed/tutorials/intermediate/data-binding/) in the designer. - [Lists and tables](https://albakore.github.io/pyzed/tutorials/intermediate/lists-tables/): search, sort and select rows. - The [Todos showcase](https://albakore.github.io/pyzed/showcase/apps/#todos), which saves the list to disk. ### A shader effect from the Shader view > Make an animated GPU background in the Shader view of Pyzed Design from a template, tweak its nodes and variables, and put it behind a form whose buttons change it. Source: https://albakore.github.io/pyzed/tutorials/intermediate/shader-effect/ A **shader** is a tiny program the graphics card runs for every pixel, every frame: that is how games draw glowing rings, clouds and fire without slowing down. In Pyzed you do not have to write that program. You wire **nodes** in the Shader view (like Godot's visual shaders or Unreal's material editor), and the designer turns them into GPU code for you. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A pulsing neon ring behind a welcome card | 25 minutes | Intermediate · little code | [Your first window](https://albakore.github.io/pyzed/tutorials/basic/first-window/) | #### What you'll learn - Create a shader from a **template** and find your way around the **Graph**, **Preview** and **WGSL** tabs. - Change a node's input, wire a new node, and read the live **preview on every node**. - Use **variables**: values your Python code changes while the shader runs. - Draw the shader on a form with the **Shader** control, and change its variables from a button. #### Steps 1. **Create the shader.** Ctrl+N → **Shader**, name it `glow`, pick the **Glow ring** template and press **Create shader**. 2. **Look around.** The **Graph** tab shows the nodes, left to right: **Time** and the variables feed some math, a **Ring** shape gives the distance to a ring, **Glow** turns it into light and **Output** is the final color. Each node draws a small preview of what it makes, so you can follow the picture as it is built. 3. **Change a value.** Inputs that are not wired show their value right on the node. On the **Glow** node change **radius** from 0.12 to 0.3: the glow grows, and the preview of every node after it updates at once. (The ring's own radius is wired: it comes from the pulse math, which is what makes it breathe.) 4. **Wire a new node.** Near the end, a **Multiply** node mixes the `color` variable with the glow and feeds the **RGBA** node. From the **Shading** sidebar drag a **Hue shift** node (category *Color*) next to them. Wire the Multiply's result into its **rgb** input and its **rgb** output into RGBA's **rgb**, then wire **Time** into **shift**: the ring now cycles through every color. Press Ctrl+Z to go back. 5. **Meet the variables.** The **Outline** lists the shader's variables: `color` (the neon color) and `pulse` (pulses per second). They are **mutable**: your app can change them every frame for free. The **Preview** tab has a slider for each. 6. **Save** (Ctrl+S). The shader is the file `glow.pzshad`. Press F5 to see it fill a window of its own. 7. **Put it behind a form.** Create a form `welcome` (560 × 380). From **Display** in the toolbox drag a **Shader** control, make it cover the whole form and anchor it on **all four sides**; name it `backdrop` and set its **Shader file** to `glow.pzshad`. Then add a **Card** in the middle (no anchors, so it stays centered) with a heading, a label named `hint` and two buttons, `faster` and `color`. Give the card a translucent background in its **Style** section (`#0f172acc`) so the glow shows through. 8. **Drive it from Python.** The control's `variables` property is a dict of the shader's variables. Double-click **faster** and **color**: ```python def faster_click(form, event): variables = dict(form.backdrop.variables) variables["pulse"] = variables.get("pulse", 2.0) + 1.5 form.backdrop.variables = variables def color_click(form, event): variables = dict(form.backdrop.variables) variables["color"] = "#a78bfa" form.backdrop.variables = variables ``` 9. **Run** the form (F5) and press the buttons. **Relative paths:** A relative **Shader file** (`glow.pzshad`) is read from the folder you run the app from. In a project that is the project folder; for a form you may run from anywhere, set the path in the form's `load` handler from `__file__`, as the example below does. **No GPU?:** On machines without a graphics card (some virtual machines and CI servers), a shader shows its **fallback** color instead, set in the shader's properties. Everything else in the window works as usual. #### Full code `examples/tutorial/designer/welcome.py`: ```python """Event handlers for the welcome form (tutorial: A shader behind a form). pyzed run examples/tutorial/designer/welcome.pzform ``backdrop`` is a Shader control that draws ``glow.pzshad``, a shader made of nodes in the Shader view. Its ``variables`` property sets the shader's variables by name. """ from pathlib import Path COLORS = ["#22d3ee", "#a78bfa", "#f472b6", "#fbbf24"] def form_load(form, event): """Runs once when welcome opens: point the Shader control at the file next to this one. (A relative path in the Shader file property is read from the folder you run the app from; building it from ``__file__`` works from anywhere.) """ form.backdrop.file = str(Path(__file__).with_name("glow.pzshad")) def faster_click(form, event): """Runs when faster is clicked: more pulses per second, back to 1 after 8.""" variables = dict(form.backdrop.variables) pulse = variables.get("pulse", 2.0) variables["pulse"] = 1.0 if pulse >= 8 else pulse + 1.5 form.backdrop.variables = variables form.hint.text = f"{variables['pulse']:.1f} pulses per second" def color_click(form, event): """Runs when color is clicked: the next neon color.""" variables = dict(form.backdrop.variables) current = variables.get("color", COLORS[0]) variables["color"] = COLORS[(COLORS.index(current) + 1) % len(COLORS)] if current in COLORS else COLORS[1] form.backdrop.variables = variables ``` The same shader in pure Python, without the designer, is one widget: ```python pz.stack([ pz.shader("glow.pzshad", uniforms={"pulse": 4, "color": "#a78bfa"}, width="fill", height="fill"), card, ]) ``` #### Events in this tutorial | Control | Event | Handler | | --- | --- | --- | | The form | `load` | `form_load`: points the Shader control at `glow.pzshad`. | | `faster` (Button) | `click` | `faster_click`: changes the `pulse` variable. | | `color` (Button) | `click` | `color_click`: the next neon color. | ##### Try more events ```python def color_right_click(form, event): # every control has right_click: back to the first color variables = dict(form.backdrop.variables) variables["color"] = COLORS[0] form.backdrop.variables = variables def backdrop_mouse_enter(form, event): # the Shader control has the mouse events too form.hint.text = "Move the pointer away to calm it down" def backdrop_mouse_leave(form, event): form.hint.text = "" ``` #### Next - [A custom shader graph](https://albakore.github.io/pyzed/tutorials/advanced/shader-graph/): build a graph in Python, node by node. - The [Shading](https://albakore.github.io/pyzed/shading/) section: every node, the Shader view and WGSL shaders. - The [shader showcase](https://albakore.github.io/pyzed/showcase/shaders/): eleven `.pzshad` files to open and study. ## 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. | ## Animation ### Animation > How animation works in Pyzed: animate any widget like the web (presets, keyframes, transitions), move many widgets on one timeline like Godot's AnimationPlayer, and edit clips in Pyzed Design. Source: https://albakore.github.io/pyzed/animation/ Every Pyzed widget can move, fade, grow, turn, blur, glow or change color. This section explains each part of the animation system, with a moving example for each one. (Image: A dashboard coming in: the header drops in, the cards rise one after another, a badge pulses and a spinner turns) ```python pz.text("Welcome back").animate("fade_in_down") # plays when it appears pz.row(pz.stagger(cards, "fade_in_up", step=80)) # one after another badge.animate("pulse", repeat="infinite") # goes on ui.button("Recording", self.stop).animate("glow_pulse") # a looping glow pz.icon("loader-circle").animate("spin") # a spinner ``` :::tip[Start here] New to animations? [Start here: apply and play](https://albakore.github.io/pyzed/animation/start/) shows where `.animate()` goes, every way to start an animation (when a widget appears, on the pointer, from your code with `pz.Trigger`, from a timeline) and how the same works in Pyzed Design. ::: #### Three ways to animate Pick the one that fits what you want to do. They work together, so one app often uses all three. | You want to... | Use | Page | | --- | --- | --- | | Give one widget its own motion: appear, react to the pointer, draw attention, loop. | `widget.animate(...)` | [animate() and triggers](https://albakore.github.io/pyzed/animation/animate/) | | Move a widget smoothly whenever one of its values changes (open a panel, select a row). | `widget.transition(...)` and style states | [Transitions and states](https://albakore.github.io/pyzed/animation/transitions/) | | Play a scene where many widgets and your app's values change together, with play, pause and seek. | `pz.AnimationPlayer` and `pz.Clip` | [Timeline animations](https://albakore.github.io/pyzed/animation/timeline/) | And in [Pyzed Design](https://albakore.github.io/pyzed/editor/animations-and-effects/) you can do the same without code: an **Animation** section for each control and an **Animation** panel with a timeline for clips. #### How it runs Python describes an animation once, in `view()`. The Rust side plays it at the display's frame rate, so **a running animation never calls your code** and doesn't slow your app down. When something changes (a click, new data), `view()` runs again as usual; animations that didn't change keep going where they were. - **Presets** are ready-made animations with a name (`"fade_in_up"`, `"lift"`, `"spin"`). See the [preset gallery](https://albakore.github.io/pyzed/animation/presets/). - **Keyframes** describe your own animation step by step, like CSS `@keyframes`. See [Keyframes and easing](https://albakore.github.io/pyzed/animation/keyframes/). - **Properties** are what an animation can change: position, size, rotation, colors, blur, glow... See [Animatable properties](https://albakore.github.io/pyzed/animation/properties/). - **Effects** such as glass, bloom and glow are style properties that animations can change too. See [Effects](https://albakore.github.io/pyzed/animation/effects/). :::tip[Try them] Run [`examples/animations.py`](https://github.com/albakore/pyzed/blob/main/examples/animations.py) for the widget animations and [`examples/timeline.py`](https://github.com/albakore/pyzed/blob/main/examples/timeline.py) for a timeline. Both are short and commented. ::: #### In this section | Page | What it explains | | --- | --- | | [Start here: apply and play](https://albakore.github.io/pyzed/animation/start/) | Where `.animate()` goes, what starts an animation, how they are named and customized, in each code style and in the editor. | | [animate() and triggers](https://albakore.github.io/pyzed/animation/animate/) | What plays an animation (appearing, hover, press, focus, click, a flag, a `pz.Trigger`) and its timing options. | | [Animatable properties](https://albakore.github.io/pyzed/animation/properties/) | Every property an animation can change, and how values work. | | [Keyframes and easing](https://albakore.github.io/pyzed/animation/keyframes/) | Your own animations with `pz.Animation`, and the curves that shape the motion. | | [Transitions and states](https://albakore.github.io/pyzed/animation/transitions/) | `transition()`, hover and pressed styles, page changes with `presence`, lists with `stagger`, and window transitions. | | [Preset gallery](https://albakore.github.io/pyzed/animation/presets/) | All the named animations, playing. | | [Effects](https://albakore.github.io/pyzed/animation/effects/) | Glass, glow, bloom, blur, rotation and opacity. | | [Timeline animations](https://albakore.github.io/pyzed/animation/timeline/) | Clips, tracks, keys, loops and method calls on one timeline. | | [The animation player](https://albakore.github.io/pyzed/animation/player/) | Playing, pausing, seeking and queueing clips, and saving them in `.pzanim` files. | | [Reduced motion, tests and limits](https://albakore.github.io/pyzed/animation/motion-and-tests/) | Respecting "reduce motion", testing animated views, and what the system can't do. | ### Start here: apply and play > The two things every Pyzed animation needs: which widget it moves (animate() on the widget) and what starts it (a trigger, your code, a timeline). Declarative and imperative examples, in classes, components and plain functions, and the same in Pyzed Design. Source: https://albakore.github.io/pyzed/animation/start/ Every animation answers two questions: 1. **Which widget moves?** You attach the animation to that widget with `.animate(...)`. 2. **When does it play?** A **trigger** decides: the widget appearing, the pointer, a click, a flag in your state, or a line of your code. This page explains both, with the same example written in the different styles Pyzed supports. The other pages of this section go deeper into each part. (Image: A login card: the card rises in when it appears, the button lifts under the pointer, a wrong password makes the field shake and a correct one makes a check pop in) #### 1. Apply it: `.animate()` on the widget An animation lives **on the widget it moves**. Call `.animate()` where you build the widget in `view()`, and use the widget it returns: ```python def view(self): return pz.column([ pz.text("Welcome back").animate("fade_in_down"), # this text moves ui.button("Sign in", self.sign_in).animate("lift", on="hover"), ]) ``` `.animate()` works on **every** widget: text, buttons, inputs, containers, rows, icons, images and your own components (which also take `animate="fade_in"` as an argument). On a container, everything inside moves with it. :::caution[Use what `.animate()` returns] Widgets don't change in place: `.animate()` returns a new widget with the animation. Writing `title.animate("fade_in")` on its own line does nothing if you then return the old `title`. ```python title = pz.text("Hello") title.animate("fade_in") # ✗ the result is thrown away title = title.animate("fade_in") # ✓ keep it (or chain it where you build the widget) ``` ::: You can call `.animate()` several times: each call adds one animation with its own trigger. ```python ui.button("Buy", self.buy).animate("fade_in_up").animate("lift", on="hover").animate("shrink", on="press") ``` #### 2. Start it: the trigger The `on=` option says what plays the animation. You choose one per `.animate()` call: | You want it to play... | Write | Plays | | --- | --- | --- | | when the widget appears | `.animate("fade_in")` (`on="mount"` is the default) | once, each time the widget shows up | | while the pointer is over it | `.animate("lift", on="hover")` | forward on enter, back on leave | | while it is pressed | `.animate("shrink", on="press")` | forward while the button is down | | while it has the focus | `.animate("glow", on="focus")` | forward while focused | | on every click on it | `.animate("tada", on="click")` | from the start, each click | | while a value in your state is true | `.animate("glow_pulse", active=self.recording)` | forward while `True`, back when `False` | | **when your code says so** | `.animate("shake", on=self.wrong)` + `self.wrong.fire()` | from the start, each `fire()` | | when a value in your state changes | `.animate("pulse", on="manual", key=self.count)` | from the start, each new `key` | The first five need nothing else: Pyzed watches the pointer and the keyboard for you. The last three are how **your code** starts an animation. ##### From your code: `pz.Trigger` A `pz.Trigger` is like a button your code presses. Keep one on the app, give it to `on=` and call `fire()` in any handler: ```python class Login(pz.App): def __init__(self): self.password = "" self.wrong = pz.Trigger() # 1. create it once def view(self): return pz.column([ ui.input(self.password, self.set_password, secure=True) .animate("shake", on=self.wrong), # 2. attach: nothing plays yet ui.button("Sign in", self.sign_in), ], spacing=12) def set_password(self, text): self.password = text def sign_in(self): if self.password != "secret": self.wrong.fire() # 3. play it now (again from the start if it was running) ``` Nothing plays when the widget appears: only `fire()` starts it. `fire` ignores its arguments, so it can be a callback itself: `ui.button("Shake it", self.wrong.fire)`. `on="manual"` with a `key` does the same without an object: the animation plays each time the key changes, and never on its own. It is handy with a counter you already have: ```python ui.badge(str(self.unread)).animate("pulse", on="manual", key=self.unread) # pulses on each new message ``` :::note[`key` with or without `on="manual"`] A plain `key` (`.animate("shake", key=self.errors)`) also replays when the key changes, but as a **mount** animation it plays once when the widget appears too. Add `on="manual"` (or use a `pz.Trigger`) when it should only play on a change. ::: ##### Stopping a loop A looping animation (`repeat="infinite"`, or a loop preset such as `spin`) runs while the widget has it. To stop it, stop asking for it in `view()`: ```python icon = pz.icon("loader-circle") if self.loading: icon = icon.animate("spin") ``` Or use `active=`, which also plays it back out smoothly: `.animate("glow_pulse", active=self.loading)`. #### 3. Name it: what you can pass to `.animate()` The first argument says **which** animation plays. It can be: | You pass | Example | Where it comes from | | --- | --- | --- | | A preset name | `"fade_in_up"`, `"lift"`, `"spin"` | The [preset gallery](https://albakore.github.io/pyzed/animation/presets/): more than 50 ready-made animations, in five groups (entrance, exit, attention, interaction, loop). | | A name you defined | `"wiggle"` | `pz.animation.define_animation("wiggle", ...)`, once, anywhere before `view()` runs. | | A `pz.Animation` | `pz.Animation([{"x": 0}, {"x": 8}])` | Your own keyframes, used in place. See [Keyframes and easing](https://albakore.github.io/pyzed/animation/keyframes/). | | Property values | `.animate(opacity=0, y=12)` | A quick animation from (or to) these values. See [Animatable properties](https://albakore.github.io/pyzed/animation/properties/). | | A timeline target | `self.player["card"]` | One part of a clip that moves many widgets. See [Timeline animations](https://albakore.github.io/pyzed/animation/timeline/). | Preset names are lowercase words joined by `_`, and they read like what they do: `fade_in_left`, `slide_out_down`, `rubber_band`. To list them from Python: ```python from pyzed import animation print(list(animation.presets())) # every name print(list(animation.presets("entrance"))) # one group print(animation.animation("pop_in").description, animation.animation("pop_in").duration) ``` #### 4. Customize it Every preset brings its own timing; the options of `.animate()` change it just for that widget: ```python card.animate("fade_in_up", duration=600, delay=200, easing="ease_out_back") badge.animate("pulse", repeat=3) arrow.animate("nudge_right", on="hover", duration=120) ``` When the change should apply **everywhere**, define your own animation once and use its name: ```python from pyzed.animation import define_animation define_animation("slow_rise", [{"opacity": 0, "y": 40}, {"opacity": 1, "y": 0}], duration=900, easing="ease_out_cubic") define_animation("fade_in_up", pz.animation.animation("fade_in_up").with_options(duration=500)) # your own default pz.text("Hello").animate("slow_rise") ``` The full list of options (`duration`, `delay`, `easing`, `repeat`, `direction`, `fill`, `on_start`, `on_end`...) is in [animate() and triggers](https://albakore.github.io/pyzed/animation/animate/#timing-options), and keyframes, easing curves and per-frame easing are in [Keyframes and easing](https://albakore.github.io/pyzed/animation/keyframes/). #### The same animation in different styles Pyzed lets you organize your code in several ways. Animations work the same in all of them, because they always end up on a widget returned by a view. ##### Declarative: the view follows your state You keep values in your state and `view()` decides which animations the widgets have. You never "start" anything: you change the state and the screen follows. This is the style to reach for first. ```python class Player(pz.App): def __init__(self): self.playing = False self.liked = 0 def view(self): return pz.row([ ui.icon_button("pause" if self.playing else "play", self.toggle) .animate("glow_pulse", active=self.playing), # glows while playing ui.icon_button("heart", self.like) .animate("heartbeat", on="manual", key=self.liked), # beats on every like pz.text(f"{self.liked} likes").transition(opacity=1 if self.liked else 0.4), ], spacing=8, align="center") def toggle(self): self.playing = not self.playing def like(self): self.liked += 1 ``` ##### Imperative: your code starts it You keep "play" handles and call them from handlers, like `button.play()` in other toolkits. In Pyzed these handles are `pz.Trigger` (one widget animation) and `pz.AnimationPlayer` (a whole clip): ```python class Checkout(pz.App): def __init__(self): self.error = pz.Trigger() self.done = pz.Trigger() self.player = pz.AnimationPlayer("checkout.pzanim") # clips saved from Pyzed Design or code def view(self): return pz.column([ ui.input(self.card, self.set_card).animate("shake", on=self.error), pz.icon("circle-check", size=32).animate("pop_in", on=self.done), pz.text("Thanks!").animate(self.player["thanks"]), ui.button("Pay", self.pay), ]) def pay(self): if not self.valid(): self.error.fire() return self.done.fire() self.player.play("celebrate") ``` ##### In a component A `pz.Component` keeps its own state, so its triggers live with it (create them in `setup()`): ```python class CopyButton(pz.Component): text: str = "" def setup(self): self.copied = pz.Trigger() def copy(self): self.copied.fire() return pz.Command.copy(self.text) def render(self): return pz.row([ ui.button("Copy", self.copy, icon="copy"), pz.text("Copied!").animate("fade_in_up", on=self.copied), ], spacing=8, align="center") ``` Every component also takes `animate=` when you place it: `CopyButton(text=link, animate="fade_in")`. ##### With plain functions Pieces of a view can be plain functions that receive values and return widgets. Pass them the state (and the triggers) they need: the animation goes on the widget they return. ```python def status_dot(online: bool) -> pz.Widget: dot = pz.container(width=10, height=10).style(background="success" if online else "text_muted", radius=5) return dot.animate("breathe", active=online) def notification(text: str, index: int) -> pz.Widget: return ui.callout("New", text).animate("fade_in_left", delay=index * 60) class Inbox(pz.App): def view(self): return pz.column([ pz.row([status_dot(self.online), pz.text("Inbox")], spacing=6, align="center"), *[notification(text, i) for i, text in enumerate(self.messages)], ], spacing=8) ``` #### In Pyzed Design The editor builds the same animations without code, and shows the line to call when code should start them. ##### One control: the Animation section Select a control and open **Properties → Animation**. **Add animation** adds a row; click it to choose **When** it plays and **which** animation (the presets above): | When (in the editor) | Same as in code | | --- | --- | | When it appears | `on="mount"` | | While hovered / pressed / focused | `on="hover"` / `"press"` / `"focus"` | | On click | `on="click"` | | **When played from code** | `on=pz.Trigger()` | A row set to **When played from code** waits for your handlers. The row shows the exact line, with a copy button: ```python def sign_in_click(form, event): if not check(form.password.text): form.password.play_animation() # plays its "When played from code" animations ``` `play_animation()` also takes any preset, `pz.Animation` or values, even for a control without rows, and `stop_animation()` stops what it started (a loop, for example): ```python form.badge.play_animation("pulse", repeat=3) form.card.play_animation(scale=1.05, duration=200) # to these values and back form.spinner.play_animation("spin") form.spinner.stop_animation() ``` ##### Many controls at once: clips The **Animation** panel (**View → Animation**) edits **clips**: timelines that move several controls together. A clip starts in one of two ways, and the panel says which one applies: - turn on **⚡ Play when the form opens** (autoplay), or - call it from a handler: `form.animations.play("intro")` (the panel shows this line with a copy button). See [Animations and effects in Pyzed Design](https://albakore.github.io/pyzed/editor/animations-and-effects/) for every field of the editor, and [The animation player](https://albakore.github.io/pyzed/animation/player/) for `play`, `pause`, `seek` and friends. #### Cheat sheet | I want to... | Library | Pyzed Design | | --- | --- | --- | | Animate a widget when it appears | `w.animate("fade_in_up")` | Animation → When it appears | | React to the pointer | `w.animate("lift", on="hover")` | Animation → While hovered | | Play on a click on it | `w.animate("tada", on="click")` | Animation → On click | | Play from a handler | `on=self.t` + `self.t.fire()` | When played from code + `form.x.play_animation()` | | Play while a flag is on | `w.animate("glow_pulse", active=flag)` | A loop set to When played from code + `play_animation()` / `stop_animation()` | | Change a value smoothly | `w.transition(opacity=...)` | Style states (hover, pressed) | | Move many widgets together | `pz.AnimationPlayer` + `player["target"]` | Animation panel (clips) | | Start a clip | `player.play("intro")` | ⚡ autoplay or `form.animations.play("intro")` | | Make my own animation | `pz.Animation([...])`, `define_animation` | A clip with tracks and keys | ### animate() and triggers > widget.animate() in Pyzed: play a preset or your own animation when a widget appears, on hover, press, focus or click, or while a flag is on, with duration, delay, repeat, direction, fill and easing. Source: https://albakore.github.io/pyzed/animation/animate/ `widget.animate(...)` gives a widget its own motion. It takes preset names, `pz.Animation` objects (or lists of them), [property values](https://albakore.github.io/pyzed/animation/properties/) and options: ```python pz.text("Saved").animate("fade_in_up") # plays when it appears card.animate("lift", on="hover") # forward on hover, back on leave icon.animate("pulse", repeat="infinite") # loops field.animate("shake", key=self.errors) # replays when key changes field.animate("shake", on=self.wrong) # plays on self.wrong.fire() button.animate(on="press", scale=0.96, duration=90) # your own values ``` :::tip[New to animations?] [Start here: apply and play](https://albakore.github.io/pyzed/animation/start/) explains in one page where `.animate()` goes and every way to start an animation, with examples in each code style. ::: `animate()` returns the widget, so you can chain it after `style()` or call it several times. Each call adds an animation that keeps its own trigger: ```python ui.button("Buy", self.buy).animate("fade_in_up").animate("lift", on="hover").animate("shrink", on="press") ``` #### Triggers The `on` option says **what plays the animation**: (Image: Six cards, one per trigger: mount plays as it appears, hover lifts under the pointer, press shrinks while pressed, focus lights the border, click plays tada and active starts a glow) | `on` | Plays | Typical use | | --- | --- | --- | | `"mount"` (default) | Once, when the widget appears on screen. | Entrances: a page, a toast, a list item. | | `"hover"` | Forward while the pointer is over the widget, back when it leaves. | Cards and buttons that react. | | `"press"` | Forward while the mouse button is down on it, back on release. | A button that sinks in. | | `"focus"` | Forward while it has the keyboard focus, back when it loses it. | Inputs that light up. | | `"click"` | From the start on every click. | A "liked" pop, a celebration. | | `"active"` | Forward while `active=True`, back when it becomes `False`. | A recording light, a selected state. | | `"manual"` | From the start each time `key` changes, never on its own. | A pulse when a counter goes up. | | a `pz.Trigger` | From the start each time your code calls `fire()`. | A shake on a wrong password, a "saved" check. | ```python ui.button("Record", self.toggle).animate("glow_pulse", active=self.recording) ``` :::tip[Hover, press and focus go back smoothly] These triggers don't jump back: if the pointer leaves halfway, the animation reverses from where it got to. That's why they suit short "interaction" presets such as `lift`, `grow` and `shrink`. ::: ##### Playing from your code with `pz.Trigger` A `pz.Trigger` starts an animation from a handler. Create it once (in `__init__`, or in a component's `setup()`), give it to `on=` and call `fire()` whenever the animation should play: ```python class Login(pz.App): def __init__(self): self.wrong = pz.Trigger() def view(self): return ui.input(self.password, self.set_password).animate("shake", on=self.wrong) def submit(self): if not self.check(): self.wrong.fire() # shakes now; again from the start if it was still shaking ``` The widget doesn't move when it appears, only on `fire()`. `trigger.count` says how many times it was fired, and `fire` ignores its arguments, so `ui.button("Again", self.wrong.fire)` works. ##### Replaying with `key` A mount animation plays once. Give it a `key` and it plays again **every time the key changes**: ```python password.animate("shake", key=self.failed_attempts) # shakes on each wrong password ui.button("Copy", self.copy).animate("pulse", key=self.copies) ``` As a mount animation it also plays when the widget first appears. With `on="manual"` it **only** plays when the key changes (this is what `pz.Trigger` does underneath): ```python ui.badge(str(self.unread)).animate("pulse", on="manual", key=self.unread) ``` #### Timing options These options override the timing that an animation (or a preset) brings: (Image: Six balls on six lanes, each with one option: duration, delay, repeat, alternate, reverse and fill none) | Option | Values | What it does | | --- | --- | --- | | `duration` | Milliseconds, or `"0.3s"` / `"300ms"`. | How long one run takes. | | `delay` | The same units. | How long to wait before starting. | | `easing` | A curve name, a Bézier `(x1, y1, x2, y2)` or `"steps(4)"`. | The shape of the motion: see [easing](https://albakore.github.io/pyzed/animation/keyframes/#easing). | | `repeat` | A number of times, or `"infinite"`. | How many runs in a row. | | `direction` | `"normal"`, `"reverse"`, `"alternate"`, `"alternate_reverse"`. | `reverse` plays backwards; `alternate` goes there and back on every other run. | | `fill` | `"both"` (default), `"forwards"`, `"backwards"`, `"none"`. | What shows before the delay and after the end, as in CSS. With `"none"` the widget goes back to its own look when the animation ends. | | `key` | Any value. | Replays a mount or manual animation when it changes. | | `active` | `True` or `False`. | Drives an `"active"` animation. | | `on_start`, `on_end` | A callable or a message. | Sent when the animation starts and when it ends. | ```python badge.animate("pulse", repeat=3, delay=400) arrow.animate(pz.Animation({0: {"x": 0}, 1: {"x": 6}}, duration=600), direction="alternate", repeat="infinite") toast.animate("fade_in_up", on_end=self.toast_shown) ``` #### Ready-made or your own - A **preset name** is the quickest: see the [preset gallery](https://albakore.github.io/pyzed/animation/presets/). - **Property values** describe a simple animation in place: `animate(opacity=0, y=12)` appears from below. See [Animatable properties](https://albakore.github.io/pyzed/animation/properties/). - A **`pz.Animation`** gives you keyframes, per-frame easing and a name you can reuse anywhere. See [Keyframes and easing](https://albakore.github.io/pyzed/animation/keyframes/). - A **timeline target** from an animation player (`self.player["card"]`) lets one clip move many widgets. See [Timeline animations](https://albakore.github.io/pyzed/animation/timeline/). :::note[Animations restart when a widget comes back] If a widget leaves the view and comes back (an `if` in `view()`, a new list item), its mount animation plays again. Use a stable `key`, or [`pz.presence`](https://albakore.github.io/pyzed/animation/transitions/#presence-and-stagger), to decide when it replays. ::: ### Animatable properties > Every property a Pyzed animation can change: opacity, position, scale, rotation, blur, colors, radius, border, glow, size, padding and font size, with their units and how property values animate. Source: https://albakore.github.io/pyzed/animation/properties/ An animation changes one or more **properties** of a widget. The same names work everywhere: in `animate()`, `transition()`, `pz.Animation` keyframes and timeline tracks. (Image: Twelve cards, each one moving one property back and forth: opacity, x, y, scale, rotate, blur, background, border color, radius, glow, width and font size) | Property | Values | Notes | | --- | --- | --- | | `opacity` | `0` (invisible) to `1`. | Fades the widget and everything inside it. | | `x`, `y` | Pixels, or `"50%"` of the widget's own size. | Moves it without moving its neighbours. | | `scale` | A number: `1` is its size, `0.5` half, `1.2` a fifth bigger. | Around its center. | | `rotate` | Degrees clockwise, or `"0.25turn"`, `"1.5rad"`, `"90deg"`. | Around its center. | | `blur` | Pixels. | Blurs the widget and everything inside it. | | `background`, `border_color`, `color`, `glow_color` | `"#rrggbb"`, `"#rrggbbaa"` or a theme token (`"accent"`, `"elevated"`...). | Colors blend smoothly. `color` changes the text inside. | | `radius`, `border_width`, `glow` | Pixels. | `glow` is the size of the halo around the box. | | `width`, `height` | Pixels. | **Change the layout**: the widgets around it move. | | `padding` | Pixels. | Inside a container or button (all four sides follow); elsewhere it adds space around. Changes the layout. | | `font_size` | Pixels. | Every text inside. Changes the layout. | `pz.animation.PROPERTIES` lists them in code. :::tip[Prefer x, y, scale and opacity for big views] `width`, `height`, `padding` and `font_size` lay the view out again on every frame while they move. That's fine for a sidebar or a title, but for a large page `x`, `y`, `scale` and `opacity` are cheaper and look the same. ::: #### Property values as an animation Without a preset, the values you pass **are** the animation. What they mean depends on the trigger: | Trigger | The widget... | | --- | --- | | `mount` | **starts** from those values and settles on its own look: `animate(opacity=0, y=12)` appears from below. | | `hover`, `press`, `focus`, `active` | **goes to** those values while the condition holds, and back after. | | `click` | goes to those values and back, on every click. | ```python pz.text("Hi").animate(opacity=0, y=12, duration=250) # appears from below card.animate(on="hover", scale=1.03, background="elevated") # grows and lightens on hover button.animate(on="press", scale=0.96, duration=90) # sinks in while pressed icon.animate(on="click", rotate=360, duration=500) # a full turn on each click ``` The default duration is 300 ms for `mount` and `click`, and 150 ms for `hover`, `press` and `focus`. #### Values that come from the widget A property you don't give at the start or the end of an animation goes **from or to the widget's own value**. So an animation can change the background of any button without knowing its color: ```python flash = pz.Animation({"50%": {"background": "#fbbf24"}}, duration=400) # to yellow and back ``` That's also why a `mount` animation can end on "the widget's own look": the last frame leaves the property out. #### More examples ```python icon.animate("spin") # a loading spinner card.animate(on="hover", rotate=-3, scale=1.02) # tilts on hover sidebar.transition(width=240 if self.open else 56, duration=200) # the content beside it reflows title.transition(font_size=28 if self.big else 16) status.animate(on="active", active=self.online, background="#14532d", border_color="#22c55e") ``` ### Keyframes and easing > Write your own Pyzed animations with pz.Animation keyframes (like CSS @keyframes), give each step its own easing, name and save them, and pick easing curves: ease, cubic, back, bounce, spring, Bézier and steps. Source: https://albakore.github.io/pyzed/animation/keyframes/ When no preset does what you want, describe the animation yourself with `pz.Animation`. It works like a CSS `@keyframes` rule with its timing. #### Keyframes A **keyframe** says what the widget looks like at one moment of the animation. Pyzed fills in the moments between keyframes. (Image: A heart beating and a card wiggling, each with a bar of its keyframes and a playhead going through them) ```python beat = pz.Animation({0: {"scale": 1}, "30%": {"scale": 1.25, "easing": "ease_out"}, "45%": {"scale": 1}, "60%": {"scale": 1.15}, 1: {"scale": 1}}, duration=1000, repeat="infinite") heart.animate(beat) wiggle = pz.Animation([{"x": 0}, {"x": -10}, {"x": 10}, {"x": -6}, {"x": 0}], duration=1000) card.animate(wiggle, key=self.errors) ``` Frames can be written two ways: - **A list:** the frames are spread evenly, from the start to the end. - **A dict:** the key is where the frame sits, from `0` (start) to `1` (end). It can also be `"50%"`, `"from"` or `"to"`. Each frame is a dict of [properties](https://albakore.github.io/pyzed/animation/properties/). A frame can also set its own `"easing"`: the curve **from that frame to the next one**. `pz.Animation` takes the same timing as `animate()`: `duration`, `delay`, `easing`, `repeat`, `direction`, `fill` and `on`. Options given to `animate()` override them. :::note[Missing values come from the widget] A property that is missing at the start or at the end goes from or to the widget's own value, so `{"50%": {"background": "#fbbf24"}}` flashes any widget to yellow and back. ::: #### Naming and saving animations `pz.define_animation(name, animation)` gives an animation a name, so `animate(name)` works anywhere in your app, like a preset: ```python pz.define_animation("wiggle", wiggle) ui.button("Retry", self.retry).animate("wiggle", key=self.failures) ``` `Animation.to_dict()` and `Animation.from_dict()` turn an animation into JSON-friendly data and back, to save it in a file or a setting. To save **many** animations that move several widgets together, use a [timeline clip and a `.pzanim` file](https://albakore.github.io/pyzed/animation/player/#libraries-and-pzanim-files). #### Easing **Easing** is the shape of the motion: whether it starts slowly, ends softly, overshoots or bounces. The same move with each curve: (Image: Nine balls moving the same distance, each with a different easing curve drawn next to it: linear, ease, ease in, ease out cubic, ease in out, ease out back, ease out bounce, spring and steps) | Curve | Feels like | Good for | | --- | --- | --- | | `"linear"` | A constant speed. | Spinners, progress, color cycles. | | `"ease"` | A soft start and a soft end (the CSS default). | Most things. | | `"ease_in"` | Starts slowly, ends fast. | Things leaving the screen. | | `"ease_out"`, `"ease_out_cubic"` | Starts fast, ends softly. | Things arriving: menus, toasts, pages. | | `"ease_in_out"` | Slow, fast, slow. | Moving between two places on screen. | | `"ease_out_back"` | Goes a little past the end and settles. | Pops, buttons, playful entrances. | | `"ease_out_bounce"` | Bounces at the end. | Drops, games. | | `"spring"` | Overshoots and wobbles like a spring. | Toggles, cards that snap into place. | | `"steps(5)"` | Jumps in 5 steps, no motion between. | Sprite frames, ticking clocks, typing. | `pz.animation.EASINGS` lists every name: `linear`, `ease`, `ease_in`, `ease_out`, `ease_in_out`, the `sine`, `quad`, `cubic`, `quart`, `expo`, `circ` and `back` families (each `ease_in_*`, `ease_out_*` and `ease_in_out_*`), `ease_in_elastic`, `ease_out_elastic`, `ease_in_bounce`, `ease_out_bounce` and `spring`. You can also give: - **A cubic Bézier** `(x1, y1, x2, y2)`, like CSS `cubic-bezier()`: `easing=(0.2, 0.8, 0.2, 1)`. - **Steps** `"steps(n)"`: `n` jumps instead of a smooth motion. `pz.animation.ease(easing, t)` evaluates any curve at `t` (from 0 to 1). Use it to animate your own [canvas](https://albakore.github.io/pyzed/guides/canvas/) drawings with the same curves: ```python from pyzed import animation y = 300 - animation.ease("ease_out_bounce", self.t) * 240 # a ball dropping on a canvas ``` ### Transitions and states > Animate changes in Pyzed like CSS: widget.transition() moves to new values, hover and pressed style states animate, pz.presence swaps pages with enter and exit animations, pz.stagger cascades lists, and windows open with transitions. Source: https://albakore.github.io/pyzed/animation/transitions/ An animation plays on its own. A **transition** plays when something changes: a panel opens, a row gets selected, the page changes. You only set the new value; Pyzed animates the way there. #### transition() `widget.transition(**props)` sets property values and **animates every change** to them, like a CSS `transition`. Change the values in `view()` and the widget moves to them: (Image: A sidebar narrows to icons and widens again when the Menu button is clicked, the cards beside it reflow, and the selected row's color slides to another row) ```python def view(self): sidebar = pz.column(items).transition(width=220 if self.open else 64, duration=220, easing="ease_out_cubic") label.transition(opacity=1 if self.open else 0) row.transition(background="accent" if selected else "panel", duration=260) ``` - `duration` (default 200 ms), `delay` and `easing` (default `"ease_out"`) apply to every property. - A property can have its own timing: `transition(scale={"value": 1.1, "duration": 90, "easing": "spring"})`. - The first time the widget appears it shows the values at once; only later changes animate. :::tip[transition() or animate()?] Use `transition()` for a value that follows your app's state (open or closed, selected or not). Use `animate()` for motion that plays by itself (an entrance, a hover effect, a loop). ::: #### Style states, like CSS A [style](https://albakore.github.io/pyzed/theming/styles/) can say how a widget looks while the pointer is over it (`hover`), while it's pressed (`pressed`) and while it has the keyboard focus (`focus`). Add `transition` and the change animates, like CSS `:hover` with `transition`: (Image: Three widgets reacting to the pointer: a button changes color and rises, an outline button turns red, a card grows a little with a colored border) ```python ui.button("Save", self.save).style( background="accent", radius=6, hover={"background": "#4f8cff", "translate": (0, -1)}, pressed={"scale": 0.97}, transition=120, # milliseconds, "0.2s", True or {"duration", "easing", "delay"} ) ``` A state can change `background`, `color`, `border_color`, `border_width`, `radius`, `opacity`, `translate`, `scale`, `rotate` and `blur`. None of these move the widgets around, so hovering never shifts the layout. See [Styles](https://albakore.github.io/pyzed/theming/styles/#states-and-transitions) for stylesheets and named styles. #### Presence and stagger `pz.presence(content, key)` animates content **in and out** when `key` changes. The old content stays on screen while it leaves, so pages, tabs and wizard steps can cross over: (Image: Three tabs: clicking one slides the old page out to the left while the new one comes in from the right, and its rows cascade in) ```python pz.presence(self.page_view(), key=self.page, enter="fade_in_right", exit="fade_out_left") ``` `enter` and `exit` take presets or `pz.Animation` objects. Give it an `id` when one line of code makes several presences (it defaults to the caller's file and line). `pz.stagger(items, animation, step=40)` gives each item the same animation, each one starting `step` milliseconds after the previous, so a list cascades in: ```python pz.column(pz.stagger([row(item) for item in self.items], "fade_in_up", step=30)) ``` #### Window transitions A window animates when it opens and closes. Set `transition` (a name from `pz.animation.WINDOW_TRANSITIONS`, a preset or a `pz.Animation`) and, optionally, `exit_transition`: (Image: Three windows opening with zoom, pop and slide up, then closing) ```python class Settings(pz.Window): transition = "zoom" # fade, zoom, pop, slide_up, slide_down, slide_left, slide_right exit_transition = "fade_out" ``` The window stays open until its exit animation ends. The app's main window takes the same attributes. See [Windows](https://albakore.github.io/pyzed/guides/windows/). ### Preset gallery > Every named animation preset in Pyzed, playing: entrances, exits, attention seekers, hover and press feedback, and loops, with their durations and when to use them. Source: https://albakore.github.io/pyzed/animation/presets/ A **preset** is a ready-made animation with a name. Pass the name to `animate()`: ```python dialog.animate("pop_in") card.animate("lift", on="hover") pz.icon("loader-circle").animate("spin") ``` Options change a preset's timing without changing the preset: `animate("fade_in_up", duration=600, delay=200)`. `pz.animation.presets(category)` lists them in code, each with its `description`, and `pz.define_animation` adds [your own](https://albakore.github.io/pyzed/animation/keyframes/#naming-and-saving-animations). #### Entrance Play once when the widget appears (`on="mount"`). Use them for pages, dialogs, toasts and list items. (Image: Fifteen cards appearing, each with an entrance preset) | Preset | What it does | Duration | | --- | --- | --- | | `fade_in` | Fades in. | 280 ms | | `fade_in_up` | Fades in while rising. | 280 ms | | `fade_in_down` | Fades in while dropping. | 280 ms | | `fade_in_left` | Fades in from the left. | 280 ms | | `fade_in_right` | Fades in from the right. | 280 ms | | `slide_in_up` | Slides in from below its own height. | 280 ms | | `slide_in_down` | Slides in from above. | 280 ms | | `slide_in_left` | Slides in from the left. | 280 ms | | `slide_in_right` | Slides in from the right. | 280 ms | | `zoom_in` | Grows in from slightly smaller. | 280 ms | | `pop_in` | Pops in with a small overshoot. | 320 ms | | `bounce_in` | Bounces in from small. | 600 ms | | `drop_in` | Falls into place with a bounce. | 520 ms | | `blur_in` | Comes into focus, like a lens. | 360 ms | | `rotate_in` | Turns in from a quarter turn. | 420 ms | #### Exit The reverse: the widget leaves. Use them as `exit=` of [`pz.presence`](https://albakore.github.io/pyzed/animation/transitions/#presence-and-stagger), as a window's `exit_transition`, or before you remove something. (Image: Thirteen cards leaving, each with an exit preset) | Preset | What it does | Duration | | --- | --- | --- | | `fade_out` | Fades out. | 220 ms | | `fade_out_up` | Fades out while rising. | 220 ms | | `fade_out_down` | Fades out while dropping. | 220 ms | | `fade_out_left` | Fades out to the left. | 220 ms | | `fade_out_right` | Fades out to the right. | 220 ms | | `slide_out_up` | Slides out upwards. | 220 ms | | `slide_out_down` | Slides out downwards. | 220 ms | | `slide_out_left` | Slides out to the left. | 220 ms | | `slide_out_right` | Slides out to the right. | 220 ms | | `zoom_out` | Shrinks away. | 220 ms | | `pop_out` | Swells a little, then pops away. | 260 ms | | `blur_out` | Goes out of focus. | 260 ms | | `rotate_out` | Turns away a quarter turn. | 320 ms | #### Attention A short nudge to look here. Replay them with `key` (`animate("shake", key=self.errors)`) or `on="click"`. (Image: Ten cards, each playing an attention preset) | Preset | What it does | Duration | | --- | --- | --- | | `pulse` | Grows and shrinks once. | 600 ms | | `heartbeat` | Two quick beats. | 1000 ms | | `shake` | Shakes sideways (wrong input). | 450 ms | | `wobble` | Wobbles and settles. | 800 ms | | `bounce` | Jumps twice. | 900 ms | | `flash` | Blinks twice. | 800 ms | | `rubber_band` | Stretches and snaps back. | 800 ms | | `tada` | A celebratory pop with a wiggle. | 900 ms | | `swing` | Swings like a hanging sign. | 900 ms | | `glow_flash` | A burst of glow around the box. | 700 ms | #### Interaction Feedback for hover, press and focus: they go forward while the pointer is on the widget and back after. Their default trigger is in the table. (Image: Eight cards going into their hover or press look and back) | Preset | What it does | Duration | Trigger | | --- | --- | --- | --- | | `lift` | Rises with a soft glow (for cards and buttons). | 160 ms | `hover` | | `grow` | Grows a little. | 160 ms | `hover` | | `shrink` | Presses in (for press). | 90 ms | `press` | | `dim` | Fades a little. | 160 ms | `hover` | | `glow` | Glows around the box. | 200 ms | `hover` | | `bloom` | A strong, wide glow that blooms out of the box. | 260 ms | `hover` | | `nudge_right` | Moves a little to the right (links, chevrons). | 140 ms | `hover` | | `tilt` | Tilts a little (stickers, cards). | 180 ms | `hover` | #### Loop Motion that goes on forever (`repeat="infinite"`): status lights, spinners, things that float. (Image: Seven cards with looping presets: float, breathe, blink, glow pulse, throb, drift and spin) | Preset | What it does | Duration | | --- | --- | --- | | `float` | Floats up and down. | 2400 ms | | `breathe` | Slowly fades and shrinks, like breathing. | 3000 ms | | `blink` | Blinks (status lights, cursors). | 1000 ms | | `glow_pulse` | The glow swells and fades (recording, live). | 1600 ms | | `throb` | Pulses forever (notifications). | 1200 ms | | `drift` | Drifts sideways. | 3000 ms | | `spin` | Turns forever (spinners, loading icons). | 1000 ms | ### Effects > Visual effects in Pyzed: opacity, rotation, blur, backdrop blur, glow, bloom, glass, translate and scale as style properties, and which of them animations can change. Source: https://albakore.github.io/pyzed/animation/effects/ Effects are [style](https://albakore.github.io/pyzed/theming/styles/) properties, so they work with `style()`, `pz.Style` and named styles. Several of them are also [animatable properties](https://albakore.github.io/pyzed/animation/properties/), so an animation can change them over time. (Image: Over a colorful gradient: a card blurring and sharpening, one tilting, one glowing, one fading, and a glass panel sliding over colored circles) | Property | What it does | Animatable | | --- | --- | --- | | `opacity` | 0 to 1. Fades the widget and everything inside it. | Yes | | `rotate` | Degrees clockwise (or `"0.25turn"`, `"1rad"`): draws the widget turned around its center. | Yes | | `blur` | Pixels: blurs the widget and everything inside it. | Yes | | `backdrop_blur` | Pixels: blurs what is behind the box, inside its rounded corners. | No | | `glow` | A soft halo around the box: a color, a size (accent color) or `(color, size)`. | Yes (`glow` and `glow_color`) | | `bloom` | Light that spills from the bright parts of the widget: a color or `(color, intensity)` with an intensity from 0 to 1. | No | | `glass` | A translucent panel that blurs what is behind it, with a light edge: `True`, a tint color or `{"tint": ..., "strength": 0.08, "blur": 20}`. | No (move it with `x`, `y`) | | `translate` | `(x, y)` in pixels or `"50%"`: draws the widget moved, without changing the layout. | Yes (as `x`, `y`) | | `scale` | Draws the widget bigger or smaller around its center. | Yes | ```python pz.container(card).style(glass=True, radius=12, padding=16) ui.button("Go", self.go).style(glow=("#22d3ee", 18)) badge.style(bloom=("accent", 0.8), opacity=0.9) sticker.style(rotate=-8) ``` (Image: Rotation, glass, bloom and blur over a gradient) #### How they're drawn Rotation, blur, backdrop blur and bloom are drawn by the renderer: the widget is drawn on its own layer, which is then turned, blurred or lit before it lands on the window. - On a box (a widget with a background or glass), bloom also adds a soft halo and a lit edge; on a bare text or icon it only glows. - Glass adds a tinted gradient, a border and a shadow; any of those you set yourself win. It shows best over a colorful background (a gradient or an image). - The `glow`, `bloom`, `glow_pulse` and `glow_flash` presets animate `glow` and `glow_color`. :::caution[Effects and input] A rotated or blurred widget takes clicks where its box is laid out, not where it is drawn. A widget with rotation, blur, backdrop blur or bloom is drawn above its siblings, so its bloom can light the widgets next to it. ::: ### Timeline animations > Animate many widgets and your app's state on one timeline with clips, tracks, keys and method calls, like Godot's AnimationPlayer: interpolation, easing per key, loops and attribute tracks. Source: https://albakore.github.io/pyzed/animation/timeline/ `animate()` gives one widget its own motion. When **several widgets** have to move together (a title screen, a cutscene, a HUD reacting to a hit, a guided tour), put them on one **timeline**. A **clip** holds that timeline and an **animation player** plays it, with play, pause, seek, speed and queues. Godot's AnimationPlayer works the same way. (Image: A level-complete screen: the card rises, the title pops, three stars spin in one after another, the score counts up and the Next level button fades in, then the button keeps breathing) ```python import pyzed as pz INTRO = pz.Clip("intro", length=2.2, tracks=[ pz.Track("card:opacity", {0: 0, 0.35: 1}), pz.Track("card:y", [(0, 40, "ease_out_back"), (0.5, 0)]), pz.Track("title:scale", [(0.3, 0.6, "ease_out_back"), (0.7, 1)]), pz.Track(":score", [(0.7, 0, "ease_out_cubic"), (1.9, 12_480)]), ], calls=[pz.Call(1.6, "chime")]) class Game(pz.App): def __init__(self): self.score = 0 self.player = pz.AnimationPlayer(INTRO, autoplay="intro") def chime(self): ... def view(self): return pz.column([ pz.text("LEVEL COMPLETE").animate(self.player["title"]), pz.text(f"{self.score:,.0f}"), ]).animate(self.player["card"]) ``` The complete example is [`examples/timeline.py`](https://github.com/albakore/pyzed/blob/main/examples/timeline.py). To make clips without code, use the [Animation panel](https://albakore.github.io/pyzed/editor/animations-and-effects/#timeline-animations) of Pyzed Design. #### The parts of a timeline (Image: A card on the left and its clip on the right: a lane per track with its keys, a lane of calls, and a playhead moving from 0 to 2 seconds while the card fades in, rises, the title grows and the score counts) | Part | What it is | | --- | --- | | **Clip** (`pz.Clip`) | One named timeline: a `length` in seconds, tracks, calls and a loop mode. | | **Track** (`pz.Track`) | One property changing over time, such as the `y` of the `card`. | | **Key** (`pz.Key`) | A value at a time on a track. Pyzed fills in the time between keys. | | **Call** (`pz.Call`) | A method of your app that runs when the playhead passes its time. | | **Target** | A name, like `"card"`, that widgets join with `.animate(self.player["card"])`. | | **Playhead** | Where the player is in the clip (`player.position`). | A clip with no `length` lasts until its last key or call. ##### Track paths A track's path says what it animates: | Path | Animates | | --- | --- | | `"card:opacity"` | A property of every widget that plays the target `card`. | | `":score"` | An attribute of the app. | | `":hud.hp"` | An attribute of an attribute (follow the dots). | **Widget tracks** use the [animatable properties](https://albakore.github.io/pyzed/animation/properties/) (`opacity`, `x`, `y`, `scale`, `rotate`, `blur`, colors, `radius`, `glow`, `width`, `font_size`...). They play natively at the display's frame rate, and your code doesn't run meanwhile. **Attribute tracks** (starting with `:`) change your app's own values, so they can control **any parameter of any widget**: the text of a label, the value of a progress bar, a size, an icon. Numbers, colors (`"#rrggbb"`) and tuples of numbers blend between keys; other values (text, booleans) change at each key. See [attribute tracks and calls](#attribute-tracks-and-calls) below. ##### Writing keys Keys can be written several ways: ```python pz.Track("card:x", {0: -40, 0.5: 0}) # {time: value} pz.Track("card:x", [(0, -40, "ease_out_back"), (0.5, 0)]) # (time, value, easing to the next key) pz.Track("card:x", [pz.Key(0, -40), pz.Key("500ms", 0)]) # Key objects; times also as "500ms" ``` #### Between keys (Image: Three tracks with the same keys, each with its curve and a ball: linear moves at a steady speed, easing per key overshoots and then eases, discrete jumps from value to value) - **Easing per key.** A key's `easing` is the curve **to the next key**. Any name of [`pz.animation.EASINGS`](https://albakore.github.io/pyzed/animation/keyframes/#easing) works, as do cubic Béziers `(x1, y1, x2, y2)` and `"steps(n)"`. The track's `easing` (default `"linear"`) is used for keys that don't set one. - **Discrete.** `interpolation="discrete"` holds each value until the next key, with no motion between. Good for sprite frames, texts and on/off switches. - **Muting.** `enabled=False` keeps the track in the clip without playing it. - **Before and after the keys.** A widget holds the first key's value before it and the last key's value after it, as in Godot. #### Loops (Image: Three playheads: once stops at the end, loop starts over, pingpong goes back and forth) | `loop=` | Plays | | --- | --- | | `"none"` (default) | Once, and stays on the last frame. | | `"loop"` | Starts over at the end, forever. | | `"pingpong"` | Goes to the end and back, forever. | ```python IDLE = pz.Clip("idle", length=1.2, loop="pingpong", tracks=[ pz.Track("button:scale", [(0, 1, "ease_in_out_sine"), (1.2, 1.06)]), ]) ``` #### Attribute tracks and calls (Image: An upload card whose status text, progress bar and file count come from attribute tracks, while the calls lane fires halfway() and finished() as the playhead passes them) ```python UPLOAD = pz.Clip("upload", length=2.0, tracks=[ pz.Track(":progress", [(0, 0, "ease_in_out"), (1.8, 1)]), pz.Track(":status", {0: "Uploading…", 0.9: "Almost there", 1.8: "Done"}), # texts change at each key pz.Track(":files", [(0, 0), (1.8, 24)]), ], calls=[pz.Call(0.9, "halfway"), pz.Call(1.8, "finished", "report.pdf")]) class Upload(pz.App): def __init__(self): self.progress, self.status, self.files = 0.0, "", 0 self.player = pz.AnimationPlayer(UPLOAD, autoplay="upload") def halfway(self): ... def finished(self, name): ... def view(self): return pz.column([pz.text(self.status), ui.progress(self.progress), pz.text(f"{self.files:.0f} / 24")]) ``` `pz.Call(time, method, *args)` calls a method of the app when the clip passes that time. `method` can also be any function. What it returns is handled like what a callback returns (a `Command`, a message). Calls run when playing forward, backwards and on every turn of a loop, but not when you jump with `seek`. :::note[The owner] Attribute tracks and calls act on the player's **owner**: the app (or window) whose attribute holds the player. Pyzed finds it on its own; pass `owner=` to `pz.AnimationPlayer` to choose another object. ::: Next: [the animation player](https://albakore.github.io/pyzed/animation/player/) plays, pauses, seeks and queues clips, and saves them in `.pzanim` files. ### The animation player > pz.AnimationPlayer plays Pyzed timeline clips: play, pause, resume, stop, seek, play backwards, speed, queues and events, and saves reusable clips in .pzanim libraries. Source: https://albakore.github.io/pyzed/animation/player/ `pz.AnimationPlayer` plays the [clips](https://albakore.github.io/pyzed/animation/timeline/) you give it, one at a time, and gives each widget its part with `player["target"]`. ```python self.player = pz.AnimationPlayer(INTRO, IDLE, "ui.pzanim", autoplay="intro", on_started=..., on_finished=...) ``` - **Sources:** clips, lists of clips, [libraries](#libraries-and-pzanim-files) or paths of `.pzanim` files. - **Where to keep it:** in an attribute of the app (or of a window). There it finds its **owner**, the object whose attributes and methods the tracks and calls use, and keeps itself running. - **Targets:** `widget.animate(self.player["card"])` gives a widget the `card` target. Every widget that uses the same target follows it. #### Controls (Image: The level-complete screen driven by the player: play, pause, seek to 0.5 s, play backwards, then play at double speed, with the call shown at the top and the controls following) | Call | What it does | | --- | --- | | `play(name, speed=1, backwards=False, restart=False)` | Plays a clip, or goes on with the current one. A clip that is already playing goes on unless `restart`; a finished one starts over. | | `play_backwards(name)` | Plays it backwards from the end. | | `pause()`, `resume()`, `toggle()` | Stops the time and starts it again. | | `stop(reset=False)` | Goes back to the first frame and clears the queue. With `reset=True` the widgets go back to their own values. | | `seek(seconds)` | Jumps to a time. Calls between don't run. | | `advance(seconds)` | Moves the time by hand and runs the calls on the way (handy in tests). | | `queue(name)`, `clear_queue()` | Plays a clip when the current one ends. | | `speed` | The playback speed of every clip (`2` is twice as fast). `play(speed=...)` multiplies it. | ```python def replay(self): self.player.play("intro", restart=True) self.player.queue("idle") # loops after the intro def set_speed(self, label): self.player.speed = float(label.rstrip("×")) ``` #### State and events | Read | Gives | | --- | --- | | `current` | The name of the clip playing or paused (`None` when stopped). | | `position`, `length` | Where the playhead is and how long the clip is, in seconds. | | `playing`, `paused`, `finished` | What the player is doing. | | `queued` | The clips waiting to play. | | `names`, `has(name)`, `clip(name)` | The clips it knows. | | `value(path)` | The value the current clip gives a path right now: `player.value(":score")`. | `on_started(name)` and `on_finished(name)` are called when a clip starts and when it ends (looping clips never end). A slider bound to `position` and `seek` makes a scrubber: ```python ui.slider(p.position, p.seek, min=0, max=p.length, step=0.01) ``` :::tip[It only wakes your app when needed] Widget tracks play natively and never need your code. A clip with attribute tracks updates them up to `fps` times a second (60 by default, `pz.AnimationPlayer(..., fps=30)` for less). Without them, the player only wakes up for the next call or the end of the clip. ::: #### Libraries and .pzanim files Clips are immutable values, so you can share them between players and apps. An `AnimationLibrary` groups them and saves them in a `.pzanim` file (JSON) that you can keep in your project and load in any app: ```python library = pz.AnimationLibrary([INTRO, IDLE], name="ui") library.save("ui.pzanim") player = pz.AnimationPlayer("ui.pzanim") # clips named "ui/intro", "ui/idle" player.play("ui/intro") library = pz.load_animations("ui.pzanim") ``` A library also has `add`, `remove`, `rename` and `names`. Forms made in Pyzed Design keep their clips inside the `.pzform`; see the [Animation panel](https://albakore.github.io/pyzed/editor/animations-and-effects/#timeline-animations). #### Reusing clips (Image: The same bouncing clip on three widgets: played on its own target, retargeted to another widget, and turned into a pz.Animation) Clip methods return a **new** clip, so the original stays as it is: - `clip.retarget({"card": "panel"})` moves the clip's tracks to other targets. - `clip.animation("card")` turns one target's tracks into a `pz.Animation`. Play it with `animate()` without a player, or give it a name with `pz.define_animation`. - `clip.with_key(path, time, value)`, `without_key`, `with_track` and `without_track` edit it. - `clip.to_dict()` and `pz.Clip.from_dict()` turn it into JSON-friendly data and back. ### Reduced motion, tests and limits > Respect the system's reduce-motion setting in Pyzed, test animated views and timeline clips, take stable screenshots, and know the limits of the animation system. Source: https://albakore.github.io/pyzed/animation/motion-and-tests/ #### Reduced motion Some people get dizzy or distracted by motion, and turn on "reduce motion" in their system. Pyzed follows that setting, read when the app starts: every animation then **shows its resting state at once**, without moving. - `pz.set_reduced_motion(True)` turns motion off, `False` turns it on, and `None` follows the system again. Offer it as a setting of your app. - The `PYZED_REDUCED_MOTION` environment variable (`1` or `0`) overrides both. See [Accessibility](https://albakore.github.io/pyzed/guides/accessibility/#reduce-motion). :::tip Don't hide information in motion alone. If a field shakes on a wrong password, also show the error message. ::: #### Tests and screenshots Screenshots (`App.screenshot`, `pz.screenshot`) always show the **resting state**: what a mount animation ends on. That keeps tests and documentation images stable, whatever the timing. To test that a widget animates, check the widget tree: `repr(widget)` ends with `.animate(2 animation(s), 1 transition(s))`. To test a timeline, drive the player by hand. `advance` runs calls and updates attribute tracks; `seek` jumps without running calls: ```python def test_intro_chimes(): app = Game() app.player.owner = app # a running app sets it; a test sets it by hand app.player.pause() app.player.advance(1.7) # passes the chime() call at 1.6 s assert app.message == "New record!" app.player.seek(0.5) assert app.player.value("card:y") < 40 ``` A paused player shows its clip at the playhead, even in a screenshot, so you can take a picture of any moment of a clip: `app.player.pause(); app.player.seek(0.8); app.screenshot("intro.png")`. #### Limits - Transforms are translation, uniform scale and rotation; there is no skew. - A rotated or blurred widget takes input where its box is laid out, not where it is drawn. - A widget with rotation, blur, backdrop blur or bloom is drawn above its siblings, so its bloom can light the widgets next to it. - `backdrop_blur` blurs what the window drew before the widget, inside the same parent layer (scrollables and other clipping widgets start a new layer). - `width`, `height`, `padding` and `font_size` lay the view out again on every frame while they move, so prefer `x`, `y` and `scale` for large views. - Animations restart when a widget leaves the tree and comes back. Give a stable `key`, or use [`presence`](https://albakore.github.io/pyzed/animation/transitions/#presence-and-stagger), to control when they replay. - A player plays one clip at a time. To play two scenes at once, use two players. ## Why Pyzed ### Why Pyzed? A modern Python GUI framework for a new generation > Why choose Pyzed for Python desktop apps: a modern look out of the box, native GPU rendering from Rust, a Pythonic API, a visual designer, hot reload and AI-ready tooling. A modern alternative to tkinter, Kivy and PyQt. Source: https://albakore.github.io/pyzed/why/ Python is the first language of a whole generation of developers, but its desktop GUI story still feels like 2005: grey widgets, layouts written by hand, restart-the-app-to-see-a-change, and a packaging step that is a project of its own. **Pyzed** is our answer for people who grew up with VS Code, Figma, dark mode and AI assistants, and expect their tools to look and feel like that. (Image: An editor-style workspace built with Pyzed, in the One Dark theme) #### What you get on day one **A modern look, with no styling work** Dozens of components modelled on the [Zed](https://zed.dev) editor: buttons, inputs, tabs, trees, command palettes, menus, toasts, a code editor and dockable panels. Themes (One, Ayu, Gruvbox), dark mode and following the OS's appearance are built in. **Native speed, drawn by Rust** Layout, text and drawing run in Rust through [Iced](https://iced.rs) and [wgpu](https://wgpu.rs) (Vulkan, Metal, DirectX 12 or OpenGL), with a CPU renderer as fallback. Python only describes the interface. **A Pythonic API** State lives in a normal class. `view()` returns what to show, and callbacks are plain methods or lambdas. No signals, no slots, no `.kv` files, no widget IDs to look up. **A visual designer** [Pyzed Design](https://albakore.github.io/pyzed/editor/) lets you draw forms, edit their code in a built-in editor with completion and diagnostics, and run them, all in one app. **Hot reload that keeps your state** `pyzed dev main.py`: save a file and the running window changes, with your data still there. **Ready for AI assistants** An [MCP server](https://albakore.github.io/pyzed/guides/mcp/) gives coding agents the exact API, the docs and screenshots of what they build, and [`pyzed.agents`](https://albakore.github.io/pyzed/guides/agents/) puts models inside your app. #### The whole workflow, in a few commands Projects are ordinary [uv](https://docs.astral.sh/uv/) projects, so dependencies, lock files, linting ([ruff](https://docs.astral.sh/ruff/)) and type checking ([ty](https://docs.astral.sh/ty/)) work the way the rest of modern Python does. See [Create a project](https://albakore.github.io/pyzed/getting-started/new-project/) and [Build and distribute](https://albakore.github.io/pyzed/packaging/build/). #### Code that reads like the UI A counter, complete: ```python title="counter.py" import pyzed as pz from pyzed import ui class Counter(pz.App): title = "Counter" def __init__(self): self.count = 0 def view(self): return pz.center(pz.row([ ui.button(icon="minus", on_press=lambda: setattr(self, "count", self.count - 1)), ui.heading(str(self.count)), ui.button(icon="plus", on_press=lambda: setattr(self, "count", self.count + 1)), ], spacing=12, align="center")) pz.run(Counter) ``` There is no widget to find and update when the count changes: you change `self.count` and Pyzed calls `view()` again. This is the [Elm architecture](https://guide.elm-lang.org/architecture/) that Iced uses, and the same idea as React or SwiftUI, so it will feel familiar if you come from the web or mobile. (Image: Pyzed components in the One Dark theme) #### Built for how people develop now - **Edit, save, see.** Hot reload keeps the app's state, and when the new code fails, the error is shown in the window until you save a fix. (Image: A failed reload shows the error inside the window) - **Your editor knows the API.** The package ships type information (`py.typed` and stubs for the Rust core), so completion and type checking work in VS Code, PyCharm, Zed and with ty or mypy. - **Screenshots without a screen.** `app.screenshot("view.png")` renders off-screen, with no window or GPU, for tests, CI and documentation. Every image on this site was made that way. - **Agents that do not guess.** Connect the [MCP server](https://albakore.github.io/pyzed/guides/mcp/) to Claude Code, Cursor, VS Code or Zed and your assistant looks up real signatures, reads these docs and checks the code it writes. The docs are also published as plain text for language models in [`llms.txt`](https://albakore.github.io/pyzed/llms.txt). - **Async when you need it.** Callbacks can be `async def` functions or generators; Pyzed runs them and sends their results back to `update`. See [Background tasks](https://albakore.github.io/pyzed/guides/async/). #### Honest limits Pyzed is young: its first release was in 2026, while tkinter, Qt and wxPython have decades behind them. Today it targets **desktop only** (Windows, macOS and Linux): no Android, iOS or browser builds, which [Kivy](https://albakore.github.io/pyzed/why/vs-kivy/) and [Flet](https://albakore.github.io/pyzed/why/vs-flet/) offer. **Screen readers are not supported yet** because Iced does not expose accessibility information ([details](https://albakore.github.io/pyzed/guides/accessibility/#screen-readers)), and its ecosystem of third-party widgets is small. The [comparison](https://albakore.github.io/pyzed/why/comparison/) spells out where other toolkits are the better choice. #### Compare - [Feature comparison](https://albakore.github.io/pyzed/why/comparison/): Pyzed next to tkinter, Qt, Kivy, Dear PyGui, Flet, Toga, NiceGUI, wxPython and CustomTkinter. - [Benchmarks](https://albakore.github.io/pyzed/why/benchmarks/): Startup, memory, update speed, install size and lines of code, measured and reproducible. - [Developer experience](https://albakore.github.io/pyzed/why/developer-experience/): From install to a packaged app: what the day-to-day feels like. - [Pyzed vs tkinter](https://albakore.github.io/pyzed/why/vs-tkinter/): The standard library toolkit, side by side. - [Pyzed vs PyQt / PySide](https://albakore.github.io/pyzed/why/vs-pyqt-pyside/): The most complete toolkit, and when its weight pays off. - [Pyzed vs Kivy](https://albakore.github.io/pyzed/why/vs-kivy/): GPU-drawn widgets on desktop and mobile. - [Pyzed vs Flet](https://albakore.github.io/pyzed/why/vs-flet/): Flutter from Python, for desktop, web and mobile. - [Pyzed vs Dear PyGui](https://albakore.github.io/pyzed/why/vs-dear-pygui/): Immediate-mode GUIs for tools and plots. ### Python GUI frameworks compared (2026) > Pyzed compared with tkinter, PyQt/PySide (Qt), Kivy, Dear PyGui, Flet, Toga, NiceGUI, wxPython and CustomTkinter: license, rendering, look, designer, hot reload, async, mobile and web, packaging, typing and AI tooling. Source: https://albakore.github.io/pyzed/why/comparison/ Every Python GUI toolkit makes different trade-offs. This page lays them out side by side, including the places where Pyzed is **not** the best pick. For measured numbers see [Benchmarks](https://albakore.github.io/pyzed/why/benchmarks/). :::note[How to read the tables] Facts were checked against each project's documentation in October 2026. "Third-party" means it exists but outside the project; "–" means the project does not offer it (as far as we could find). If something here is out of date, please [open an issue](https://github.com/albakore/pyzed/issues) and we will fix it. ::: #### The contenders | Toolkit | What it is | Since | | --- | --- | --- | | **[Pyzed](https://github.com/albakore/pyzed)** | Python API over [Iced](https://iced.rs) (Rust), with ready-made modern components | 2026 | | **[tkinter](https://docs.python.org/3/library/tkinter.html)** | Python's standard binding to Tcl/Tk | 1990s | | **[CustomTkinter](https://customtkinter.tomschimansky.com)** | Modern-looking widgets drawn on top of tkinter | 2021 | | **[PyQt](https://www.riverbankcomputing.com/software/pyqt/) / [PySide](https://doc.qt.io/qtforpython-6/)** | Bindings to the Qt framework (C++) | 1998 / 2009 | | **[wxPython](https://wxpython.org)** | Bindings to wxWidgets, which wraps native widgets | 1998 | | **[Kivy](https://kivy.org)** | OpenGL-drawn widgets, with its own KV language, for desktop and mobile | 2011 | | **[Dear PyGui](https://github.com/hoffstadt/DearPyGui)** | Bindings to Dear ImGui, an immediate-mode GPU toolkit | 2020 | | **[Flet](https://flet.dev)** | Python driving a Flutter front end | 2022 | | **[Toga](https://toga.readthedocs.io)** | BeeWare's toolkit over each platform's native widgets | 2014 | | **[NiceGUI](https://nicegui.io)** | Web UI (Vue + Quasar) served from Python, shown in a browser or a webview | 2021 | #### Rendering, look and platforms | Toolkit | License | How it draws | Default look | Desktop | Mobile | Web | | --- | --- | --- | --- | --- | --- | --- | | **Pyzed** | MIT | Rust: wgpu (GPU), tiny-skia (CPU fallback) | Modern code-editor style, themes, dark mode | Win, macOS, Linux | – | – | | tkinter | PSF + Tcl/Tk (BSD-style) | Tk's own widgets; `ttk` uses native themes on Windows and macOS | Classic; ttk is closer to native | Win, macOS, Linux | – | – | | CustomTkinter | MIT | Tk canvas drawing | Modern, light and dark | Win, macOS, Linux | – | – | | PyQt / PySide | GPL or commercial / LGPL or commercial | Qt Widgets (CPU raster) or Qt Quick (GPU scene graph) | Native-like styles, fully stylable | Win, macOS, Linux | PySide: Android (technical preview) | – | | wxPython | wxWindows Library Licence | Native widgets of each OS | Native | Win, macOS, Linux | – | – | | Kivy | MIT | OpenGL (ES 2) | Kivy's own; Material with KivyMD | Win, macOS, Linux | Android, iOS | – | | Dear PyGui | MIT | Dear ImGui on DirectX 11, Metal or OpenGL | ImGui tool style | Win, macOS, Linux | – | – | | Flet | Apache 2.0 | Flutter (Skia / Impeller) in a separate client process | Material or Cupertino | Win, macOS, Linux | Android, iOS | Yes | | Toga | BSD 3-Clause | Native widgets (GTK, Cocoa, WinForms, Android, iOS) | Native | Win, macOS, Linux | Android, iOS | Experimental | | NiceGUI | MIT | The browser (HTML, CSS, JavaScript) | Quasar (Material) | In a browser or webview | Through the browser | Yes | #### Tooling and developer experience | Toolkit | Visual designer | Hot reload | `async` | Type hints | Packaging | | --- | --- | --- | --- | --- | --- | | **Pyzed** | **[Pyzed Design](https://albakore.github.io/pyzed/editor/)**, built in | **Yes, keeps state** (`pyzed dev`) | Yes (coroutines and async generators in callbacks) | Yes (`py.typed`, stubs for the core) | `pyzed build` (PyInstaller) | | tkinter | Third-party (PAGE, Pygubu) | – | – (manual loop integration) | Stubs in typeshed | PyInstaller, Nuitka… | | CustomTkinter | – | – | – | Partial | PyInstaller, Nuitka… | | PyQt / PySide | Qt Designer, Qt Creator, Qt Design Studio | QML live preview in Qt tools; not for Widgets code | PySide: QtAsyncio; third-party `qasync` | Yes (bundled stubs) | `pyside6-deploy` (Nuitka), PyInstaller | | wxPython | Third-party (wxFormBuilder, wxGlade) | – | Third-party (`wxasync`) | Partial | PyInstaller… | | Kivy | – | Third-party | Yes (asyncio and trio event loops) | Partial | Buildozer, python-for-android, kivy-ios, PyInstaller | | Dear PyGui | – | – | – (own render loop) | Yes (annotated API) | PyInstaller… | | Flet | – | Yes (`flet run` restarts the app on save) | Yes | Yes | `flet build` (needs the Flutter SDK) | | Toga | – | – | Yes (asyncio) | Partial | [Briefcase](https://briefcase.readthedocs.io) | | NiceGUI | – | Yes (server reload, page state resets) | Yes | Yes | Web deploy; `nicegui-pack` for desktop | #### AI assistants, accessibility and maturity | Toolkit | AI tooling from the project | Screen readers | Maturity and ecosystem | | --- | --- | --- | --- | | **Pyzed** | **[MCP server](https://albakore.github.io/pyzed/guides/mcp/), [`llms.txt`](https://albakore.github.io/pyzed/llms.txt), [`pyzed.agents`](https://albakore.github.io/pyzed/guides/agents/), agent panel in the designer** | **Not yet** ([why](https://albakore.github.io/pyzed/guides/accessibility/#screen-readers)) | **New (2026)**, small ecosystem | | tkinter | – | Limited | Decades of answers, tutorials and books; in every Python install | | CustomTkinter | – | Limited (as tkinter) | Popular, one main maintainer | | PyQt / PySide | Qt AI Assistant in Qt Creator (commercial, aimed at C++/QML) | Yes | The most complete toolkit: charts, multimedia, web engine, 3D… | | wxPython | – | Yes (native widgets) | Long-lived, smaller community today | | Kivy | – | – | Mature, strong on touch and mobile | | Dear PyGui | – | – | Great for tools and plots; immediate-mode model | | Flet | [MCP server](https://flet.dev/docs/cookbook/flet-mcp) | Yes (Flutter semantics) | Fast-growing, large Flutter widget set | | Toga | – | Yes (native widgets) | Part of BeeWare; still pre-1.0 | | NiceGUI | Community MCP servers (third-party) | Browser accessibility | Active; strongest for web dashboards | #### Which one should I use? - **You need Android or iOS:** Kivy, Flet or Toga. Pyzed is desktop only. - **You need a web app:** NiceGUI or Flet (or a web framework). Pyzed draws native windows. - **Screen reader support is a requirement today:** Qt, wxPython, Toga or Flet. - **You need a huge widget set** (charts, video, an embedded browser, 3D) and can live with its size and license: Qt. See [Pyzed vs PyQt/PySide](https://albakore.github.io/pyzed/why/vs-pyqt-pyside/). - **A tiny script that must run with nothing installed:** tkinter, which ships with Python. See [Pyzed vs tkinter](https://albakore.github.io/pyzed/why/vs-tkinter/). - **Real-time plots and debug tools:** Dear PyGui is excellent. See [Pyzed vs Dear PyGui](https://albakore.github.io/pyzed/why/vs-dear-pygui/). - **A modern desktop app** (an internal tool, an editor, a dashboard, a launcher, a client for your API) that should look good without design work, start fast, be built visually and with AI help, and ship as one executable: **that is what Pyzed is for.** Start with the [quickstart](https://albakore.github.io/pyzed/getting-started/quickstart/). ### Python GUI benchmarks — Pyzed vs tkinter, Qt, Kivy, Flet… > Measured, reproducible benchmarks of Python GUI toolkits: import time, time to first frame, memory, update latency with 1,000 rows, install size and lines of code for Pyzed, tkinter, CustomTkinter, PySide6, Kivy, Dear PyGui, Flet and Toga. Source: https://albakore.github.io/pyzed/why/benchmarks/ These numbers were **measured**, not estimated: every chart on this page is drawn from the JSON file the benchmark script wrote, and you can run the same script on your machine. They come from one machine without a GPU, so read them as a comparison between toolkits under the same conditions, not as absolute numbers for your computer. :::caution[Software rendering] The machine has no GPU. Windows were shown on a virtual X server (Xvfb) and every toolkit that uses OpenGL or Vulkan (Pyzed, Kivy, Dear PyGui, Flet) rendered on the CPU through Mesa's llvmpipe and lavapipe drivers. That is the worst case for GPU toolkits and the normal case for tkinter, Qt Widgets and GTK, which draw on the CPU anyway. On a computer with a GPU, the drawing part of the GPU toolkits' numbers moves off the CPU. ::: #### Key findings - **Startup is fast everywhere except Flet and, with many widgets, Kivy and Toga.** Pyzed shows a 10-row window in about 0.16 s from launch and a 1,000-row window in about 0.24 s: building and laying out 2,000 more widgets costs it some 80 ms. tkinter, Dear PyGui and Qt start sooner; Kivy, Toga and Flet need close to a second for 1,000 rows. - **Updating 1,000 rows takes Pyzed about 18 ms** from state change to screen, even though it rebuilds the whole view from Python each time. That is a little faster than Qt Widgets, about 3× faster than Kivy and 9× faster than Flet; tkinter and Dear PyGui are faster still. Toga (GTK) needed about 37 s per update in this test, because every label change triggers a new layout. - **Memory depends on the renderer.** Through the software Vulkan driver used here, Pyzed's GPU renderer takes about 151 MB; the same app on Pyzed's CPU renderer (tiny-skia, set with `ICED_BACKEND=tiny-skia`) takes about 48 MB, between tkinter (25 MB) and Qt (72 MB). The other GPU toolkits pay a similar driver cost here (Dear PyGui 123 MB, Kivy 146 MB, Flet 268 MB). - **Install size:** about 30 MB for Pyzed, against 238 MB for Qt's essentials, 83 MB for Kivy and 51 MB for Flet with its client. tkinter, Dear PyGui, Toga (which uses the system's GTK) and CustomTkinter (which uses tkinter) are smaller. - **Code:** the to-do app takes 31 lines in Pyzed, half of PySide6 (62) and well under Kivy (81). Flet's is the shortest (22). #### Startup **Import** is the time to `import` the toolkit in a fresh Python process. **First frame** is the time from launching the app's process until its window is on the screen with its content: the script watches the screen for a colored bar at the bottom of the window. #### Memory Resident memory (RSS) of the app's processes one second after the first frame. For Flet it includes the Flutter client process that draws the window. #### Updating 1,000 rows Every 250 ms the app changes the text of all 1,000 labels and the color of the bottom bar in the same update. The latency is the time from the app changing its state to the new color being on the screen; 10 updates per launch, 7 launches. #### Install size How much a fresh virtual environment grows when the toolkit is installed with its dependencies. tkinter counts the Tcl/Tk files that come with Python. Flet includes the desktop client it downloads on first run. Toga does not include GTK, which comes from the operating system. NiceGUI is shown for reference only: it draws in a web browser, so it was not part of the window benchmarks. #### Lines of code The same counter and to-do apps written in each toolkit ([source](https://github.com/albakore/pyzed/tree/main/benchmarks/apps)), counting lines that are not blank, comments or docstrings. See [Developer experience](https://albakore.github.io/pyzed/why/developer-experience/#how-much-code) for the charts and what the lines do. #### Raw numbers Median in bold; below it the minimum–maximum and the number of samples. The complete results, with every sample, are in [`benchmarks/results/`](https://github.com/albakore/pyzed/tree/main/benchmarks/results). #### Machine and versions
  • Date: {bench.date}
  • OS: {m.os}
  • CPU: {m.cpu}, {m.cores} cores; {m.ram_gb} GB of RAM (a cloud container)
  • Display: {m.display}; Mesa {m.mesa}
  • Python {m.python} (python-build-standalone, via uv); Tk {m.tk}
  • Toolkits:{' '} {versionsLine()}
#### Method - **Same app everywhere.** Each toolkit's `bench.py` builds a 640×480 window with a scrollable list of N rows (a label and an "Open" button each) and a 40 px bar at the bottom painted in a sentinel color. Lists are built the plain way each toolkit documents, with one widget per row: no `RecycleView` in Kivy, no lazy `ListView` in Flet, no `virtual_list` in Pyzed. - **First frame** is measured from outside: the harness starts the process and grabs the screen about every millisecond (with [mss](https://github.com/BoboTiG/python-mss)) until 8,000 pixels of the sentinel color are visible. - **Memory** is the RSS of the process and its children ([psutil](https://github.com/giampaolo/psutil)), one second after the first frame. - **Updates** are timed between the app's own timestamp (`time.monotonic()`, printed right before it changes its state) and the moment the harness sees the bar's new color. - **Repetitions:** 7 launches per toolkit and size, 10 updates per launch, 10 imports; the charts show the median and a thin line from the minimum to the maximum. - **Pyzed (CPU renderer)** is the same Pyzed app started with `ICED_BACKEND=tiny-skia`, which makes Iced use its CPU renderer instead of wgpu: what Pyzed falls back to on machines without a working GPU driver. - **Install size:** a fresh `uv venv`, `uv pip install --link-mode=copy `, size difference. #### Reproduce it Without a display it starts Xvfb by itself; on a desktop, keep the benchmark windows uncovered while it runs. See [`benchmarks/README.md`](https://github.com/albakore/pyzed/blob/main/benchmarks/README.md) for the options and the system packages some toolkits need. Results on other machines are very welcome in an [issue](https://github.com/albakore/pyzed/issues). #### Caveats - **One machine, no GPU** (see above). Results on Windows and macOS, and with a real GPU, will differ. - **The harness shares the CPU** with the app: grabbing the screen costs about one core while it polls. - **Tk draws widget by widget**, so for tkinter the bar may appear a moment before every row has been drawn; the GPU toolkits present whole frames. - **Dear PyGui redraws continuously**, so its update latency includes waiting for its next frame; it also uses the CPU while idle, which is not measured here. - **CustomTkinter has no 1,000-row results**: its app died under Xvfb with an X error (BadAlloc or BadValue from `CreatePixmap`) once its scrollable frame held more than about 100 rows. - **Not measured:** wxPython has no Linux wheels on PyPI and building it from source was not practical here; NiceGUI runs in a web browser, so a desktop window timing would measure the browser. - **Lines of code** depend on style; the apps follow each toolkit's documented idioms, and improvements are welcome. ### Developer experience with Pyzed > What building a Python desktop app with Pyzed feels like day to day: install, first window, hot reload, visual designer, autocomplete and type checking, error messages, packaging and AI assistants, compared with other Python GUI toolkits. Source: https://albakore.github.io/pyzed/why/developer-experience/ Speed numbers matter less than how a toolkit feels at 11 pm when you are trying to finish something. This page walks through a Pyzed project from the first command to a packaged app, and compares each step with what the other toolkits ask of you. Where we say "we measured" or "we hit", it happened while preparing the [benchmarks](https://albakore.github.io/pyzed/why/benchmarks/). #### 1. Install One wheel with the Rust core already compiled, for Windows, macOS and Linux, and no runtime dependencies. Compare what we ran into on a clean Ubuntu 24.04 container: | Toolkit | What `pip install` needed there | | --- | --- | | Pyzed | Nothing else (a desktop Linux already has the `libxkbcommon-x11` library it loads; our bare container did not) | | tkinter | Not installable from pip: it comes with Python, or with a `python3-tk` system package on Debian and Ubuntu | | PySide6 | Nothing else, but a large download (see [install size](https://albakore.github.io/pyzed/why/benchmarks/#install-size)) | | Kivy, Dear PyGui | Nothing else | | Flet | `flet[desktop]`; the desktop client is downloaded the first time an app runs, and on Linux it needed the `libsecret` and `libgles2` system packages | | Toga | Failed until we installed `libgirepository-2.0-dev` and `libcairo2-dev` with apt, because PyGObject was compiled from source | | wxPython | No Linux wheels on PyPI: pip compiles wxWidgets from source (wheels for some distributions are on wxpython.org) | #### 2. First window or a full [uv project](https://albakore.github.io/pyzed/guides/projects/) with linting, type checking and git with `pyzed new hello`. The app is a class with a `view()`: ```python import pyzed as pz from pyzed import ui class Hello(pz.App): title = "Hello" name = "" def view(self): return pz.center(pz.column([ ui.input(self.name, lambda text: setattr(self, "name", text), placeholder="Your name"), ui.heading(f"Hello, {self.name or 'world'}!"), ], spacing=12, max_width=320)) pz.run(Hello) ``` It already has a dark theme, focus rings, hover states and proper text rendering; nothing to style. ##### How much code? We wrote the same two apps in every toolkit, in the style of each project's own documentation ([source](https://github.com/albakore/pyzed/tree/main/benchmarks/apps)), and counted the lines that are not blank, comments or docstrings. Line counts depend on style, so read them as a rough guide. The bigger difference is *what* the lines do: in tkinter, Qt, Flet and Dear PyGui part of the code copies data into widgets and keeps them in sync; in Pyzed, `view()` is the only place that turns data into widgets. #### 3. Hot reload Edit `view()`, a helper module or a style and save: the window updates in place, **with your data still there** (the app object is kept and gets the new class). New methods and new attributes in `__init__` show up without restarting, and a changed initial value applies if the app has not changed it (see [Hot reload](https://albakore.github.io/pyzed/guides/hot-reload/)). If the new code fails, the error appears in the window, and the next save that fixes it brings the app back. (Image: Before saving) (Image: After saving: same data, new layout) Flet restarts the app on save, and NiceGUI reloads its server, so in-memory state starts over. Qt offers a live preview for QML in its tools, not for Python Widgets code. tkinter, Kivy, Dear PyGui and Toga need a restart (or a third-party tool). #### 4. A visual designer that writes Python `pyzed design` opens [Pyzed Design](https://albakore.github.io/pyzed/editor/): drag components onto a form, set their properties, wire events to Python handlers, edit code with completion and diagnostics (ty and ruff), and run the form. (Image: Pyzed Design) Qt has the most powerful designers (Qt Designer, Qt Creator, Qt Design Studio). tkinter and wxPython have third-party designers; Kivy, Dear PyGui, Flet, Toga and NiceGUI have none. #### 5. Autocomplete and type checking The package is typed (`py.typed`, with stubs for the Rust core), so editors complete widget arguments and type checkers catch mistakes before you run anything: ```python ui.button("Save", self.save, variant="solid", size="sm", keys="ctrl-s") # ^ completion lists every option, with its documentation ``` New projects run [ty](https://docs.astral.sh/ty/) and [ruff](https://docs.astral.sh/ruff/) with `pyzed check`. #### 6. Error messages that say what to fix Mistakes are caught when the widget is created, with the value that was wrong: ```text ValueError: unknown button variant 'soild' ValueError: unknown icon 'plsu'; see pyzed.icons.names() ValueError: invalid color: "#12345" TypeError: text() got an unexpected keyword argument 'colr' ``` An exception inside `view()` or a callback shows up in the window (with hot reload) or as a normal Python traceback that points at your line, not at the C++ or Rust code underneath. #### 7. Screenshots and tests without a screen ```python app = Todo() app.tasks = [["Write docs", False], ["Ship it", True]] app.screenshot("todo.png", size=(420, 320)) ``` `screenshot` renders off-screen with the CPU, so it works in CI and in tests; see [Tests and screenshots](https://albakore.github.io/pyzed/guides/testing/). Other toolkits need a virtual display (Xvfb) or a screen grabber for the same thing. #### 8. Packaging `pyzed build` wraps PyInstaller and fills in what forms load at runtime. Elsewhere: Qt has `pyside6-deploy` (Nuitka), Flet's `flet build` needs the Flutter SDK, Toga uses Briefcase (very good, and it also targets phones), Kivy uses Buildozer for Android and PyInstaller for desktop. See [Build and distribute](https://albakore.github.io/pyzed/packaging/build/). #### 9. Working with AI assistants Language models write better code for APIs they can look up. Pyzed gives them: - an [MCP server](https://albakore.github.io/pyzed/guides/mcp/) with the exact signature of every widget, the docs, code checks and screenshots of the result, so an agent in Claude Code, Cursor, VS Code or Zed can see what it built; - this documentation as plain text in [`llms.txt`](https://albakore.github.io/pyzed/llms.txt); - the designer's [Agent panel](https://albakore.github.io/pyzed/editor/ai-agents/), which edits forms with you; - [`pyzed.agents`](https://albakore.github.io/pyzed/guides/agents/) to put models with tools inside your own app. Flet also publishes an [MCP server](https://flet.dev/docs/cookbook/flet-mcp); Qt offers Qt AI Assistant in Qt Creator (commercial, aimed at C++ and QML). ### Pyzed vs tkinter — a modern tkinter alternative > Pyzed vs tkinter (and CustomTkinter) for Python desktop apps: the same apps side by side, measured startup, memory and speed, concept mapping and how to migrate from tkinter to Pyzed. Source: https://albakore.github.io/pyzed/why/vs-tkinter/ [tkinter](https://docs.python.org/3/library/tkinter.html) is the GUI toolkit that comes with Python. It is everywhere, it is small and it is fast to start, which is why most people meet it first. It also shows its age: widgets that look like the 2000s, a layout model split between `pack`, `grid` and `place`, and no answer for dark mode, hot reload or a visual designer. [CustomTkinter](https://customtkinter.tomschimansky.com) modernises the look on top of tkinter, and is included in the numbers below too. #### In short | | Pyzed | tkinter | | --- | --- | --- | | Install | `pip install pyzed-ui` | Already there (on Linux, sometimes a `python3-tk` package) | | Look | Modern ready-made components, themes, dark mode | Classic Tk, or `ttk` themes | | Programming model | State + `view()`: the UI is rebuilt from your data | Widgets you create, keep references to and mutate | | Layout | Rows, columns, grids, stacks, docks, anchors | `pack`, `grid`, `place` | | Drawing | GPU (wgpu), CPU fallback | Tk, on the CPU | | Designer, hot reload | Pyzed Design, `pyzed dev` | Third-party designers; no hot reload | | Rich widgets | Code editor, command palette, file tree, tabs, toasts, markdown… | Text, Canvas, Treeview; the rest is DIY | | Accessibility | Keyboard; no screen readers yet | Limited | #### The same counter Both files are the reference apps from [the benchmarks](https://albakore.github.io/pyzed/why/benchmarks/), counted line by line. #### The same to-do list Add with the button or Enter, tick tasks off, delete them, and show how many are left. In tkinter, keeping the screen in sync with the data is your job (`refresh()` destroys and recreates the rows); in Pyzed, `view()` always describes the current data. #### Measured Medians from [the benchmarks](https://albakore.github.io/pyzed/why/benchmarks/) (Linux, no GPU: software rendering, which penalises Pyzed's GPU renderer and favours tkinter's CPU drawing). tkinter wins on startup and size: it is a few megabytes of C that is already loaded with Python, and it draws with the CPU, which is exactly what a machine without a GPU does well. Pyzed brings a GPU renderer, a text engine and dozens of components, and still opens a 1,000-row window in a fraction of a second. #### From tkinter concepts to Pyzed | tkinter | Pyzed | | --- | --- | | `root = tk.Tk()` … `root.mainloop()` | `class MyApp(pz.App)` … `pz.run(MyApp)` | | `root.title(...)`, `root.geometry("400x300")` | class attributes `title = ...`, `size = (400, 300)` | | `tk.StringVar`, `IntVar` | plain attributes: `self.name = ""` | | `widget.configure(text=...)` | change the attribute; `view()` shows it | | `ttk.Label`, `ttk.Button`, `ttk.Entry` | `ui.label`, `ui.button`, `ui.input` | | `command=callback` | `on_press=callback` (or a message for `update()`) | | `pack(side="left")` / `grid(row, column)` | `pz.row([...])`, `pz.column([...])`, `pz.grid(...)` | | `Canvas` + `Scrollbar` + inner `Frame` | `pz.scrollable(...)` | | `ttk.Treeview` | [`ui.tree`](https://albakore.github.io/pyzed/library/tree/), [`ui.table`](https://albakore.github.io/pyzed/library/table/) | | `root.after(ms, fn)` | `pz.every(seconds, message)` in `subscriptions()`, or an `async` callback | | `messagebox` | [`ui.confirm`](https://albakore.github.io/pyzed/library/confirm/), [`ui.prompt`](https://albakore.github.io/pyzed/library/prompt/), [`ui.modal`](https://albakore.github.io/pyzed/library/modal/) | | `filedialog` | No native file dialog yet: use a [`ui.tree`](https://albakore.github.io/pyzed/library/tree/) of files or keep a helper library for it | | `ttk.Style` | [themes](https://albakore.github.io/pyzed/theming/themes/) and [styles](https://albakore.github.io/pyzed/theming/styles/) | #### Migration tips 1. **Move state out of the widgets.** Every `StringVar`, `IntVar` and every value you read with `.get()` becomes an attribute of your `App`. That is most of the work. 2. **Replace update code with `view()`.** Functions that destroy and rebuild frames, or `configure` labels, disappear: write `view()` once from the attributes. 3. **Map layouts one-to-one.** A `Frame` packed horizontally is a `pz.row`, vertically a `pz.column`; `grid` becomes `pz.grid`; spacing and padding are arguments instead of `padx`/`pady` on every widget. 4. **Timers and threads.** `root.after` loops become `subscriptions()`; long work goes in an `async def` callback or a thread, and its result comes back as a message. 5. **Keep tkinter where it fits.** A five-line dialog in a script that must run on a bare Python is still a good tkinter use case. #### When to choose which - **Choose tkinter** for tiny utilities and teaching material that must run with nothing installed, or when your users need screen readers on platforms where Tk supports them. - **Choose Pyzed** when the app should look current, grow beyond a few windows, be designed visually, reload while you work and ship as a polished executable. ### Pyzed vs PyQt and PySide (Qt for Python) > Pyzed vs PyQt6 and PySide6: license, size, look, programming model, designer and hot reload, with the same apps side by side, measured numbers and a guide to migrate Qt Widgets code to Pyzed. Source: https://albakore.github.io/pyzed/why/vs-pyqt-pyside/ [Qt](https://www.qt.io) is the most complete GUI framework you can use from Python, through [PySide6](https://doc.qt.io/qtforpython-6/) (by The Qt Company, LGPL) or [PyQt6](https://www.riverbankcomputing.com/software/pyqt/) (by Riverbank, GPL or commercial). It has everything: charts, multimedia, an embedded Chromium, 3D, printing, accessibility, and thirty years of production use. That breadth comes with weight: a large download, a C++ object model (signals, slots, parents, layouts) and licensing to think about. #### In short | | Pyzed | PyQt6 / PySide6 | | --- | --- | --- | | License | MIT | PyQt: GPL v3 or commercial · PySide: LGPL v3 or commercial | | Model | State + `view()`; plain Python callbacks | Object tree, signals and slots, or declarative QML | | Look | Modern components and themes, out of the box | Native-like styles; modern looks through QSS or QML | | Designer | Pyzed Design, generates Python | Qt Designer / Creator (`.ui` files), Qt Design Studio (QML) | | Hot reload | `pyzed dev`, keeps state | QML preview in Qt tools; Widgets code needs a restart | | Breadth | Desktop app essentials + IDE components | Almost anything, through dozens of modules | | Mobile | – | PySide6 on Android (technical preview) | | Accessibility | Keyboard; no screen readers yet | Full screen reader support | #### The same counter #### The same to-do list In Qt Widgets you build the widgets once and keep them in sync with your data by hand (`refresh()` deletes and recreates the rows). In Pyzed you only describe the rows for the current data. #### Measured Medians from [the benchmarks](https://albakore.github.io/pyzed/why/benchmarks/) (Linux, no GPU: software rendering). The install size for Qt is `pyside6-essentials` only; the full `pyside6` package with add-ons is several times larger. #### From Qt concepts to Pyzed | Qt | Pyzed | | --- | --- | | `QApplication` + `QMainWindow` + `app.exec()` | `class MyApp(pz.App)` + `pz.run(MyApp)` | | `QVBoxLayout`, `QHBoxLayout`, `QGridLayout` | `pz.column`, `pz.row`, `pz.grid` | | `QLabel`, `QPushButton`, `QLineEdit`, `QCheckBox` | `ui.label`, `ui.button`, `ui.input`, `ui.checkbox` | | `clicked.connect(slot)` | `on_press=method` | | custom signals | messages sent to `update()`, or `pz.post(message)` from a thread | | `QTimer` | `pz.every(...)` in `subscriptions()` | | `QThread` / `QRunnable` | `async def` callbacks, generators, or plain threads | | `QDockWidget`, `QTabWidget`, `QSplitter` | [`ui.workspace`](https://albakore.github.io/pyzed/library/workspace/), [`ui.tab_bar`](https://albakore.github.io/pyzed/library/tab-bar/), [`ui.split`](https://albakore.github.io/pyzed/library/split/) | | `QTreeView` / `QTableView` + models | [`ui.tree`](https://albakore.github.io/pyzed/library/tree/), [`ui.table`](https://albakore.github.io/pyzed/library/table/), [`ui.virtual_list`](https://albakore.github.io/pyzed/library/virtual-list/) from Python lists | | QSS stylesheets | [themes](https://albakore.github.io/pyzed/theming/themes/) and [`pz.Style`](https://albakore.github.io/pyzed/theming/styles/) | | `QShortcut`, `QAction` | `on_key(combo)`, [commands](https://albakore.github.io/pyzed/guides/commands/) and `keys="ctrl-s"` on buttons | | Qt Designer `.ui` files | [Pyzed Design](https://albakore.github.io/pyzed/editor/) `.pzform` files with Python handlers | #### Migration tips 1. **Start from the data, not the widgets.** List what your `QWidget` subclasses store (`self.items`, `self.current`…) and make those the attributes of your `App`. Everything that copies them into widgets (`setText`, `setChecked`, model resets) disappears. 2. **Turn slots into methods.** A slot connected with `clicked.connect(self.save)` becomes `on_press=self.save`. 3. **Replace models with lists.** A `QAbstractListModel` over a Python list becomes the list itself plus `ui.virtual_list` when it is long. 4. **Keep Qt where you depend on it.** If the app needs `QtWebEngine`, `QtMultimedia`, printing or 3D, Qt is still the right tool for that app. #### When to choose which - **Choose Qt** when you need its breadth, a screen-reader-accessible app today, or decades-proven stability across a large team, and its size and license fit your project. - **Choose Pyzed** when you want a modern-looking desktop app with far less code and ceremony, a permissive MIT license, a much smaller install, hot reload and AI assistance built into the workflow. ### Pyzed vs Kivy for Python desktop apps > Pyzed vs Kivy: both draw their own GPU widgets, but differ in platforms, look, programming model and tooling. Same apps side by side, measured numbers and how to move from Kivy and KV to Pyzed. Source: https://albakore.github.io/pyzed/why/vs-kivy/ [Kivy](https://kivy.org) and Pyzed have the same basic idea: draw every widget yourself on the GPU instead of asking the operating system for native ones. Kivy was built for touch screens and **runs on Android and iOS**, which Pyzed does not. Pyzed is built for **desktop productivity apps** and comes with the components, themes and tooling for them. #### In short | | Pyzed | Kivy | | --- | --- | --- | | Platforms | Windows, macOS, Linux | Windows, macOS, Linux, **Android, iOS** | | Drawing | wgpu (Vulkan, Metal, DX12, GL), CPU fallback | OpenGL ES 2 | | Look | Modern desktop components, themes, dark mode | Kivy's own minimal style; Material with [KivyMD](https://kivymd.readthedocs.io) | | UI description | Python only | Python plus the KV language | | State | Plain attributes, `view()` rebuilt on change | Kivy properties and bindings | | Desktop details | Menus, shortcuts, tabs, trees, docks, code editor, multi-window | Touch-first; desktop conventions are up to you | | Designer, hot reload | Pyzed Design, `pyzed dev` | Third-party tools | | Accessibility | Keyboard; no screen readers yet | No screen readers | #### The same counter #### The same to-do list The Kivy version uses a `RecycleView`, Kivy's recommended way to show lists, with its row layout in KV. #### Measured Medians from [the benchmarks](https://albakore.github.io/pyzed/why/benchmarks/) (Linux, no GPU: both toolkits render through Mesa's software OpenGL/Vulkan, so the comparison between these two is like for like). #### From Kivy concepts to Pyzed | Kivy | Pyzed | | --- | --- | | `App.build()` returns the root widget | `App.view()` returns the tree, every time state changes | | `NumericProperty`, `StringProperty`, `ListProperty` | plain attributes | | KV rules (`:`) | Python functions that return widgets, or [components](https://albakore.github.io/pyzed/guides/components/) | | `BoxLayout(orientation=...)` | `pz.row` / `pz.column` | | `GridLayout`, `AnchorLayout`, `FloatLayout` | `pz.grid`, `pz.center` / alignment, `pz.stack` + [`pz.anchor_layout`](https://albakore.github.io/pyzed/guides/layout/) | | `ScrollView`, `RecycleView` | `pz.scrollable`, [`ui.virtual_list`](https://albakore.github.io/pyzed/library/virtual-list/) | | `on_press: app.do()` | `on_press=self.do` | | `Clock.schedule_interval` | `pz.every(...)` in `subscriptions()` | | `Animation(...)` | [`pz.Animation` and transitions](https://albakore.github.io/pyzed/animation/) | | `Window.clearcolor`, KivyMD themes | [themes](https://albakore.github.io/pyzed/theming/themes/) | #### Migration tips 1. **Collapse KV into `view()`.** Each KV rule becomes a function that takes data and returns widgets. Bindings such as `text: str(app.count)` become plain expressions: `ui.heading(str(self.count))`. 2. **Drop the properties.** Kivy properties exist so bindings can observe changes; Pyzed re-runs `view()` after every event, so ordinary attributes are enough. 3. **Rethink touch-only patterns.** Desktop users expect menus, keyboard shortcuts and hover states, which Pyzed components provide. #### When to choose which - **Choose Kivy** when the same app must run on phones and tablets, or for touch kiosks and multimedia apps. - **Choose Pyzed** for desktop apps that should look and behave like modern desktop software, with a designer, hot reload and AI tooling, and no second language to learn. ### Pyzed vs Flet for Python desktop apps > Pyzed vs Flet (Flutter for Python): architecture, platforms, look, tooling and measured startup and memory, with the same apps side by side and how to move a Flet desktop app to Pyzed. Source: https://albakore.github.io/pyzed/why/vs-flet/ [Flet](https://flet.dev) lets you build [Flutter](https://flutter.dev) apps in Python. Your Python code runs in one process and sends the control tree to a Flutter client that draws it, on the desktop, on the web or on a phone. That reach is Flet's big strength. Pyzed draws in the same process as your Python code, through a Rust renderer, and focuses on the desktop. #### In short | | Pyzed | Flet | | --- | --- | --- | | Architecture | One process: Python + Rust renderer (Iced) | Python process + Flutter client process, talking over a protocol | | Platforms | Windows, macOS, Linux | Windows, macOS, Linux, **web, Android, iOS** | | Look | Modern code-editor style, dense, desktop-first | Material 3 or Cupertino, mobile-first | | Model | State + `view()` rebuilt from data | Mutable controls + `page.update()`; Flet 1.0 adds declarative components with hooks | | Packaging | `pyzed build` (PyInstaller), no extra SDK | `flet build` (needs the Flutter SDK) | | Designer | Pyzed Design | – | | Hot reload | Keeps app state | `flet run` restarts the app on save | | Accessibility | Keyboard; no screen readers yet | Screen readers through Flutter's semantics | #### The same counter #### The same to-do list #### Measured Medians from [the benchmarks](https://albakore.github.io/pyzed/why/benchmarks/) (Linux, no GPU: software rendering for both). Flet's memory includes its Flutter client process, which is where the window lives. Its install size counts the Python packages plus the desktop client that `flet` downloads on first run. #### From Flet concepts to Pyzed | Flet | Pyzed | | --- | --- | | `ft.run(main)` with `main(page)` | `pz.run(MyApp)` with a `pz.App` class | | `page.add(...)`, `page.update()` | return the tree from `view()`; no manual update | | `ft.Column`, `ft.Row`, `ft.Container` | `pz.column`, `pz.row`, `pz.container` | | `ft.Text`, `ft.Button`, `ft.TextField`, `ft.Checkbox` | `ui.label`, `ui.button`, `ui.input`, `ui.checkbox` | | `on_click=handler(e)` | `on_press=method` (no event argument needed) | | `page.theme_mode = ft.ThemeMode.DARK` | `theme = "One Dark"` or `pz.themes.auto(...)` | | `page.run_task(coro)` | return a coroutine from a callback | | `ft.ListView` | [`ui.virtual_list`](https://albakore.github.io/pyzed/library/virtual-list/) | | `ft.NavigationRail`, `ft.Tabs` | [`ui.workspace`](https://albakore.github.io/pyzed/library/workspace/) panels, [`ui.tab_bar`](https://albakore.github.io/pyzed/library/tab-bar/) | #### Migration tips 1. **Move control state into the app.** Values you read back from controls (`field.value`, `checkbox.value`) become attributes, set by the controls' callbacks. 2. **Delete `page.update()` calls.** Pyzed redraws after every callback. 3. **Expect a denser UI.** Material spacing is made for touch; Pyzed's components are sized like a desktop editor. Use `size="lg"` variants or [styles](https://albakore.github.io/pyzed/theming/styles/) if you want more air. #### When to choose which - **Choose Flet** when one codebase must run on the web and on phones, or you want Flutter's Material widgets. - **Choose Pyzed** for desktop-first apps: one process, no Flutter SDK to package, a visual designer, hot reload that keeps your state, and a dense, professional look. ### Pyzed vs Dear PyGui > Pyzed vs Dear PyGui for Python desktop GUIs: retained Elm-style UI vs immediate-mode ImGui, look, tooling and measured numbers, with the same apps side by side and when each one fits. Source: https://albakore.github.io/pyzed/why/vs-dear-pygui/ [Dear PyGui](https://github.com/hoffstadt/DearPyGui) wraps [Dear ImGui](https://github.com/ocornut/imgui), the immediate-mode toolkit used for debug tools in game engines. It is GPU-drawn and very fast, and it shines for real-time plots, node editors and internal tools. Its look is the ImGui "tool" look, and it redraws continuously, which suits a dashboard that changes every frame more than a form that changes when you click. #### In short | | Pyzed | Dear PyGui | | --- | --- | --- | | Model | State + `view()` (Elm architecture) | Items created once with tags, changed with `set_value` / `configure_item` | | Redraws | When something changes | Every frame, by default | | Look | Modern ready-made components, themes, dark mode | ImGui tool style, configurable with themes | | Text | Full shaping, any script, emoji, rich text and markdown | Fonts and glyph ranges you load yourself | | Plots | [Sparklines, bar and donut charts](https://albakore.github.io/pyzed/library/bar-chart/), a canvas | Excellent built-in plotting (ImPlot) | | Desktop features | Menus, shortcuts, multi-window, docks, code editor | Docking and viewports from ImGui | | Designer, hot reload | Pyzed Design, `pyzed dev` | – | | Accessibility | Keyboard; no screen readers yet | No screen readers | #### The same counter #### The same to-do list #### Measured Medians from [the benchmarks](https://albakore.github.io/pyzed/why/benchmarks/) (Linux, no GPU: both render through Mesa's software drivers). #### From Dear PyGui concepts to Pyzed | Dear PyGui | Pyzed | | --- | --- | | `create_context`, `create_viewport`, `start_dearpygui` | `pz.run(MyApp)` | | `with dpg.window(...)` | the root returned by `view()` | | `dpg.add_text(tag="x")` + `dpg.set_value("x", ...)` | `ui.label(self.x)`: change `self.x` | | `callback=fn` | `on_press=fn` | | `dpg.group(horizontal=True)` | `pz.row` | | `dpg.child_window` | `pz.scrollable`, `ui.card`, `ui.panel` | | `dpg.theme()` + `bind_item_theme` | [themes](https://albakore.github.io/pyzed/theming/themes/) and [styles](https://albakore.github.io/pyzed/theming/styles/) | | `dpg.add_plot` | [`ui.bar_chart`](https://albakore.github.io/pyzed/library/bar-chart/), [`ui.sparkline`](https://albakore.github.io/pyzed/library/sparkline/), or [`pz.canvas`](https://albakore.github.io/pyzed/guides/canvas/) for custom drawing | #### When to choose which - **Choose Dear PyGui** for real-time plots, node editors, instrument panels and debug tools where the ImGui look is fine. - **Choose Pyzed** for end-user apps: forms, lists, editors and dashboards that should look polished, with proper text, a designer, hot reload and packaging. ## Component library ### Components > Browse 90+ Pyzed UI components for Python desktop GUIs: buttons, tables, trees, editors, charts and dialogs, each with a preview, examples and its API. Source: https://albakore.github.io/pyzed/library/ Every component of `pyzed.ui`, plus the layout and content widgets of `pyzed`, with a live preview, the code that draws it, examples and its API. Components are **functions that return widgets**, so they combine freely with each other and with the [base widgets](https://albakore.github.io/pyzed/reference/widgets/). ```python import pyzed as pz from pyzed import ui ui.button("Save", self.save, icon="save", variant="solid", keys="ctrl-s") ``` - **Theme colors.** Every component reads the current theme (`App.theme`, or the window's theme in [multi-window](https://albakore.github.io/pyzed/guides/windows/) apps), and accepts `theme=` to use another one. - **Tokens or hex.** Color parameters (`color`, `icon_color`, `tint`…) take a theme token such as `"accent"`, `"text_muted"` or `"error"`, or a `"#rrggbb"` color. - **Messages or callables.** `on_press`, `on_select`, `on_toggle`… take a message for `update` or a callable; those that produce a value (text, `bool`, id) call it with that value. - **Icons.** `icon=` takes any [Lucide](https://lucide.dev) name (`pyzed icons` lists them). To see them all running, open the gallery: `python examples/gallery.py`. Looking for whole screens built from them (login, dashboard, settings, chat, kanban...)? See [Blocks](https://albakore.github.io/pyzed/blocks/). :::note[What is on each page] A short explanation in plain words, a preview with its code, every variant, size and state captured in light and dark with the snippet that draws it, examples, tips and the API table. Pages of interactive components, such as [Button](https://albakore.github.io/pyzed/library/button/) and [Input](https://albakore.github.io/pyzed/library/input/), also show a short recording of them in use. ::: :::tip[Inspiration] The look of these components takes inspiration from the UI of the [Zed](https://zed.dev) editor; the idea of building desktop tools from ready-made pieces comes from Godot, Unreal Engine, Visual Basic and Qt Designer. pyzed is an independent project. ::: #### Actions Buttons and small controls that run something. #### Display Text, badges, keys, avatars and surfaces. #### Forms Controlled inputs: they show your state and report every change. #### Dates and times Calendars and fields for dates, ranges and times, in the format you choose. #### Feedback Progress, loading, notices and empty states. #### Data Tables, numbers and small charts drawn with the theme colors. #### Lists and menus Rows, file trees, context menus and collapsible sections. #### Navigation Tabs, breadcrumbs and toolbars. #### Overlays Things drawn on top of the UI: layers, modals, palettes and notifications. #### Dialogs Ready-made dialogs with translated buttons. #### System Pieces that talk to the operating system: its file dialogs and files dropped from the desktop. #### Workspace The frame of an editor-style window: docks, title bar, status bar and panels. #### Code and tools The code editor, a terminal view, a log console and help boxes. #### AI agents The pieces of an AI agent's chat and settings: tool calls, diffs, plans, history and sign-in. #### Layout The base layout widgets of `pyzed` (`pz.*`) every component is built with. #### Content Base widgets for text, icons, images, Markdown, QR codes and drawing. ### Events of every component > Every event of every Pyzed component: what each one reports, what the handler gets, and how many events each component can have. Source: https://albakore.github.io/pyzed/library/events/ Pyzed widgets react to events in two layers: - **Their own events**: the `on_*` parameters of each component, such as `on_press` of a button or `on_select` of a tree. They report what the component means (a tab was closed, a row was opened) and usually pass a value (the text, the item, the new state). - **The common events**: 18 events every widget has, whatever it is, attached with `.on(...)`: clicks, hover, drag, focus, keys and size. A `pz.Component` also raises `on_mount` and `on_unmount`. So a component can have up to its own events plus 18 common ones. Across the 114 components of the library there are **160 own events**; the ones with the most are listed first in the table at the end. ```python ui.list_item("main.py", self.open, icon="file").on( # on_press is its own event right_click=self.show_menu, # the common ones: any widget mouse_enter=lambda: setattr(self, "hovered", "main.py"), drag=lambda e: self.move(e.dx, e.dy), # takes the pz.Event ) ``` #### The common events Every widget has them. A handler without arguments is called with nothing; one with an argument gets a `pz.Event`. | Group | Event | When | | --- | --- | --- | | Mouse | `click` | The widget was clicked (pressed and released with the left button). | | Mouse | `double_click` | Two clicks in quick succession. | | Mouse | `right_click` | The right button was pressed (use it for context menus). | | Mouse | `middle_click` | The middle button (wheel) was pressed. | | Mouse | `mouse_down` | Any button was pressed over the widget (`event.button`). | | Mouse | `mouse_up` | Any button was released over the widget (`event.button`). | | Mouse | `mouse_enter` | The pointer moved onto the widget. | | Mouse | `mouse_leave` | The pointer left the widget. | | Mouse | `mouse_move` | The pointer moved over the widget (`event.x`, `event.y`). | | Mouse | `scroll` | The wheel turned over the widget (`event.dx`, `event.dy` in lines). | | Drag | `drag_start` | The left button was pressed and the pointer moved a few pixels. | | Drag | `drag` | The pointer moved while dragging (`event.dx`, `event.dy` since the last one). | | Drag | `drag_end` | The button was released after a drag. | | Focus | `focus` | The widget was clicked and now receives key events. | | Focus | `blur` | A click elsewhere took the focus away. | | Keyboard | `key_down` | A key was pressed while focused (`event.key`, e.g. `"ctrl-s"`). | | Keyboard | `key_up` | A key was released while focused (`event.key`). | | Layout | `resize` | The widget got a new size, including its first one (`event.width`, `event.height`). | ##### Fields of `pz.Event` | Property | Description | | --- | --- | | `type` | The event name (`"click"`, or a component's own event). | | `x`, `y` | Pointer position relative to the widget (`position` gives both). | | `window_x`, `window_y` | Pointer position in the window. | | `screen_x`, `screen_y` | On the screen (`None` where the system does not say, such as Wayland). | | `dx`, `dy` | Scroll amount, or drag movement since the previous `drag`. | | `width`, `height` | The widget's size (`size` gives both). | | `button` | `"left"`, `"right"`, `"middle"`… | | `key` | The key combo, such as `"ctrl-s"`. | | `shift`, `ctrl`, `alt`, `logo` | Modifiers held. | | `value`, `source` | For a component's own events: the data and the component. | #### What handlers get Controls that produce a value call your callable with it. The most common shapes: | Handler | Description | Events | | --- | --- | --- | | `()` | Nothing: the event is the news (a press, a close, Enter). | `ui.alert(on_close=)`, `ui.badge(on_press=)`, `ui.breadcrumbs(on_more=)`, `ui.button(on_press=)`… | | `(event)` | A `pz.Event`: its `value` holds the data, `source` the component. | `ui.Calendar(on_change=)`, `ui.Calendar(on_month=)`, `ui.Catalog(on_install=)`, `ui.ColorPicker(on_change=)`… | | `(text)` | The new text, on every keystroke. | `ui.command_palette(on_query=)`, `ui.console(on_query=)`, `ui.editor(on_change=)`, `ui.input(on_input=)`… | | `(x, y)` | The pointer position, relative to the widget. | `pz.canvas(on_press=)`, `pz.canvas(on_release=)`, `pz.canvas(on_right_press=)`, `pz.canvas(on_move=)`… | | `(info)` | A dict (scroll offset and sizes) or a `ui.ErrorInfo`. | `ui.editor(on_scroll=)`, `ui.error_boundary(on_error=)`, `ui.error_boundary(on_details=)`, `pz.scrollable(on_scroll=)`… | | `(value)` | The new value: a number, a color, an option… | `ui.file_input(on_change=)`, `ui.number_field(on_change=)`, `ui.radio_group(on_select=)`, `ui.slider(on_change=)`… | | `(tab_id)` | The id of the tab. | `ui.tab_bar(on_select=)`, `ui.tab_bar(on_close=)`, `ui.tab_bar(on_drag=)`, `ui.tab_bar(on_context=)` | | `(id)` | The id of the row or tab (for a tree, its path). | `ui.tree(on_toggle=)`, `ui.tree(on_select=)`, `ui.tree(on_open=)`, `ui.tree(on_context=)` | | `(row)` | The row (the dict or object you passed in `rows`). | `ui.table(on_row_press=)`, `ui.table(on_row_double_press=)`, `ui.table(on_row_context=)` | | `(dx, dy)` | How far the wheel turned, in lines. | `pz.canvas(on_scroll=)`, `pz.mouse_area(on_scroll=)` | | `(checked)` | The new state, `True` or `False`. | `ui.checkbox(on_toggle=)`, `ui.switch(on_toggle=)` | | `(line)` | The log line (a `ui.LogEntry`). | `ui.console(on_context=)`, `ui.console(on_open=)` | | `(name)` | A name: of the icon, or of the editor action. | `ui.editor(on_action=)`, `ui.icon_picker_dialog(on_pick=)` | | `(url)` | The URL of the link. | `ui.markdown(on_link=)`, `ui.reference_browser(on_open_url=)` | | `(size)` | The new size in pixels, when a drag ends. | `ui.panel(on_resize=)`, `pz.resizable(on_resize=)` | | `(item)` | The option that was chosen. | `ui.segmented(on_select=)`, `ui.select(on_select=)` | #### Events of each component **Own** counts the component's `on_*` events; **Total** adds the 18 common ones (and `mount` / `unmount` for `pz.Component` classes). | Component | Own | Total | Its events | | --- | --- | --- | --- | | [Mouse area](https://albakore.github.io/pyzed/library/mouse-area/#events) | 10 | 28 | `on_press`, `on_release`, `on_right_press`, `on_double_click`, `on_enter`, `on_exit`, `on_move`, `on_scroll`, `on_middle_press`, `on_middle_release` | | [Console](https://albakore.github.io/pyzed/library/console/#events) | 8 | 26 | `on_query`, `on_toggle_level`, `on_clear`, `on_copy`, `on_context`, `on_open`, `on_source`, `on_write` | | [Tab bar](https://albakore.github.io/pyzed/library/tab-bar/#events) | 7 | 25 | `on_select`, `on_close`, `on_drag`, `on_drop`, `on_drag_over`, `on_context`, `on_new` | | [Editor](https://albakore.github.io/pyzed/library/editor/#events) | 6 | 24 | `on_change`, `on_cursor`, `on_scroll`, `on_zoom`, `on_action`, `on_hover` | | [Canvas](https://albakore.github.io/pyzed/library/canvas/#events) | 5 | 23 | `on_press`, `on_release`, `on_right_press`, `on_move`, `on_scroll` | | [Drop zone](https://albakore.github.io/pyzed/library/drop-zone/#events) | 5 | 25 | `on_drop`, `on_files`, `on_reject`, `on_enter`, `on_leave` | | [Edits bar](https://albakore.github.io/pyzed/library/edits-bar/#events) | 5 | 25 | `on_keep`, `on_reject`, `on_keep_file`, `on_reject_file`, `on_open` | | [Window frame](https://albakore.github.io/pyzed/library/window-frame/#events) | 5 | 23 | `on_drag`, `on_minimize`, `on_maximize`, `on_close`, `on_menu` | | [Date picker](https://albakore.github.io/pyzed/library/date-picker/#events) | 4 | 24 | `on_change`, `on_open`, `on_close`, `on_dismiss` | | [Sign-in prompt](https://albakore.github.io/pyzed/library/auth-prompt/#events) | 4 | 24 | `on_pick`, `on_cancel`, `on_open_url`, `on_copy` | | [Table](https://albakore.github.io/pyzed/library/table/#events) | 4 | 22 | `on_sort`, `on_row_press`, `on_row_double_press`, `on_row_context` | | [Tree](https://albakore.github.io/pyzed/library/tree/#events) | 4 | 22 | `on_toggle`, `on_select`, `on_open`, `on_context` | | [Command palette](https://albakore.github.io/pyzed/library/command-palette/#events) | 3 | 21 | `on_query`, `on_submit`, `on_press` | | [Date range picker](https://albakore.github.io/pyzed/library/date-range-picker/#events) | 3 | 23 | `on_change`, `on_open`, `on_close` | | [Error dialog](https://albakore.github.io/pyzed/library/error-dialog/#events) | 3 | 21 | `on_close`, `on_go_to`, `on_logs` | | [Icon picker](https://albakore.github.io/pyzed/library/icon-picker/#events) | 3 | 23 | `on_change`, `on_pick`, `on_close` | | [Input](https://albakore.github.io/pyzed/library/input/#events) | 3 | 21 | `on_input`, `on_submit`, `on_clear` | | [List item](https://albakore.github.io/pyzed/library/list-item/#events) | 3 | 21 | `on_press`, `on_right_press`, `on_double_press` | | [Prompt](https://albakore.github.io/pyzed/library/prompt/#events) | 3 | 21 | `on_input`, `on_submit`, `on_cancel` | | [Select](https://albakore.github.io/pyzed/library/select/#events) | 3 | 21 | `on_select`, `on_open`, `on_close` | | [Time picker](https://albakore.github.io/pyzed/library/time-picker/#events) | 3 | 23 | `on_change`, `on_open`, `on_close` | | [Tool call](https://albakore.github.io/pyzed/library/tool-call/#events) | 3 | 23 | `on_allow`, `on_allow_always`, `on_deny` | | [Unsaved changes](https://albakore.github.io/pyzed/library/unsaved-changes/#events) | 3 | 21 | `on_save`, `on_discard`, `on_cancel` | | [Breadcrumbs](https://albakore.github.io/pyzed/library/breadcrumbs/#events) | 2 | 20 | `on_press`, `on_more` | | [Calendar](https://albakore.github.io/pyzed/library/calendar/#events) | 2 | 22 | `on_change`, `on_month` | | [Chip](https://albakore.github.io/pyzed/library/chip/#events) | 2 | 20 | `on_press`, `on_remove` | | [Color picker](https://albakore.github.io/pyzed/library/color-picker/#events) | 2 | 22 | `on_change`, `on_press` | | [Confirm](https://albakore.github.io/pyzed/library/confirm/#events) | 2 | 20 | `on_confirm`, `on_cancel` | | [Error boundary](https://albakore.github.io/pyzed/library/error-boundary/#events) | 2 | 20 | `on_error`, `on_details` | | [Number field](https://albakore.github.io/pyzed/library/number-field/#events) | 2 | 20 | `on_change`, `on_submit` | | [Picker](https://albakore.github.io/pyzed/library/picker/#events) | 2 | 20 | `on_query`, `on_submit` | | [Service row](https://albakore.github.io/pyzed/library/service-row/#events) | 2 | 22 | `on_toggle`, `on_primary` | | [Slider](https://albakore.github.io/pyzed/library/slider/#events) | 2 | 20 | `on_change`, `on_release` | | [Split button](https://albakore.github.io/pyzed/library/split-button/#events) | 2 | 20 | `on_press`, `on_menu` | | [Thread history](https://albakore.github.io/pyzed/library/thread-history/#events) | 2 | 22 | `on_open`, `on_delete` | | [Toast](https://albakore.github.io/pyzed/library/toast/#events) | 2 | 20 | `on_dismiss`, `on_press` | | [Accordion](https://albakore.github.io/pyzed/library/accordion/#events) | 1 | 19 | `on_toggle` | | [Alert](https://albakore.github.io/pyzed/library/alert/#events) | 1 | 19 | `on_close` | | [Badge](https://albakore.github.io/pyzed/library/badge/#events) | 1 | 19 | `on_press` | | [Button](https://albakore.github.io/pyzed/library/button/#events) | 1 | 19 | `on_press` | | [Callout](https://albakore.github.io/pyzed/library/callout/#events) | 1 | 19 | `on_dismiss` | | [Card](https://albakore.github.io/pyzed/library/card/#events) | 1 | 19 | `on_press` | | [Catalog](https://albakore.github.io/pyzed/library/catalog/#events) | 1 | 21 | `on_install` | | [Checkbox](https://albakore.github.io/pyzed/library/checkbox/#events) | 1 | 19 | `on_toggle` | | [Choice](https://albakore.github.io/pyzed/library/choice/#events) | 1 | 19 | `on_cancel` | | [Dialog layer](https://albakore.github.io/pyzed/library/dialog-layer/#events) | 1 | 19 | `on_dismiss` | | [Disclosure](https://albakore.github.io/pyzed/library/disclosure/#events) | 1 | 19 | `on_toggle` | | [File picker](https://albakore.github.io/pyzed/library/file-input/#events) | 1 | 19 | `on_change` | | [Icon button](https://albakore.github.io/pyzed/library/icon-button/#events) | 1 | 19 | `on_press` | | [Layer](https://albakore.github.io/pyzed/library/layer/#events) | 1 | 19 | `on_dismiss` | | [Markdown](https://albakore.github.io/pyzed/library/markdown/#events) | 1 | 19 | `on_link` | | [Menu](https://albakore.github.io/pyzed/library/menu/#events) | 1 | 19 | `on_press` | | [Modal](https://albakore.github.io/pyzed/library/modal/#events) | 1 | 19 | `on_close` | | [Panel](https://albakore.github.io/pyzed/library/panel/#events) | 1 | 19 | `on_resize` | | [Popover](https://albakore.github.io/pyzed/library/popover/#events) | 1 | 19 | `on_dismiss` | | [Radio group](https://albakore.github.io/pyzed/library/radio-group/#events) | 1 | 19 | `on_select` | | [Reference browser](https://albakore.github.io/pyzed/library/reference-browser/#events) | 1 | 19 | `on_open_url` | | [Resizable](https://albakore.github.io/pyzed/library/resizable/#events) | 1 | 19 | `on_resize` | | [Scrollable](https://albakore.github.io/pyzed/library/scrollable/#events) | 1 | 19 | `on_scroll` | | [Search input](https://albakore.github.io/pyzed/library/search-input/#events) | 1 | 19 | `on_input` | | [Segmented](https://albakore.github.io/pyzed/library/segmented/#events) | 1 | 19 | `on_select` | | [Shortcuts](https://albakore.github.io/pyzed/library/shortcuts/#events) | 1 | 19 | `on_query` | | [Split](https://albakore.github.io/pyzed/library/split/#events) | 1 | 19 | `on_resize` | | [Stat](https://albakore.github.io/pyzed/library/stat/#events) | 1 | 19 | `on_press` | | [Status bar](https://albakore.github.io/pyzed/library/status-bar/#events) | 1 | 19 | `on_press` | | [Switch](https://albakore.github.io/pyzed/library/switch/#events) | 1 | 19 | `on_toggle` | | [Title bar](https://albakore.github.io/pyzed/library/title-bar/#events) | 1 | 19 | `on_press` | | [Toggle group](https://albakore.github.io/pyzed/library/toggle-group/#events) | 1 | 19 | `on_select` | | [Virtual list](https://albakore.github.io/pyzed/library/virtual-list/#events) | 1 | 19 | `on_scroll` | | [Workspace](https://albakore.github.io/pyzed/library/workspace/#events) | 1 | 19 | `on_resize` | | [About](https://albakore.github.io/pyzed/library/about/#events) | 0 | 18 | — | | [Anchor layout](https://albakore.github.io/pyzed/library/anchor-layout/#events) | 0 | 18 | — | | [Animation](https://albakore.github.io/pyzed/library/animation/#events) | 0 | 18 | — | | [Avatar](https://albakore.github.io/pyzed/library/avatar/#events) | 0 | 18 | — | | [Bar chart](https://albakore.github.io/pyzed/library/bar-chart/#events) | 0 | 18 | — | | [Column](https://albakore.github.io/pyzed/library/column/#events) | 0 | 18 | — | | [Container](https://albakore.github.io/pyzed/library/container/#events) | 0 | 18 | — | | [Context meter](https://albakore.github.io/pyzed/library/context-meter/#events) | 0 | 18 | — | | [Count badge](https://albakore.github.io/pyzed/library/count-badge/#events) | 0 | 18 | — | | [Diff stat](https://albakore.github.io/pyzed/library/diff-stat/#events) | 0 | 18 | — | | [Diff view](https://albakore.github.io/pyzed/library/diff-view/#events) | 0 | 18 | — | | [Divider](https://albakore.github.io/pyzed/library/divider/#events) | 0 | 18 | — | | [Donut](https://albakore.github.io/pyzed/library/donut/#events) | 0 | 18 | — | | [Effects](https://albakore.github.io/pyzed/library/effects/#events) | 0 | 18 | — | | [Empty state](https://albakore.github.io/pyzed/library/empty-state/#events) | 0 | 18 | — | | [Facepile](https://albakore.github.io/pyzed/library/facepile/#events) | 0 | 18 | — | | [Field](https://albakore.github.io/pyzed/library/field/#events) | 0 | 18 | — | | [Flex](https://albakore.github.io/pyzed/library/flex/#events) | 0 | 18 | — | | [Grid](https://albakore.github.io/pyzed/library/grid/#events) | 0 | 18 | — | | [Heading](https://albakore.github.io/pyzed/library/heading/#events) | 0 | 18 | — | | [Icon](https://albakore.github.io/pyzed/library/icon/#events) | 0 | 18 | — | | [Image](https://albakore.github.io/pyzed/library/image/#events) | 0 | 18 | — | | [Indicator](https://albakore.github.io/pyzed/library/indicator/#events) | 0 | 18 | — | | [Kbd](https://albakore.github.io/pyzed/library/kbd/#events) | 0 | 18 | — | | [Keybinding hint](https://albakore.github.io/pyzed/library/keybinding-hint/#events) | 0 | 18 | — | | [Label](https://albakore.github.io/pyzed/library/label/#events) | 0 | 18 | — | | [List header](https://albakore.github.io/pyzed/library/list-header/#events) | 0 | 18 | — | | [Panel header](https://albakore.github.io/pyzed/library/panel-header/#events) | 0 | 18 | — | | [Plan](https://albakore.github.io/pyzed/library/plan/#events) | 0 | 20 | — | | [Progress](https://albakore.github.io/pyzed/library/progress/#events) | 0 | 18 | — | | [QR code](https://albakore.github.io/pyzed/library/qr-code/#events) | 0 | 18 | — | | [Responsive](https://albakore.github.io/pyzed/library/responsive/#events) | 0 | 18 | — | | [Rich text](https://albakore.github.io/pyzed/library/rich-text/#events) | 0 | 18 | — | | [Row](https://albakore.github.io/pyzed/library/row/#events) | 0 | 18 | — | | [Settings group](https://albakore.github.io/pyzed/library/settings-group/#events) | 0 | 18 | — | | [Shader](https://albakore.github.io/pyzed/library/shader/#events) | 0 | 18 | — | | [Space](https://albakore.github.io/pyzed/library/spacing/#events) | 0 | 18 | — | | [Sparkline](https://albakore.github.io/pyzed/library/sparkline/#events) | 0 | 18 | — | | [Spinner](https://albakore.github.io/pyzed/library/spinner/#events) | 0 | 18 | — | | [Stack](https://albakore.github.io/pyzed/library/stack/#events) | 0 | 18 | — | | [Terminal](https://albakore.github.io/pyzed/library/terminal/#events) | 0 | 18 | — | | [Text](https://albakore.github.io/pyzed/library/text/#events) | 0 | 18 | — | | [Toolbar](https://albakore.github.io/pyzed/library/toolbar/#events) | 0 | 18 | — | | [Tooltip](https://albakore.github.io/pyzed/library/tooltip/#events) | 0 | 18 | — | Next: [App, windows, forms and components](https://albakore.github.io/pyzed/guides/classes/) explains how to create your own events, and [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/) shows the three ways of handling them. ### About > An About box with icon, version, details and links. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/about/ :::tip[In plain words] The “About this app” box: name, version and links. ::: An About box: an icon, the app name and version, a description, a few `(label, value)` details and link buttons. `ui.about` · Category: [Code and tools](https://albakore.github.io/pyzed/library/#code-and-tools) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#about) **Code:** ```python ui.about("Notes", "1.2.0", "A small notes app built with Pyzed.", icon="notebook-pen", details=[("Python", "3.12"), ("Pyzed", "0.4")], links=[("Website", on_website), ("GitHub", on_github)]) ``` #### When to use - In a `ui.modal` opened from the Help menu. #### Usage ```python from pyzed import ui ui.about("Notes", "1.2.0", "A small notes app built with Pyzed.", icon="notebook-pen", details=[("Python", "3.12"), ("Pyzed", "0.4")], links=[("Website", on_website), ("GitHub", on_github)]) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content ```python ui.about("Notes", "1.2.0") ``` ```python ui.about("Notes", "1.2.0", "A small notes app.", icon="notebook-pen", details=[("Python", "3.12"), ("Pyzed", "0.4")]) ``` ```python ui.about("Notes", "1.2.0", icon="notebook-pen", links=[("Website", on_web), ("GitHub", on_gh)]) ``` #### Events About has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.about` ```python about( name, version='', description='', *, icon='app-window', details=(), links=(), theme=None, ) ``` An About box: icon, name, version, a description, a few details and link buttons. | Property | Default | Description | | --- | --- | --- | | `name` | *required* | App name. | | `version` | `""` | Version text. | | `description` | `""` | One or two sentences. | | `icon` | `"app-window"` | Lucide icon. | | `details` | `()` | `(label, value)` rows. | | `links` | `()` | `(label, on_press)` buttons. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#about) #### Related - [Modal](https://albakore.github.io/pyzed/library/modal/): A dialog surface with title, content and footer buttons. - [Shortcuts](https://albakore.github.io/pyzed/library/shortcuts/): A searchable list of key bindings, grouped. ### Accordion > Stacked collapsible sections, like the panels of a settings screen. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/accordion/ :::tip[In plain words] Several sections stacked up, each one opens or closes when you click its title. ::: Stacked collapsible sections separated by dividers. Sections are `(key, title, content)` tuples or `ui.AccordionSection` objects (with `icon` and a header `end` widget). You own the open state: `open` is a set of keys, a `{key: bool}` dict or `True`, and `on_toggle` receives the key. `ui.accordion` · Category: [Lists and menus](https://albakore.github.io/pyzed/library/#lists-and-menus) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#accordion) **Code:** ```python rows = lambda *items: pz.column([ui.field(k, ui.switch(v, on_toggle)) for k, v in items], spacing=8) pz.container(ui.accordion([ ("editor", "Editor", rows(("Soft wrap", True), ("Minimap", False))), ("files", "Files", rows(("Autosave", True))), ("git", "Git", ui.muted("Collapsed sections only show their header.")), ], open={"editor", "files"}, on_toggle=on_toggle, icons={"editor": "file-code", "files": "folder", "git": "git-branch"}), width=400) ``` #### When to use - For long settings or property panels where users only need a few sections at a time. - For a single collapsible block, use a [Disclosure](https://albakore.github.io/pyzed/library/disclosure/). #### Usage ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.open = {"editor"} def view(self): return ui.accordion( [("editor", "Editor", ui.muted("…")), ("files", "Files", ui.muted("…"))], open=self.open, on_toggle=lambda key: self.open.symmetric_difference_update({key}), ) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Open sections ```python pz.container(ui.accordion([("a", "Editor", ui.muted("…")), ("b", "Files", ui.muted("…")), ("c", "Git", ui.muted("…"))], open=set(), on_toggle=on_toggle), width=320) ``` ```python pz.container(ui.accordion([("a", "Editor", ui.muted("Font, wrapping, minimap.")), ("b", "Files", ui.muted("…"))], open={"a"}, on_toggle=on_toggle), width=320) ``` ```python pz.container(ui.accordion([("a", "Editor", ui.muted("Font, wrapping.")), ("b", "Files", ui.muted("Autosave."))], open=True, on_toggle=on_toggle), width=320) ``` ##### Content ```python pz.container(ui.accordion([("e", "Editor", ui.muted("…")), ("g", "Git", ui.muted("…"))], open=set(), icons={"e": "file-code", "g": "git-branch"}, on_toggle=on_toggle), width=320) ``` ```python pz.container(ui.accordion([ui.AccordionSection("p", "Problems", ui.muted("…"), icon="circle-x", end=ui.count_badge(3, tint="error"))], open=set(), on_toggle=on_toggle), width=320) ``` #### Events Accordion has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_toggle` | `(key)` | Called with the key of the clicked header. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Settings(pz.App): def __init__(self): self.open = {"editor"} def toggle(self, key): # on_toggle: the key of the clicked header self.open ^= {key} def view(self): return ui.accordion([("editor", "Editor", ui.muted("Font, tabs, wrapping")), ("files", "Files", ui.muted("Autosave, encoding"))], open=self.open, on_toggle=self.toggle) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Toggle: key: str class Settings(pz.App): open = frozenset({"editor"}) def update(self, message): match message: case Toggle(key): self.open = self.open ^ {key} def view(self): return ui.accordion([("editor", "Editor", ui.muted("…")), ("files", "Files", ui.muted("…"))], open=self.open, on_toggle=Toggle) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: open: frozenset = frozenset({"editor"}) def view(state, set_state): return ui.accordion([("editor", "Editor", ui.muted("…")), ("files", "Files", ui.muted("…"))], open=state.open, on_toggle=lambda key: set_state(open=state.open ^ {key})) ``` #### Properties and API ##### `ui.accordion` ```python accordion( sections, *, open=(), on_toggle=None, icons=None, spacing=8, padding=(6, 12, 10, 12), theme=None, ) ``` Stacked collapsible sections separated by dividers, like the panels of a settings screen. Each header spans the whole width, so the content lines up with the rest of the panel. You own the open state: change it in `on_toggle`. | Property | Default | Description | | --- | --- | --- | | `sections` | *required* | `ui.AccordionSection(key, title, content, icon=None, end=None)` items (or `(key, title, content)` tuples). | | `open` | `()` | The set of open ids, a `{id: bool}` dict, or `True` for all. | | `icons` | `None` | `{id: icon}`: a Lucide icon for each header. | | `spacing` | `8` | Space around the dividers between sections. | | `padding` | `(6, 12, 10, 12)` | Padding around the content of an open section. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_toggle` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#accordion) ##### `ui.AccordionSection` ```python class AccordionSection ``` One section of `ui.accordion`: `key`, `title`, `content` and optional `icon` and header `end` widget. | Property | Default | Description | | --- | --- | --- | | `key` | *required* | Key that identifies the subscription (defaults to the message or the function). | | `title` | *required* | Title. | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `end` | `None` | Widgets at the end (right). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#accordionsection) #### Related - [Disclosure](https://albakore.github.io/pyzed/library/disclosure/): One collapsible section with an arrow. - [Settings group](https://albakore.github.io/pyzed/library/settings-group/): A titled block of field rows, for settings screens. ### Alert > A message with one button. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/alert/ :::tip[In plain words] A message box with a single OK button. ::: A message with a single OK button. `kind` (`"info"`, `"success"`, `"warning"`, `"error"`) adds an icon. `ui.alert` · Category: [Dialogs](https://albakore.github.io/pyzed/library/#dialogs) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#alert) **Code:** ```python ui.dialog_layer(main_view(), ui.alert("The export finished: 128 files were written.", on_close, title="Export", kind="success", width=360)) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - To report something the user must acknowledge (an export finished, a file could not be opened). #### Usage ```python from pyzed import ui ui.dialog_layer(main_view(), ui.alert("The export finished: 128 files were written.", on_close, title="Export", kind="success", width=360)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Kind ```python ui.alert("The file was moved to the trash.", on_close, width=320) ``` ```python ui.alert("A new version is available.", on_close, kind="info", width=320) ``` ```python ui.alert("128 files were exported.", on_close, kind="success", title="Export", width=320) ``` ```python ui.alert("The disk is almost full.", on_close, kind="warning", width=320) ``` ```python ui.alert("notes.py could not be opened.", on_close, kind="error", title="Error", width=320) ``` ##### Options ```python ui.alert("Shortcuts changed in this version.", on_close, ok_label="Got it", width=320) ``` #### Events Alert has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_close` | `()` | The button and the close icon. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Export(pz.App): def __init__(self): self.done = True def view(self): alert = ui.alert("128 files were written.", lambda: setattr(self, "done", False), # on_close title="Export", kind="success") return ui.dialog_layer(main_view(), alert if self.done else None) ``` **Declarative:** ```python class Export(pz.App): done = True def update(self, message): if message == "ok": self.done = False def view(self): return ui.dialog_layer(main_view(), ui.alert("128 files were written.", "ok", title="Export") if self.done else None) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: done: bool = True def view(state, set_state): alert = ui.alert("128 files were written.", lambda: set_state(done=False), title="Export") return ui.dialog_layer(main_view(), alert if state.done else None) ``` #### Properties and API ##### `ui.alert` ```python alert(message, on_close, *, title=None, kind=None, ok_label=None, width=400, theme=None) ``` A message with one button. | Property | Default | Description | | --- | --- | --- | | `message` | *required* | Text or widget. | | `title` | `None` | Title. | | `kind` | `None` | `"info"`, `"success"`, `"warning"` or `"error"` adds an icon. | | `ok_label` | `None` | Button text (default: OK, translated). | | `width` | `400` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_close` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#alert) #### Related - [Confirm](https://albakore.github.io/pyzed/library/confirm/): A yes/no question, red for destructive actions. - [Dialog layer](https://albakore.github.io/pyzed/library/dialog-layer/): Centers a dialog over your view with a dimmed backdrop. - [Toast](https://albakore.github.io/pyzed/library/toast/): Notifications stacked in the bottom-right corner. ### Anchor layout > Free positions like a form designer, with anchors that follow resizes. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/anchor-layout/ :::tip[In plain words] Places things exactly where you put them, and says which corner each one sticks to when the window grows. ::: Places children at exact positions, the way a visual form designer does, and says how each one follows a resize: `pz.anchor(child, x=, y=, width=, height=, anchor=)` keeps the distance to the edges it is anchored to, stretches between opposite edges and stays centered otherwise. Presets such as `"top_right"`, `"bottom_wide"`, `"center"` or `"full"` cover the usual cases. `pyzed.anchors.place` is the math behind it, for your own layouts. `pz.anchor_layout` · Category: [Layout](https://albakore.github.io/pyzed/library/#layout) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#anchor_layout) **Code:** ```python t = pz.theme.current() pz.container(pz.anchor_layout([ pz.anchor(ui.heading("Report", level=3), x=16, y=12, width=200, height=28, anchor="top_left"), pz.anchor(ui.icon_button("x", on_close), x=356, y=12, width=28, height=28, anchor="top_right"), pz.anchor(ui.input("", on_input, placeholder="Search…"), x=16, y=56, width=368, height=30, anchor="top_wide"), pz.anchor(ui.button("Close", on_close, variant="solid"), x=284, y=172, width=100, height=30, anchor="bottom_right"), ], design=(400, 216)), width=400, height=216, bg=t.panel, border_color=t.border, border_width=1, radius=8) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - For forms drawn in Pyzed Design (it is what the Form view generates) or ported from Visual Basic, Qt Designer or WinForms. - For game HUDs and overlays where things stick to corners. - For forms that grow and shrink with the window, prefer `row`, `column` and `grid`. #### Usage ```python import pyzed as pz from pyzed import ui class Report(pz.App): def view(self): return pz.anchor_layout([ pz.anchor(ui.heading("Report"), x=16, y=12, width=368, height=28, anchor="top_wide"), pz.anchor(self.table(), x=16, y=48, width=368, height=196, anchor="full"), pz.anchor(ui.button("Close", self.close), x=284, y=252, width=100, height=32, anchor="bottom_right"), ], design=(400, 300)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Presets Each layout was designed at 110 × 44 and drawn at 260 × 110: the badge keeps its distance to the edges it is anchored to. ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(ui.badge("top_left"), x=10, y=10, width=90, height=24, anchor="top_left")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(ui.badge("top_right"), x=10, y=10, width=90, height=24, anchor="top_right")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(ui.badge("bottom_left"), x=10, y=10, width=90, height=24, anchor="bottom_left")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(ui.badge("bottom_right"), x=10, y=10, width=90, height=24, anchor="bottom_right")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(ui.badge("center"), x=10, y=10, width=90, height=24, anchor="center")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ##### Stretching ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(pz.container(width="fill", height="fill", bg=t.accent, radius=4), x=10, y=10, width=90, height=24, anchor="top_wide")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(pz.container(width="fill", height="fill", bg=t.accent, radius=4), x=10, y=10, width=90, height=24, anchor="bottom_wide")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(pz.container(width="fill", height="fill", bg=t.accent, radius=4), x=10, y=10, width=90, height=24, anchor="left_tall")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(pz.container(width="fill", height="fill", bg=t.accent, radius=4), x=10, y=10, width=90, height=24, anchor="center_wide")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(pz.container(width="fill", height="fill", bg=t.accent, radius=4), x=10, y=10, width=90, height=24, anchor="full")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` #### Events Anchor layout has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Tips :::tip `design` is the size the positions were measured in; when the layout gets more or less room, each child moves or stretches from there. Try it live: drag the window corner of a form in Pyzed Design. ::: #### Properties and API ##### `pz.anchor_layout` ```python anchor_layout(children, *, design=None, width='fill', height='fill') ``` Places children made with `pz.anchor` freely, like a designed form: when it is resized, each child keeps its distance to the edges it is anchored to, stretches between opposite ones and stays centered otherwise. Later children are drawn on top. See [Anchored layouts](https://albakore.github.io/pyzed/guides/layout/#anchored-layouts). | Property | Default | Description | | --- | --- | --- | | `children` | *required* | Children made with `pz.anchor(...)` (`None` is skipped). | | `design` | `None` | `(width, height)` the children were laid out in; by default the smallest size that holds them. | | `width` | `"fill"` | Width. | | `height` | `"fill"` | Height. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#anchor_layout) ##### `pz.anchor` ```python anchor( content, *, x=0, y=0, width=100, height=28, anchor='top_left', min_width=0, min_height=0, max_width=0, max_height=0, pivot=(0.5, 0.5), rotate=0, ) ``` Places a child of `pz.anchor_layout` at `x`, `y` with `width` and `height` (in the layout's design size) and says how it follows a resize. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | The widget. | | `x` | `0` | Left edge in the design size. | | `y` | `0` | Top edge in the design size. | | `width` | `100` | Width in the design size. | | `height` | `28` | Height in the design size. | | `anchor` | `"top_left"` | A preset of `pyzed.anchors.PRESETS` (`"top_right"`, `"bottom_wide"`, `"center"`, `"full"`...), words (`"left right bottom"`) or a tuple of edges. | | `min_width` | `0` | Smallest width when it stretches (`0`: no limit). | | `min_height` | `0` | Smallest height when it stretches. | | `max_width` | `0` | Largest width when it stretches (`0`: no limit). | | `max_height` | `0` | Largest height when it stretches. | | `pivot` | `(0.5, 0.5)` | `(x, y)` from `0` to `1`: the point that stays put when a limit stops it, and that `rotate` turns around. | | `rotate` | `0` | Degrees clockwise. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#anchor) ##### `pz.anchors.place` ```python place( rect, anchor, design, actual, *, min_size=(0, 0), max_size=(0, 0), pivot=(0.5, 0.5), ) ``` Where a box designed in a parent of size `design` goes in a parent of size `actual`, following its anchor, with size limits and a pivot. The math behind `pz.anchor_layout` and designed forms. | Property | Default | Description | | --- | --- | --- | | `rect` | *required* | `(x, y, width, height)` in the design size. | | `anchor` | *required* | A preset, words or edges. | | `design` | *required* | The parent's design size. | | `actual` | *required* | The parent's current size. | | `min_size` | `(0, 0)` | `(width, height)` lower limits (`0`: none). | | `max_size` | `(0, 0)` | `(width, height)` upper limits (`0`: none). | | `pivot` | `(0.5, 0.5)` | The point that stays put when a limit stops the box from stretching. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#place) #### Related - [Stack](https://albakore.github.io/pyzed/library/stack/): Layers children on top of each other; `pin` places one at x, y. - [Grid](https://albakore.github.io/pyzed/library/grid/): Equal columns, or CSS-like tracks with spans. - [Container](https://albakore.github.io/pyzed/library/container/): Size, padding, alignment, background, border and shadow for one child. ### Animation > Presets, keyframes and transitions for any widget. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/animation/ :::tip[In plain words] Makes things move: fade in, slide, bounce or glow, without extra work. ::: Every widget has `animate()` and `transition()`. `animate` plays one of 47 presets or a `pz.Animation` when the widget appears, on hover, press, focus, click or while `active`; `transition` animates property changes like CSS. `pz.stagger` cascades a list in and `pz.presence` animates content in and out when it changes. Everything runs natively, without calling Python per frame. See the [Animation guide](https://albakore.github.io/pyzed/animation/). `pz.Animation` · Category: [Content](https://albakore.github.io/pyzed/library/#content) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#animation-1) **Code:** ```python pz.column(pz.stagger([ ui.card(ui.label(name), width=320).animate("lift", on="hover") for name in ("fade_in_up", "lift on hover", "stagger") ], "fade_in_up", step=60), spacing=10) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - To show that something appeared, changed or needs attention: a toast, a new row, an error shake. - For hover and press feedback on cards and custom buttons. - For page and tab transitions with `pz.presence`. #### Usage ```python import pyzed as pz from pyzed import ui class Inbox(pz.App): errors = 0 page = 0 def view(self): return pz.column([ ui.input(self.query, self.search).animate("shake", key=self.errors), pz.presence(self.page_view(), key=self.page, enter="fade_in_right", exit="fade_out"), ui.button("Record", self.record).animate("glow_pulse", active=self.recording), ], spacing=12) ``` #### Events Animation has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Tips :::tip Screenshots and `pz.set_reduced_motion(True)` show every animation at rest, so tests are stable. Respect users who turn motion off. ::: #### Properties and API ##### `pz.Animation` ```python @dataclass class Animation(keyframes=(), duration=300, delay=0, easing='ease', repeat=1, direction='normal', fill='both', on='mount', category='custom', description='', _frames=()) ``` A keyframe animation, like a CSS `@keyframes` rule with its timing. [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#animation-1) ##### `pz.stagger` ```python stagger(items, animation='fade_in_up', *, step=40, delay=0, **options) ``` `items` with the same animation, each starting `step` milliseconds after the previous one (a list that cascades in):: [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#stagger) ##### `pz.presence` ```python presence( content, key, *, enter='fade_in', exit='fade_out', id=None, duration=None, easing=None, ) ``` Shows `content` and, when `key` changes, animates the old content out (`exit`) while the new one comes in (`enter`), like page transitions:: [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#presence) ##### `pz.define_animation` ```python define_animation(name, animation=None, **options) ``` Names an animation so `animate(name)` plays it; returns it. [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#define_animation) ##### `pz.set_reduced_motion` ```python set_reduced_motion(on=True) ``` Turns motion off (`True`) or back on: animations show their resting state at once. [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#set_reduced_motion) #### Related - [Effects](https://albakore.github.io/pyzed/library/effects/): Glow, bloom, glass and opacity as style properties. - [Card](https://albakore.github.io/pyzed/library/card/): A bordered surface with an optional title and actions. - [Toast](https://albakore.github.io/pyzed/library/toast/): Notifications stacked in the bottom-right corner. ### Avatar > A round avatar with initials in a collaborator color. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/avatar/ :::tip[In plain words] A circle with someone's initials, in their own color. ::: A circle with initials. `index` picks one of the theme's collaborator (player) colors, so the same person keeps the same color; `color` sets it directly. `ui.avatar` · Category: [Display](https://albakore.github.io/pyzed/library/#display) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#avatar) **Code:** ```python pz.row([ ui.avatar("AL", index=0), ui.avatar("KV", index=1), ui.avatar("MZ", index=2, size=28), ui.avatar("JD", index=3, size=36), ], spacing=8, align="center") ``` #### When to use - For the account in the title bar, comment authors or collaborators. - For several people at once use [Facepile](https://albakore.github.io/pyzed/library/facepile/). #### Usage ```python import pyzed as pz from pyzed import ui pz.row([ ui.avatar("AL", index=0), ui.avatar("KV", index=1), ui.avatar("MZ", index=2, size=28), ui.avatar("JD", index=3, size=36), ], spacing=8, align="center") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Collaborator color ```python ui.avatar("AL", index=0) ``` ```python ui.avatar("KV", index=1) ``` ```python ui.avatar("MZ", index=2) ``` ```python ui.avatar("JD", index=3) ``` ```python ui.avatar("SR", index=4) ``` ```python ui.avatar("PB", index=5) ``` ##### Size ```python ui.avatar("KV", size=18) ``` ```python ui.avatar("KV", size=22) ``` ```python ui.avatar("KV", size=32) ``` ```python ui.avatar("KV", size=48) ``` ##### Color ```python ui.avatar("PZ", color="#c678dd", size=32) ``` ```python ui.avatar("UI", color="#e5c07b", size=32) ``` ##### States and events `image=` shows a picture instead of the initials and `tooltip=` names the person on hover. ```python ui.avatar("KK", status="online", size=32) ``` ```python ui.avatar("AL", status="away", index=2, size=32) ``` ```python ui.avatar("MZ", status="busy", index=4, size=32) ``` #### Events Avatar has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.avatar` ```python avatar( initials, *, color=None, size=22, index=0, image=None, status=None, tooltip=None, theme=None, ) ``` A round avatar with initials. | Property | Default | Description | | --- | --- | --- | | `initials` | *required* | Initials (the first two are used). | | `color` | `None` | Background color. | | `size` | `22` | Diameter. | | `index` | `0` | Picks a collaborator color from the theme (`theme.players`). | | `image` | `None` | A picture (path or bytes) instead of the initials. | | `status` | `None` | Presence dot: `"online"`, `"away"`, `"busy"`, `"offline"` or a color. | | `tooltip` | `None` | Text shown on hover (the person's name). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#avatar) #### Related - [Facepile](https://albakore.github.io/pyzed/library/facepile/): Overlapping collaborator avatars with +N. - [Title bar](https://albakore.github.io/pyzed/library/title-bar/): The top bar with project and branch pickers. ### Badge > A small pill for statuses, versions and counters. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/badge/ :::tip[In plain words] A tiny colored pill with a word or two, like “beta” or “3 errors”. ::: A small pill with text and an optional icon. `tint` picks a semantic color and `variant` is `"soft"` (default), `"solid"` or `"outline"`. `ui.badge` · Category: [Display](https://albakore.github.io/pyzed/library/#display) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#badge) **Code:** ```python pz.row([ ui.badge("accent"), ui.badge("success", tint="success"), ui.badge("warning", tint="warning"), ui.badge("error", tint="error", icon="circle-x"), ], spacing=8) ``` #### When to use - For short read-only statuses: `beta`, `M` (modified), `v0.4`, `3 errors`. - For a round number on top of an icon use [Count badge](https://albakore.github.io/pyzed/library/count-badge/). #### Usage ```python import pyzed as pz from pyzed import ui pz.row([ ui.badge("accent"), ui.badge("success", tint="success"), ui.badge("warning", tint="warning"), ui.badge("error", tint="error", icon="circle-x"), ], spacing=8) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Tint ```python ui.badge("accent") ``` ```python ui.badge("passed", tint="success") ``` ```python ui.badge("pending", tint="warning") ``` ```python ui.badge("failed", tint="error") ``` ```python ui.badge("info", tint="info") ``` ```python ui.badge("custom", tint="#c678dd") ``` ##### Variant ```python ui.badge("soft") ``` ```python ui.badge("solid", variant="solid") ``` ```python ui.badge("outline", variant="outline") ``` ##### With an icon ```python ui.badge("new", icon="sparkles") ``` ```python ui.badge("3 errors", tint="error", icon="circle-x") ``` ```python ui.badge("verified", tint="success", variant="solid", icon="check") ``` ##### States and events ```python ui.badge("beta", size="sm") ``` ```python ui.badge("Pro", size="lg", icon="star") ``` ```python ui.badge("3 issues", tint="warning", on_press=on_press) ``` #### Events Badge has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_press` | `()` | Makes it clickable. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Inbox(pz.App): def __init__(self): self.unread = 3 def view(self): return ui.badge(f"{self.unread} new", tint="accent", on_press=lambda: setattr(self, "unread", 0)) # on_press makes it clickable ``` **Declarative:** ```python class Inbox(pz.App): unread = 3 def update(self, message): if message == "read all": self.unread = 0 def view(self): return ui.badge(f"{self.unread} new", on_press="read all") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: unread: int = 3 def view(state, set_state): return ui.badge(f"{state.unread} new", on_press=lambda: set_state(unread=0)) ``` #### Properties and API ##### `ui.badge` ```python badge( content, *, tint='accent', variant='soft', icon=None, size='md', on_press=None, theme=None, ) ``` A small pill for statuses and counters. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `tint` | `"accent"` | Semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` or a token. | | `variant` | `"soft"` | `"soft"`, `"solid"` or `"outline"`. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `size` | `"md"` | `"sm"`, `"md"` or `"lg"`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_press` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#badge) #### Related - [Count badge](https://albakore.github.io/pyzed/library/count-badge/): A round counter that shows 99+ above its max. - [Indicator](https://albakore.github.io/pyzed/library/indicator/): A colored dot for status, presence or unsaved changes. - [Chip](https://albakore.github.io/pyzed/library/chip/): A compact tag: clickable as a filter, removable with an ✕. ### Bar chart > A vertical bar chart with labels. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/bar-chart/ :::tip[In plain words] A chart of bars to compare a few amounts. ::: A vertical bar chart drawn with `pz.canvas` in the theme colors, with optional labels under the bars. `ui.bar_chart` · Category: [Data](https://albakore.github.io/pyzed/library/#data) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#bar_chart) **Code:** ```python ui.bar_chart([12, 19, 8, 15, 22, 17, 9], labels=["M", "T", "W", "T", "F", "S", "S"], width=360, height=180) ``` #### When to use - To compare a handful of values side by side (per day, per category). #### Usage ```python from pyzed import ui ui.bar_chart([12, 19, 8, 15, 22, 17, 9], labels=["M", "T", "W", "T", "F", "S", "S"], width=360, height=180) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python ui.bar_chart([12, 19, 8, 15, 22], labels=["M", "T", "W", "T", "F"], width=240, height=120) ``` ```python ui.bar_chart([4, 8, 6, 9, 3, 7, 5, 10], width=240, height=100) ``` ```python ui.bar_chart([5, 9, 7, 12], labels=["Q1", "Q2", "Q3", "Q4"], width=240, height=120, tint="success") ``` ```python ui.bar_chart([30, 12, 18], labels=["cpu", "io", "net"], width=240, height=120, tint="warning") ``` #### Events Bar chart has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.bar_chart` ```python bar_chart(values, *, labels=None, width=320, height=160, tint='accent', theme=None) ``` A vertical bar chart with optional labels. | Property | Default | Description | | --- | --- | --- | | `values` | *required* | Values. | | `labels` | `None` | Labels under each bar. | | `width` | `320` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `160` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `tint` | `"accent"` | Semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` or a token. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#bar_chart) #### Related - [Donut](https://albakore.github.io/pyzed/library/donut/): A donut chart with a label in the middle. - [Sparkline](https://albakore.github.io/pyzed/library/sparkline/): A tiny line chart for trends. - [Stat](https://albakore.github.io/pyzed/library/stat/): A dashboard number with change and sparkline. ### Breadcrumbs > `src › main.rs › fn main`, optionally clickable. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/breadcrumbs/ :::tip[In plain words] Shows where you are, step by step: Folder › Subfolder › File. ::: The path to the current location. Parts are strings or `(icon, text)` tuples; `on_press` receives the index of the clicked part. `ui.breadcrumbs` · Category: [Navigation](https://albakore.github.io/pyzed/library/#navigation) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#breadcrumbs) **Code:** ```python pz.column([ ui.breadcrumbs(["src", "ui", "button.rs"]), ui.breadcrumbs([("folder", "src"), ("file-code", "main.rs"), "fn main"], on_press=on_press), ui.breadcrumbs(["Settings", "Editor", "Font"], separator="/"), ], spacing=14) ``` #### When to use - Above an editor or a nested view, to show where you are and jump to a parent. #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ ui.breadcrumbs(["src", "ui", "button.rs"]), ui.breadcrumbs([("folder", "src"), ("file-code", "main.rs"), "fn main"], on_press=on_press), ui.breadcrumbs(["Settings", "Editor", "Font"], separator="/"), ], spacing=14) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python ui.breadcrumbs(["src", "ui", "button.rs"]) ``` ```python ui.breadcrumbs([("folder", "src"), ("file-code", "main.rs"), "fn main"]) ``` ```python ui.breadcrumbs(["Settings", "Editor", "Font"], separator="/") ``` ```python ui.breadcrumbs(["Home", "Projects", "pyzed"], on_press=on_press) ``` ##### States and events ```python ui.breadcrumbs(["pyzed", "python", "pyzed", "ui", "lists.py"], max_items=3, on_press=on_press, on_more=on_more) ``` #### Events Breadcrumbs has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_press` | `(index)` | Called with the index of the clicked part. | | `on_more` | `()` | Makes the `…` clickable. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Browser(pz.App): def __init__(self): self.path = ["home", "ada", "projects", "pyzed"] def go(self, index): # on_press: the index of the clicked part self.path = self.path[:index + 1] def view(self): return ui.breadcrumbs(self.path, on_press=self.go, max_items=3, on_more=lambda: self.go(0)) # the … of the hidden parts ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class GoTo: index: int class Browser(pz.App): path = ["home", "ada", "projects", "pyzed"] def update(self, message): match message: case GoTo(index): self.path = self.path[:index + 1] def view(self): return ui.breadcrumbs(self.path, on_press=GoTo) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: path: tuple = ("home", "ada", "projects", "pyzed") def view(state, set_state): return ui.breadcrumbs(list(state.path), on_press=lambda i: set_state(path=state.path[:i + 1])) ``` #### Properties and API ##### `ui.breadcrumbs` ```python breadcrumbs( parts, *, separator='›', on_press=None, max_items=None, on_more=None, theme=None, ) ``` `src › main.rs › fn main`. Parts can be strings or `(icon, text)` tuples. | Property | Default | Description | | --- | --- | --- | | `parts` | *required* | The parts. | | `separator` | `"›"` | Separator. | | `max_items` | `None` | Keeps the first part and the last ones and shows `…` for the rest. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_press`, `on_more` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#breadcrumbs) #### Related - [Toolbar](https://albakore.github.io/pyzed/library/toolbar/): The bar under the tabs: breadcrumbs and buffer actions. - [Tab bar](https://albakore.github.io/pyzed/library/tab-bar/): Editor tabs with icons, dirty dots and close buttons. ### Button > The themed button: variants, sizes, icons and shortcuts. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/button/ :::tip[In plain words] A button is something you click to make the app do something: save, send, open, delete. ::: Displays a themed button. It reads its colors from the current theme, can carry an icon and a keyboard shortcut, and comes in several variants and sizes. Any style parameter of `pz.button` (`bg`, `hover_bg`, `color`, `shadow`, `radius`…) overrides the theme's. `ui.button` · Category: [Actions](https://albakore.github.io/pyzed/library/#actions) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#button) **Code:** ```python pz.row([ ui.button("Cancel", on_cancel, variant="ghost"), ui.button("Save", on_save, icon="save", variant="solid", keys="ctrl-s"), ], spacing=8) ``` #### When to use - For the main actions of a form, dialog or toolbar. - Use `variant="solid"` for the one primary action and `"ghost"` for secondary ones. - For a button with only an icon, use [Icon button](https://albakore.github.io/pyzed/library/icon-button/). #### Usage ```python import pyzed as pz from pyzed import ui class Editor(pz.App): def save(self): ... def view(self): return ui.button("Save", self.save, icon="save", keys="ctrl-s") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Variant `filled` is the default; use `solid` for the one main action of a screen. ```python ui.button("Filled", on_press) ``` ```python ui.button("Ghost", on_press, variant="ghost") ``` ```python ui.button("Subtle", on_press, variant="subtle") ``` ```python ui.button("Outline", on_press, variant="outline") ``` ```python ui.button("Solid", on_press, variant="solid") ``` ```python ui.button("Tinted", on_press, variant="tinted") ``` ##### Semantic colors A color name as `variant` gives a tinted button; `tint=` colors a `solid` one. ```python ui.button("Upgrade", on_press, variant="accent", icon="sparkles") ``` ```python ui.button("Done", on_press, variant="success", icon="check") ``` ```python ui.button("Careful", on_press, variant="warning", icon="triangle-alert") ``` ```python ui.button("Delete", on_press, variant="error", icon="trash-2") ``` ```python ui.button("Details", on_press, variant="info", icon="info") ``` ```python ui.button("Delete forever", on_press, variant="solid", tint="error") ``` ##### Size ```python ui.button("Extra small", on_press, size="xs") ``` ```python ui.button("Small", on_press, size="sm") ``` ```python ui.button("Medium", on_press, size="md") ``` ```python ui.button("Large", on_press, size="lg") ``` ##### Content ```python ui.button("Save", on_press, icon="save") ``` ```python ui.button("Next", on_press, icon="arrow-right", icon_position="end") ``` ```python ui.button("Save", on_press, icon="save", keys="ctrl-s") ``` ```python ui.button("Online", on_press, icon="circle", icon_color="success") ``` ```python ui.button(icon="plus", on_press=on_press) ``` ##### State A button without `on_press` is drawn disabled, so unfinished actions look unfinished. ```python ui.button("Selected", on_press, selected=True) ``` ```python ui.button("Disabled", on_press, disabled=True) ``` ```python ui.button("Not wired yet") ``` ```python pz.container(ui.button("Sign in", on_press, variant="solid", full_width=True), width=280) ``` ##### Style overrides The style arguments of `pz.button` (`bg`, `hover_bg`, `pressed_bg`, `color`, `border_color`, `shadow`, `radius`, `padding`) replace the theme's values for that button only. ```python ui.button("Lifted", on_press, variant="solid", shadow=("#00000066", (0, 4), 12)) ``` ```python ui.button("Custom", on_press, bg="#c678dd", hover_bg="#d08ae6", color="#ffffff") ``` ```python ui.button("Gold border", on_press, variant="outline", border_color="#e5c07b") ``` ```python ui.button("Pill", on_press, variant="solid", radius=999) ``` ##### States and events ```python ui.button("Saving", on_press, icon="save", loading=True) ``` ```python ui.button("Save", on_press, tooltip="Writes the file") ``` ##### Base widget The `pz.*` widget underneath: no theme, every color and size is yours. ```python pz.button("pz.button", on_press, padding=(6, 14), bg="#3a7bc8", color="#ffffff", radius=6) ``` ```python pz.button(pz.row([pz.icon("heart", size=14), pz.text("Like")], spacing=6, align="center"), on_press, padding=(6, 12), bg="#c678dd22", color="#c678dd", border_color="#c678dd", border_width=1, radius=999) ``` #### Examples ##### Full width `full_width=True` stretches the button to its container. **Code:** ```python pz.column([ ui.button("Sign in", on_press, variant="solid", full_width=True), ui.button("Create an account", on_press, variant="outline", full_width=True), ], spacing=8, width=320) ``` #### Events Button has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_press` | `ui.button`, `pz.button` | `()` | Message or callable sent on press. `None` disables the control. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Editor(pz.App): def __init__(self): self.saved = 0 def save(self): # on_press: called with nothing self.saved += 1 def view(self): return pz.row([ ui.button("Save", self.save, icon="save", variant="solid", keys="ctrl-s"), ui.button("Reset", lambda: setattr(self, "saved", 0), variant="ghost"), ui.label(f"Saved {self.saved} times"), ], spacing=8, align="center") ``` **Declarative:** ```python class Editor(pz.App): saved = 0 def update(self, message): # every button sends a message here match message: case "save": self.saved += 1 case "reset": self.saved = 0 def view(self): return pz.row([ ui.button("Save", "save", icon="save", variant="solid", keys="ctrl-s"), ui.button("Reset", "reset", variant="ghost"), ui.label(f"Saved {self.saved} times"), ], spacing=8, align="center") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: saved: int = 0 def view(state, set_state): # a pure function: state in, widgets out return pz.row([ ui.button("Save", lambda: set_state(saved=state.saved + 1), icon="save", variant="solid"), ui.button("Reset", lambda: set_state(saved=0), variant="ghost"), ui.label(f"Saved {state.saved} times"), ], spacing=8, align="center") ``` #### Tips :::tip To pass a value to the callback, bind it with a default argument: `ui.button(name, lambda n=name: self.open(n))`. ::: #### Properties and API ##### `ui.button` ```python button( content=None, on_press=None, *, icon=None, icon_position='start', icon_color=None, variant='filled', tint=None, size='md', selected=False, disabled=False, full_width=False, keys=None, loading=False, tooltip=None, theme=None, **style, ) ``` A themed button with variants, sizes, an icon and a shortcut. | Property | Default | Description | | --- | --- | --- | | `content` | `None` | Text or widget. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `icon_position` | `"start"` | `"start"` or `"end"`. | | `icon_color` | `None` | Icon color: a theme token or a hex color. | | `variant` | `"filled"` | `"filled"`, `"ghost"`, `"subtle"`, `"outline"`, `"solid"`, `"tinted"`, or a semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"`. | | `tint` | `None` | Color for the `solid` and `tinted` variants. | | `size` | `"md"` | `"xs"`, `"sm"`, `"md"` or `"lg"`. | | `selected` | `False` | Marks it as selected. | | `disabled` | `False` | Disables it (ignores clicks). | | `full_width` | `False` | Fills the full width. | | `keys` | `None` | Shortcut shown at the end, written as a string: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `loading` | `False` | Shows a spinner in place of the icon and ignores presses (keep a `pz.every(1/30, ...)` subscription while it spins). | | `tooltip` | `None` | Text shown on hover. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | | `**style` | — | Any `pyzed.button` style parameter (`bg`, `hover_bg`, `color`…). | Events: `on_press` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#button) ##### `pz.button` ```python button( content, on_press=None, *, padding=None, width=None, height=None, bg=None, color=None, border_color=None, border_width=0.0, radius=None, shadow=None, hover_bg=None, hover_color=None, hover_border_color=None, pressed_bg=None, disabled_color=None, clip=False, ) ``` A button with no predefined style. For the themed button use `ui.button`. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Button content. | | `padding` | `None` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `color` | `None` | Text or icon color. | | `border_color` | `None` | Border color. | | `border_width` | `0.0` | Border width in pixels. | | `radius` | `None` | Corner radius: a number or `(top_left, top_right, bottom_right, bottom_left)`. | | `shadow` | `None` | Shadow: `(color, (dx, dy), blur)`. | | `hover_bg` | `None` | Background on hover. | | `hover_color` | `None` | Text color on hover. | | `hover_border_color` | `None` | Border color on hover. | | `pressed_bg` | `None` | Background while pressed. | | `disabled_color` | `None` | Text color when disabled. | | `clip` | `False` | Clips content that overflows the bounds. | Events: `on_press` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#button) #### Related - [Icon button](https://albakore.github.io/pyzed/library/icon-button/): A square button with just an icon and an optional tooltip. - [Split button](https://albakore.github.io/pyzed/library/split-button/): A main action plus an arrow that opens more options. - [Toggle group](https://albakore.github.io/pyzed/library/toggle-group/): An icon-only segmented control. - [Tooltip](https://albakore.github.io/pyzed/library/tooltip/): A hint shown on hover, optionally with its shortcut. ### Calendar > A month grid to pick a day, a range or several days, always on screen. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/calendar/ :::tip[In plain words] A month on screen, like a wall calendar, where you click days to choose them. ::: `ui.Calendar` shows one or more months. `mode` picks a single day (`"single"`), a range (`"range"`, two clicks) or any number of days (`"multiple"`, each click toggles one). The header switches to a grid of months and then of years to jump far, and the arrows of the keyboard move the focused day (Page Up/Page Down by month, Enter picks). The pickers on this page open one of these in a popup. `ui.Calendar` · Category: [Dates and times](https://albakore.github.io/pyzed/library/#dates-and-times) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#calendar) **Code:** ```python pz.row([ ui.Calendar(value="2026-10-22", today_button=True, on_change=on_change), ui.Calendar(value=("2026-10-05", "2026-10-14"), mode="range", disabled=["weekend"], on_change=on_change), ], spacing=24) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - In a side panel or a dashboard, where the calendar stays visible. - `first_weekday` (`"monday"`, `"sunday"`...) and `week_numbers=True` follow local customs. - Inside a form field, use [Date picker](https://albakore.github.io/pyzed/library/date-picker/) instead. #### Usage ```python import pyzed as pz from pyzed import ui class Planner(pz.App): def __init__(self): self.days = [] def view(self): return pz.row([ ui.Calendar(value=self.days, mode="multiple", week_numbers=True, on_change=lambda e: setattr(self, "days", e.value)), pz.column([ui.label(ui.format_date(d, "ddd D MMM")) for d in self.days], spacing=4), ], spacing=16) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Mode ```python ui.Calendar(value="2026-10-22", on_change=on_change) ``` ```python ui.Calendar(value=("2026-10-05", "2026-10-14"), mode="range", on_change=on_change) ``` ```python ui.Calendar(value=["2026-10-06", "2026-10-13", "2026-10-20", "2026-10-27"], mode="multiple", on_change=on_change) ``` ##### Options ```python ui.Calendar(value="2026-10-22", week_numbers=True, first_weekday="monday", on_change=on_change) ``` ```python ui.Calendar(value="2026-10-22", first_weekday="sunday", on_change=on_change) ``` ```python ui.Calendar(value="2026-10-22", min="2026-10-08", max="2026-10-28", disabled=["weekend"], on_change=on_change) ``` ```python ui.Calendar(value="2026-10-22", show_outside=False, today_button=True, on_change=on_change) ``` ##### Views A click on the title goes to the months, and another one to the years. ```python ui.Calendar(value=("2026-10-26", "2026-11-04"), mode="range", months=2, on_change=on_change) ``` ```python ui.Calendar(value="2026-10-22", on_change=on_change) ``` ```python ui.Calendar(value="2026-10-22", on_change=on_change) ``` #### Events Calendar has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. | Event | Handler gets | When | | --- | --- | --- | | `on_change` | `(event)` | Receives `event.value` (a date, a tuple or a list). | | `on_month` | `(event)` | Receives `event.value`, the `(year, month)` shown. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Planner(pz.App): def __init__(self): self.days = [] def changed(self, event): # on_change: the list of days ("multiple" mode) self.days = event.value def view(self): return pz.row([ ui.Calendar(value=self.days, mode="multiple", on_change=self.changed), ui.label(f"{len(self.days)} days"), ], spacing=16) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Day: value: object class Planner(pz.App): day = None def update(self, message): match message: case Day(value): self.day = value def view(self): return ui.Calendar(value=self.day, today_button=True, on_change=lambda e: Day(e.value)) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: stay: tuple = ("2026-10-05", "2026-10-09") def view(state, set_state): return ui.Calendar(value=state.stay, mode="range", months=2, on_change=lambda e: set_state(stay=e.value)) ``` #### Properties and API ##### `ui.Calendar` ```python class Calendar ``` A component with a month to pick a day on (`mode="single"`), a range (`"range"`: the first click starts it, the second ends it) or several days (`"multiple"`). The title opens a grid of months, then of years. Once clicked it takes the keyboard: arrows move, Enter picks, Page Up/Down change month. See the [Calendar](https://albakore.github.io/pyzed/library/calendar/) page. | Property | Default | Description | | --- | --- | --- | | `value` | `None` | A date (`datetime.date` or `"2026-10-22"`), a `(start, end)` pair or a list of dates, by `mode`. | | `mode` | `"single"` | `"single"`, `"range"` or `"multiple"`. | | `min` | `None` | The earliest day that can be picked. | | `max` | `None` | The latest day that can be picked. | | `disabled` | `()` | Days that cannot be picked: dates, weekday names (`"sunday"`) or `"weekends"`. | | `first_weekday` | `"monday"` | `"monday"`, `"sunday"` or `"saturday"`. | | `months` | `1` | Months shown side by side. | | `week_numbers` | `False` | Shows the ISO week number of each row. | | `show_outside` | `True` | Shows the days of the months around. | | `today_button` | `False` | Adds a Today link. | Events: `on_change`, `on_month` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#calendar) #### Related - [Date picker](https://albakore.github.io/pyzed/library/date-picker/): A date field with a calendar that drops down, in the format you choose. - [Date range picker](https://albakore.github.io/pyzed/library/date-range-picker/): Two dates in one field, with two months side by side and quick ranges. ### Callout > An inline notice with a title, text and actions. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/callout/ :::tip[In plain words] A colored box inside the page with a message: a tip, a warning or an error. ::: An inline notice (a callout or banner): an icon, a title, an optional description, action buttons and a close button with `on_dismiss`. `tint` is `"info"` (default), `"success"`, `"warning"`, `"error"` or any theme token. `ui.callout` · Category: [Feedback](https://albakore.github.io/pyzed/library/#feedback) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#callout) **Code:** ```python pz.column([ ui.callout("A new version is out", "Pyzed 0.4 adds the component designer.", tint="info", actions=[ui.button("What's new", on_open, size="sm")], on_dismiss=on_dismiss), ui.callout("Could not save", "The file is read-only.", tint="error"), ], spacing=10, width=520) ``` #### When to use - For messages that belong to a place in the UI: a read-only file, a new version, a failed sync. - For short-lived messages, use a [Toast](https://albakore.github.io/pyzed/library/toast/). #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ ui.callout("A new version is out", "Pyzed 0.4 adds the component designer.", tint="info", actions=[ui.button("What's new", on_open, size="sm")], on_dismiss=on_dismiss), ui.callout("Could not save", "The file is read-only.", tint="error"), ], spacing=10, width=520) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Tint Each tint picks a matching icon; `icon=` chooses another. ```python pz.container(ui.callout("Heads up", "Pyzed 0.5 is out."), width=380) ``` ```python pz.container(ui.callout("Synced", "All changes are saved.", tint="success"), width=380) ``` ```python pz.container(ui.callout("Unsaved changes", "Save before closing.", tint="warning"), width=380) ``` ```python pz.container(ui.callout("Could not save", "The file is read-only.", tint="error"), width=380) ``` ```python pz.container(ui.callout("Tip", "Ctrl+Shift+P opens the palette.", tint="accent", icon="lightbulb"), width=380) ``` ##### Content ```python pz.container(ui.callout("Read-only file"), width=380) ``` ```python pz.container(ui.callout("A new version is out", "Restart to update.", actions=[ui.button("Restart", on_restart, size="sm")]), width=380) ``` ```python pz.container(ui.callout("Welcome!", "Open a folder to start.", on_dismiss=on_dismiss), width=380) ``` #### Events Callout has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_dismiss` | `()` | Adds a close button. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Updates(pz.App): def __init__(self): self.visible = True def view(self): if not self.visible: return ui.muted("No news.") return ui.callout("A new version is out", "Pyzed adds the component designer.", tint="info", on_dismiss=lambda: setattr(self, "visible", False)) # adds the ✕ ``` **Declarative:** ```python class Updates(pz.App): visible = True def update(self, message): if message == "dismiss": self.visible = False def view(self): return ui.callout("A new version is out", tint="info", on_dismiss="dismiss") if self.visible \ else ui.muted("No news.") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: visible: bool = True def view(state, set_state): if not state.visible: return ui.muted("No news.") return ui.callout("A new version is out", tint="info", on_dismiss=lambda: set_state(visible=False)) ``` #### Properties and API ##### `ui.callout` ```python callout( title, description=None, *, tint='info', icon=None, actions=(), on_dismiss=None, theme=None, ) ``` An inline notice (a callout or banner). | Property | Default | Description | | --- | --- | --- | | `title` | *required* | Title (text or widget). | | `description` | `None` | Secondary text. | | `tint` | `"info"` | `"info"`, `"success"`, `"warning"`, `"error"` or `"hint"`: picks the color and icon. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `actions` | `()` | Extra widgets (usually buttons) shown as actions. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_dismiss` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#callout) #### Related - [Toast](https://albakore.github.io/pyzed/library/toast/): Notifications stacked in the bottom-right corner. - [Alert](https://albakore.github.io/pyzed/library/alert/): A message with one button. - [Empty state](https://albakore.github.io/pyzed/library/empty-state/): A centered placeholder for empty lists and searches. ### Canvas > 2D drawing with shapes, paths, text and mouse events. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/canvas/ :::tip[In plain words] A blank sheet to draw shapes, lines and text on, for charts and games. ::: A 2D drawing surface. `shapes` is a list made with [`pyzed.draw`](https://albakore.github.io/pyzed/reference/draw/) (rectangles, circles, arcs, paths, text, images…). Mouse callbacks receive coordinates relative to the canvas; with an `id` the geometry is cached between frames. The charts of this library are drawn with it. `pz.canvas` · Category: [Content](https://albakore.github.io/pyzed/library/#content) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#canvas) **Code:** ```python pz.canvas([ draw.rect(10, 10, 120, 80, fill="#74ade8", radius=8), draw.circle(200, 50, 40, fill="#a1c181"), draw.arc(300, 50, 40, 0, 270, stroke="#e5c07b", width=8, cap="round"), draw.polygon([(360, 90), (400, 10), (440, 90)], fill="#d07277"), draw.line(10, 120, 440, 120, stroke="#5d636f", width=2, dash=[6, 4]), draw.text(225, 150, "pz.canvas + pyzed.draw", color="#a9afbc", size=16, align_x="center"), ], width=450, height=170) ``` #### When to use - For charts, games, diagrams, drawing apps and custom widgets. #### Usage ```python import pyzed as pz from pyzed import draw pz.canvas([ draw.rect(10, 10, 120, 80, fill="#74ade8", radius=8), draw.circle(200, 50, 40, fill="#a1c181"), draw.arc(300, 50, 40, 0, 270, stroke="#e5c07b", width=8, cap="round"), draw.polygon([(360, 90), (400, 10), (440, 90)], fill="#d07277"), draw.line(10, 120, 440, 120, stroke="#5d636f", width=2, dash=[6, 4]), draw.text(225, 150, "pz.canvas + pyzed.draw", color="#a9afbc", size=16, align_x="center"), ], width=450, height=170) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Shapes ```python pz.canvas([draw.rect(10, 10, 140, 70, fill="#74ade8", radius=10)], width=160, height=90) ``` ```python pz.canvas([draw.circle(50, 45, 36, fill="#a1c181")], width=100, height=90) ``` ```python pz.canvas([draw.arc(50, 45, 34, 0, 270, stroke="#e5c07b", width=8, cap="round")], width=100, height=90) ``` ```python pz.canvas([draw.polygon([(10, 80), (50, 10), (90, 80)], fill="#d07277")], width=100, height=90) ``` ```python pz.canvas([draw.line(10, 45, 190, 45, stroke="#c678dd", width=3, dash=[8, 5])], width=200, height=90) ``` ```python pz.canvas([draw.text(100, 45, "pyzed.draw", color="#74ade8", size=20, align_x="center", align_y="center")], width=200, height=90) ``` #### Events Canvas has **5 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_press` | `(x, y)` | Callable that receives `(x, y)` on press. | | `on_release` | `(x, y)` | Callable that receives `(x, y)` on release. | | `on_right_press` | `(x, y)` | Callable that receives `(x, y)` on right click. | | `on_move` | `(x, y)` | Called with `(x, y)` when the mouse moves. | | `on_scroll` | `(dx, dy)` | Callable that receives `(dx, dy)` in lines when the wheel is used. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Paint(pz.App): def __init__(self): self.dots, self.drawing = [], False def press(self, x, y): # on_press / on_move get the position self.drawing = True self.dots.append((x, y)) def move(self, x, y): if self.drawing: self.dots.append((x, y)) def view(self): return pz.canvas([draw.circle(x, y, 3, fill="#74ade8") for x, y in self.dots], width=420, height=200, cursor="crosshair", on_press=self.press, on_move=self.move, on_release=lambda x, y: setattr(self, "drawing", False), on_right_press=lambda x, y: self.dots.clear()) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Dot: x: float y: float class Paint(pz.App): dots = [] def update(self, message): match message: case Dot(x, y): self.dots = self.dots + [(x, y)] case "clear": self.dots = [] def view(self): return pz.canvas([draw.circle(x, y, 3, fill="#74ade8") for x, y in self.dots], width=420, height=200, on_press=Dot, on_right_press=lambda x, y: "clear") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: dots: tuple = () zoom: float = 1.0 def view(state, set_state): shapes = [draw.circle(x, y, 3 * state.zoom, fill="#74ade8") for x, y in state.dots] return pz.canvas(shapes, width=420, height=200, on_press=lambda x, y: set_state(dots=state.dots + ((x, y),)), on_scroll=lambda dx, dy: set_state(zoom=max(0.5, state.zoom + dy * 0.1))) ``` #### Properties and API ##### `pz.canvas` ```python canvas( shapes=None, *, width=None, height=None, id=None, on_press=None, on_release=None, on_right_press=None, on_move=None, on_scroll=None, cursor=None, ) ``` A 2D drawing surface. `shapes` is a list created with [`pyzed.draw`](https://albakore.github.io/pyzed/reference/draw/). Mouse callbacks receive coordinates relative to the canvas. With an `id`, the geometry is cached between frames and only recomputed when the shapes change. See the [drawing with canvas](https://albakore.github.io/pyzed/guides/canvas/) guide. | Property | Default | Description | | --- | --- | --- | | `shapes` | `None` | List of `pyzed.draw` shapes (nested lists are accepted). | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `id` | `None` | Enables the geometry cache. | | `cursor` | `None` | Mouse cursor (`"crosshair"`, `"pointer"`…). | Events: `on_press`, `on_release`, `on_right_press`, `on_move`, `on_scroll` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#canvas) #### Related - [Bar chart](https://albakore.github.io/pyzed/library/bar-chart/): A vertical bar chart with labels. - [Donut](https://albakore.github.io/pyzed/library/donut/): A donut chart with a label in the middle. - [Stack](https://albakore.github.io/pyzed/library/stack/): Layers children on top of each other; `pin` places one at x, y. ### Card > A bordered surface with an optional title and actions. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/card/ :::tip[In plain words] A box with a border that keeps related things together, like a card on a table. ::: A rounded, bordered surface that groups content. `title` and `actions` add a header row; `padding` and `width` control its box. `ui.card` · Category: [Display](https://albakore.github.io/pyzed/library/#display) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#card) **Code:** ```python ui.card( pz.column([ ui.muted("Pyzed draws the UI with Iced, in Rust."), pz.row([pz.hspace(), ui.button("Cancel", on_cancel, variant="ghost"), ui.button("OK", on_ok, variant="solid")], spacing=8), ], spacing=12), title="Card", actions=[ui.icon_button("ellipsis", on_menu, size="sm")], width=380, ) ``` #### When to use - To group related content on a page: a summary, a form section, a dashboard tile. - For a dialog over the UI use [Modal](https://albakore.github.io/pyzed/library/modal/). #### Usage ```python import pyzed as pz from pyzed import ui ui.card( pz.column([ ui.muted("Pyzed draws the UI with Iced, in Rust."), pz.row([pz.hspace(), ui.button("Cancel", on_cancel, variant="ghost"), ui.button("OK", on_ok, variant="solid")], spacing=8), ], spacing=12), title="Card", actions=[ui.icon_button("ellipsis", on_menu, size="sm")], width=380, ) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content ```python ui.card(ui.muted("A bordered surface."), width=260) ``` **} alt={** ```python ui.card(ui.muted("Three files changed."), title="Changes", width=260) ``` ```python ui.card(ui.muted("Three files changed."), title="Changes", actions=[ui.icon_button("ellipsis", on_menu, size="sm")], width=260) ``` ##### Padding ```python ui.card(ui.label("Compact"), padding=8) ``` ```python ui.card(ui.label("Default")) ``` ```python ui.card(ui.label("Roomy"), padding=28) ``` ##### States and events ```python ui.card(ui.muted("Python app with a window"), title="Desktop app", on_press=on_press, selected=True, width=260) ``` ```python ui.card(ui.muted("3 files changed"), title="Commit", footer=[ui.button("Push", on_press)], elevated=True, width=260) ``` #### Events Card has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_press` | `()` | Makes the whole card clickable. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Plans(pz.App): def __init__(self): self.plan = "Pro" def view(self): return pz.row([ ui.card(ui.muted(f"Plan {name}"), title=name, width=160, selected=name == self.plan, on_press=lambda name=name: setattr(self, "plan", name)) # the whole card for name in ("Free", "Pro", "Team") ], spacing=8) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Choose: plan: str class Plans(pz.App): plan = "Pro" def update(self, message): match message: case Choose(plan): self.plan = plan def view(self): return pz.row([ui.card(ui.muted(name), title=name, width=160, selected=name == self.plan, on_press=Choose(name)) for name in ("Free", "Pro", "Team")], spacing=8) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: plan: str = "Pro" def plan_card(name, selected, choose): # a small function component return ui.card(ui.muted(name), title=name, width=160, selected=selected, on_press=choose) def view(state, set_state): return pz.row([plan_card(n, n == state.plan, lambda n=n: set_state(plan=n)) for n in ("Free", "Pro", "Team")], spacing=8) ``` #### Properties and API ##### `ui.card` ```python card( content, *, title=None, actions=(), padding=16, width=None, footer=(), on_press=None, selected=False, elevated=False, theme=None, ) ``` A bordered surface to group content, with an optional title and actions. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `title` | `None` | Title. | | `actions` | `()` | Extra widgets (usually buttons) shown as actions. | | `padding` | `16` | Inner padding. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `footer` | `()` | Widgets at the bottom right (buttons). | | `selected` | `False` | Outlines it in the accent color. | | `elevated` | `False` | Adds a shadow. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_press` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#card) #### Related - [Modal](https://albakore.github.io/pyzed/library/modal/): A dialog surface with title, content and footer buttons. - [Settings group](https://albakore.github.io/pyzed/library/settings-group/): A titled block of field rows, for settings screens. - [Empty state](https://albakore.github.io/pyzed/library/empty-state/): A centered placeholder for empty lists and searches. ### Catalog > Things to install, with a search box and Install or Update. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/catalog/ :::tip[In plain words] A searchable shop-like list of add-ons, each with an Install button. ::: `ui.Catalog` is a component for things to install (agents from a registry, MCP servers, skills): a search box over a scrolling list where each row shows the name, `version` and `description`, and on the right Install, Installed or Update to a newer version. `items` are dicts or objects with `id`, `name` and optional `version`, `description`, `installed` (a bool) and `update` (the newer version). Install and Update report `on_install` with the item's id in `event.value`; while `busy` holds an id, that row shows a spinner and the other Install buttons are disabled. `height` is the list's height. `ui.Catalog` · Category: [AI agents](https://albakore.github.io/pyzed/library/#ai-agents) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#catalog) **Code:** ```python pz.container(ui.Catalog(items=[ {"id": "gemini", "name": "Gemini CLI", "version": "0.64.0", "description": "A coding agent for the terminal.", "installed": True, "update": "0.65.0"}, {"id": "codex", "name": "Codex CLI", "version": "0.40.0", "description": "Writes and runs code for you."}, {"id": "goose", "name": "Goose", "version": "1.9.0", "description": "An open source agent.", "installed": True}, {"id": "opencode", "name": "OpenCode", "version": "0.12.1", "description": "An agent that edits whole projects."}, ], busy="codex", height=220, on_install=on_install), width=460) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - To add agents, MCP servers, skills or plugins from a list, inside a dialog or a settings page. #### Usage ```python import pyzed as pz from pyzed import ui class Agents(pz.App): def __init__(self): self.registry = load_registry() # [{"id", "name", "version", "description", "installed"}, ...] self.installing = "" def view(self): return ui.Catalog(items=self.registry, busy=self.installing, on_install=lambda e: self.install(e.value)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### State While one item installs, the other Install buttons are disabled. ```python pz.container(ui.Catalog(items=[{"id": "goose", "name": "Goose", "version": "1.9.0", "description": "An open source agent."}], height=50), width=380) ``` ```python pz.container(ui.Catalog(items=[{"id": "goose", "name": "Goose", "version": "1.9.0", "description": "An open source agent.", "installed": True}], height=50), width=380) ``` ```python pz.container(ui.Catalog(items=[{"id": "goose", "name": "Goose", "version": "1.9.0", "description": "An open source agent.", "installed": True, "update": "2.0.0"}], height=50), width=380) ``` ```python pz.container(ui.Catalog(items=[ {"id": "goose", "name": "Goose", "version": "1.9.0", "description": "An open source agent."}, {"id": "codex", "name": "Codex CLI", "version": "0.40.0", "description": "Writes and runs code."}, ], busy="goose", height=100), width=380) ``` ```python pz.container(ui.Catalog(items=[{"id": "goose", "name": "Goose"}], height=50), width=380) ``` #### Events Catalog has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. | Event | Handler gets | When | | --- | --- | --- | | `on_install` | `(event)` | Install or Update was pressed (`event.value` is the id). | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Agents(pz.App): def __init__(self): self.items = [{"id": "goose", "name": "Goose", "version": "1.9.0"}, {"id": "codex", "name": "Codex CLI", "version": "0.40.0"}] self.installing = "" def install(self, ident): # on_install: event.value is the item's id self.installing = ident def view(self): return ui.Catalog(items=self.items, busy=self.installing, height=200, on_install=lambda e: self.install(e.value)) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Install: ident: str class Agents(pz.App): items = [{"id": "goose", "name": "Goose", "version": "1.9.0"}] installing = "" def update(self, message): match message: case Install(ident): self.installing = ident def view(self): return ui.Catalog(items=self.items, busy=self.installing, height=200, on_install=lambda e: Install(e.value)) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: installed: frozenset = frozenset() ITEMS = [{"id": "goose", "name": "Goose", "version": "1.9.0"}] def view(state, set_state): items = [{**i, "installed": i["id"] in state.installed} for i in ITEMS] return ui.Catalog(items=items, height=200, on_install=lambda e: set_state(installed=state.installed | {e.value})) ``` #### Properties and API ##### `ui.Catalog` ```python class Catalog ``` A component for things to install with a search box (agents from a registry, servers, skills…): each row shows Install, Installed or Update to a newer version. | Property | Default | Description | | --- | --- | --- | | `items` | `None` | Dicts or objects with `id`, `name` and optional `version`, `description`, `installed` and `update`. | | `busy` | `""` | The id being installed (a spinner). | | `height` | `320` | Height of the list. | | `theme` | `None` | Theme. | Events: `on_install` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#catalog) #### Related - [Service row](https://albakore.github.io/pyzed/library/service-row/): A settings row for an MCP server, agent or skill: status, switch and menu. - [Search input](https://albakore.github.io/pyzed/library/search-input/): An input with a magnifying glass, for filtering. - [List item](https://albakore.github.io/pyzed/library/list-item/): A list row with icon, text and an end slot. ### Checkbox > A checkbox with an optional label. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/checkbox/ :::tip[In plain words] A square you tick to say yes to an option. ::: A themed checkbox. `on_toggle` receives the new `bool`; `label` adds clickable text next to it. `ui.checkbox` · Category: [Forms](https://albakore.github.io/pyzed/library/#forms) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#checkbox) **Code:** ```python pz.column([ ui.checkbox(True, on_toggle, label="Format on save"), ui.checkbox(False, on_toggle, label="Show hidden files"), ui.checkbox(True, on_toggle, label="Use tabs"), ], spacing=10) ``` #### When to use - For options that are applied later (a form you submit) or a list of independent choices. - For a setting that takes effect at once, a [Switch](https://albakore.github.io/pyzed/library/switch/) reads better. #### Usage ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.accepted = False def view(self): return ui.checkbox(self.accepted, lambda value: setattr(self, "accepted", value), label="I accept the terms") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### State ```python ui.checkbox(True, on_toggle, label="Format on save") ``` ```python ui.checkbox(False, on_toggle, label="Format on save") ``` ```python ui.checkbox(True, label="Managed by your admin") ``` ##### Content ```python ui.checkbox(True, on_toggle) ``` ```python pz.column([ui.checkbox(True, on_toggle, label="Python"), ui.checkbox(False, on_toggle, label="Rust"), ui.checkbox(True, on_toggle, label="TypeScript")], spacing=8) ``` ##### States and events ```python ui.checkbox(None, on_toggle, label="Some files") ``` ```python ui.checkbox(True, on_toggle, label="Locked", disabled=True) ``` ```python ui.checkbox(True, on_toggle, label="Small", size="sm") ``` ```python ui.checkbox(True, on_toggle, label="Large", size="lg") ``` ##### Base widget The `pz.*` widget underneath: no theme, every color and size is yours. ```python pz.checkbox(True, "pz.checkbox", on_toggle=on_toggle, accent="#c678dd", size=18) ``` #### Events Checkbox has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_toggle` | `ui.checkbox`, `pz.checkbox` | `(checked)` | Callable that receives the new `bool` (or a message to send). | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Settings(pz.App): def __init__(self): self.options = {"Format on save": True, "Show hidden files": False} def toggle(self, name, checked): # on_toggle gets True or False self.options[name] = checked def view(self): return pz.column([ ui.checkbox(on, lambda checked, name=name: self.toggle(name, checked), label=name) for name, on in self.options.items() ], spacing=10) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Toggle: name: str checked: bool class Settings(pz.App): options = {"Format on save": True, "Show hidden files": False} def update(self, message): match message: case Toggle(name, checked): self.options = {**self.options, name: checked} def view(self): return pz.column([ui.checkbox(on, lambda c, n=name: Toggle(n, c), label=name) for name, on in self.options.items()], spacing=10) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: format_on_save: bool = True hidden_files: bool = False def view(state, set_state): return pz.column([ ui.checkbox(state.format_on_save, lambda c: set_state(format_on_save=c), label="Format on save"), ui.checkbox(state.hidden_files, lambda c: set_state(hidden_files=c), label="Show hidden files"), ], spacing=10) ``` #### Tips :::tip Once clicked or reached with Tab it has the keyboard focus (a ring shows when you use the keys): Space toggles it. ::: #### Properties and API ##### `ui.checkbox` ```python checkbox( checked, on_toggle=None, *, label=None, size='md', disabled=False, id=None, theme=None, ) ``` A checkbox. `checked=None` shows the mixed state (part of a group checked); clicking it checks it. | Property | Default | Description | | --- | --- | --- | | `checked` | *required* | Current state. | | `label` | `None` | Label text. | | `size` | `"md"` | `"sm"`, `"md"` or `"lg"`. | | `disabled` | `False` | Greys it out and ignores clicks. | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_toggle` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#checkbox) ##### `pz.checkbox` ```python checkbox( checked, label=None, *, on_toggle=None, size=None, text_size=None, accent=None, bg=None, border_color=None, check_color=None, color=None, ) ``` A checkbox. | Property | Default | Description | | --- | --- | --- | | `checked` | *required* | Current state. | | `label` | `None` | Label text. | | `size` | `None` | Box size. | | `text_size` | `None` | Text size. | | `accent` | `None` | Accent color (active part). | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `border_color` | `None` | Border color. | | `check_color` | `None` | Check mark color. | | `color` | `None` | Text or icon color. | Events: `on_toggle` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#checkbox) #### Related - [Switch](https://albakore.github.io/pyzed/library/switch/): An on/off switch for settings that apply at once. - [Radio group](https://albakore.github.io/pyzed/library/radio-group/): Mutually exclusive options as radio buttons. - [Field](https://albakore.github.io/pyzed/library/field/): A settings row: title and description left, control right. ### Chip > A compact tag: clickable as a filter, removable with an ✕. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/chip/ :::tip[In plain words] A small rounded tag. Click it to turn a filter on or off, or press its ✕ to remove it. ::: A small rounded tag. With `on_press` it acts as a filter you can toggle (`selected=True` highlights it), and with `on_remove` it shows a close button, as in a list of tags. `ui.chip` · Category: [Actions](https://albakore.github.io/pyzed/library/#actions) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#chip) **Code:** ```python pz.row([ ui.chip("python", icon="tag", selected=True, on_press=on_filter), ui.chip("rust", on_press=on_filter), ui.chip("iced", on_remove=on_remove), ui.chip("zed", icon="sparkles", on_remove=on_remove), ], spacing=6) ``` #### When to use - For tags, filters and selected values (a multi-select, recipients of a message). - For read-only statuses use a [Badge](https://albakore.github.io/pyzed/library/badge/). #### Usage ```python import pyzed as pz from pyzed import ui class Tags(pz.App): def __init__(self): self.tags = ["python", "rust", "iced"] def view(self): return pz.row([ui.chip(t, on_remove=lambda t=t: self.tags.remove(t)) for t in self.tags], spacing=6, wrap=True) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Kind ```python ui.chip("python") ``` ```python ui.chip("rust", on_press=on_filter) ``` ```python ui.chip("python", on_press=on_filter, selected=True) ``` ```python ui.chip("iced", on_remove=on_remove) ``` ##### Content ```python ui.chip("design", icon="tag") ``` ```python ui.chip("ada@example.com", icon="user", on_remove=on_remove) ``` ```python ui.chip("favorites", icon="star", selected=True, on_press=on_filter) ``` ##### States and events ```python ui.chip("Failing", on_press=on_press, selected=True, tint="error") ``` ```python ui.chip("Archived", on_press=on_press, on_remove=on_remove, disabled=True) ``` #### Events Chip has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_press` | `()` | Message or callable sent on press. `None` disables the control. | | `on_remove` | `()` | Shows a ✕ that sends this when clicked. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Filters(pz.App): def __init__(self): self.tags = ["python", "rust", "iced"] self.active = "python" def view(self): return pz.row([ ui.chip(t, selected=t == self.active, on_press=lambda t=t: setattr(self, "active", t), # pick it on_remove=lambda t=t: self.tags.remove(t)) # its ✕ for t in self.tags ], spacing=6, wrap=True) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Pick: tag: str @dataclass class Remove: tag: str class Filters(pz.App): tags = ["python", "rust", "iced"] active = "python" def update(self, message): match message: case Pick(tag): self.active = tag case Remove(tag): self.tags = [t for t in self.tags if t != tag] def view(self): return pz.row([ui.chip(t, selected=t == self.active, on_press=Pick(t), on_remove=Remove(t)) for t in self.tags], spacing=6, wrap=True) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: tags: tuple = ("python", "rust", "iced") active: str = "python" def view(state, set_state): return pz.row([ ui.chip(t, selected=t == state.active, on_press=lambda t=t: set_state(active=t), on_remove=lambda t=t: set_state(tags=tuple(x for x in state.tags if x != t))) for t in state.tags ], spacing=6, wrap=True) ``` #### Properties and API ##### `ui.chip` ```python chip( content, *, on_press=None, on_remove=None, selected=False, icon=None, tint='accent', disabled=False, theme=None, ) ``` A compact tag, optionally clickable (filters) and with a button to remove it. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Text. | | `selected` | `False` | Marks it as selected. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `tint` | `"accent"` | Color when selected (a theme token). | | `disabled` | `False` | Greys it out and ignores presses. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_press`, `on_remove` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#chip) #### Related - [Badge](https://albakore.github.io/pyzed/library/badge/): A small pill for statuses, versions and counters. - [Toggle group](https://albakore.github.io/pyzed/library/toggle-group/): An icon-only segmented control. ### Choice > A message with your own buttons. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/choice/ :::tip[In plain words] A question with several possible answers, each one a button. ::: A message with your own buttons: `options` are `(label, on_press)` pairs and the last one is the main one. `ui.choice` · Category: [Dialogs](https://albakore.github.io/pyzed/library/#dialogs) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#choice) **Code:** ```python ui.dialog_layer(main_view(), ui.choice( "notes.py already exists in this folder.", [("Keep both", on_keep), ("Skip", on_skip), ("Replace", on_replace)], on_cancel, title="File exists", width=420, )) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - When a question has more than two answers (Keep both / Replace / Skip). #### Usage ```python from pyzed import ui ui.dialog_layer(main_view(), ui.choice( "notes.py already exists in this folder.", [("Keep both", on_keep), ("Skip", on_skip), ("Replace", on_replace)], on_cancel, title="File exists", width=420, )) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python ui.choice("Open the project in…", [("This window", on_here), ("New window", on_new)], on_cancel, width=360) ``` ```python ui.choice("notes.py already exists.", [("Keep both", on_keep), ("Skip", on_skip), ("Replace", on_replace)], on_cancel, title="File exists", width=400) ``` #### Events Choice has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_cancel` | `()` | The close icon. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Copy(pz.App): def __init__(self): self.asking, self.answer = True, None def answer_with(self, what): self.asking, self.answer = False, what def view(self): dialog = ui.choice("notes.py already exists in this folder.", [("Keep both", lambda: self.answer_with("both")), # one handler per option ("Replace", lambda: self.answer_with("replace"))], lambda: setattr(self, "asking", False), # on_cancel: the ✕ title="File exists") return ui.dialog_layer(main_view(), dialog if self.asking else None) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Answer: what: str class Copy(pz.App): asking = True def update(self, message): match message: case Answer(what): self.asking = False print("chose", what) case "cancel": self.asking = False def view(self): dialog = ui.choice("notes.py already exists.", [("Keep both", Answer("both")), ("Replace", Answer("replace"))], "cancel") return ui.dialog_layer(main_view(), dialog if self.asking else None) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: answer: str = "" def view(state, set_state): if state.answer: return ui.label(f"You chose: {state.answer}") return ui.dialog_layer(main_view(), ui.choice( "notes.py already exists.", [(label, lambda a=answer: set_state(answer=a)) for label, answer in (("Keep both", "both"), ("Replace", "replace"))], lambda: set_state(answer="cancel"))) ``` #### Properties and API ##### `ui.choice` ```python choice(message, options, on_cancel, *, title=None, width=440, theme=None) ``` A message with your own buttons. | Property | Default | Description | | --- | --- | --- | | `message` | *required* | Any Python value, or a callable. | | `options` | *required* | `(label, on_press)` pairs; the last one is the main button. | | `title` | `None` | Title. | | `width` | `440` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_cancel` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#choice) #### Related - [Confirm](https://albakore.github.io/pyzed/library/confirm/): A yes/no question, red for destructive actions. - [Unsaved changes](https://albakore.github.io/pyzed/library/unsaved-changes/): “Save your changes before closing?” ### Color picker > Pick a color, a gradient or a masked fill; swatches show them. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/color-picker/ :::tip[In plain words] A palette to choose a color, its transparency, or a blend of colors. ::: `ui.ColorPicker` is a component that picks a color with alpha, a linear gradient (stops you add, drag and remove, plus an angle) or a fill with an opacity mask. `on_change` receives an event whose `value` is a `"#rrggbb[aa]"` string, a `pz.Gradient` or a `pz.Paint`, all valid backgrounds. `ui.color_swatch` shows any of them over a checkerboard. `ui.ColorPicker` · Category: [Forms](https://albakore.github.io/pyzed/library/#forms) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#colorpicker) **Code:** ```python pz.row([ ui.ColorPicker(value="#3b82f6cc", on_change=on_change), pz.column([ ui.color_swatch("#3b82f6", size=28), ui.color_swatch("#a1c181aa", size=28), ui.color_swatch(pz.Gradient(["#f97316", "#7c3aed"], angle=90), size=28), ], spacing=8), ], spacing=16) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - For theme editors, drawing apps and style properties. - Show a `color_swatch` and open the picker in a [Popover](https://albakore.github.io/pyzed/library/popover/) when it is pressed. #### Usage ```python import pyzed as pz from pyzed import ui class Styles(pz.App): def __init__(self): self.fill = "#3b82f6" def view(self): return pz.row([ ui.ColorPicker(value=self.fill, on_change=lambda e: setattr(self, "fill", e.value)), pz.container(width=120, height=120, bg=self.fill, radius=8), ], spacing=16) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python ui.ColorPicker(value="#3b82f6cc", on_change=on_change) ``` ```python ui.ColorPicker(value="#a1c181", alpha=False, gradients=False, masks=False, on_change=on_change) ``` ```python ui.ColorPicker(value="#e5c07b", gradients=False, masks=False, swatches=["#e06c75", "#e5c07b", "#98c379", "#61afef", "#c678dd"], on_change=on_change) ``` ##### Swatches A swatch draws any color, gradient or paint over a checkerboard, so transparency shows. ```python ui.color_swatch("#3b82f6", size=32) ``` ```python ui.color_swatch("#a1c181aa", size=32) ``` ```python ui.color_swatch(pz.Gradient(["#f97316", "#7c3aed"], angle=90), size=32) ``` ```python ui.checkerboard(96, 48, size=8) ``` #### Events Color picker has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_change` | `ui.ColorPicker` | `(event)` | Receives `event.value`. | | `on_press` | `ui.color_swatch` | `()` | Makes it clickable. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Styles(pz.App): def __init__(self): self.fill = "#3b82f6" def picked(self, event): # a pz.Component: the color is event.value self.fill = event.value def view(self): return pz.row([ ui.ColorPicker(value=self.fill, on_change=self.picked), pz.container(width=80, height=80, bg=self.fill, radius=8), ui.color_swatch("#ef4444", on_press=lambda: setattr(self, "fill", "#ef4444")), ], spacing=16) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Fill: color: str class Styles(pz.App): fill = "#3b82f6" def update(self, message): match message: case Fill(color): self.fill = color def view(self): return pz.row([ui.ColorPicker(value=self.fill, on_change=lambda e: Fill(e.value)), ui.color_swatch("#ef4444", on_press=Fill("#ef4444"))], spacing=16) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: fill: str = "#3b82f6" def view(state, set_state): return pz.row([ui.ColorPicker(value=state.fill, on_change=lambda e: set_state(fill=e.value)), pz.container(width=80, height=80, bg=state.fill, radius=8)], spacing=16) ``` #### Properties and API ##### `ui.ColorPicker` ```python class ColorPicker ``` A component that picks a color with alpha, a linear gradient (stops you add, drag and remove, plus an angle) or a fill with an opacity mask. `on_change` gets a `"#rrggbb[aa]"` string, a `pz.Gradient` or a `pz.Paint`, all valid as a `background`. Pass the picked value back as `value`. See [Styles](https://albakore.github.io/pyzed/theming/styles/#color-picker). | Property | Default | Description | | --- | --- | --- | | `value` | `"#3b82f6"` | A color, theme token, `pz.Gradient` or `pz.Paint`. | | `alpha` | `True` | Shows the alpha bar and field. | | `gradients` | `True` | Shows the Solid/Gradient switch. | | `masks` | `True` | Shows the mask editor. | | `swatches` | `None` | Quick colors (default: the theme's accent and status colors). | Events: `on_change` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#colorpicker) ##### `ui.color_swatch` ```python color_swatch(value, *, size=20, on_press=None) ``` A small square showing a color, gradient or paint over a checkerboard. | Property | Default | Description | | --- | --- | --- | | `value` | *required* | What to show. | | `size` | `20` | Side in pixels. | Events: `on_press` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#color_swatch) ##### `ui.checkerboard` ```python checkerboard(width, height, size=6) ``` A light/dark checkerboard to draw under translucent colors. | Property | Default | Description | | --- | --- | --- | | `width` | *required* | Width. | | `height` | *required* | Height. | | `size` | `6` | Cell size. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#checkerboard) #### Related - [Popover](https://albakore.github.io/pyzed/library/popover/): Floating content at a point: context menus, color pickers. - [Card](https://albakore.github.io/pyzed/library/card/): A bordered surface with an optional title and actions. ### Column > Stacks children vertically. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/column/ :::tip[In plain words] Puts things one under the other, from top to bottom. ::: Stacks its children vertically, with `spacing`, `padding`, `align` on the cross axis and an optional `max_width`. Like `row`, nested lists are flattened and `None`/`False` are ignored. `pz.column` · Category: [Layout](https://albakore.github.io/pyzed/library/#layout) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#column) **Code:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([ pz.column([box("1"), box("2"), box("3")], spacing=8), pz.column([box("wide", w=160), box("center"), box("end")], spacing=8, align="center"), pz.column([box("top"), pz.vspace(), box("bottom")], spacing=8, height=170), ], spacing=32) ``` #### When to use - For forms, lists, page content and most `view()` roots. #### Usage ```python import pyzed as pz from pyzed import ui box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([ pz.column([box("1"), box("2"), box("3")], spacing=8), pz.column([box("wide", w=160), box("center"), box("end")], spacing=8, align="center"), pz.column([box("top"), pz.vspace(), box("bottom")], spacing=8, height=170), ], spacing=32) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Cross-axis alignment ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([box("wide", w=160), box("B"), box("C")], spacing=8, align="start") ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([box("wide", w=160), box("B"), box("C")], spacing=8, align="center") ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([box("wide", w=160), box("B"), box("C")], spacing=8, align="end") ``` ##### Options ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([box("1"), box("2")], spacing=16) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([box("top"), pz.vspace(), box("bottom")], height=170) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.column([box("1"), box("2")], spacing=8, padding=16), border_color=pz.theme.current().border, border_width=1, radius=8) ``` #### Events Column has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.column` ```python column( children=None, *, spacing=0.0, padding=None, width=None, height=None, align=None, max_width=None, clip=False, ) ``` Stacks its children vertically. | Property | Default | Description | | --- | --- | --- | | `children` | `None` | List of children. Nested lists are flattened and `None` and `False` are ignored, so you can write `cond and widget`. | | `spacing` | `0.0` | Space between children, in pixels. | | `padding` | `None` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `align` | `None` | Alignment of the children on the cross axis: `"start"`, `"center"` or `"end"`. | | `max_width` | `None` | Maximum width in pixels. | | `clip` | `False` | Clips content that overflows the bounds. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#column) #### Related - [Row](https://albakore.github.io/pyzed/library/row/): Lays out children horizontally. - [Scrollable](https://albakore.github.io/pyzed/library/scrollable/): Scrolls content that doesn't fit. - [Space](https://albakore.github.io/pyzed/library/spacing/): Fixed gaps and flexible spacers. ### Command palette > A command palette with fuzzy search and shortcuts. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/command-palette/ :::tip[In plain words] A search box for every action of the app: type what you want to do and press Enter. ::: A command palette: it fuzzy-filters `commands` (`ui.PaletteItem(label, on_press, keys=)`) by `query`, highlights the matched letters and shows each shortcut. You own the query and the `selected` index; Enter calls `on_submit`. `ui.command_palette` · Category: [Overlays](https://albakore.github.io/pyzed/library/#overlays) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#command_palette) **Code:** ```python ui.command_palette("tog", on_query, [ ui.PaletteItem("editor: toggle soft wrap", on_wrap, keys="alt-z"), ui.PaletteItem("theme selector: toggle", on_theme, keys="ctrl-k ctrl-t"), ui.PaletteItem("workspace: toggle left dock", on_left, keys="ctrl-b"), ui.PaletteItem("terminal panel: toggle", on_terminal, keys="ctrl-j"), ], selected=0, width=520) ``` #### When to use - To make every action reachable from the keyboard (usually with Ctrl+Shift+P). #### Usage ```python import pyzed as pz from pyzed import ui class App(pz.App): def __init__(self): self.open, self.query, self.selected = False, "", 0 def commands(self): return [ui.PaletteItem("editor: toggle soft wrap", self.toggle_wrap, keys="alt-z"), ui.PaletteItem("workspace: toggle left dock", self.toggle_left, keys="ctrl-b")] def on_key(self, combo): if combo == "ctrl-shift-p": self.open, self.query, self.selected = True, "", 0 return pz.Command.focus("command-palette") if self.open and combo in ("up", "down"): self.selected += 1 if combo == "down" else -1 elif combo == "escape": self.open = False def run_selected(self): matches = ui.fuzzy_filter(self.query, self.commands(), key=lambda c: c.label) if matches: self.open = False return matches[self.selected % len(matches)][0].on_press() def view(self): palette = ui.command_palette(self.query, lambda q: setattr(self, "query", q), self.commands(), selected=self.selected, on_submit=self.run_selected) return ui.layer(self.main_view(), palette if self.open else None, on_dismiss=lambda: setattr(self, "open", False)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Query ```python ui.command_palette("", on_query, [ ui.PaletteItem("file: new", on_new, keys="ctrl-n"), ui.PaletteItem("file: open", on_open, keys="ctrl-o"), ui.PaletteItem("theme selector: toggle", on_theme), ], width=420) ``` ```python ui.command_palette("op", on_query, [ ui.PaletteItem("file: new", on_new, keys="ctrl-n"), ui.PaletteItem("file: open", on_open, keys="ctrl-o"), ui.PaletteItem("workspace: open recent", on_recent, keys="ctrl-r"), ], width=420) ``` ```python ui.command_palette("", on_query, [ ui.PaletteItem("file: new", on_new, keys="ctrl-n"), ui.PaletteItem("file: open", on_open, keys="ctrl-o"), ui.PaletteItem("file: save", on_save, keys="ctrl-s"), ], selected=1, width=420) ``` #### Events Command palette has **3 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_query` | `ui.command_palette` | `(text)` | Callable that receives the new text. | | `on_submit` | `ui.command_palette` | `()` | Message or callable sent when Enter is pressed. | | `on_press` | `ui.PaletteItem` | `()` | Message or callable sent on press. `None` disables the control. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class App(pz.App): def __init__(self): self.open, self.query, self.wrap = True, "", False def toggle_wrap(self): self.wrap, self.open = not self.wrap, False def commands(self): # each item's on_press runs the command return [ui.PaletteItem("editor: toggle soft wrap", self.toggle_wrap, keys="alt-z")] def run_first(self): # on_submit: Enter runs the first match matches = ui.fuzzy_filter(self.query, self.commands(), key=lambda c: c.label) return matches[0][0].on_press() if matches else None def view(self): palette = ui.command_palette(self.query, lambda q: setattr(self, "query", q), # on_query self.commands(), on_submit=self.run_first) return ui.layer(main_view(), palette if self.open else None, on_dismiss=lambda: setattr(self, "open", False)) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Query: text: str class App(pz.App): open, query, wrap = True, "", False def update(self, message): match message: case Query(text): self.query = text case "toggle wrap" | "submit": self.wrap, self.open = not self.wrap, False def view(self): palette = ui.command_palette(self.query, Query, [ui.PaletteItem("editor: toggle soft wrap", "toggle wrap")], on_submit="submit") return ui.layer(main_view(), palette if self.open else None) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: query: str = "" wrap: bool = False def view(state, set_state): items = [ui.PaletteItem("editor: toggle soft wrap", lambda: set_state(wrap=not state.wrap))] return ui.command_palette(state.query, lambda q: set_state(query=q), items, on_submit=lambda: set_state(wrap=not state.wrap)) ``` #### Properties and API ##### `ui.command_palette` ```python command_palette( query, on_query, commands, *, selected=0, on_submit=None, placeholder='Execute a command…', input_id='command-palette', theme=None, **kwargs, ) ``` A command palette: fuzzy-filters `commands` by `query` and shows their shortcuts. | Property | Default | Description | | --- | --- | --- | | `query` | *required* | Search text. | | `commands` | *required* | List of `PaletteItem`. | | `selected` | `0` | Highlighted index (for arrow-key navigation). | | `placeholder` | `"Execute a command…"` | Hint text shown when empty. | | `input_id` | `"command-palette"` | Field id (for `Command.focus`). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | | `**kwargs` | — | Same as `picker`. | Events: `on_query`, `on_submit` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#command_palette) ##### `ui.PaletteItem` ```python @dataclass class PaletteItem(label, on_press=None, keys=None, icon=None, detail=None) ``` An entry of `command_palette` or `picker`. | Property | Default | Description | | --- | --- | --- | | `label` | *required* | Label text. | | `keys` | `None` | Shortcut shown at the end, written as a string: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `detail` | `None` | Secondary text (for example, a file path). | Events: `on_press` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#paletteitem) #### Related - [Picker](https://albakore.github.io/pyzed/library/picker/): A searchable list on a floating surface, with fuzzy matching. - [Layer](https://albakore.github.io/pyzed/library/layer/): Draws an overlay over your view; clicking outside dismisses it. - [Kbd](https://albakore.github.io/pyzed/library/kbd/): Keyboard shortcuts drawn as keys, with symbols on macOS. ### Confirm > A yes/no question, red for destructive actions. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/confirm/ :::tip[In plain words] Asks “are you sure?” before doing something that can't be undone. ::: Asks a yes/no question. `danger=True` paints the confirm button red; `confirm_label` and `cancel_label` rename the buttons. `ui.confirm` · Category: [Dialogs](https://albakore.github.io/pyzed/library/#dialogs) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#confirm) **Code:** ```python ui.dialog_layer(main_view(), ui.confirm( "Delete notes.py? This can't be undone.", on_delete, on_cancel, title="Delete file", danger=True, confirm_label="Delete", width=360, )) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - Before destructive or irreversible actions: delete, overwrite, discard. #### Usage ```python from pyzed import ui ui.dialog_layer(main_view(), ui.confirm( "Delete notes.py? This can't be undone.", on_delete, on_cancel, title="Delete file", danger=True, confirm_label="Delete", width=360, )) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Kind ```python ui.confirm("Close all tabs?", on_confirm, on_cancel, title="Close tabs", width=320) ``` ```python ui.confirm("Delete notes.py?", on_delete, on_cancel, title="Delete file", danger=True, confirm_label="Delete", width=320) ``` ```python ui.confirm("Overwrite the existing file?", on_confirm, on_cancel, kind="warning", width=320) ``` ```python ui.confirm("Push 3 commits to main?", on_push, on_cancel, confirm_label="Push", cancel_label="Not now", width=320) ``` #### Events Confirm has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_confirm` | `()` | The confirm button. | | `on_cancel` | `()` | Cancel, the close icon. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Files(pz.App): def __init__(self): self.files = ["notes.py", "todo.txt"] self.deleting = "notes.py" # the file waiting for an answer def delete(self): # on_confirm self.files.remove(self.deleting) self.deleting = None def view(self): dialog = None if self.deleting: dialog = ui.confirm(f"Delete {self.deleting}?", self.delete, lambda: setattr(self, "deleting", None), # on_cancel title="Delete file", danger=True, confirm_label="Delete") return ui.dialog_layer(main_view(), dialog) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Delete: name: str class Files(pz.App): files = ["notes.py", "todo.txt"] deleting = "notes.py" def update(self, message): match message: case Delete(name): self.files = [f for f in self.files if f != name] self.deleting = None case "cancel": self.deleting = None def view(self): dialog = ui.confirm(f"Delete {self.deleting}?", Delete(self.deleting), "cancel", danger=True) \ if self.deleting else None return ui.dialog_layer(main_view(), dialog) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: files: tuple = ("notes.py", "todo.txt") deleting: str = "notes.py" def view(state, set_state): if not state.deleting: return main_view() keep = tuple(f for f in state.files if f != state.deleting) return ui.dialog_layer(main_view(), ui.confirm( f"Delete {state.deleting}?", lambda: set_state(files=keep, deleting=""), lambda: set_state(deleting=""), danger=True)) ``` #### Properties and API ##### `ui.confirm` ```python confirm( message, on_confirm, on_cancel, *, title=None, confirm_label=None, cancel_label=None, danger=False, kind=None, width=420, theme=None, ) ``` Asks a yes/no question. | Property | Default | Description | | --- | --- | --- | | `message` | *required* | Text or widget. | | `title` | `None` | Title. | | `confirm_label` | `None` | Confirm button text. | | `cancel_label` | `None` | Cancel button text. | | `danger` | `False` | Paints the confirm button red and labels it Delete. | | `kind` | `None` | Adds an icon, as in `alert`. | | `width` | `420` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_confirm`, `on_cancel` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#confirm) #### Related - [Alert](https://albakore.github.io/pyzed/library/alert/): A message with one button. - [Choice](https://albakore.github.io/pyzed/library/choice/): A message with your own buttons. - [Unsaved changes](https://albakore.github.io/pyzed/library/unsaved-changes/): “Save your changes before closing?” ### Console > A dev-tools style log with levels, filter, copy and clear. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/console/ :::tip[In plain words] A list of messages from the app (info, warnings, errors) that you can filter and copy. ::: `ui.Log` collects entries (thread-safe, capped at `limit`, repeated lines folded) and can capture `print` and tracebacks. `ui.console` shows them like a browser's dev tools console: a filter box, one toggle per level with its count, copy and clear. Levels are `debug`, `info`, `event`, `stdout`, `stderr`, `warning` and `error` (`ui.LOG_LEVELS`). With a `ui.LogSelection` the lines can be selected (click, Shift+click, Ctrl+click) and a selected line shows its `detail` (a traceback); `on_context` opens your menu on right click, and `sources` adds a dropdown to show the lines of one source. `ui.console` · Category: [Code and tools](https://albakore.github.io/pyzed/library/#code-and-tools) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#console) **Code:** ```python log = ui.Log() log.write("info", "Loaded ui/main.pzform") log.write("event", "button1.click") log.write("stdout", "Saving 3 files…") log.write("warning", "settings.json not found, using defaults") log.write("error", "ValueError: invalid literal for int()") pz.container(ui.console(log.entries, on_clear=on_clear, on_copy=on_copy, on_query=on_query), width=600, height=220) ``` #### When to use - For a debug panel in your app, or to show what a background job is doing. #### Usage ```python import pyzed as pz from pyzed import ui class Debug(pz.App): def __init__(self): self.log = ui.Log(on_write=lambda: pz.post("log")) # redraw when a line arrives self.log.capture() # also collect print() and stderr self.query, self.hidden = "", set() def view(self): return ui.console(self.log.entries, query=self.query, on_query=lambda q: setattr(self, "query", q), hidden=self.hidden, on_toggle_level=lambda lvl: self.hidden.symmetric_difference_update({lvl}), on_clear=self.log.clear, on_copy=lambda: pz.Command.copy(self.log.text())) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python log = ui.Log() for level in ui.LOG_LEVELS: log.write(level, f"{level} message") pz.container(ui.console(log.entries, on_clear=on_clear), width=520, height=220) ``` ```python log = ui.Log() for level in ui.LOG_LEVELS: log.write(level, f"{level} message") pz.container(ui.console(log.entries, hidden={"info", "event"}, on_toggle_level=on_toggle), width=520, height=200) ``` ```python log = ui.Log() log.write("info", "started") log.write("error", "ValueError: bad input") pz.container(ui.console(log.entries, query="error", on_query=on_query), width=520, height=140) ``` #### Events Console has **8 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_query` | `ui.console` | `(text)` | Receives each edit of the filter. | | `on_toggle_level` | `ui.console` | `(level)` | Called with the level of the clicked toggle. | | `on_clear` | `ui.console` | `()` | Adds a Clear button. | | `on_copy` | `ui.console` | `()` | Adds a Copy button. | | `on_context` | `ui.console` | `(line)` | Called with the line right-clicked (`None` under the lines): open a menu there (copy, go to source, clear...). | | `on_open` | `ui.console` | `(line)` | Called with the line double-clicked. | | `on_source` | `ui.console` | `(source)` | Receives the source picked in the dropdown (`None` for all). | | `on_write` | `ui.Log` | `()` | Called after every write (from any thread). | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Debug(pz.App): def __init__(self): self.log = ui.Log() self.log.write("info", "Started") self.log.write("error", "ValueError: bad input") self.log.on_write = lambda: pz.post("log") # a line arrived (from any thread) self.query, self.hidden = "", set() def update(self, message): pass # "log" just redraws def view(self): return ui.console(self.log.entries, query=self.query, hidden=self.hidden, on_query=lambda q: setattr(self, "query", q), # the filter on_toggle_level=lambda lvl: self.hidden.symmetric_difference_update({lvl}), on_clear=self.log.clear, # adds Clear on_copy=lambda: pz.Command.copy(self.log.text()), # adds Copy on_open=lambda entry: print("open", entry.text)) # double click ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Filter: query: str @dataclass class ToggleLevel: level: str class Debug(pz.App): log = ui.Log() query, hidden = "", frozenset() def update(self, message): match message: case Filter(query): self.query = query case ToggleLevel(level): self.hidden = self.hidden ^ {level} case "clear": self.log.clear() def view(self): return ui.console(self.log.entries, query=self.query, hidden=self.hidden, on_query=Filter, on_toggle_level=ToggleLevel, on_clear="clear") ``` **Functional:** ```python from dataclasses import dataclass LOG = ui.Log() LOG.write("info", "Started") @dataclass(frozen=True) class State: query: str = "" hidden: frozenset = frozenset() def view(state, set_state): return ui.console(LOG.entries, query=state.query, hidden=state.hidden, on_query=lambda q: set_state(query=q), on_toggle_level=lambda lvl: set_state(hidden=state.hidden ^ {lvl})) ``` #### Properties and API ##### `ui.console` ```python console( entries, *, query='', on_query=None, hidden=(), on_toggle_level=None, on_clear=None, on_copy=None, actions=(), selection=None, on_context=None, on_open=None, sources=(), source=None, on_source=None, show_source=False, placeholder=None, empty=None, font_size=12.5, id=None, theme=None, ) ``` A log view like a browser's dev tools console: a filter box, one toggle per level with its count, copy and clear, and the entries in a monospace list that keeps scrolling to the newest one. | Property | Default | Description | | --- | --- | --- | | `entries` | *required* | `ui.LogEntry` items (`log.entries`). | | `query` | `""` | Filter text. | | `hidden` | `()` | Levels not shown. | | `actions` | `()` | Extra widgets at the end of the toolbar. | | `selection` | `None` | A `ui.LogSelection`: the lines can be selected with the mouse, and a selected line with `detail` shows it below. | | `sources` | `()` | `(value, label)` pairs: adds a dropdown that shows the lines of one `source`. | | `source` | `None` | The source shown (`None` for all). | | `show_source` | `False` | Tags each line with its source. | | `placeholder` | `None` | Text of the empty filter box. | | `empty` | `None` | What shows while there are no lines. | | `font_size` | `12.5` | Size of the entries. | | `id` | `None` | Scrollable id. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_query`, `on_toggle_level`, `on_clear`, `on_copy`, `on_context`, `on_open`, `on_source` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#console) ##### `ui.Log` ```python class Log ``` The entries of a `ui.console`: thread-safe, capped at `limit`, with repeated lines folded into one. `write(level, text)` adds a line; `capture()` (or `with log:`) copies `print` output and tracebacks into it; `pipe(stream)` reads a subprocess's output in a background thread; `text()` returns everything for copying (`text(ids)` only those lines). `write` also takes the line's `source`, `detail` (a traceback) and `origin` (`(path, line)`) and returns the entry; `find(id)` looks one up. | Property | Default | Description | | --- | --- | --- | | `limit` | `5000` | Most entries kept. | Events: `on_write` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#log) ##### `ui.LogEntry` ```python @dataclass class LogEntry(level, text, time=time.time(), count=1, source='', detail='', origin=None, id=0) ``` One line of a `ui.Log`: `level`, `text`, `time`, `count` (identical lines in a row), `source`, `detail`, `origin`, its number `id`, `clock` (`HH:MM:SS`) and `line()` (the entry as plain text). | Property | Default | Description | | --- | --- | --- | | `level` | *required* | Level name. | | `text` | *required* | The line. | | `time` | `time()` | When it was written (`time.time()`). | | `count` | `1` | Identical lines in a row. | | `source` | `""` | What it comes from (`"project"`, `"agent"`...); `ui.console` can show and filter it. | | `detail` | `""` | More text shown under the line while it is selected (a traceback). | | `origin` | `None` | `(path, line)` of the code it is about. | | `id` | `0` | Its number in the log (what a `ui.LogSelection` keeps). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#logentry) ##### `ui.LogSelection` ```python class LogSelection ``` The selected lines of a `ui.console`, like in a file list: a click selects one line, Shift+click a range from the last one clicked and Ctrl+click (Cmd on macOS) adds or removes one. Keep one in your app and pass it to `ui.console(selection=...)`; `select(ids)` selects exactly those (Ctrl+A), `clear()` none, `keep(entries)` forgets lines that are gone, and `ids` are the selected entries' `id`s for `log.text(ids)`. [Full reference →](https://albakore.github.io/pyzed/reference/ui/#logselection) ##### `ui.LOG_LEVELS` The levels `ui.console` colors, in the order of its filter buttons: debug, info, event, stdout, stderr, warning, error. [Full reference →](https://albakore.github.io/pyzed/reference/ui/#log_levels) #### Related - [Terminal](https://albakore.github.io/pyzed/library/terminal/): A read-only terminal view with ANSI colors. - [Editor](https://albakore.github.io/pyzed/library/editor/): A full code editor: highlighting, folding, minimap, git markers. ### Container > Size, padding, alignment, background, border and shadow for one child. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/container/ :::tip[In plain words] A box around one thing, to give it a size, a margin, a background or a border. ::: Wraps one child to give it a size, padding and alignment, and to draw a background (color or gradient), a border, rounded corners and a shadow. `pz.center(child)` is a container that centers its child in all the available space. `pz.container` · Category: [Layout](https://albakore.github.io/pyzed/library/#layout) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#container) **Code:** ```python t = pz.theme.current() pz.row([ pz.container(ui.label("padding + bg"), padding=16, bg=t.element, radius=8), pz.container(ui.label("border"), padding=16, border_color=t.accent, border_width=2, radius=8), pz.container(ui.label("shadow", color="#ffffff"), padding=16, radius=12, gradient=(135, ["#7c3aed", "#3b82f6"]), shadow=("#00000066", (0, 6), 18)), ], spacing=16, align="center") ``` #### When to use - Whenever a widget needs a box: cards, colored areas, fixed sizes, centering. #### Usage ```python import pyzed as pz from pyzed import ui t = pz.theme.current() pz.row([ pz.container(ui.label("padding + bg"), padding=16, bg=t.element, radius=8), pz.container(ui.label("border"), padding=16, border_color=t.accent, border_width=2, radius=8), pz.container(ui.label("shadow", color="#ffffff"), padding=16, radius=12, gradient=(135, ["#7c3aed", "#3b82f6"]), shadow=("#00000066", (0, 6), 18)), ], spacing=16, align="center") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Box ```python pz.container(ui.label("padding + bg"), padding=16, bg=pz.theme.current().element, radius=8) ``` ```python pz.container(ui.label("border"), padding=16, border_color=pz.theme.current().accent, border_width=2, radius=8) ``` ```python pz.container(ui.label("pill"), padding=(6, 16), bg=pz.theme.current().element, radius=999) ``` ##### Paint ```python pz.container(ui.label("gradient", color="#ffffff"), padding=16, radius=10, gradient=(135, ["#7c3aed", "#3b82f6"])) ``` ```python pz.container(ui.label("shadow"), padding=16, radius=10, bg=pz.theme.current().elevated, shadow=("#00000066", (0, 6), 18)) ``` ##### Alignment ```python pz.container(ui.label("centered"), center=True, width=220, height=90, border_color=pz.theme.current().border, border_width=1, radius=8) ``` ```python pz.container(ui.label("bottom right"), align_x="end", align_y="end", padding=8, width=220, height=90, border_color=pz.theme.current().border, border_width=1, radius=8) ``` #### Examples ##### Centering `pz.center` (or `center=True`) centers the child in the space it gets. **Code:** ```python pz.container(pz.center(ui.label("centered")), width=360, height=120, border_color=pz.theme.current().border, border_width=1, radius=8) ``` #### Events Container has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.container` ```python container( content=None, *, padding=None, width=None, height=None, max_width=None, max_height=None, align_x=None, align_y=None, center=False, bg=None, color=None, border_color=None, border_width=0.0, radius=None, shadow=None, clip=False, gradient=None, ) ``` Wraps a child to give it size, alignment, background, border, radius and shadow. | Property | Default | Description | | --- | --- | --- | | `content` | `None` | The child (or `None` for an empty box, handy as a colored rectangle). | | `padding` | `None` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `max_width` | `None` | Maximum width in pixels. | | `max_height` | `None` | Maximum height in pixels. | | `align_x` | `None` | Horizontal alignment: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `align_y` | `None` | Vertical alignment: `"start"`/`"top"`, `"center"`, `"end"`/`"bottom"`. | | `center` | `False` | Centers the content on both axes and fills all the space. | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `color` | `None` | Text or icon color. | | `border_color` | `None` | Border color. | | `border_width` | `0.0` | Border width in pixels. | | `radius` | `None` | Corner radius: a number or `(top_left, top_right, bottom_right, bottom_left)`. | | `shadow` | `None` | Shadow: `(color, (dx, dy), blur)`. | | `clip` | `False` | Clips content that overflows the bounds. | | `gradient` | `None` | Linear background gradient: `(angle, [colors])` or `(angle, [(position, color), ...])`. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#container) ##### `pz.center` ```python center(content, **kwargs) ``` Centers `content` in all the available space. Equivalent to `container(content, center=True)`. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `**kwargs` | — | Any `container` parameter. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#center) #### Related - [Card](https://albakore.github.io/pyzed/library/card/): A bordered surface with an optional title and actions. - [Stack](https://albakore.github.io/pyzed/library/stack/): Layers children on top of each other; `pin` places one at x, y. - [Row](https://albakore.github.io/pyzed/library/row/): Lays out children horizontally. ### Context meter > How full a model's context window is: a ring and `48k / 200k`. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/context-meter/ :::tip[In plain words] A little ring that fills up as a chat with an AI gets longer, like a fuel gauge. ::: `ui.context_meter(used, limit)` shows how full an AI model's context window is: a small ring and `48k / 200k`. It is gray below 80%, yellow from 80% and red from 95%, and its tooltip gives the exact counts. `ui.format_tokens(count)` writes a count the short way: `950`, `4.5k`, `48k`, `1.25M`. `ui.context_meter` · Category: [AI agents](https://albakore.github.io/pyzed/library/#ai-agents) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#context_meter) **Code:** ```python pz.row([ ui.context_meter(48_000, 200_000), ui.context_meter(170_000, 200_000), ui.context_meter(196_000, 200_000), ], spacing=24, align="center") ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - In the toolbar or the input box of an agent's chat, so the user sees when a conversation gets long. #### Usage ```python import pyzed as pz from pyzed import ui pz.row([ ui.context_meter(48_000, 200_000), ui.context_meter(170_000, 200_000), ui.context_meter(196_000, 200_000), ], spacing=24, align="center") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Level Drawn as the pointer shows it: tooltips only open on hover. ```python ui.context_meter(48_000, 200_000) ``` ```python ui.context_meter(170_000, 200_000) ``` ```python ui.context_meter(196_000, 200_000) ``` ```python ui.context_meter(48_000, 200_000) ``` ##### Short counts `ui.format_tokens` returns the text the meter shows, for your own labels. ```python ui.label(ui.format_tokens(950), mono=True) ``` ```python ui.label(ui.format_tokens(4_500), mono=True) ``` ```python ui.label(ui.format_tokens(48_000), mono=True) ``` ```python ui.label(ui.format_tokens(1_250_000), mono=True) ``` #### Events Context meter has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.context_meter` ```python context_meter(used, limit, *, theme=None) ``` How full a model's context window is, like Zed's token counter: a small ring and `48k / 200k`, yellow past 80% and red past 95%. | Property | Default | Description | | --- | --- | --- | | `used` | *required* | Tokens in use. | | `limit` | *required* | The model's window. | | `theme` | `None` | Theme. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#context_meter) ##### `ui.format_tokens` ```python format_tokens(count) ``` A short token count: `950`, `4.5k`, `48k`, `1.25M`. | Property | Default | Description | | --- | --- | --- | | `count` | *required* | Tokens. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#format_tokens) #### Related - [Donut](https://albakore.github.io/pyzed/library/donut/): A donut chart with a label in the middle. - [Progress](https://albakore.github.io/pyzed/library/progress/): A thin progress bar in a semantic color. - [Tool call](https://albakore.github.io/pyzed/library/tool-call/): One step of an AI agent: what it did, its result, and Allow or Reject. ### Count badge > A round counter that shows 99+ above its max. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/count-badge/ :::tip[In plain words] A little round number, like the count of unread messages on an app icon. ::: A round counter for notifications or problems. Above `max` it shows `99+`. `ui.count_badge` · Category: [Feedback](https://albakore.github.io/pyzed/library/#feedback) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#count_badge) **Code:** ```python pz.row([ ui.count_badge(3), ui.count_badge(42, tint="success"), ui.count_badge(120, tint="error"), pz.row([ui.icon_button("bell", on_press), ui.count_badge(7)], spacing=2, align="center"), ], spacing=16, align="center") ``` #### When to use - Next to an icon or tab to show how many unread items or errors there are. - For text statuses use a [Badge](https://albakore.github.io/pyzed/library/badge/). #### Usage ```python import pyzed as pz from pyzed import ui pz.row([ ui.count_badge(3), ui.count_badge(42, tint="success"), ui.count_badge(120, tint="error"), pz.row([ui.icon_button("bell", on_press), ui.count_badge(7)], spacing=2, align="center"), ], spacing=16, align="center") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Count Above `max` it shows `max+`. ```python ui.count_badge(3) ``` ```python ui.count_badge(42) ``` ```python ui.count_badge(120) ``` ```python ui.count_badge(12, max=9) ``` ##### Tint ```python ui.count_badge(5, tint="success") ``` ```python ui.count_badge(5, tint="warning") ``` ```python ui.count_badge(5, tint="error") ``` ##### Next to an icon ```python pz.row([ui.icon_button("bell", on_press), ui.count_badge(7)], spacing=2, align="center") ``` ##### States and events With `show_zero=False` it draws nothing at 0 but keeps its place in the layout. ```python pz.row([ui.icon_button("bell", on_press), ui.count_badge(0, show_zero=False)], align="center") ``` #### Events Count badge has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.count_badge` ```python count_badge(count, *, tint='accent', max=99, show_zero=True, theme=None) ``` A round counter (notifications, problems…). Shows `99+` above `max`. | Property | Default | Description | | --- | --- | --- | | `count` | *required* | The number. | | `tint` | `"accent"` | Semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` or a token. | | `max` | `99` | Maximum before showing `N+`. | | `show_zero` | `True` | With `False`, a count of 0 draws nothing. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#count_badge) #### Related - [Badge](https://albakore.github.io/pyzed/library/badge/): A small pill for statuses, versions and counters. - [Indicator](https://albakore.github.io/pyzed/library/indicator/): A colored dot for status, presence or unsaved changes. - [Status bar](https://albakore.github.io/pyzed/library/status-bar/): The bottom bar: diagnostics, cursor position, language. ### Date picker > A date field with a calendar that drops down, in the format you choose. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/date-picker/ :::tip[In plain words] A box for a date. Type it, or press the little calendar and click a day. ::: `ui.DatePicker` is a text field for one date. The calendar button (or Alt+↓) opens a [Calendar](https://albakore.github.io/pyzed/library/calendar/) under it, and the user can also type the date: it is read with `format` when they press Enter or leave the field. With the focus in the field, ↑ and ↓ move one day and Page Up/Page Down one month, without opening anything. `on_change` receives a `datetime.date` (or `None` when the field is cleared) in `event.value`. `ui.DatePicker` · Category: [Dates and times](https://albakore.github.io/pyzed/library/#dates-and-times) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#datepicker) **Code:** ```python pz.container( ui.DatePicker(value="2026-10-22", format="DD/MM/YYYY", on_change=on_change), width=220, height=330, align_y="start") ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - For birthdays, due dates, bookings and any form field that holds a date. - `format` takes tokens (`DD/MM/YYYY`, `MMM D, YYYY`, `dddd D MMMM`) or a `strftime` pattern (`%d.%m.%Y`). Month and day names follow the app's language. - `min`, `max` and `disabled_dates` (dates, `"weekend"` or day names) turn days off. - For two dates (from–to), use [Date range picker](https://albakore.github.io/pyzed/library/date-range-picker/). #### Usage ```python import pyzed as pz from pyzed import ui import datetime class Booking(pz.App): def __init__(self): self.day = datetime.date.today() def view(self): return pz.column([ ui.field("Check-in", ui.DatePicker(value=self.day, format="ddd D MMM YYYY", min=datetime.date.today(), disabled_dates=["sunday"], on_change=lambda e: setattr(self, "day", e.value))), ui.label(f"Booked for {ui.format_date(self.day, 'dddd D MMMM')}"), ], spacing=12, width=320) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Format Tokens: `YYYY` `YY`, `MMMM` `MMM` `MM` `M`, `DD` `D`, `dddd` `ddd`; `[text]` is copied as is. ```python ui.DatePicker(value="2026-10-22", on_change=on_change) ``` ```python ui.DatePicker(value="2026-10-22", format="DD/MM/YYYY", on_change=on_change) ``` ```python ui.DatePicker(value="2026-10-22", format="MMM D, YYYY", on_change=on_change) ``` ```python ui.DatePicker(value="2026-10-22", format="ddd D MMM", on_change=on_change) ``` ##### States ```python ui.DatePicker(placeholder="Pick a date", on_change=on_change) ``` ```python ui.DatePicker(value="2026-10-22", invalid=True, on_change=on_change) ``` ```python ui.DatePicker(value="2026-10-22", disabled=True, on_change=on_change) ``` ```python ui.DatePicker(value="2026-10-22", on_change=on_change) ``` #### Examples ##### Formats `ui.format_date` and `ui.parse_date` use the same tokens as the field, for your own labels. **Code:** ```python pz.column([ ui.label(ui.format_date("2026-10-22", "DD/MM/YYYY"), mono=True), ui.label(ui.format_date("2026-10-22", "MMM D, YYYY"), mono=True), ui.label(ui.format_date("2026-10-22", "dddd, D MMMM YYYY"), mono=True), ui.label(ui.format_date("2026-10-22", "[week of] D/M/YY"), mono=True), ], spacing=6) ``` #### Events Date picker has **4 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_change` | `ui.DatePicker` | `(event)` | Receives `event.value`: a `datetime.date` or `None`. | | `on_open` | `ui.DatePicker` | `(event)` | The calendar opened. | | `on_close` | `ui.DatePicker` | `(event)` | The calendar closed. | | `on_dismiss` | `pz.popup` | `()` | Message or callable sent on a press outside or Escape. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python import datetime class Booking(pz.App): def __init__(self): self.day = datetime.date(2026, 10, 22) def changed(self, event): # on_change: event.value is a datetime.date (or None) self.day = event.value def view(self): return pz.column([ ui.DatePicker(value=self.day, format="DD/MM/YYYY", on_change=self.changed), ui.label(ui.format_date(self.day, "dddd D MMMM") or "No date"), ], spacing=8, width=240) ``` **Declarative:** ```python import datetime from dataclasses import dataclass @dataclass class Picked: day: datetime.date class Booking(pz.App): day = datetime.date(2026, 10, 22) def update(self, message): match message: case Picked(day): self.day = day def view(self): # DatePicker is a pz.Component: its handler gets a pz.Event return ui.DatePicker(value=self.day, on_change=lambda e: Picked(e.value)) ``` **Functional:** ```python import datetime from dataclasses import dataclass @dataclass(frozen=True) class State: day: datetime.date = datetime.date(2026, 10, 22) def view(state, set_state): return pz.column([ ui.DatePicker(value=state.day, format="MMM D, YYYY", on_change=lambda e: set_state(day=e.value)), ui.label(f"Week {state.day.isocalendar().week}" if state.day else "No date"), ], spacing=8, width=240) ``` #### Tips :::tip The calendar opens in a `pz.popup`, a layer drawn over the window: it is never clipped by a scrollable or a small container, flips above the field when there is no room below and closes with Escape or a click outside. ::: #### Properties and API ##### `ui.DatePicker` ```python class DatePicker ``` A component: a date field that opens a calendar below it, in a popup that panels never cut off. It can be typed in `format` (Enter or leaving the field reads it); once clicked, ↑/↓ move the date a day and Page Up/Down a month without opening anything, Alt+↓ opens the calendar and Escape closes it. See the [Date picker](https://albakore.github.io/pyzed/library/date-picker/) page. | Property | Default | Description | | --- | --- | --- | | `value` | `None` | The date (`datetime.date`, ISO text or `None`). | | `format` | `"YYYY-MM-DD"` | How it is written: `"DD/MM/YYYY"`, `"MMM D, YYYY"`... (see `format_date`). | | `placeholder` | `""` | Hint while empty (default: the format). | | `min` | `None` | The earliest date. | | `max` | `None` | The latest date. | | `disabled_dates` | `()` | Days that cannot be picked (as in `Calendar`). | | `first_weekday` | `"monday"` | `"monday"`, `"sunday"` or `"saturday"`. | | `week_numbers` | `False` | Shows week numbers in the calendar. | | `clearable` | `True` | Shows an x that empties it. | | `today_button` | `True` | Adds a Today link to the calendar. | | `disabled` | `False` | Greys it out. | | `invalid` | `False` | Paints the border in the error color. | Events: `on_change`, `on_open`, `on_close` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#datepicker) ##### `ui.format_date` ```python format_date(value, fmt='YYYY-MM-DD', *, language=None) ``` Writes a date with a pattern of tokens: `YYYY` 2026, `YY` 26, `MMMM` October, `MMM` Oct, `MM` 10, `M` 10, `DD` 05, `D` 5, `dddd` Monday, `ddd` Mon, `HH`/`hh` hours, `mm` minutes, `ss` seconds, `A` AM/PM and `[text]` kept as is. A pattern with `%` is a `strftime` one. Month and day names follow the language of `pyzed.i18n`. | Property | Default | Description | | --- | --- | --- | | `value` | *required* | A date, a datetime, ISO text or `None` (gives `""`). | | `fmt` | `"YYYY-MM-DD"` | The pattern. | | `language` | `None` | A language for the names instead of the current one. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#format_date) ##### `ui.parse_date` ```python parse_date(text, fmt='YYYY-MM-DD', *, language=None) ``` Reads a date written with a pattern of `format_date` (month names in the current language or English); ISO `YYYY-MM-DD` always works. Returns `None` when the text is not a date. | Property | Default | Description | | --- | --- | --- | | `text` | *required* | The text. | | `fmt` | `"YYYY-MM-DD"` | The pattern. | | `language` | `None` | The language of the names. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#parse_date) ##### `pz.popup` ```python popup(base, content=None, *, on_dismiss=None, side='bottom', align='start', gap=4.0) ``` Shows `content` over everything else next to `base`, like the menu of a drop-down: it is not cut off by the panels `base` sits in, flips to the other side when it does not fit and stays inside the window. A press outside it (and outside `base`) or Escape calls `on_dismiss`. With `content=None` only `base` is shown. The date pickers open their calendars with it. | Property | Default | Description | | --- | --- | --- | | `base` | *required* | The widget it belongs to (a field, a button). | | `content` | `None` | What opens (a panel, a menu), or `None` while closed. | | `side` | `"bottom"` | `"bottom"`, `"top"`, `"right"` or `"left"` of `base`. | | `align` | `"start"` | `"start"`, `"center"` or `"end"`: how it lines up with `base`. | | `gap` | `4.0` | Space between `base` and the content. | Events: `on_dismiss` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#popup) #### Related - [Calendar](https://albakore.github.io/pyzed/library/calendar/): A month grid to pick a day, a range or several days, always on screen. - [Date range picker](https://albakore.github.io/pyzed/library/date-range-picker/): Two dates in one field, with two months side by side and quick ranges. - [Time picker](https://albakore.github.io/pyzed/library/time-picker/): A time field with columns of hours and minutes, in 24 or 12 hours. - [Input](https://albakore.github.io/pyzed/library/input/): A controlled text field with icon, placeholder and password mode. ### Date range picker > Two dates in one field, with two months side by side and quick ranges. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/date-range-picker/ :::tip[In plain words] A box for two dates, from and to. You click where the period starts and where it ends. ::: `ui.DateRangePicker` holds a start and an end date. Its popup shows two months: the first click picks the start, the pointer previews the range and the second click picks the end (in any order). The column on the left has quick ranges (today, last 7 days, this week, this month, last month...). `on_change` receives `(start, end)` as two `datetime.date`. `ui.DateRangePicker` · Category: [Dates and times](https://albakore.github.io/pyzed/library/#dates-and-times) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#daterangepicker) **Code:** ```python pz.container( ui.DateRangePicker(value=("2026-10-05", "2026-10-14"), format="DD/MM/YYYY", on_change=on_change), width=260, height=330, align_y="start") ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - For reports, filters, stays and anything that spans days. - `months=1` shows a single month and `presets=False` hides the quick ranges. - The field can be typed too: two dates in `format` with any separator. #### Usage ```python import pyzed as pz from pyzed import ui class Report(pz.App): def __init__(self): self.period = (None, None) def view(self): start, end = self.period days = (end - start).days + 1 if start and end else 0 return pz.column([ ui.DateRangePicker(value=self.period, format="MMM D, YYYY", on_change=lambda e: setattr(self, "period", e.value)), ui.label(f"{days} days selected", color="text_muted"), ], spacing=8, width=300) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Popup ```python ui.DateRangePicker(value=("2026-10-05", "2026-10-14"), on_change=on_change) ``` ```python ui.DateRangePicker(months=1, presets=False, on_change=on_change) ``` #### Events Date range picker has **3 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. | Event | Handler gets | When | | --- | --- | --- | | `on_change` | `(event)` | Receives `event.value`: `(start, end)`. | | `on_open` | `(event)` | The calendar opened. | | `on_close` | `(event)` | The calendar closed. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Report(pz.App): def __init__(self): self.period = ("2026-10-05", "2026-10-14") def changed(self, event): # on_change: event.value is (start, end) self.period = event.value def view(self): start, end = self.period return pz.column([ ui.DateRangePicker(value=self.period, on_change=self.changed), ui.label(f"From {ui.format_date(start, 'D MMM')} to {ui.format_date(end, 'D MMM')}"), ], spacing=8, width=280) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Period: days: tuple class Report(pz.App): period = (None, None) def update(self, message): match message: case Period(days): self.period = days def view(self): return ui.DateRangePicker(value=self.period, on_change=lambda e: Period(e.value)) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: period: tuple = (None, None) def view(state, set_state): return ui.DateRangePicker(value=state.period, months=1, presets=False, on_change=lambda e: set_state(period=e.value)) ``` #### Properties and API ##### `ui.DateRangePicker` ```python class DateRangePicker ``` A component: a field for a start and an end date, with two months and quick ranges (Today, Last 7 days, This month...) in its popup. Typed text is two dates separated by `separator`. See the [Date range picker](https://albakore.github.io/pyzed/library/date-range-picker/) page. | Property | Default | Description | | --- | --- | --- | | `value` | `None` | `(start, end)`: dates, ISO text or `None`. | | `format` | `"YYYY-MM-DD"` | How each date is written. | | `separator` | `" – "` | Text between the two dates. | | `placeholder` | `""` | Hint while empty. | | `min` | `None` | The earliest date. | | `max` | `None` | The latest date. | | `disabled_dates` | `()` | Days that cannot be picked. | | `first_weekday` | `"monday"` | `"monday"`, `"sunday"` or `"saturday"`. | | `months` | `2` | Months shown side by side. | | `presets` | `True` | Shows the quick ranges. | | `clearable` | `True` | Shows an x that empties it. | | `disabled` | `False` | Greys it out. | | `invalid` | `False` | Paints the border in the error color. | Events: `on_change`, `on_open`, `on_close` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#daterangepicker) #### Related - [Date picker](https://albakore.github.io/pyzed/library/date-picker/): A date field with a calendar that drops down, in the format you choose. - [Calendar](https://albakore.github.io/pyzed/library/calendar/): A month grid to pick a day, a range or several days, always on screen. ### Dialog layer > Centers a dialog over your view with a dimmed backdrop. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/dialog-layer/ :::tip[In plain words] Darkens the app and shows a dialog in the middle, so the question can't be missed. ::: Shows a dialog centered over `base` with a dimmed backdrop; clicking the backdrop calls `on_dismiss`. With `dialog=None` it returns `base` unchanged, so keep which dialog is open in your state and always wrap the view with it. `ui.dialog_layer` · Category: [Dialogs](https://albakore.github.io/pyzed/library/#dialogs) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#dialog_layer) **Code:** ```python ui.dialog_layer( main_view(), ui.confirm("Delete notes.py? This can't be undone.", on_delete, on_cancel, title="Delete file", danger=True, width=340), on_dismiss=on_cancel, ) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - Around your whole view, to show any of the dialogs on this page or a `ui.modal`. #### Usage ```python import pyzed as pz from pyzed import ui class Files(pz.App): def __init__(self): self.asking = False def close(self): self.asking = False def view(self): dialog = None if self.asking: dialog = ui.confirm("Delete main.py?", self.delete, self.close, title="Delete file", danger=True) return ui.dialog_layer(self.main_view(), dialog, on_dismiss=self.close) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Dialog ```python ui.dialog_layer(main_view(), ui.alert("Export finished.", on_close, kind="success", width=280)) ``` ```python ui.dialog_layer(main_view(), ui.modal(ui.muted("Any content."), title="Modal", on_close=on_close, width=280), on_dismiss=on_close) ``` ```python ui.dialog_layer(main_view(), None) ``` #### Events Dialog layer has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_dismiss` | `()` | Sent when the backdrop is clicked. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Files(pz.App): def __init__(self): self.asking = True def close(self): self.asking = False def view(self): dialog = ui.confirm("Delete main.py?", self.close, self.close, danger=True) if self.asking else None return ui.dialog_layer(main_view(), dialog, on_dismiss=self.close) # click on the backdrop ``` **Declarative:** ```python class Files(pz.App): asking = True def update(self, message): if message in ("delete", "cancel"): self.asking = False def view(self): dialog = ui.confirm("Delete main.py?", "delete", "cancel", danger=True) if self.asking else None return ui.dialog_layer(main_view(), dialog, on_dismiss="cancel") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: asking: bool = True def view(state, set_state): close = lambda: set_state(asking=False) dialog = ui.confirm("Delete main.py?", close, close, danger=True) if state.asking else None return ui.dialog_layer(main_view(), dialog, on_dismiss=close) ``` #### Tips :::tip Button labels (OK, Cancel, Save…) are translated with [`pyzed.i18n`](https://albakore.github.io/pyzed/guides/i18n/). ::: #### Properties and API ##### `ui.dialog_layer` ```python dialog_layer(base, dialog=None, *, on_dismiss=None, theme=None) ``` Shows a dialog centered over `base` with a dimmed backdrop. With `dialog=None` it returns `base` unchanged, so it can always wrap the view. See [Dialogs](https://albakore.github.io/pyzed/guides/i18n/#dialogs). | Property | Default | Description | | --- | --- | --- | | `base` | *required* | The UI underneath. | | `dialog` | `None` | A dialog from the functions below, or `ui.modal`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_dismiss` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#dialog_layer) #### Related - [Alert](https://albakore.github.io/pyzed/library/alert/): A message with one button. - [Confirm](https://albakore.github.io/pyzed/library/confirm/): A yes/no question, red for destructive actions. - [Prompt](https://albakore.github.io/pyzed/library/prompt/): Asks for a line of text. - [Modal](https://albakore.github.io/pyzed/library/modal/): A dialog surface with title, content and footer buttons. ### Diff stat > `+12 −3` with GitHub-style blocks. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/diff-stat/ :::tip[In plain words] How many lines were added (green) and removed (red) in a change. ::: Lines added and removed, with a row of green and red blocks, as in the git panel of a code editor. `ui.diff_stat` · Category: [Data](https://albakore.github.io/pyzed/library/#data) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#diff_stat) **Code:** ```python pz.column([ pz.row([ui.label("render.rs", mono=True), pz.hspace(), ui.diff_stat(42, 9)], align="center"), pz.row([ui.label("app.py", mono=True), pz.hspace(), ui.diff_stat(3, 27)], align="center"), pz.row([ui.label("README.md", mono=True), pz.hspace(), ui.diff_stat(12, 0)], align="center"), ], spacing=10, width=320) ``` #### When to use - Next to a file, commit or pull request in a list of changes. #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ pz.row([ui.label("render.rs", mono=True), pz.hspace(), ui.diff_stat(42, 9)], align="center"), pz.row([ui.label("app.py", mono=True), pz.hspace(), ui.diff_stat(3, 27)], align="center"), pz.row([ui.label("README.md", mono=True), pz.hspace(), ui.diff_stat(12, 0)], align="center"), ], spacing=10, width=320) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python ui.diff_stat(42, 3) ``` ```python ui.diff_stat(2, 57) ``` ```python ui.diff_stat(12, 0) ``` ```python ui.diff_stat(120, 40, blocks=10) ``` #### Events Diff stat has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.diff_stat` ```python diff_stat(added, removed, *, blocks=5, theme=None) ``` `+12 −3` with GitHub-style colored blocks. | Property | Default | Description | | --- | --- | --- | | `added` | *required* | Lines added. | | `removed` | *required* | Lines removed. | | `blocks` | `5` | Number of blocks. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#diff_stat) #### Related - [Badge](https://albakore.github.io/pyzed/library/badge/): A small pill for statuses, versions and counters. - [Tree](https://albakore.github.io/pyzed/library/tree/): A collapsible file tree with icons and git colors. ### Diff view > An edit as colored lines: added in green, removed in red. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/diff-view/ :::tip[In plain words] Shows what changed in a file: new lines in green, deleted lines in red. ::: `ui.diff_view(diff)` shows an edit: the file's name, then the lines that changed (added in green, removed in red) with two lines of context around each change; changes far apart are split by `⋯`. `diff` is a dict with `path`, `old` and `new`, the whole texts before and after, and `max_lines` limits how many lines show before `… N more lines`. It is what a [Tool call](https://albakore.github.io/pyzed/library/tool-call/) shows for the edits it made. `ui.diff_view` · Category: [AI agents](https://albakore.github.io/pyzed/library/#ai-agents) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#diff_view) **Code:** ```python old = "def save(path):\n data = load()\n write(path, data)\n return True\n" new = "def save(path, data):\n write(path, data)\n log(path)\n return True\n" pz.container(ui.diff_view({"path": "app/storage.py", "old": old, "new": new}), width=420) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - To show what an agent or a tool changed in a file. - To review a change before saving it, next to Keep and Reject buttons. - For the counts alone (`+12 −3`), use [Diff stat](https://albakore.github.io/pyzed/library/diff-stat/). #### Usage ```python import pyzed as pz from pyzed import ui old = "def save(path):\n data = load()\n write(path, data)\n return True\n" new = "def save(path, data):\n write(path, data)\n log(path)\n return True\n" pz.container(ui.diff_view({"path": "app/storage.py", "old": old, "new": new}), width=420) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content Two lines of context surround each change; changes further apart are split by `⋯`. ```python pz.container(ui.diff_view({"path": "app.py", "old": "x = 1\ny = 2\n", "new": "x = 10\ny = 2\n"}), width=300) ``` ```python pz.container(ui.diff_view({"path": "main.py", "old": "", "new": "import pyzed as pz\n\npz.run(App)\n"}), width=300) ``` ```python pz.container(ui.diff_view({"path": "todo.txt", "old": "milk\neggs\nbread\n", "new": "milk\n"}), width=300) ``` ```python old = "".join(f"line {i}\n" for i in range(1, 13)) new = old.replace("line 2\n", "line two\n").replace("line 11\n", "") pz.container(ui.diff_view({"path": "notes.txt", "old": old, "new": new}), width=300) ``` ```python new = "".join(f"item {i}\n" for i in range(1, 11)) pz.container(ui.diff_view({"path": "list.txt", "old": "", "new": new}, max_lines=4), width=300) ``` #### Events Diff view has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.diff_view` ```python diff_view(diff, *, theme=None, max_lines=120) ``` An edit as Zed shows it in a thread: the file's name, then the changed lines (green added, red removed) with a few lines around them. | Property | Default | Description | | --- | --- | --- | | `diff` | *required* | A dict with `path`, `old` and `new` (the texts before and after). | | `theme` | `None` | Theme. | | `max_lines` | `120` | Lines shown before `… N more lines`. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#diff_view) #### Related - [Tool call](https://albakore.github.io/pyzed/library/tool-call/): One step of an AI agent: what it did, its result, and Allow or Reject. - [Diff stat](https://albakore.github.io/pyzed/library/diff-stat/): `+12 −3` with GitHub-style blocks. - [Edits bar](https://albakore.github.io/pyzed/library/edits-bar/): The files an agent changed, with Keep all and Reject all. ### Disclosure > One collapsible section with an arrow. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/disclosure/ :::tip[In plain words] A section with a little arrow that shows or hides extra details. ::: A header with an arrow that shows or hides `content`. You own the open state: change it in `on_toggle`. `ui.disclosure` · Category: [Lists and menus](https://albakore.github.io/pyzed/library/#lists-and-menus) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#disclosure) **Code:** ```python pz.column([ ui.disclosure("Advanced options", ui.muted("Shown while the section is open."), open=True, on_toggle=on_toggle), ui.disclosure("Environment variables", ui.muted("Hidden"), open=False, on_toggle=on_toggle, end=ui.badge("3")), ], spacing=10, width=380) ``` #### When to use - To hide advanced options or details until they are needed. #### Usage ```python import pyzed as pz from pyzed import ui class Form(pz.App): def __init__(self): self.advanced = False def view(self): return ui.disclosure("Advanced options", self.advanced_options(), open=self.advanced, on_toggle=lambda: setattr(self, "advanced", not self.advanced)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### State ```python pz.container(ui.disclosure("Advanced options", ui.muted("…"), open=False, on_toggle=on_toggle), width=300) ``` ```python pz.container(ui.disclosure("Advanced options", ui.muted("Shown while open."), open=True, on_toggle=on_toggle), width=300) ``` ```python pz.container(ui.disclosure("Environment", ui.muted("…"), open=False, on_toggle=on_toggle, end=ui.badge("3")), width=300) ``` #### Events Disclosure has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_toggle` | `()` | Message or callable when the header is clicked. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Form(pz.App): def __init__(self): self.advanced = False def view(self): return ui.disclosure("Advanced options", ui.muted("Proxy, timeouts…"), open=self.advanced, on_toggle=lambda: setattr(self, "advanced", not self.advanced)) # no argument ``` **Declarative:** ```python class Form(pz.App): advanced = False def update(self, message): if message == "toggle advanced": self.advanced = not self.advanced def view(self): return ui.disclosure("Advanced options", ui.muted("…"), open=self.advanced, on_toggle="toggle advanced") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: advanced: bool = False def view(state, set_state): return ui.disclosure("Advanced options", ui.muted("…"), open=state.advanced, on_toggle=lambda: set_state(advanced=not state.advanced)) ``` #### Properties and API ##### `ui.disclosure` ```python disclosure(title, content, *, open, on_toggle=None, end=None, theme=None) ``` A collapsible section: a header with an arrow that shows or hides `content`. You own the open state: change it in `on_toggle`. | Property | Default | Description | | --- | --- | --- | | `title` | *required* | Title (text or widget). | | `content` | *required* | Content. | | `open` | *required* | Whether it is open. | | `end` | `None` | Widgets at the end (right). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_toggle` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#disclosure) #### Related - [Accordion](https://albakore.github.io/pyzed/library/accordion/): Stacked collapsible sections, like the panels of a settings screen. - [Card](https://albakore.github.io/pyzed/library/card/): A bordered surface with an optional title and actions. ### Divider > A thin line in the theme's border color. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/divider/ :::tip[In plain words] A thin line that separates groups of things. ::: A one-pixel line in the theme's border color, horizontal by default (`vertical=True` for a vertical one). `pz.rule` is the base version with your own `color` and `thickness`. `ui.divider` · Category: [Display](https://albakore.github.io/pyzed/library/#display) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#divider) **Code:** ```python pz.column([ ui.label("Account"), ui.divider(), pz.row([ui.label("Edit"), ui.divider(vertical=True), ui.label("View"), ui.divider(vertical=True), ui.label("Help")], spacing=12, height=20), ui.divider(), ui.muted("Sign out"), ], spacing=10, width=300) ``` #### When to use - To separate groups in a column or items in a toolbar. - Prefer spacing first; add a divider when groups really need a visual break. #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ ui.label("Account"), ui.divider(), pz.row([ui.label("Edit"), ui.divider(vertical=True), ui.label("View"), ui.divider(vertical=True), ui.label("Help")], spacing=12, height=20), ui.divider(), ui.muted("Sign out"), ], spacing=10, width=300) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Direction ```python pz.column([ui.label("Account"), ui.divider(), ui.label("Sign out")], spacing=8, width=220) ``` ```python pz.row([ui.label("Edit"), ui.divider(vertical=True), ui.label("View")], spacing=10, height=18) ``` ##### Color ```python pz.container(ui.divider(color="accent"), width=220) ``` ```python pz.container(pz.rule(color="#e5c07b", thickness=3), width=220) ``` ##### States and events ```python pz.container(ui.divider(label="or"), width=300) ``` ```python pz.column([ui.label("Above"), ui.divider(spacing=12), ui.label("Below")], width=300) ``` #### Events Divider has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.divider` ```python divider(*, vertical=False, color=None, label=None, spacing=0, theme=None) ``` A divider line in the theme color. | Property | Default | Description | | --- | --- | --- | | `vertical` | `False` | Vertical instead of horizontal. | | `color` | `None` | Text or icon color. | | `label` | `None` | Muted text in the middle of a horizontal line (`"or"`, a date…). | | `spacing` | `0` | Space on both sides of the line. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#divider) ##### `pz.rule` ```python rule(*, vertical=False, thickness=1.0, color=None) ``` A horizontal (or vertical) divider line. | Property | Default | Description | | --- | --- | --- | | `vertical` | `False` | Vertical line instead of horizontal. | | `thickness` | `1.0` | Thickness in pixels. | | `color` | `None` | Text or icon color. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#rule) #### Related - [Card](https://albakore.github.io/pyzed/library/card/): A bordered surface with an optional title and actions. - [Toolbar](https://albakore.github.io/pyzed/library/toolbar/): The bar under the tabs: breadcrumbs and buffer actions. ### Donut > A donut chart with a label in the middle. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/donut/ :::tip[In plain words] A ring cut into colored slices that shows the parts of a whole. ::: A donut chart of `values`, with a `label` in the center. `colors` overrides the theme's palette. `ui.donut` · Category: [Data](https://albakore.github.io/pyzed/library/#data) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#donut) **Code:** ```python pz.row([ ui.donut([60, 25, 15], label="60%"), ui.donut([45, 35, 20], colors=["#e5c07b", "#c678dd", "#56b6c2"], size=120, thickness=20, label="3"), ], spacing=32, align="center") ``` #### When to use - To show the parts of a whole (disk usage, languages of a project) with three or four slices. #### Usage ```python import pyzed as pz from pyzed import ui pz.row([ ui.donut([60, 25, 15], label="60%"), ui.donut([45, 35, 20], colors=["#e5c07b", "#c678dd", "#56b6c2"], size=120, thickness=20, label="3"), ], spacing=32, align="center") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python ui.donut([60, 25, 15], label="60%") ``` ```python ui.donut([45, 35, 20], colors=["#e5c07b", "#c678dd", "#56b6c2"], label="3") ``` ```python ui.donut([70, 30], thickness=6, label="70%") ``` ```python ui.donut([40, 30, 20, 10], size=140, thickness=24) ``` ```python ui.donut([82, 18], size=110, label="82%") ``` #### Events Donut has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.donut` ```python donut(values, *, colors=None, size=96, thickness=14, label=None, theme=None) ``` A donut chart; `label` is drawn in the center. | Property | Default | Description | | --- | --- | --- | | `values` | *required* | Values. | | `colors` | `None` | Colors (defaults to the theme's). | | `size` | `96` | Size. | | `thickness` | `14` | Ring thickness. | | `label` | `None` | Center text. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#donut) #### Related - [Bar chart](https://albakore.github.io/pyzed/library/bar-chart/): A vertical bar chart with labels. - [Stat](https://albakore.github.io/pyzed/library/stat/): A dashboard number with change and sparkline. ### Drop zone > An area that takes files dragged from the desktop or the file manager. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/drop-zone/ :::tip[In plain words] A dashed area where you drag files from your desktop and let go to add them. ::: `ui.DropZone` lights up when files are dragged over the window and receives them when they are dropped: `on_drop` gets each path and `on_files` the whole list. `accept` limits the types (`["png", "jpg"]`, `["*.tar.gz"]`, `["folder"]`) and files of other types go to `on_reject`. With `browse=True` (the default) a click opens the file dialog too. When a window has several zones, the one under the pointer gets the files, or else the one that accepts their type. `ui.DropZone` · Category: [System](https://albakore.github.io/pyzed/library/#system) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#dropzone) **Code:** ```python pz.row([ ui.DropZone(text="Drop images here", hint="PNG or JPG, up to 10 MB", accept=["png", "jpg"], on_files=on_files), ui.DropZone(icon_name="file-spreadsheet", text="Import a CSV", accept=["csv"], multiple=False, show_files=True, files=["~/data/sales-2026.csv"], on_files=on_files), ], spacing=16, width=600) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - For importing images, documents or data files, and attaching files to a message. - `show_files=True` lists what was dropped, with a button to remove each one; `multiple=False` keeps only the last file. - Dropped files also reach `on_file_drop(path)` of the window, for apps that don't need a zone. #### Usage ```python import pyzed as pz from pyzed import ui class Gallery(pz.App): def __init__(self): self.images = [] def view(self): return pz.column([ ui.DropZone(text="Drop photos", accept=["png", "jpg", "webp"], on_files=lambda e: setattr(self, "images", list(e.value))), pz.row([pz.image(path, width=96, height=96) for path in self.images], spacing=8), ], spacing=12) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### States ```python ui.DropZone(text="Drop files here", on_files=on_files) ``` ```python ui.DropZone(text="Drop files here", on_files=on_files) ``` ```python ui.DropZone(text="Drop images here", accept=["png", "jpg"], on_files=on_files) ``` ```python ui.DropZone(text="Drop files here", disabled=True, on_files=on_files) ``` ##### Files ```python ui.DropZone(text="Attach files", show_files=True, files=["~/docs/brief.pdf", "~/img/cover.png"], on_files=on_files) ``` ```python ui.DropZone(icon_name="image", text="Drag a picture", hint="Only by dragging", browse=False, on_files=on_files) ``` #### Events Drop zone has **5 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. | Event | Handler gets | When | | --- | --- | --- | | `on_drop` | `(event)` | Receives `event.value`: one dropped path (once per file). | | `on_files` | `(event)` | Receives `event.value`: the list of files. | | `on_reject` | `(event)` | Receives a path it does not accept. | | `on_enter` | `(event)` | Files are dragged over the window. | | `on_leave` | `(event)` | The drag left without dropping. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Attach(pz.App): def __init__(self): self.files = [] def received(self, event): # on_files: event.value is every file so far self.files = list(event.value) def view(self): return pz.column([ ui.DropZone(text="Drop files to attach", on_files=self.received), ui.label(f"{len(self.files)} files"), ], spacing=8, width=320) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Dropped: path: str class Attach(pz.App): files = () def update(self, message): match message: case Dropped(path): self.files = (*self.files, path) def view(self): # on_drop runs once per file return ui.DropZone(accept=["png", "jpg"], on_drop=lambda e: Dropped(e.value)) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: rejected: str = "" def view(state, set_state): return pz.column([ ui.DropZone(text="Drop a CSV", accept=["csv"], multiple=False, on_reject=lambda e: set_state(rejected=e.value)), ui.label(f"Not a CSV: {state.rejected}" if state.rejected else ""), ], spacing=8, width=320) ``` #### Tips :::tip Pyzed Design has a Drop zone in the System section of the toolbox, next to the non-visual helpers of the operating system (system information, launcher, network and battery monitors). ::: #### Properties and API ##### `ui.DropZone` ```python class DropZone ``` A component that takes files dragged from the system's file manager and dropped on the window, and opens the file dialog on a click. It lights up while files are dragged over the window and keeps the dropped paths in `files`. The window's `on_file_drop` hook hands each file to the zone under the pointer, else to one whose `accept` lists its type. See the [Drop zone](https://albakore.github.io/pyzed/library/drop-zone/) page. | Property | Default | Description | | --- | --- | --- | | `text` | `""` | The main text (default: Drop files here). | | `hint` | `""` | The line under it. | | `icon_name` | `"upload"` | Its icon. | | `accept` | `()` | Extensions (`["png", "jpg"]`), patterns (`"*.tar.gz"`) or `"folder"`; empty takes everything. | | `multiple` | `True` | `False` keeps only the last file. | | `browse` | `True` | A click opens the file dialog. | | `show_files` | `False` | Lists the files under the text (each with an x). | | `files` | `()` | The dropped paths. | | `disabled` | `False` | Takes nothing. | | `live` | `True` | `False` draws it without taking files (the designer's canvas). | Events: `on_drop`, `on_files`, `on_reject`, `on_enter`, `on_leave` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#dropzone) #### Related - [File picker](https://albakore.github.io/pyzed/library/file-input/): A path field with a button that opens the system's file or folder dialog. - [Card](https://albakore.github.io/pyzed/library/card/): A bordered surface with an optional title and actions. ### Editor > A full code editor: highlighting, folding, minimap, git markers. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/editor/ :::tip[In plain words] A box for writing code, with colors, line numbers and everything a programmer expects. ::: A code editor with syntax highlighting in the theme colors (100+ languages), line numbers, git markers, auto-closing pairs, folding, a minimap and font zoom. It is **uncontrolled**: the text, cursor and undo history live in Rust, tied to `id`, and `text` is only the initial value. `on_change` receives the full text and `on_cursor` the `(line, column)`. `pz.code_editor` is the base widget without the theme. `ui.editor` · Category: [Code and tools](https://albakore.github.io/pyzed/library/#code-and-tools) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#editor) **Code:** ```python code = '''import pyzed as pz from pyzed import ui class Counter(pz.App): def __init__(self): self.count = 0 def view(self): return ui.button(f"Clicks: {self.count}", self.increment) ''' pz.container(ui.editor("counter.py", code, path="counter.py", markers={7: "modified", 10: "created"}), width=620, height=290) ``` #### When to use - For code, configuration files, logs you can edit, or any long text. - Use the file path as `id`: each file keeps its cursor and history when you switch tabs. - To replace the text from code, return `pz.Command.set_editor_text(id, text)`. #### Usage ```python import pyzed as pz from pyzed import ui from pathlib import Path class Editor(pz.App): def __init__(self): self.path = "main.py" self.text = Path(self.path).read_text() def save(self): Path(self.path).write_text(self.text) def view(self): return ui.editor(self.path, self.text, path=self.path, on_change=lambda text: setattr(self, "text", text)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Language ```python pz.container(ui.editor("v-py", "class Counter(pz.App):\n count = 0\n\n def view(self):\n return ui.button('Add', self.add)", path="counter.py"), width=470, height=110) ``` ```python pz.container(ui.editor("v-rs", "fn main() {\n println!(\"hi\");\n}\n", language="rust"), width=420, height=90) ``` ```python pz.container(ui.editor("v-json", '{\n "name": "notes",\n "version": 2\n}\n', language="json"), width=420, height=110) ``` ##### Options ```python pz.container(ui.editor("v-g", "class Counter(pz.App):\n count = 0\n\n def view(self):\n return ui.button('Add', self.add)", path="a.py", gutter=False), width=470, height=110) ``` ```python pz.container(ui.editor("v-m", "class Counter(pz.App):\n count = 0\n\n def view(self):\n return ui.button('Add', self.add)", path="a.py", markers={2: "created", 5: "modified"}), width=470, height=110) ``` ```python pz.container(ui.editor("v-mm", "class Counter(pz.App):\n count = 0\n\n def view(self):\n return ui.button('Add', self.add)", path="a.py", minimap=True), width=470, height=124) ``` ```python pz.container(ui.editor("v-ws", "class Counter(pz.App):\n count = 0\n\n def view(self):\n return ui.button('Add', self.add)", path="a.py", show_whitespace=True), width=470, height=110) ``` ```python pz.container(ui.editor("v-ph", "", placeholder="Write something…"), width=420, height=60) ``` ```python pz.container(ui.editor("v-fs", "print(\"big\")\n", path="a.py", font_size=16), width=420, height=60) ``` #### Examples ##### Folding, code lens and minimap Blocks fold by indentation (Ctrl+Shift+[ and ]), `annotations` adds a code lens above a line and `minimap=True` draws the overview on the right. **Code:** ```python code = '''import pyzed as pz from pyzed import ui class Counter(pz.App): def __init__(self): self.count = 0 def increment(self): self.count += 1 def view(self): return ui.button(f"Clicks: {self.count}", self.increment) ''' pz.container(ui.editor("lens.py", code, path="counter.py", minimap=True, annotations={5: "1 reference"}), width=680, height=290) ``` ##### Cursor and whitespace `cursor_shape` is `"bar"`, `"block"`, `"underline"` or `"hollow"`; `show_whitespace=True` draws spaces and tabs; `wrap=True` wraps long lines. **Code:** ```python pz.container(ui.editor("ws.py", "def greet(name):\n return f'Hello, {name}'\n", path="ws.py", cursor_shape="block", show_whitespace=True, font_size=15), width=620, height=120) ``` #### Events Editor has **6 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_change` | `ui.editor`, `pz.code_editor` | `(text)` | Callable that receives the new value. | | `on_cursor` | `ui.editor`, `pz.code_editor` | `(line, column)` | Callable that receives `(line, column)`. | | `on_scroll` | `ui.editor`, `pz.code_editor` | `(info)` | Called on scroll. | | `on_zoom` | `ui.editor`, `pz.code_editor` | `(step)` | Callable that receives `1`, `-1` or `0` for Ctrl+= / Ctrl+- / Ctrl+0 and Ctrl+wheel. | | `on_action` | `ui.editor`, `pz.code_editor` | `(name)` | Callable that receives the name of each editor action that runs (`"undo"`, `"select_next"`…), e.g. to count them. | | `on_hover` | `ui.editor`, `pz.code_editor` | `()` | Callable that receives `{line, column, start, end, x, y, height}` (the word under the mouse, from 1, and its place in the editor) after the mouse rests on it, and `None` when it leaves (hover cards). | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Editor(pz.App): def __init__(self): self.text = "print('hello')\n" self.cursor = (1, 1) self.font_size = 14 def zoom(self, step): # on_zoom: 1, -1 or 0 (Ctrl+=, Ctrl+-, Ctrl+0) self.font_size = 14 if step == 0 else self.font_size + step def view(self): line, column = self.cursor return pz.column([ ui.editor("main.py", self.text, path="main.py", font_size=self.font_size, height=200, on_change=lambda text: setattr(self, "text", text), # every edit on_cursor=lambda ln, col: setattr(self, "cursor", (ln, col)), # from 1 on_zoom=self.zoom, on_action=lambda name: print("ran", name)), # "undo"... ui.status_bar([], [ui.status_item(f"Ln {line}, Col {column}")]), ]) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Edited: text: str @dataclass class Moved: line: int column: int class Editor(pz.App): text, cursor = "print('hello')\n", (1, 1) def update(self, message): match message: case Edited(text): self.text = text case Moved(line, column): self.cursor = (line, column) def view(self): return ui.editor("main.py", self.text, path="main.py", height=200, on_change=Edited, on_cursor=Moved) # the classes build the messages ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: text: str = "print('hello')\n" line: int = 1 column: int = 1 def view(state, set_state): return pz.column([ ui.editor("main.py", state.text, path="main.py", height=200, on_change=lambda text: set_state(text=text), on_cursor=lambda line, column: set_state(line=line, column=column)), ui.label(f"Ln {state.line}, Col {state.column}"), ]) ``` #### Properties and API ##### `ui.editor` ```python editor( id, text='', *, language=None, path=None, on_change=None, on_cursor=None, markers=None, font_size=None, line_height=None, gutter=True, wrap=False, active_line=True, height='fill', placeholder=None, annotations=None, show_whitespace=False, tab_size=None, hard_tabs=False, backspace_unindent=True, repeat_delay=None, repeat_rate=None, capture_keys=(), on_scroll=None, auto_close=True, folding=True, minimap=False, cursor_shape='bar', cursor_blink=True, cursor_width=2.0, cursor_color=None, on_zoom=None, on_action=None, highlights=None, match_brackets=True, selection_highlight=True, font=None, keymap=None, scrollbar=True, readonly=False, on_hover=None, hover_delay=500, sticky_scroll=0, theme=None, ) ``` A code editor: highlighting with the theme colors, line numbers and git markers. | Property | Default | Description | | --- | --- | --- | | `id` | *required* | Identifies the buffer (use the file path). | | `text` | `""` | Initial text. | | `language` | `None` | Language. If missing, it is inferred from `path`. | | `path` | `None` | File path. | | `markers` | `None` | `{line: "created" \| "modified" \| "deleted"}`. | | `font_size` | `None` | Font size (defaults to the theme's). | | `line_height` | `None` | Line height. | | `gutter` | `True` | Shows line numbers. | | `wrap` | `False` | Line wrapping. | | `active_line` | `True` | Background color of the cursor line. | | `height` | `"fill"` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `placeholder` | `None` | Hint text shown when empty. | | `annotations` | `None` | `{line: "text"}` or `{line: ("text", color)}`: virtual text at the end of the line (code lens, diagnostics). | | `show_whitespace` | `False` | Draws spaces as `·` and tabs as `→`. | | `tab_size` | `None` | Tab inserts spaces up to the next multiple; Enter keeps the indentation (one more level after `:`); Backspace removes a level. | | `hard_tabs` | `False` | With `tab_size`: Tab, Enter and indent (Ctrl+]) use tab characters instead of spaces. | | `backspace_unindent` | `True` | With `tab_size`: Backspace in leading spaces removes a whole level (`False`: one space). | | `repeat_delay` | `None` | Milliseconds Backspace and Delete wait, held down, before repeating (`None`: the system's). | | `repeat_rate` | `None` | Deletions per second at most while Backspace or Delete is held down (`None`: the system's pace). | | `capture_keys` | `()` | Keys the editor leaves to the app's `on_key` (`"up"`, `"enter"`…), e.g. while a completion list is open. | | `auto_close` | `True` | Closes brackets and quotes as you type (and wraps the selection). | | `folding` | `True` | Fold chevrons in the gutter; Ctrl+Shift+[ folds and Ctrl+Shift+] unfolds. | | `minimap` | `False` | `True` (or a width in pixels) shows a minimap on the right. | | `cursor_shape` | `"bar"` | `"bar"`, `"block"`, `"underline"` or `"hollow"`. | | `cursor_blink` | `True` | Whether the cursor blinks. | | `cursor_width` | `2.0` | Width of the bar cursor. | | `cursor_color` | `None` | Cursor color (the theme's player color by default). | | `highlights` | `None` | Ranges painted behind the text in the theme's search color: `[(line, column, end_line, end_column)]`, from 1, with exclusive ends. | | `match_brackets` | `True` | Outlines the bracket that matches the one next to the cursor (Ctrl+M jumps to it). | | `selection_highlight` | `True` | Highlights the other occurrences of the selected text (Ctrl+D adds the next one as a cursor). | | `font` | `None` | Typeface: `"mono"` by default; `"sans"` makes a multi-line text box, like a chat's message box. | | `keymap` | `None` | Zed-style `{binding: action}` dict (`{"ctrl-shift-d": "duplicate_line_down"}`); `None` as the action removes a default key. Build it from per-action overrides with `pyzed.ui.editor.keymap_for`. | | `scrollbar` | `True` | `False` hides the scrollbars. | | `readonly` | `False` | `True` lets the text be selected and copied but not changed (a log, a traceback). | | `hover_delay` | `500` | Milliseconds the mouse rests on a word before `on_hover` is called. | | `sticky_scroll` | `0` | How many lines of the enclosing blocks (`class`, `def`, `if`…) stay pinned at the top while scrolling (`0`: none). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_change`, `on_cursor`, `on_scroll`, `on_zoom`, `on_action`, `on_hover` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#editor) ##### `ui.language_token` ```python language_token(path) ``` Returns the highlighter language token for a path (`"main.rs"` → `"rs"`). | Property | Default | Description | | --- | --- | --- | | `path` | *required* | File path. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#language_token) ##### `pz.code_editor` ```python code_editor( id, text='', *, language='txt', size=14.0, line_height=None, font=None, width=None, height=None, padding=None, syntax=None, bg=None, color=None, selection_color=None, placeholder=None, placeholder_color=None, gutter=True, gutter_color=None, gutter_active_color=None, gutter_width=None, markers=None, wrap=False, on_change=None, on_cursor=None, active_line=None, scrollbar_color=None, annotations=None, whitespace=None, tab_size=None, capture_keys=None, on_scroll=None, auto_close=False, folding=False, minimap=None, minimap_thumb=None, cursor_shape='bar', cursor_width=2.0, cursor_blink=True, cursor_color=None, on_zoom=None, on_action=None, highlights=None, highlight_color=None, match_brackets=True, selection_highlight=True, keymap=None, scrollbar=True, readonly=False, hard_tabs=False, backspace_unindent=True, repeat_delay=None, repeat_rate=None, on_hover=None, hover_delay=500.0, sticky_scroll=0, ) ``` A code editor with syntax highlighting, line numbers and git markers, based on Iced's `text_editor`. It is **uncontrolled**: `text` is only the initial value for that `id`; the text, cursor and history live in Rust. Use `Command.set_editor_text` to replace it. For the themed editor use [`ui.editor`](https://albakore.github.io/pyzed/components/code/). | Property | Default | Description | | --- | --- | --- | | `id` | *required* | Identifies the buffer. Reuse it to keep state across renders. | | `text` | `""` | Initial text. | | `language` | `"txt"` | Language extension or name (`"py"`, `"rs"`, `"json"`…). | | `size` | `14.0` | Font size. | | `line_height` | `None` | Line height in pixels. | | `font` | `None` | Family (monospace by default). | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `padding` | `None` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `syntax` | `None` | Map of TextMate scopes to colors (provided by `Theme.syntax_scopes()`). | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `color` | `None` | Text or icon color. | | `selection_color` | `None` | Selection color. | | `placeholder` | `None` | Hint text shown when empty. | | `placeholder_color` | `None` | Placeholder color. | | `gutter` | `True` | Shows line numbers. | | `gutter_color` | `None` | Line number color. | | `gutter_active_color` | `None` | Color of the cursor line's number. | | `gutter_width` | `None` | Gutter width (automatic by default). | | `markers` | `None` | `{line: color}` for git markers in the gutter (lines from 1). | | `wrap` | `False` | Line wrapping. | | `active_line` | `None` | Background color of the cursor line. | | `scrollbar_color` | `None` | Scrollbar color. | | `annotations` | `None` | `{line: "text"}` or `{line: ("text", color)}`: virtual text at the end of the line (code lens, diagnostics). | | `whitespace` | `None` | Draws spaces as `·` and tabs as `→` in this color. | | `tab_size` | `None` | Tab inserts spaces up to the next multiple; Enter keeps the indentation; Backspace removes a level. | | `capture_keys` | `None` | Keys the editor leaves to the app's `on_key` (`"up"`, `"enter"`…), e.g. while a completion list is open. | | `auto_close` | `False` | Typing a bracket or quote also inserts its closing pair. | | `folding` | `False` | Fold chevrons in the gutter and Ctrl+Shift+[ / ]. | | `minimap` | `None` | Width of the minimap in pixels (`None`: no minimap). | | `minimap_thumb` | `None` | Color of the minimap's visible-area highlight. | | `cursor_shape` | `"bar"` | `"bar"`, `"block"`, `"underline"` or `"hollow"`. | | `cursor_width` | `2.0` | Width of the bar cursor. | | `cursor_blink` | `True` | Whether the cursor blinks. | | `cursor_color` | `None` | Cursor color. | | `highlights` | `None` | Ranges painted behind the text (search results): `[(line, column, end_line, end_column)]`, from 1, with exclusive ends. | | `highlight_color` | `None` | Color of `highlights`. | | `match_brackets` | `True` | Outlines the bracket that matches the one next to the cursor. | | `selection_highlight` | `True` | Highlights the other occurrences of the selected text. | | `keymap` | `None` | Zed-style `{binding: action}` dict; `None` as the action removes a default key (`pyzed.ui.editor.keymap_for` builds one from per-action overrides). | | `scrollbar` | `True` | `False` hides the scrollbars. | | `readonly` | `False` | `True` lets the text be selected and copied but not changed (a log, a traceback). | | `hard_tabs` | `False` | With `tab_size`: Tab, Enter and indent (Ctrl+]) use tab characters instead of spaces. | | `backspace_unindent` | `True` | With `tab_size`: Backspace in leading spaces removes a whole level (`False`: one space). | | `repeat_delay` | `None` | Milliseconds Backspace and Delete wait, held down, before repeating (`None`: the system's). | | `repeat_rate` | `None` | Deletions per second at most while Backspace or Delete is held down (`None`: the system's pace). | | `hover_delay` | `500.0` | Milliseconds the mouse rests on a word before `on_hover` is called. | | `sticky_scroll` | `0` | How many lines of the enclosing blocks (`class`, `def`, `if`…) stay pinned at the top while scrolling (`0`: none). | Events: `on_change`, `on_cursor`, `on_scroll`, `on_zoom`, `on_action`, `on_hover` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#code_editor) #### Related - [Terminal](https://albakore.github.io/pyzed/library/terminal/): A read-only terminal view with ANSI colors. - [Console](https://albakore.github.io/pyzed/library/console/): A dev-tools style log with levels, filter, copy and clear. - [Workspace](https://albakore.github.io/pyzed/library/workspace/): The layout of an editor-style window: title bar, docks, center, status bar. ### Edits bar > The files an agent changed, with Keep all and Reject all. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/edits-bar/ :::tip[In plain words] A bar that lists the files an AI assistant changed, so you can keep or undo the changes. ::: `ui.EditsBar` is a component that lists the files an agent changed: `3 files changed` with the lines added and removed, Reject all and Keep all. Clicking the title unfolds one row per file with its diff stat and buttons to reject or keep that file alone; clicking a file's name reports `on_open`. `files` are dicts or objects with `path` and optional `added` and `removed` line counts. The file events (`on_keep_file`, `on_reject_file`, `on_open`) carry the path in `event.value`. `ui.EditsBar` · Category: [AI agents](https://albakore.github.io/pyzed/library/#ai-agents) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#editsbar) **Code:** ```python pz.container(ui.EditsBar(files=[ {"path": "app/storage.py", "added": 12, "removed": 3}, {"path": "app/forms/login.py", "added": 40, "removed": 0}, {"path": "tests/test_login.py", "added": 18, "removed": 2}, ], on_keep=on_keep, on_reject=on_reject), width=520) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - Under an agent's chat, once it has edited files, so the user reviews them before keeping them. #### Usage ```python import pyzed as pz from pyzed import ui class Review(pz.App): def __init__(self): self.files = [{"path": "app/storage.py", "added": 12, "removed": 3}] def keep(self, path=None): # None: every file self.files = [f for f in self.files if path is not None and f["path"] != path] def view(self): return ui.EditsBar(files=self.files, on_keep=lambda e: self.keep(), on_keep_file=lambda e: self.keep(e.value), on_open=lambda e: self.open(e.value)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### State Unfolded ones are drawn as they look after a click on the header. ```python pz.container(ui.EditsBar(files=[{"path": "app.py", "added": 12, "removed": 3}, {"path": "ui/form.py", "added": 40, "removed": 0}]), width=440) ``` ```python pz.container(ui.EditsBar(files=[{"path": "app.py", "added": 12, "removed": 3}, {"path": "ui/form.py", "added": 40, "removed": 0}]), width=440) ``` ```python pz.container(ui.EditsBar(files=[{"path": "app.py", "added": 2, "removed": 2}]), width=440) ``` ```python pz.container(ui.EditsBar(files=[{"path": "app.py"}, {"path": "README.md"}]), width=440) ``` #### Events Edits bar has **5 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. | Event | Handler gets | When | | --- | --- | --- | | `on_keep` | `(event)` | Keep all. | | `on_reject` | `(event)` | Reject all. | | `on_keep_file` | `(event)` | Keep on a file (`event.value` is its path). | | `on_reject_file` | `(event)` | Reject on a file. | | `on_open` | `(event)` | A file was clicked. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Review(pz.App): def __init__(self): self.files = [{"path": "app/storage.py", "added": 12, "removed": 3}, {"path": "tests/test_storage.py", "added": 8, "removed": 0}] def drop(self, path=None): # None: every file self.files = [f for f in self.files if path is not None and f["path"] != path] def view(self): # event.value is the file's path return ui.EditsBar(files=self.files, on_keep=lambda e: self.drop(), on_reject=lambda e: self.drop(), on_keep_file=lambda e: self.drop(e.value), on_reject_file=lambda e: self.drop(e.value), on_open=lambda e: print("open", e.value)) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Done: path: str = "" class Review(pz.App): files = [{"path": "app/storage.py", "added": 12, "removed": 3}] def update(self, message): match message: case Done(""): self.files = [] case Done(path): self.files = [f for f in self.files if f["path"] != path] def view(self): return ui.EditsBar(files=self.files, on_keep=Done(), on_keep_file=lambda e: Done(e.value)) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: files: tuple = ({"path": "app/storage.py", "added": 12, "removed": 3},) def view(state, set_state): without = lambda path: tuple(f for f in state.files if f["path"] != path) return ui.EditsBar(files=list(state.files), on_keep=lambda e: set_state(files=()), on_keep_file=lambda e: set_state(files=without(e.value))) ``` #### Properties and API ##### `ui.EditsBar` ```python class EditsBar ``` A component listing the files an agent changed, like Zed's edits bar: `3 files changed` with Reject all and Keep all, unfolding to one row per file with its diff stat, Reject and Keep. | Property | Default | Description | | --- | --- | --- | | `files` | `None` | Dicts or objects with `path` and optional `added` and `removed` line counts. | | `theme` | `None` | Theme. | Events: `on_keep`, `on_reject`, `on_keep_file`, `on_reject_file`, `on_open` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#editsbar) #### Related - [Diff view](https://albakore.github.io/pyzed/library/diff-view/): An edit as colored lines: added in green, removed in red. - [Diff stat](https://albakore.github.io/pyzed/library/diff-stat/): `+12 −3` with GitHub-style blocks. - [Tool call](https://albakore.github.io/pyzed/library/tool-call/): One step of an AI agent: what it did, its result, and Allow or Reject. ### Effects > Glow, bloom, glass and opacity as style properties. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/effects/ :::tip[In plain words] Glows, frosted glass and transparency you can add to any box. ::: Visual effects are [style](https://albakore.github.io/pyzed/theming/styles/) properties: `glow` (a soft halo), `bloom` (a wide, bright spill of light), `glass` (a translucent, frosted panel), `opacity`, and `translate` and `scale`, which move or scale the drawing without changing the layout. Animations can change `glow` too. `pz.Style` · Category: [Content](https://albakore.github.io/pyzed/library/#content) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#style) **Code:** ```python pz.container(pz.row([ pz.container(ui.label("Glass"), width=150, height=70, padding=14).style(glass=True, radius=12), pz.container(ui.label("Glow"), width=150, height=70, padding=14).style( background="#0b1220", radius=12, glow=("#22d3ee", 18)), pz.container(ui.label("Bloom"), width=150, height=70, padding=14).style( background="#1a0b16", radius=12, bloom=("#f472b6", 0.8)), pz.container(ui.label("Opacity"), width=150, height=70, padding=14).style( background="#0b1220", radius=12, opacity=0.5), ], spacing=24), padding=32).style( background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=14) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - To highlight the primary action or a live state (recording, connected). - For floating panels over colorful backgrounds (glass). #### Usage ```python import pyzed as pz from pyzed import ui pz.container(pz.row([ pz.container(ui.label("Glass"), width=150, height=70, padding=14).style(glass=True, radius=12), pz.container(ui.label("Glow"), width=150, height=70, padding=14).style( background="#0b1220", radius=12, glow=("#22d3ee", 18)), pz.container(ui.label("Bloom"), width=150, height=70, padding=14).style( background="#1a0b16", radius=12, bloom=("#f472b6", 0.8)), pz.container(ui.label("Opacity"), width=150, height=70, padding=14).style( background="#0b1220", radius=12, opacity=0.5), ], spacing=24), padding=32).style( background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=14) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Effects ```python pz.container(pz.container(ui.label("Glass"), width=130, height=60, padding=12).style(glass=True, radius=12), padding=24).style(background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=12) ``` ```python pz.container(pz.container(ui.label("Glow"), width=130, height=60, padding=12).style(background="#0b1220", radius=12, glow=("#22d3ee", 18)), padding=24).style(background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=12) ``` ```python pz.container(pz.container(ui.label("Bloom"), width=130, height=60, padding=12).style(background="#1a0b16", radius=12, bloom=("#f472b6", 0.8)), padding=24).style(background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=12) ``` ```python pz.container(pz.container(ui.label("Opacity"), width=130, height=60, padding=12).style(background="#0b1220", radius=12, opacity=0.5), padding=24).style(background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=12) ``` ```python pz.container(pz.container(ui.label("Scale"), width=130, height=60, padding=12).style(background="#0b1220", radius=12, scale=1.15), padding=24).style(background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=12) ``` ```python pz.container(pz.container(ui.label("Translate"), width=130, height=60, padding=12).style(background="#0b1220", radius=12, translate=(16, 8)), padding=24).style(background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=12) ``` #### Events Effects has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.Style` ```python class Style ``` A reusable set of style properties for `widget.style(...)`, a component's `style=` or `App.style`. Combine styles with `|` (the right one wins). See [Styles](https://albakore.github.io/pyzed/theming/styles/). | Property | Default | Description | | --- | --- | --- | | `*styles` | — | Styles, dicts or style names to start from. | | `**props` | — | Properties: `background`, `color`, `font`, `weight`, `italic`, `size`, `line_height`, `border_color`, `border_width`, `border` (`(width, color)`), `radius`, `shadow`, `padding`, `margin`, `width`, `height`, `max_width`, `max_height`, `min_width`, `min_height`, `align_x`, `align_y`, `clip`. Effects: `opacity`, `translate` (`(x, y)`, pixels or `"50%"`), `scale`, `glow` (a color, a size or `(color, size)`), `bloom` (a color or `(color, intensity)`) and `glass` (`True`, a tint or `{"tint", "strength"}`). States: `hover`, `pressed` (or `active`) and `focus`, each with the properties to show then. `transition`: milliseconds, `True` or `{"duration", "easing", "delay"}`. `media`: `{query: style}` applied when the window matches. Colors may be theme tokens. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#style) #### Related - [Animation](https://albakore.github.io/pyzed/library/animation/): Presets, keyframes and transitions for any widget. - [Card](https://albakore.github.io/pyzed/library/card/): A bordered surface with an optional title and actions. ### Empty state > A centered placeholder for empty lists and searches. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/empty-state/ :::tip[In plain words] What a screen shows when there is nothing in it yet, with a hint of what to do next. ::: A centered icon, title, description and actions for a place that has nothing to show yet. `ui.empty_state` · Category: [Feedback](https://albakore.github.io/pyzed/library/#feedback) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#empty_state) **Code:** ```python ui.empty_state("No results", "Try a different search.", icon="search", actions=[ui.button("Clear filters", on_clear, size="sm")]) ``` #### When to use - For an empty list, a search with no results, or a panel before the first file is opened. - Offer the next step as an action (Create, Clear filters, Open folder). #### Usage ```python from pyzed import ui ui.empty_state("No results", "Try a different search.", icon="search", actions=[ui.button("Clear filters", on_clear, size="sm")]) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content ```python ui.empty_state("No notifications") ``` ```python ui.empty_state("No results", "Try a different search.", icon="search") ``` ```python ui.empty_state("No projects yet", "Create one to get started.", icon="folder-plus", actions=[ui.button("New project", on_new, variant="solid", size="sm")]) ``` ##### States and events ```python ui.empty_state("No results", "Try another search", icon="search", size="sm", height="shrink") ``` ```python ui.empty_state("No projects", "Create one to start", icon="folder", size="lg", height=220) ``` #### Events Empty state has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.empty_state` ```python empty_state( title, description=None, *, icon='inbox', actions=(), size='md', height='fill', theme=None, ) ``` A centered placeholder for empty lists, searches with no results, etc. | Property | Default | Description | | --- | --- | --- | | `title` | *required* | Title. | | `description` | `None` | Secondary text. | | `icon` | `"inbox"` | Name of a Lucide icon (`pyzed icons` lists them). | | `actions` | `()` | Extra widgets (usually buttons) shown as actions. | | `size` | `"md"` | `"sm"`, `"md"` or `"lg"`. | | `height` | `"fill"` | `"fill"` (default), pixels or `"shrink"`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#empty_state) #### Related - [Callout](https://albakore.github.io/pyzed/library/callout/): An inline notice with a title, text and actions. - [Keybinding hint](https://albakore.github.io/pyzed/library/keybinding-hint/): A muted “Open file Ctrl P” hint. - [Card](https://albakore.github.io/pyzed/library/card/): A bordered surface with an optional title and actions. ### Error boundary > Draws a part of the view, or an error card in its place when it fails. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/error-boundary/ :::tip[In plain words] A safety net for one part of a window: if that part breaks, a small error box shows there and the rest of the app keeps working. ::: `ui.error_boundary(build)` returns `build()` or, when it raises, an error card in its place, so one broken part of the view (a sidebar, a panel) never takes the whole window down. The card shows the error, the file and line it comes from and a Copy error button; `on_details` adds a Details button and `fallback` replaces the card with your own widget. `on_error(info)` is told about the error each time the part fails to draw. `ui.ErrorInfo` describes an error (its type, message, traceback and origin) and `ui.error_card` is the card on its own. `ui.error_boundary` · Category: [Feedback](https://albakore.github.io/pyzed/library/#feedback) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#error_boundary) **Code:** ```python def totals(): values = [] return ui.label(f"Average: {sum(values) / len(values)}") pz.row([ ui.card(ui.muted("This panel keeps working."), title="Files", width=200), pz.container(ui.error_boundary(totals, title="Totals", on_details=on_details), width=380), ], spacing=12, align="center") ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - Around each panel of a big window, so a bug in one leaves the others working. - Around parts that draw data you don't control: a plugin, a file the user opened, a reply from a server. - Open an [Error dialog](https://albakore.github.io/pyzed/library/error-dialog/) from `on_details` to show the whole traceback. #### Usage ```python import pyzed as pz from pyzed import ui class Report(pz.App): def __init__(self): self.error = None # the ui.ErrorInfo shown in the dialog def totals(self): ... # may raise def show(self, info): self.error = info def close(self): self.error = None def view(self): page = pz.row([ self.files(), ui.error_boundary(self.totals, title="Totals", on_details=self.show), ]) dialog = ui.error_dialog(self.error, self.close) if self.error else None return ui.dialog_layer(page, dialog, on_dismiss=self.close) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Boundary While `build()` works the boundary returns its widget untouched; only a part that raises is replaced. ```python ui.error_boundary(lambda: ui.label("Average: 4.2")) ``` ```python def totals(): values = [] return ui.label(f"Average: {sum(values) / len(values)}") pz.container(ui.error_boundary(totals), width=360) ``` ```python def totals(): values = [] return ui.label(f"Average: {sum(values) / len(values)}") pz.container(ui.error_boundary(totals, title="Totals"), width=360) ``` ```python def totals(): values = [] return ui.label(f"Average: {sum(values) / len(values)}") pz.container(ui.error_boundary(totals, title="Totals", on_details=on_details), width=360) ``` ```python def totals(): values = [] return ui.label(f"Average: {sum(values) / len(values)}") ui.error_boundary(totals, fallback=lambda info: ui.callout( "Totals are not available", info.summary, tint="warning")) ``` ##### Error card `ui.error_card` is the card on its own, for errors you catch yourself. ```python info = ui.ErrorInfo("KeyError", "'email'", origin=("app/forms.py", 42), where="Sign-up form") pz.container(ui.error_card(info), width=360) ``` ```python info = ui.ErrorInfo("KeyError", "'email'", origin=("app/forms.py", 42), where="Sign-up form") pz.container(ui.error_card(info, title="The form could not be loaded", actions=[ui.button("Retry", on_retry, icon="rotate-ccw", size="sm", variant="ghost")]), width=360) ``` ```python pz.container(ui.error_card(ui.ErrorInfo("TimeoutError", "the server took too long")), width=360) ``` #### Events Error boundary has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_error` | `ui.error_boundary` | `(info)` | Receives the `ui.ErrorInfo`. | | `on_details` | `ui.error_boundary`, `ui.error_card` | `(info)` | Adds a Details button; receives the `ui.ErrorInfo` (open a `ui.error_dialog`). | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Report(pz.App): def __init__(self): self.values = [] self.error = None # the ui.ErrorInfo shown in the dialog def totals(self): return ui.label(f"Average: {sum(self.values) / len(self.values)}") # raises on [] def view(self): page = ui.error_boundary(self.totals, title="Totals", on_error=lambda info: print(info.message), # every failure on_details=lambda info: setattr(self, "error", info)) # Details dialog = ui.error_dialog(self.error, lambda: setattr(self, "error", None)) if self.error else None return ui.dialog_layer(page, dialog) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class ShowError: info: object class Report(pz.App): error = None def totals(self): return ui.label(f"Average: {sum([]) / len([])}") def update(self, message): match message: case ShowError(info): self.error = info case "close": self.error = None def view(self): page = ui.error_boundary(self.totals, title="Totals", on_details=ShowError) return ui.dialog_layer(page, ui.error_dialog(self.error, "close") if self.error else None) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: values: tuple = () failures: int = 0 def totals(values): return ui.label(f"Average: {sum(values) / len(values)}") def view(state, set_state): return pz.column([ ui.error_boundary(lambda: totals(state.values), title="Totals", on_error=lambda info: set_state(failures=state.failures + 1)), ui.button("Add 10", lambda: set_state(values=state.values + (10,))), ], spacing=8, width=380) ``` #### Tips :::tip The boundary runs `build()` on every redraw, so `on_error` is called again each time the part fails. To report an error once, compare it with the last one: two `ErrorInfo` with the same type, message, traceback and origin are equal (`time` is left out). `ErrorInfo.from_exception(err)` describes any exception you catch yourself, and `info.report()` gives the text to paste in a bug report. ::: #### Properties and API ##### `ui.error_boundary` ```python error_boundary( build, *, on_error=None, title=None, where='', on_details=None, fallback=None, theme=None, ) ``` Draws `build()`, or an error card when it raises, so one part of the view that fails never takes the whole window down (like React's error boundaries). `on_error(info)` is told about the error each time the part is drawn and fails, so report it once (compare `info.traceback`). | Property | Default | Description | | --- | --- | --- | | `build` | *required* | A function that returns the part's widget. | | `title` | `None` | Heading of the card (and the error's `where`). | | `where` | `""` | What the part is, for the report. | | `fallback` | `None` | Your own widget for the error: `fallback(info)`. | | `theme` | `None` | Theme override. | Events: `on_error`, `on_details` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#error_boundary) ##### `ui.error_card` ```python error_card(info, *, title=None, on_details=None, actions=(), theme=None) ``` The card `ui.error_boundary` draws in place of what failed: the error, where it comes from and a Copy error button (and Details with `on_details`). | Property | Default | Description | | --- | --- | --- | | `info` | *required* | A `ui.ErrorInfo`. | | `title` | `None` | Heading. | | `actions` | `()` | More buttons after them. | | `theme` | `None` | Theme override. | Events: `on_details` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#error_card) ##### `ui.ErrorInfo` ```python @dataclass class ErrorInfo(kind, message, traceback='', origin=None, where='', time=0.0) ``` An error as `ui.error_card` and `ui.error_dialog` show it: `kind` (the exception's type), `message`, the whole `traceback`, the `origin` `(path, line)` it comes from (the last frame outside the standard library, installed packages and Pyzed), `where` it happened and `time`. `ErrorInfo.from_exception(err, where=...)` makes one (`prefer=` picks which frames may be the origin); `ErrorInfo.current()` describes the exception being handled. `summary` is `Type: message`, `location` is `file.py:12` and `report()` is everything as text for a bug report. | Property | Default | Description | | --- | --- | --- | | `kind` | *required* | The exception's type name. | | `message` | *required* | Its message. | | `traceback` | `""` | The whole traceback, as Python prints it. | | `origin` | `None` | `(path, line)` of the code it comes from, or `None`. | | `where` | `""` | What was running. | | `time` | `0.0` | When it happened (`time.time()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#errorinfo) #### Related - [Error dialog](https://albakore.github.io/pyzed/library/error-dialog/): What went wrong, where, and the traceback, with Copy and Go to source. - [Callout](https://albakore.github.io/pyzed/library/callout/): An inline notice with a title, text and actions. - [Console](https://albakore.github.io/pyzed/library/console/): A dev-tools style log with levels, filter, copy and clear. ### Error dialog > What went wrong, where, and the traceback, with Copy and Go to source. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/error-dialog/ :::tip[In plain words] A window that pops up when something fails, saying what went wrong and where, with a button to copy it for a bug report. ::: `ui.error_dialog(info, on_close)` shows an error in a dialog: its message, where it happened, the file and line it comes from and the traceback in a read-only editor, where any part can be selected and copied. Its buttons copy the whole report, go to the origin (`on_go_to`, labeled `go_to_label`), show it among the logs (`on_logs`) and close. `more` adds a line saying how many other errors came after it. `info` is a `ui.ErrorInfo`; show the dialog with a [Dialog layer](https://albakore.github.io/pyzed/library/dialog-layer/). `ui.error_dialog` · Category: [Dialogs](https://albakore.github.io/pyzed/library/#dialogs) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#error_dialog) **Code:** ```python def average(values): return sum(values) / len(values) try: average([]) except ZeroDivisionError as err: info = ui.ErrorInfo.from_exception(err, where="Totals") ui.dialog_layer(main_view(), ui.error_dialog(info, on_close, on_go_to=on_go_to, on_logs=on_logs, width=620)) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - When something the user asked for fails (saving, exporting, running a script) and the details matter. - From the Details button of an [Error boundary](https://albakore.github.io/pyzed/library/error-boundary/). - For a short message without a traceback, use an [Alert](https://albakore.github.io/pyzed/library/alert/) with `kind="error"`. #### Usage ```python import pyzed as pz from pyzed import ui class Notes(pz.App): def __init__(self): self.error = None def save(self): try: write_notes(self.notes) except OSError as err: self.error = ui.ErrorInfo.from_exception(err, where="Save") def close_error(self): self.error = None def view(self): dialog = ui.error_dialog(self.error, self.close_error) if self.error else None return ui.dialog_layer(self.page(), dialog, on_dismiss=self.close_error) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options Without a traceback the dialog leaves out the code box, as with an `ErrorInfo` you build yourself. ```python def average(values): return sum(values) / len(values) try: average([]) except ZeroDivisionError as err: info = ui.ErrorInfo.from_exception(err, where="Totals") ui.error_dialog(info, on_close, width=480) ``` ```python def average(values): return sum(values) / len(values) try: average([]) except ZeroDivisionError as err: info = ui.ErrorInfo.from_exception(err, where="Totals") ui.error_dialog(info, on_close, on_go_to=on_go_to, go_to_label="Open app.py", width=480) ``` ```python def average(values): return sum(values) / len(values) try: average([]) except ZeroDivisionError as err: info = ui.ErrorInfo.from_exception(err, where="Totals") ui.error_dialog(info, on_close, on_logs=on_logs, width=480) ``` ```python def average(values): return sum(values) / len(values) try: average([]) except ZeroDivisionError as err: info = ui.ErrorInfo.from_exception(err, where="Totals") ui.error_dialog(info, on_close, more=2, width=480) ``` **} alt={** ```python def average(values): return sum(values) / len(values) try: average([]) except ZeroDivisionError as err: info = ui.ErrorInfo.from_exception(err, where="Totals") ui.error_dialog(info, on_close, title="The totals could not be computed", width=480) ``` ```python info = ui.ErrorInfo("PermissionError", "[Errno 13] Permission denied: 'notes.md'", where="Save") ui.error_dialog(info, on_close, width=480) ``` #### Events Error dialog has **3 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_close` | `()` | Close button and Escape. | | `on_go_to` | `()` | Adds the go-to button (open the file at the line). | | `on_logs` | `()` | Adds a Show in Logs button. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Notes(pz.App): def __init__(self): try: open("/no/such/file") except OSError as err: self.error = ui.ErrorInfo.from_exception(err, where="Open") def close(self): # on_close: the button and Escape self.error = None def view(self): dialog = ui.error_dialog(self.error, self.close, on_go_to=lambda: print("go to", self.error.origin), # adds Go to on_logs=lambda: print("show logs")) if self.error else None return ui.dialog_layer(main_view(), dialog, on_dismiss=self.close) ``` **Declarative:** ```python class Notes(pz.App): error = ui.ErrorInfo("FileNotFoundError", "No such file: notes.txt") def update(self, message): match message: case "close": self.error = None case "logs": print("show logs") def view(self): dialog = ui.error_dialog(self.error, "close", on_logs="logs") if self.error else None return ui.dialog_layer(main_view(), dialog, on_dismiss="close") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: error: object = ui.ErrorInfo("FileNotFoundError", "No such file: notes.txt") def view(state, set_state): close = lambda: set_state(error=None) dialog = ui.error_dialog(state.error, close) if state.error else None return ui.dialog_layer(main_view(), dialog, on_dismiss=close) ``` #### Tips :::tip `on_go_to` receives no arguments: read the file and line from `info.origin` (a `(path, line)` pair, or `None`) and open it there. The traceback box is a read-only editor, so the user can select a single line and copy it, while Copy error copies `info.report()`. ::: #### Properties and API ##### `ui.error_dialog` ```python error_dialog( info, on_close, *, title=None, on_go_to=None, go_to_label=None, on_logs=None, more=0, width=680, theme=None, ) ``` A dialog with an error: its message, where it happened, the file and line it comes from and the traceback in a read-only editor (select and copy any part), with buttons to copy everything, go to the origin, see it among the logs and close. Show it with `ui.dialog_layer` or `ui.layer`. | Property | Default | Description | | --- | --- | --- | | `info` | *required* | A `ui.ErrorInfo`. | | `title` | `None` | Heading ("Something went wrong"). | | `go_to_label` | `None` | Its text ("Go to source"). | | `more` | `0` | How many other errors came after it (a line says so). | | `width` | `680` | Width in pixels or a modal size. | | `theme` | `None` | Theme override. | Events: `on_close`, `on_go_to`, `on_logs` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#error_dialog) #### Related - [Error boundary](https://albakore.github.io/pyzed/library/error-boundary/): Draws a part of the view, or an error card in its place when it fails. - [Alert](https://albakore.github.io/pyzed/library/alert/): A message with one button. - [Dialog layer](https://albakore.github.io/pyzed/library/dialog-layer/): Centers a dialog over your view with a dimmed backdrop. - [Modal](https://albakore.github.io/pyzed/library/modal/): A dialog surface with title, content and footer buttons. ### Facepile > Overlapping collaborator avatars with +N. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/facepile/ :::tip[In plain words] A row of overlapping avatars that shows who is here. ::: Overlapping avatars of collaborators, as in the title bar of a collaborative editor. People beyond `max_shown` are grouped into `+N`. `ui.facepile` · Category: [Data](https://albakore.github.io/pyzed/library/#data) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#facepile) **Code:** ```python pz.column([ ui.facepile(["Ana", "Luis", "Kev", "Marta"]), ui.facepile(["Ana", "Luis", "Kev", "Marta", "Sam", "Jo", "Lee", "Max"], size=28, max_shown=4), ], spacing=16, align="center") ``` #### When to use - To show who is in a project, a call or a document without listing names. #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ ui.facepile(["Ana", "Luis", "Kev", "Marta"]), ui.facepile(["Ana", "Luis", "Kev", "Marta", "Sam", "Jo", "Lee", "Max"], size=28, max_shown=4), ], spacing=16, align="center") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python ui.facepile(["Ana", "Luis"]) ``` ```python ui.facepile(["Ana", "Luis", "Kev", "Marta", "Sam"]) ``` ```python ui.facepile(["Ana", "Luis", "Kev", "Marta", "Sam", "Jo", "Lee"], max_shown=3) ``` ```python ui.facepile(["Ana", "Luis", "Kev", "Marta"], size=32, overlap=10) ``` #### Events Facepile has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.facepile` ```python facepile(people, *, size=22, overlap=6, max_shown=5, theme=None) ``` Overlapping collaborator avatars, as in the title bar of a collaborative editor. Those beyond `max_shown` are grouped into `+N`. | Property | Default | Description | | --- | --- | --- | | `people` | *required* | Initials or names. | | `size` | `22` | Size. | | `overlap` | `6` | Overlap in pixels. | | `max_shown` | `5` | How many to show. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#facepile) #### Related - [Avatar](https://albakore.github.io/pyzed/library/avatar/): A round avatar with initials in a collaborator color. - [Title bar](https://albakore.github.io/pyzed/library/title-bar/): The top bar with project and branch pickers. ### Field > A settings row: title and description left, control right. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/field/ :::tip[In plain words] A row of a form: the name of a setting on the left and its control on the right. ::: A form row like the ones of a settings screen: the title (and an optional description) on the left and any control on the right. Stack several in a column or a [Settings group](https://albakore.github.io/pyzed/library/settings-group/). `ui.field` · Category: [Forms](https://albakore.github.io/pyzed/library/#forms) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#field) **Code:** ```python pz.column([ ui.field("Autosave", ui.switch(True, on_toggle), description="Saves when you switch tabs"), ui.field("Format on save", ui.checkbox(False, on_toggle)), ui.field("Font size", ui.number_field(14, on_change, suffix="px")), ], spacing=14, width=440) ``` #### When to use - For preferences and forms where each control needs a label and maybe a hint. - It lines up switches, selects and number fields of different widths. #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ ui.field("Autosave", ui.switch(True, on_toggle), description="Saves when you switch tabs"), ui.field("Format on save", ui.checkbox(False, on_toggle)), ui.field("Font size", ui.number_field(14, on_change, suffix="px")), ], spacing=14, width=440) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Control ```python pz.container(ui.field("Autosave", ui.switch(True, on_toggle)), width=380) ``` ```python pz.container(ui.field("Font size", ui.number_field(14, on_change, suffix="px")), width=380) ``` ```python pz.container(ui.field("Theme", ui.select(["One Dark", "One Light"], "One Dark", on_select, width=150)), width=380) ``` ```python pz.container(ui.field("Name", ui.input("Pyzed", on_input, width=180)), width=380) ``` ##### Help and errors ```python pz.container(ui.field("Autosave", ui.switch(True, on_toggle), description="Saves when you switch tabs"), width=380) ``` ```python pz.container(ui.field("Port", ui.number_field(80, on_change), error="Ports below 1024 need admin rights"), width=380) ``` ##### States and events ```python pz.container(ui.field("Name", ui.input("", on_input, width=180), required=True), width=380) ``` ```python pz.container(ui.field("Email", ui.input("ada@", on_input, invalid=True), vertical=True, required=True, error="Enter a valid email"), width=320) ``` #### Events Field has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.field` ```python field( title, control, *, description=None, error=None, required=False, vertical=False, theme=None, ) ``` A form row: title and description on the left, control on the right (like a settings screen). | Property | Default | Description | | --- | --- | --- | | `title` | *required* | Title. | | `control` | *required* | The control (switch, input…). | | `description` | `None` | Help text. | | `error` | `None` | A validation message shown under the title in the error color (see `pz.validation_errors`). | | `required` | `False` | Adds a red `*` after the title. | | `vertical` | `False` | Puts the control under the title. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#field) #### Related - [Settings group](https://albakore.github.io/pyzed/library/settings-group/): A titled block of field rows, for settings screens. - [Switch](https://albakore.github.io/pyzed/library/switch/): An on/off switch for settings that apply at once. - [Number field](https://albakore.github.io/pyzed/library/number-field/): A number with −/+ buttons, limits, step and suffix. ### File picker > A path field with a button that opens the system's file or folder dialog. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/file-input/ :::tip[In plain words] A box with the location of a file and a button that opens the usual window to look for it. ::: `ui.file_input` shows a path and a browse button. The button opens the operating system's own dialog to open a file (`mode="open"`), several (`"open_many"`), a folder (`"folder"`) or to choose where to save (`"save"`), and `on_change` receives what the user chose: a path, or a list of paths with `open_many`. The path can also be typed or pasted. `filters` limits the file types (`"Images: png, jpg"`). `ui.file_input` · Category: [System](https://albakore.github.io/pyzed/library/#system) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#file_input) **Code:** ```python pz.column([ ui.field("Project folder", ui.file_input("~/projects/notes", on_change, mode="folder")), ui.field("Logo", ui.file_input("", on_change, filters="Images: png, jpg, svg", placeholder="Choose an image")), ui.field("Export to", ui.file_input("report.pdf", on_change, mode="save", button="Browse…")), ], spacing=12, width=420) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - For settings that point to a file or folder: a project folder, a log file, an export target. - `button="Browse…"` shows text instead of the folder icon. - `ui.system.pick(then, mode=...)` returns the same dialog as a `Command`, for your own buttons. #### Usage ```python import pyzed as pz from pyzed import ui class Export(pz.App): def __init__(self): self.target = "" def view(self): return ui.file_input(self.target, lambda path: setattr(self, "target", path), mode="save", file_name="report.pdf", filters="PDF: pdf") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Mode ```python ui.file_input("~/notes/todo.md", on_change, width=320) ``` ```python ui.file_input(["a.png", "b.png"], on_change, mode="open_many", width=320) ``` ```python ui.file_input("~/projects", on_change, mode="folder", width=320) ``` ```python ui.file_input("report.pdf", on_change, mode="save", width=320) ``` ##### States ```python ui.file_input("", on_change, button="Browse…", width=320) ``` ```python ui.file_input("~/notes/todo.md", on_change, editable=False, width=320) ``` ```python ui.file_input("~/missing.txt", on_change, invalid=True, width=320) ``` ```python ui.file_input("~/notes/todo.md", on_change, disabled=True, width=320) ``` #### Events File picker has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_change` | `ui.file_input`, `ui.path_input` | `(value)` | Receives the path (a list with `"open_many"`). | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Export(pz.App): def __init__(self): self.folder = "" def chosen(self, path): # on_change: the path (a list with mode="open_many") self.folder = path def view(self): return pz.column([ ui.file_input(self.folder, self.chosen, mode="folder", placeholder="Choose a folder"), ui.label(f"Saving to {self.folder or '…'}"), ], spacing=8, width=360) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Chosen: path: str class Export(pz.App): path = "" def update(self, message): match message: case Chosen(path): self.path = path def view(self): return ui.file_input(self.path, lambda path: Chosen(path), mode="save", file_name="report.pdf") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: image: str = "" def view(state, set_state): return ui.file_input(state.image, lambda path: set_state(image=path), filters="Images: png, jpg") ``` #### Properties and API ##### `ui.file_input` ```python file_input( value, on_change=None, *, mode='open', filters=None, title=None, directory=None, file_name='', placeholder=None, button=None, editable=True, disabled=False, invalid=False, id=None, width='fill', theme=None, ) ``` A path field with a button that opens the system's file dialog, so nobody types a path by hand. See the [File picker](https://albakore.github.io/pyzed/library/file-input/) page. | Property | Default | Description | | --- | --- | --- | | `value` | *required* | Current value. | | `mode` | `"open"` | `"open"` (a file), `"open_many"` (a list), `"save"` or `"folder"`. | | `filters` | `None` | Files shown: `{"Images": "png jpg"}` or `"Images: png, jpg"`. | | `title` | `None` | The dialog's title. | | `directory` | `None` | Where the dialog opens (default: the folder of the current path). | | `file_name` | `""` | The name suggested when saving. | | `placeholder` | `None` | Hint text shown when empty. | | `button` | `None` | Text for the button instead of a folder icon. | | `editable` | `True` | `False` keeps the path from being typed. | | `disabled` | `False` | Disables it (ignores clicks). | | `invalid` | `False` | Paints the border in the error color. | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `width` | `"fill"` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_change` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#file_input) ##### `ui.path_input` ```python path_input( value, on_change=None, *, folder=False, save=False, filters=None, kind=None, directory=None, relative_to=None, title=None, placeholder=None, size='md', mono=True, disabled=False, invalid=False, theme=None, ) ``` A text field for a file or folder path with a button that opens the system's file dialog, so the path never has to be typed (it still can be). | Property | Default | Description | | --- | --- | --- | | `value` | *required* | Current path. | | `folder` | `False` | Picks a folder instead of a file. | | `save` | `False` | Shows the "Save as" dialog. | | `filters` | `None` | `(name, extensions)` pairs: `[("Images", ["png", "jpg"])]`. | | `kind` | `None` | Common filters by name: `"image"`, `"audio"`, `"video"`, `"font"`, `"shader"` or `"data"`. | | `directory` | `None` | Folder the dialog opens in (by default, the current path's). | | `relative_to` | `None` | A chosen path inside this folder is given relative to it. | | `title` | `None` | Title of the dialog. | | `placeholder` | `None` | Text shown while it is empty. | | `size` | `"md"` | `"sm"`, `"md"` or `"lg"`. | | `mono` | `True` | Monospaced text. | | `disabled` | `False` | Greys it out. | | `invalid` | `False` | Paints the border in the error color. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_change` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#path_input) ##### `ui.system.pick` ```python pick(then, *, mode='open', title=None, filters=None, directory=None, file_name='') ``` The system's file dialog as a `Command`: return it from a callback and `then` receives what the user chose (a path, a list of paths with `"open_many"`, or `None` when they cancel). `ui.file_input` uses it. | Property | Default | Description | | --- | --- | --- | | `then` | *required* | Called with the result. | | `mode` | `"open"` | `"open"`, `"open_many"`, `"save"` or `"folder"`. | | `title` | `None` | The dialog's title. | | `filters` | `None` | Files shown: `{"Images": "png jpg"}` or `"Images: png, jpg"`. | | `directory` | `None` | Where the dialog opens. | | `file_name` | `""` | The name suggested when saving. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#pick) #### Related - [Drop zone](https://albakore.github.io/pyzed/library/drop-zone/): An area that takes files dragged from the desktop or the file manager. - [Input](https://albakore.github.io/pyzed/library/input/): A controlled text field with icon, placeholder and password mode. - [Field](https://albakore.github.io/pyzed/library/field/): A settings row: title and description left, control right. ### Flex > CSS flexbox: justify, align, gap, wrap and grow. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/flex/ :::tip[In plain words] Spreads things along a line: packed together, evenly spaced, or with one that grows. ::: Lays children out along one axis like CSS flexbox: `justify` on the main axis (`start`, `center`, `end`, `between`, `around`, `evenly`), `align` on the cross axis, `gap` and `wrap`. `pz.flex_item(child, grow=, basis=)` makes a child grow or gives it a base size. `pz.flex` · Category: [Layout](https://albakore.github.io/pyzed/library/#layout) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#flex) **Code:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([ pz.flex([box("A"), box("B"), box("C")], justify="between", width=440), pz.flex([box("fixed"), pz.flex_item(box("grows", w="fill"), grow=1), box("fixed")], gap=8, width=440), pz.flex([box("A"), box("B")], justify="center", gap=8, width=440), ], spacing=14) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - When `row`/`column` are not enough: even distribution, growing items, wrapping cards. #### Usage ```python import pyzed as pz from pyzed import ui box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([ pz.flex([box("A"), box("B"), box("C")], justify="between", width=440), pz.flex([box("fixed"), pz.flex_item(box("grows", w="fill"), grow=1), box("fixed")], gap=8, width=440), pz.flex([box("A"), box("B")], justify="center", gap=8, width=440), ], spacing=14) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Justify ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("A"), box("B")], justify="start", gap=8, width=360) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("A"), box("B")], justify="center", gap=8, width=360) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("A"), box("B")], justify="end", gap=8, width=360) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("A"), box("B"), box("C")], justify="between", width=360) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("A"), box("B"), box("C")], justify="around", width=360) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("A"), box("B"), box("C")], justify="evenly", width=360) ``` ##### Grow ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("fixed"), pz.flex_item(box("grows", w="fill"), grow=1), box("fixed")], gap=8, width=360) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box(str(i)) for i in range(1, 7)], gap=8, wrap=True, width=300) ``` #### Events Flex has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.flex` ```python flex( children=(), *, direction='row', justify='start', align='start', gap=0, wrap=False, padding=None, width=None, height=None, ) ``` Lays children out along one axis, like CSS flexbox, with `justify` on the main axis and `align` on the cross axis. Use `pz.flex_item` to make a child grow or give it a fixed size. | Property | Default | Description | | --- | --- | --- | | `children` | `()` | The widgets, or `pz.flex_item(...)` for children with their own sizing. | | `direction` | `"row"` | `"row"` or `"column"`. | | `justify` | `"start"` | `"start"`, `"center"`, `"end"`, `"between"`, `"around"` or `"evenly"`. No effect when a child grows. | | `align` | `"start"` | Cross-axis alignment: `"start"`, `"center"` or `"end"`. | | `gap` | `0` | Space between children. | | `wrap` | `False` | Wraps onto new lines (rows with `justify="start"` only). | | `padding` | `None` | Space around the children. | | `width` | `None` | Width. | | `height` | `None` | Height. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#flex) ##### `pz.flex_item` ```python flex_item(content, *, grow=1, basis=None, align=None) ``` Sizes a child of `pz.flex`. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | The widget. | | `grow` | `1` | Share of the free space on the main axis (`0` keeps its natural size). | | `basis` | `None` | A fixed main size in pixels, instead of growing. | | `align` | `None` | Cross-axis alignment for this child. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#flex_item) #### Related - [Grid](https://albakore.github.io/pyzed/library/grid/): Equal columns, or CSS-like tracks with spans. - [Row](https://albakore.github.io/pyzed/library/row/): Lays out children horizontally. ### Grid > Equal columns, or CSS-like tracks with spans. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/grid/ :::tip[In plain words] Arranges things in rows and columns, like tiles on a wall. ::: Lays out children in a grid. With `columns=3` the columns are equal, and `max_width` fits as many as possible. With track lists (`columns=[200, "1fr", "auto"]`) it works like a CSS grid, and `pz.cell(child, column=, row=, column_span=, row_span=)` places a child explicitly. `pz.grid` · Category: [Layout](https://albakore.github.io/pyzed/library/#layout) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#grid) **Code:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([box(str(i), w="fill") for i in range(1, 9)], columns=4, spacing=8), width=440) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - For galleries, dashboards and page layouts with a header, sidebar and content. #### Usage ```python import pyzed as pz from pyzed import ui box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([box(str(i), w="fill") for i in range(1, 9)], columns=4, spacing=8), width=440) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Columns ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([box(str(i), w="fill") for i in range(1, 5)], columns=2, spacing=8, height=44), width=220) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([box(str(i), w="fill") for i in range(1, 7)], columns=3, spacing=8, height=44), width=300) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([box(str(i), w="fill") for i in range(1, 9)], columns=4, spacing=8, height=44), width=380) ``` ##### Tracks ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([box("100 px", w="fill"), box("1fr", w="fill")], columns=[100, "1fr"], spacing=8), width=340, height=44) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([pz.cell(box("header", w="fill"), column=0, row=0, column_span=2), pz.cell(box("a", w="fill"), column=0, row=1), pz.cell(box("b", w="fill"), column=1, row=1)], columns=["1fr", "1fr"], spacing=8), width=300, height=96) ``` #### Examples ##### Tracks and spans Numbers are pixels, `"1fr"` shares the free space and `"auto"` fits the content. **Code:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([ pz.cell(box("header", w="fill"), column=0, row=0, column_span=2), pz.cell(box("sidebar", w="fill", h="fill"), column=0, row=1), pz.cell(box("content", w="fill", h="fill"), column=1, row=1), ], columns=[120, "1fr"], rows=[44, 110], spacing=8), width=440) ``` #### Events Grid has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.grid` ```python grid( children=None, *, columns=None, rows=None, max_width=None, spacing=0.0, row_spacing=None, height=None, aspect_ratio=None, width=None, padding=None, align_x=None, align_y=None, ) ``` Lays out its children in a grid. With `columns=3` the columns are equal (or `max_width` fits as many as possible). With track lists (`columns=[200, "1fr", "auto"]`, `rows=...`) it works like a CSS grid: numbers are pixels, `"auto"` fits the content and `"fr"` shares the space left. Wrap children in `pz.cell` to place them and make them span; the rest fill the free cells row by row. See [Layout](https://albakore.github.io/pyzed/guides/layout/#grids). | Property | Default | Description | | --- | --- | --- | | `children` | `None` | List of children. Nested lists are flattened and `None` and `False` are ignored, so you can write `cond and widget`. | | `columns` | `None` | Number of equal columns, or tracks: a list (`[200, "1fr", "auto"]`) or a string (`"200 1fr auto"`). | | `rows` | `None` | Row tracks, like `columns`. Rows beyond the list are `"auto"`. | | `max_width` | `None` | Fits as many columns of at most this width as possible (`columns=` only). | | `spacing` | `0.0` | Space between cells. | | `row_spacing` | `None` | Space between rows, if different. | | `height` | `None` | With `columns=`: height of each row. With tracks: the grid's height. | | `aspect_ratio` | `None` | Keeps cells at this width/height ratio (`columns=` only). | | `width` | `None` | With `columns=`: pixels. With tracks: the grid's width (default `"fill"`). | | `padding` | `None` | Space around the cells. | | `align_x` | `None` | Horizontal alignment of items in their cells: `"start"`, `"center"`, `"end"`. | | `align_y` | `None` | Vertical alignment of items in their cells. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#grid) ##### `pz.cell` ```python cell( content, *, column=None, row=None, column_span=1, row_span=1, align_x=None, align_y=None, ) ``` Places a child in a track grid: its column and row (from 0) and how many it spans. Children without a cell fill the free cells row by row. Outside a grid it is just the child. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | The widget. | | `column` | `None` | Column index (from 0). | | `row` | `None` | Row index (from 0). | | `column_span` | `1` | Columns it covers. | | `row_span` | `1` | Rows it covers. | | `align_x` | `None` | Horizontal alignment in the cell. | | `align_y` | `None` | Vertical alignment in the cell. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#cell) #### Related - [Flex](https://albakore.github.io/pyzed/library/flex/): CSS flexbox: justify, align, gap, wrap and grow. - [Row](https://albakore.github.io/pyzed/library/row/): Lays out children horizontally. - [Column](https://albakore.github.io/pyzed/library/column/): Stacks children vertically. ### Heading > Semibold titles in three levels. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/heading/ :::tip[In plain words] A title: bigger, bolder text that names a page or a section. ::: A semibold title. `level=1` (default), `2` or `3` pick the size, so headings stay consistent across screens. `ui.heading` · Category: [Display](https://albakore.github.io/pyzed/library/#display) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#heading) **Code:** ```python pz.column([ ui.heading("Heading 1"), ui.heading("Heading 2", level=2), ui.heading("Heading 3", level=3), ui.muted("Body text below the headings."), ], spacing=6) ``` #### When to use - For the title of a page, a dialog section or a settings group. - For a small uppercase list title, use [List header](https://albakore.github.io/pyzed/library/list-header/). #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ ui.heading("Heading 1"), ui.heading("Heading 2", level=2), ui.heading("Heading 3", level=3), ui.muted("Body text below the headings."), ], spacing=6) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Level ```python ui.heading("Project settings") ``` ```python ui.heading("Project settings", level=2) ``` ```python ui.heading("Project settings", level=3) ``` ```python ui.heading("Project settings", level=4) ``` ##### Color ```python ui.heading("Welcome back", level=2, color="accent") ``` ```python pz.column([ui.heading("Billing", level=2), ui.muted("Invoices and payment methods.")], spacing=4) ``` #### Events Heading has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.heading` ```python heading(content, *, level=1, theme=None, **kwargs) ``` A semibold heading. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `level` | `1` | 1 (26 px), 2 (20 px), 3 (16 px) or 4 (13.5 px). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | | `**kwargs` | — | Same as `label`. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#heading) #### Related - [Label](https://albakore.github.io/pyzed/library/label/): Text with a theme color, size and weight; `muted` for secondary text. - [Card](https://albakore.github.io/pyzed/library/card/): A bordered surface with an optional title and actions. ### Icon > Lucide icons by name, and SVG images. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/icon/ :::tip[In plain words] Small pictures (a folder, a bell, a gear) from a library of more than 1,500. ::: `pz.icon(name)` draws one of the 1,500+ [Lucide](https://lucide.dev) icons; `color` defaults to the surrounding text color. `pz.svg(source)` draws any SVG from a path or bytes, tinted with `color` if you want. `pz.icon` · Category: [Content](https://albakore.github.io/pyzed/library/#content) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#icon) **Code:** ```python pz.row([ pz.icon("sparkles", size=28, color="#e5c07b"), pz.icon("git-branch", size=28, color="#a1c181"), pz.icon("folder-open", size=28, color="#74ade8"), pz.icon("bug", size=28, color="#d07277"), pz.icon("terminal", size=28, color="#c678dd"), pz.svg("logo.svg", width=40, height=40), ], spacing=20, align="center") ``` #### When to use - Inside buttons, list rows and headers. Run `pyzed icons` to search the names. #### Usage ```python import pyzed as pz pz.row([ pz.icon("sparkles", size=28, color="#e5c07b"), pz.icon("git-branch", size=28, color="#a1c181"), pz.icon("folder-open", size=28, color="#74ade8"), pz.icon("bug", size=28, color="#d07277"), pz.icon("terminal", size=28, color="#c678dd"), pz.svg("logo.svg", width=40, height=40), ], spacing=20, align="center") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Size ```python pz.icon("sparkles", size=12) ``` ```python pz.icon("sparkles", size=16) ``` ```python pz.icon("sparkles", size=24) ``` ```python pz.icon("sparkles", size=40) ``` ##### Color ```python pz.icon("git-branch", size=24, color="#a1c181") ``` ```python pz.icon("bug", size=24, color="#d07277") ``` ```python pz.icon("folder-open", size=24, color="#74ade8") ``` ##### SVG `color` paints every visible pixel of the SVG in one color and keeps the transparent ones, like an icon tint. An SVG with a filled background, such as `logo.svg`, becomes a plain tile. ```python pz.svg("logo.svg", width=48, height=48) ``` ```python shapes = ('' '') pz.svg(shapes, width=48, height=48, color="#e5c07b") ``` #### Events Icon has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.icon` ```python icon(name, *, size=16, color=None) ``` A [Lucide](https://lucide.dev) icon. `color` defaults to the surrounding text color. | Property | Default | Description | | --- | --- | --- | | `name` | *required* | Icon name (`pyzed icons` lists them). | | `size` | `16` | Size in pixels. | | `color` | `None` | Color. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#icon) ##### `pz.svg` ```python svg( source, *, width=None, height=None, color=None, content_fit=None, opacity=1.0, rotation=0.0, ) ``` An SVG image. | Property | Default | Description | | --- | --- | --- | | `source` | *required* | Path to an `.svg`, the SVG code as a string (starting with `<`) or `bytes`. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `color` | `None` | If given, tints the whole SVG with that color (useful for monochrome icons). | | `content_fit` | `None` | `"contain"`, `"cover"`, `"fill"`, `"none"` or `"scale-down"`. | | `opacity` | `1.0` | Opacity from 0 to 1. | | `rotation` | `0.0` | Rotation in degrees. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#svg) #### Related - [Icon button](https://albakore.github.io/pyzed/library/icon-button/): A square button with just an icon and an optional tooltip. - [Image](https://albakore.github.io/pyzed/library/image/): Bitmaps with fit, opacity, radius; a pan-and-zoom viewer. ### Icon button > A square button with just an icon and an optional tooltip. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/icon-button/ :::tip[In plain words] A small button with just a picture (a magnifying glass, a gear) instead of words. ::: A compact square button that shows a [Lucide](https://lucide.dev) icon. It is ghost by default, so it fits in toolbars, panel headers and tab bars. `tooltip=` adds a tooltip on hover. `ui.icon_button` · Category: [Actions](https://albakore.github.io/pyzed/library/#actions) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#icon_button) **Code:** ```python pz.row([ ui.icon_button("search", on_search, tooltip="Search"), ui.icon_button("settings", on_settings, tooltip="Settings"), ui.icon_button("bell", on_notifications, selected=True), ui.icon_button("plus", on_new, variant="filled"), ], spacing=6) ``` #### When to use - For frequent actions whose icon is obvious (search, settings, close, add). - Always give it a `tooltip` when the icon alone may be ambiguous. - `selected=True` shows a toggled state (a panel that is open, a filter that is on). #### Usage ```python import pyzed as pz from pyzed import ui pz.row([ ui.icon_button("search", on_search, tooltip="Search"), ui.icon_button("settings", on_settings, tooltip="Settings"), ui.icon_button("bell", on_notifications, selected=True), ui.icon_button("plus", on_new, variant="filled"), ], spacing=6) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Size ```python ui.icon_button("star", on_press, size="xs") ``` ```python ui.icon_button("star", on_press, size="sm") ``` ```python ui.icon_button("star", on_press, size="md") ``` ```python ui.icon_button("star", on_press, size="lg") ``` ##### Variant ```python ui.icon_button("settings", on_press) ``` ```python ui.icon_button("settings", on_press, variant="filled") ``` ```python ui.icon_button("settings", on_press, variant="outline") ``` ```python ui.icon_button("settings", on_press, variant="solid") ``` ```python ui.icon_button("settings", on_press, variant="tinted") ``` ##### Color ```python ui.icon_button("sparkles", on_press, color="accent") ``` ```python ui.icon_button("circle-check", on_press, color="success") ``` ```python ui.icon_button("triangle-alert", on_press, color="warning") ``` ```python ui.icon_button("trash-2", on_press, color="error", variant="outline") ``` ##### State Drawn as the pointer shows it: tooltips only open on hover. ```python ui.icon_button("panel-left", on_press, selected=True) ``` ```python ui.icon_button("lock", on_press, disabled=True) ``` ```python ui.icon_button("search", on_press, tooltip="Search") ``` ##### States and events ```python ui.icon_button("bell", on_press, indicator="accent") ``` ```python ui.icon_button("trash-2", on_press, tint="error") ``` ```python ui.icon_button("refresh-cw", on_press, loading=True) ``` ```python ui.icon_button("search", on_press, tooltip="Search", keys="ctrl-f") ``` #### Events Icon button has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_press` | `()` | Message or callable sent on press. `None` disables the control. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Toolbar(pz.App): def __init__(self): self.bold = False def toggle_bold(self): self.bold = not self.bold def view(self): return pz.row([ ui.icon_button("bold", self.toggle_bold, selected=self.bold, tooltip="Bold", keys="ctrl-b"), ui.icon_button("trash-2", self.clear, tooltip="Clear", disabled=not self.bold), ], spacing=4) def clear(self): self.bold = False ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class ToggleBold: pass class Toolbar(pz.App): bold = False def update(self, message): if isinstance(message, ToggleBold): self.bold = not self.bold def view(self): return ui.icon_button("bold", ToggleBold(), selected=self.bold, tooltip="Bold") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: bold: bool = False def view(state, set_state): return ui.icon_button("bold", lambda: set_state(bold=not state.bold), selected=state.bold, tooltip="Bold") ``` #### Tips :::tip Run `pyzed icons` to search the icon names. ::: #### Properties and API ##### `ui.icon_button` ```python icon_button( name, on_press=None, *, size='md', variant='ghost', color=None, selected=False, disabled=False, tooltip=None, keys=None, tint=None, loading=False, indicator=None, theme=None, ) ``` A square button with just an icon. | Property | Default | Description | | --- | --- | --- | | `name` | *required* | Icon name. | | `size` | `"md"` | `"xs"`, `"sm"`, `"md"` or `"lg"`. | | `variant` | `"ghost"` | As in `ui.button`. Defaults to `"ghost"`. | | `color` | `None` | Icon color. | | `selected` | `False` | Marks it as selected. | | `disabled` | `False` | Disables it (ignores clicks). | | `tooltip` | `None` | Tooltip text. | | `keys` | `None` | Key binding shown in the tooltip. | | `tint` | `None` | Colors it (`"error"`, `"accent"`…). | | `loading` | `False` | Swaps the icon for a spinner and ignores presses. | | `indicator` | `None` | Adds a small dot in this color at the top right (unread, changes…). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_press` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#icon_button) #### Related - [Button](https://albakore.github.io/pyzed/library/button/): The themed button: variants, sizes, icons and shortcuts. - [Tooltip](https://albakore.github.io/pyzed/library/tooltip/): A hint shown on hover, optionally with its shortcut. - [Toolbar](https://albakore.github.io/pyzed/library/toolbar/): The bar under the tabs: breadcrumbs and buffer actions. ### Icon picker > Pick one of the ~2000 Lucide icons from a searchable grid. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/icon-picker/ :::tip[In plain words] A searchable grid of little pictures to choose an icon from. ::: `ui.IconPicker` is a component with a search box, a category drop-down and a grid of every bundled Lucide icon. The search matches names and tags ("person" finds `user`), Enter picks the first match and `on_change` receives the icon's name in `event.value`. `ui.icon_picker_dialog` puts it in a dialog that picks on a click, and `ui.icon_grid` is the bare grid, which only builds the rows in view. `ui.IconPicker` · Category: [Forms](https://albakore.github.io/pyzed/library/#forms) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#iconpicker) **Code:** ```python ui.IconPicker(value="rocket", columns=10, height=200, clearable=True, on_change=on_change) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - To let users choose an icon for a folder, a tag, a shortcut or a toolbar button. - Show the current icon as a button and open `icon_picker_dialog` in a [Dialog layer](https://albakore.github.io/pyzed/library/dialog-layer/) when it is pressed. #### Usage ```python import pyzed as pz from pyzed import ui class Shortcuts(pz.App): def __init__(self): self.icon = "rocket" self.picking = False def pick(self, name): self.icon, self.picking = name or "circle-dashed", False def view(self): page = ui.button("Change icon", lambda: setattr(self, "picking", True), icon=self.icon) dialog = ui.icon_picker_dialog(self.icon, self.pick, on_close=lambda: setattr(self, "picking", False)) return ui.dialog_layer(page, dialog if self.picking else None, on_dismiss=lambda: setattr(self, "picking", False)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Picker ```python ui.IconPicker(value="rocket", columns=8, height=180, on_change=on_change) ``` ```python ui.IconPicker(value="heart", columns=8, height=180, clearable=True, on_change=on_change) ``` ##### Building blocks ```python ui.icon_grid(pz.icons.search("arrow")[:24], on_pick, selected="arrow-right", columns=8, height=110) ``` ```python ui.icon_picker_dialog("rocket", on_pick, on_close=on_close, columns=8) ``` #### Events Icon picker has **3 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_change` | `ui.IconPicker` | `(event)` | Receives `event.value`. | | `on_pick` | `ui.icon_picker_dialog`, `ui.icon_grid` | `(name)` | Called with the clicked icon's name (`""` when cleared). | | `on_close` | `ui.icon_picker_dialog` | `()` | The close button. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Shortcut(pz.App): def __init__(self): self.icon = "rocket" self.picking = False def pick(self, name): # on_pick: the icon's name ("" when cleared) self.icon, self.picking = name or "circle-dashed", False def view(self): page = ui.button("Change icon", lambda: setattr(self, "picking", True), icon=self.icon) dialog = ui.icon_picker_dialog(self.icon, self.pick, on_close=lambda: setattr(self, "picking", False)) return ui.dialog_layer(page, dialog if self.picking else None, on_dismiss=lambda: setattr(self, "picking", False)) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Icon: name: str class Shortcut(pz.App): icon = "rocket" def update(self, message): match message: case Icon(name): self.icon = name or "circle-dashed" def view(self): # IconPicker is a pz.Component: its handler gets a pz.Event return ui.IconPicker(value=self.icon, height=200, on_change=lambda e: Icon(e.value)) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: icon: str = "rocket" def view(state, set_state): return pz.column([ ui.icon_grid(["rocket", "star", "heart", "bell"], lambda name: set_state(icon=name), selected=state.icon, columns=4, height=60), ui.button("Launch", None, icon=state.icon), ], spacing=8) ``` #### Properties and API ##### `ui.IconPicker` ```python class IconPicker ``` A component that picks a Lucide icon: a search box (names and tags), a category drop-down and a grid that only builds the rows in view. Enter picks the first match. `on_change` gets the icon's name (`""` when cleared); pass it back as `value`. See the [Icon picker](https://albakore.github.io/pyzed/library/icon-picker/) page. | Property | Default | Description | | --- | --- | --- | | `value` | `""` | The picked icon's name. | | `columns` | `8` | Icons per row. | | `height` | `288` | Height of the grid. | | `clearable` | `False` | Shows a button that clears the icon. | | `autofocus` | `False` | Focuses the search box when it appears. | Events: `on_change` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#iconpicker) ##### `ui.icon_picker_dialog` ```python icon_picker_dialog( value, on_pick, *, on_close=None, title=None, columns=10, clearable=True, key='icon-picker', theme=None, ) ``` A dialog with an `IconPicker` that picks on a click; use it as the dialog of `ui.dialog_layer`. The search box gets the focus. | Property | Default | Description | | --- | --- | --- | | `value` | *required* | The current icon. | | `title` | `None` | Default: "Choose an icon", translated. | | `columns` | `10` | Icons per row. | | `clearable` | `True` | Shows the "No icon" button. | | `key` | `"icon-picker"` | Component key, for several pickers in one view. | | `theme` | `None` | Theme. | Events: `on_pick`, `on_close` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#icon_picker_dialog) ##### `ui.icon_grid` ```python icon_grid( names, on_pick, *, selected=None, columns=8, cell=34, height=288, id=None, theme=None, ) ``` The bare grid of icons, virtualized: only the rows in view are built, so thousands of icons cost the same as a few. | Property | Default | Description | | --- | --- | --- | | `names` | *required* | Icon names, in order. | | `selected` | `None` | The highlighted name. | | `columns` | `8` | Icons per row. | | `cell` | `34` | Side of each cell in pixels. | | `height` | `288` | Height of the grid. | | `id` | `None` | Id of the scrollable, for `Command.scroll_to`. | | `theme` | `None` | Theme. | Events: `on_pick` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#icon_grid) ##### `pz.icons.search` ```python search(query, among=None) ``` Icons matching a search by name and tag, best first: exact names, then names that start with the query, names with every word and icons found by their tags (`search("person")` finds `user`). | Property | Default | Description | | --- | --- | --- | | `query` | *required* | Words that must all appear in the name or the tags. | | `among` | `None` | Only search these names (default: every icon). | [Full reference →](https://albakore.github.io/pyzed/reference/icons/#search) ##### `pz.icons.category` ```python category(key, among=None) ``` The icons of a category of `CATEGORIES`, sorted. An icon can be in several. | Property | Default | Description | | --- | --- | --- | | `key` | *required* | A key of `CATEGORIES` (`"arrows"`, `"files"`, `"people"`...). | | `among` | `None` | Only look at these names. | [Full reference →](https://albakore.github.io/pyzed/reference/icons/#category) #### Related - [Icon](https://albakore.github.io/pyzed/library/icon/): Lucide icons by name, and SVG images. - [Dialog layer](https://albakore.github.io/pyzed/library/dialog-layer/): Centers a dialog over your view with a dimmed backdrop. - [Color picker](https://albakore.github.io/pyzed/library/color-picker/): Pick a color, a gradient or a masked fill; swatches show them. ### Image > Bitmaps with fit, opacity, radius; a pan-and-zoom viewer. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/image/ :::tip[In plain words] Shows a picture from a file: a photo, a logo, a screenshot. ::: `pz.image(source)` shows a PNG, JPEG, GIF, WebP, BMP… from a path, `bytes`, or raw RGBA pixels with `size=(w, h)`. Options: `content_fit` (`"contain"`, `"cover"`, `"fill"`, `"none"`, `"scale_down"`), `filter` (`"linear"` or `"nearest"`), `opacity`, `radius`, `rotation` and `scale`. `pz.image_viewer` adds pan (drag) and zoom (wheel). `pz.image` · Category: [Content](https://albakore.github.io/pyzed/library/#content) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#image) **Code:** ```python pz.row([ pz.image("photo.png", width=300, height=180, content_fit="cover", radius=10), pz.image("photo.png", width=140, height=180, content_fit="contain", opacity=0.5), ], spacing=16, align="center") ``` #### When to use - For screenshots, avatars, thumbnails and photo viewers. #### Usage ```python import pyzed as pz pz.row([ pz.image("photo.png", width=300, height=180, content_fit="cover", radius=10), pz.image("photo.png", width=140, height=180, content_fit="contain", opacity=0.5), ], spacing=16, align="center") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Fit ```python pz.image("photo.png", width=200, height=110, content_fit="cover") ``` ```python pz.image("photo.png", width=200, height=110, content_fit="contain") ``` ```python pz.image("photo.png", width=200, height=110, content_fit="fill") ``` ##### Look `rotation` is in degrees, clockwise; the corners of a rotated image may reach outside its box. ```python pz.image("photo.png", width=200, height=110, content_fit="cover", radius=16) ``` ```python pz.image("photo.png", width=200, height=110, content_fit="cover", opacity=0.4) ``` ```python pz.image("photo.png", width=200, height=110, content_fit="cover", radius=10, rotation=8) ``` #### Events Image has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.image` ```python image( source, *, width=None, height=None, content_fit=None, filter='linear', opacity=1.0, radius=0.0, rotation=0.0, scale=1.0, size=None, ) ``` A bitmap image (PNG, JPEG, GIF, BMP, WebP…) from a path or `bytes`, or raw RGBA pixels. | Property | Default | Description | | --- | --- | --- | | `source` | *required* | File path, `bytes` of the encoded file, or raw RGBA pixels with `size=(width, height)`. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `content_fit` | `None` | `"contain"`, `"cover"`, `"fill"`, `"none"` or `"scale-down"`. | | `filter` | `"linear"` | `"linear"` (smooth) or `"nearest"` (crisp pixels, for pixel art). | | `opacity` | `1.0` | Opacity from 0 to 1. | | `radius` | `0.0` | Corner radius. | | `rotation` | `0.0` | Rotation in degrees. | | `scale` | `1.0` | Scale. | | `size` | `None` | `(width, height)` when `source` is raw RGBA pixels. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#image) ##### `pz.image_viewer` ```python image_viewer( source, *, width=None, height=None, min_scale=0.25, max_scale=10.0, scale_step=0.1, filter='linear', size=None, ) ``` An image you can pan (by dragging) and zoom (mouse wheel), like Iced's viewer. | Property | Default | Description | | --- | --- | --- | | `source` | *required* | File path, `bytes` of the encoded file, or raw RGBA pixels with `size=(width, height)`. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `min_scale` | `0.25` | Minimum zoom. | | `max_scale` | `10.0` | Maximum zoom. | | `scale_step` | `0.1` | How much the zoom changes per wheel step. | | `filter` | `"linear"` | `"linear"` (smooth) or `"nearest"` (crisp pixels, for pixel art). | | `size` | `None` | `(width, height)` for raw RGBA pixels. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#image_viewer) #### Related - [Icon](https://albakore.github.io/pyzed/library/icon/): Lucide icons by name, and SVG images. - [QR code](https://albakore.github.io/pyzed/library/qr-code/): A QR code for any text or URL. ### Indicator > A colored dot for status, presence or unsaved changes. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/indicator/ :::tip[In plain words] A small colored dot that tells a state at a glance: green is fine, red is a problem. ::: A small colored dot. `color` is a theme token or a hex color and `size` its diameter. `ui.indicator` · Category: [Display](https://albakore.github.io/pyzed/library/#display) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#indicator) **Code:** ```python pz.column([ pz.row([ui.indicator("success"), ui.label("Server running")], spacing=8, align="center"), pz.row([ui.indicator("warning"), ui.label("Reconnecting")], spacing=8, align="center"), pz.row([ui.indicator("error"), ui.label("Build failed")], spacing=8, align="center"), pz.row([ui.indicator(size=10), ui.label("Unsaved changes")], spacing=8, align="center"), ], spacing=10) ``` #### When to use - Next to a name to show presence (online, busy) or a state (running, failed). - On a tab or list item to mark unsaved changes. #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ pz.row([ui.indicator("success"), ui.label("Server running")], spacing=8, align="center"), pz.row([ui.indicator("warning"), ui.label("Reconnecting")], spacing=8, align="center"), pz.row([ui.indicator("error"), ui.label("Build failed")], spacing=8, align="center"), pz.row([ui.indicator(size=10), ui.label("Unsaved changes")], spacing=8, align="center"), ], spacing=10) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Color ```python ui.indicator() ``` ```python ui.indicator("success") ``` ```python ui.indicator("warning") ``` ```python ui.indicator("error") ``` ```python ui.indicator("text_muted") ``` ```python ui.indicator("#c678dd") ``` ##### Size ```python ui.indicator("success", size=6) ``` ```python ui.indicator("success", size=10) ``` ```python ui.indicator("success", size=14) ``` ##### Next to text ```python pz.row([ui.indicator("success"), ui.label("Ana is online")], spacing=8, align="center") ``` ```python pz.row([ui.label("notes.py"), ui.indicator(size=8)], spacing=6, align="center") ``` #### Events Indicator has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.indicator` ```python indicator(color='accent', *, size=6, theme=None) ``` A colored dot (status, unsaved changes, presence). | Property | Default | Description | | --- | --- | --- | | `color` | `"accent"` | Token or color. | | `size` | `6` | Diameter. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#indicator) #### Related - [Badge](https://albakore.github.io/pyzed/library/badge/): A small pill for statuses, versions and counters. - [Avatar](https://albakore.github.io/pyzed/library/avatar/): A round avatar with initials in a collaborator color. ### Input > A controlled text field with icon, placeholder and password mode. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/input/ :::tip[In plain words] A box where people type one line of text: a name, an email, a password. ::: A themed single-line text field. It is **controlled**: it shows `value` and reports every keystroke to `on_input`, so you store the text in your state and pass it back in `view()`. `on_submit` runs on Enter. `ui.input` · Category: [Forms](https://albakore.github.io/pyzed/library/#forms) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#input) **Code:** ```python pz.column([ ui.input("", on_input, placeholder="Project name"), ui.input("ada@example.com", on_input, icon="mail"), ui.input("hunter2", on_input, secure=True, icon="lock"), ], spacing=10, width=360) ``` #### When to use - For names, paths, search boxes, emails and passwords (`secure=True`). - For multi-line text or code use [Editor](https://albakore.github.io/pyzed/library/editor/). - Give it an `id` to focus it from code with `pz.Command.focus(id)`. #### Usage ```python import pyzed as pz from pyzed import ui class Form(pz.App): def __init__(self): self.name = "" def view(self): return ui.input(self.name, lambda text: setattr(self, "name", text), placeholder="Project name", on_submit=self.create, id="name") def on_start(self): return pz.Command.focus("name") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content ```python ui.input("", on_input, placeholder="Project name", width=260) ``` ```python ui.input("Pyzed", on_input, width=260) ``` ```python ui.input("ada@example.com", on_input, icon="mail", width=260) ``` ```python ui.input("hunter2", on_input, secure=True, icon="lock", width=260) ``` ##### Size ```python ui.input("Small", on_input, size="sm", width=260) ``` ```python ui.input("Medium", on_input, size="md", width=260) ``` ```python ui.input("Large", on_input, size="lg", width=260) ``` ##### Style ```python ui.input("Borderless", on_input, borderless=True, width=260) ``` ```python ui.input("SELECT * FROM users", on_input, mono=True, width=260) ``` ```python ui.input("Can't edit me", width=260) ``` ##### Validation Show errors with [Field](https://albakore.github.io/pyzed/library/field/)'s `error=`, next to the input. ```python ui.field("Email", ui.input("ada@", on_input, width=220), error="Enter a valid email") ``` ##### States and events Focus, blur and keys come from the common events: `.on(focus=..., blur=..., key_down=...)`. ```python ui.input("Locked", on_input, disabled=True, width=220) ``` ```python ui.input("Copy me, not edit me", on_input, readonly=True, width=220) ``` ```python ui.input("ada@", on_input, invalid=True, width=220) ``` ```python ui.input("pyzed", on_input, on_clear=on_clear, width=220) ``` ```python ui.input("12345678", on_input, max_length=8, width=220) ``` ```python ui.input("centered", on_input, align="center", width=220) ``` ##### Base widget The `pz.*` widget underneath: no theme, every color and size is yours. ```python pz.text_input("", placeholder="pz.text_input", on_input=on_input, width=260, padding=8, radius=8, border_color="#c678dd", focus_border_color="#e5c07b") ``` #### Events Input has **3 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_input` | `ui.input`, `pz.text_input` | `(text)` | Callable that receives the new text on every keystroke. | | `on_submit` | `ui.input`, `pz.text_input` | `()` | Message or callable sent when Enter is pressed. | | `on_clear` | `ui.input` | `()` | Shows an ✕ while there is text; sent when it is clicked. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class SignUp(pz.App): def __init__(self): self.email = "" self.sent = False def set_email(self, text): # on_input: the new text on every keystroke self.email, self.sent = text, False def send(self): # on_submit: Enter self.sent = "@" in self.email def view(self): return pz.column([ ui.input(self.email, self.set_email, placeholder="Email", icon="mail", id="email", on_submit=self.send, on_clear=lambda: self.set_email(""), invalid=bool(self.email) and "@" not in self.email), ui.label("Thanks!" if self.sent else "Press Enter to send"), ], spacing=8, width=320) def on_start(self): return pz.Command.focus("email") ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Typed: text: str class SignUp(pz.App): email = "" sent = False def update(self, message): match message: case Typed(text): self.email, self.sent = text, False case "send": self.sent = "@" in self.email case "clear": self.email = "" def view(self): return ui.input(self.email, Typed, placeholder="Email", icon="mail", on_submit="send", on_clear="clear") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: email: str = "" sent: bool = False def view(state, set_state): valid = "@" in state.email return pz.column([ ui.input(state.email, lambda text: set_state(email=text, sent=False), placeholder="Email", on_submit=lambda: set_state(sent=valid), invalid=bool(state.email) and not valid), ui.label("Thanks!" if state.sent else "Press Enter to send"), ], spacing=8, width=320) ``` #### Properties and API ##### `ui.input` ```python input( value, on_input=None, *, placeholder='', icon=None, on_submit=None, on_clear=None, id=None, size='md', secure=False, width='fill', borderless=False, mono=False, disabled=False, readonly=False, invalid=False, max_length=None, align='start', theme=None, ) ``` A text field. Focus, blur and keys come from the common events: `.on(focus=..., blur=..., key_down=...)`. | Property | Default | Description | | --- | --- | --- | | `value` | *required* | Current text. | | `placeholder` | `""` | Hint text shown when empty. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `size` | `"md"` | `"sm"`, `"md"` or `"lg"`. | | `secure` | `False` | Hides the text. | | `width` | `"fill"` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `borderless` | `False` | No border or background (as in the command palette). | | `mono` | `False` | Uses the monospace font (IBM Plex Mono). | | `disabled` | `False` | Greys it out and ignores typing. | | `readonly` | `False` | Selectable and copyable, but it can't be changed. | | `invalid` | `False` | Paints the border in the error color. | | `max_length` | `None` | Ignores edits longer than this. | | `align` | `"start"` | Text alignment: `"start"`, `"center"` or `"end"`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_input`, `on_submit`, `on_clear` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#input) ##### `pz.text_input` ```python text_input( value, *, placeholder='', on_input=None, on_submit=None, id=None, size=None, font=None, padding=None, width=None, secure=False, icon=None, icon_size=None, bg=None, border_color=None, hover_border_color=None, focus_border_color=None, border_width=None, radius=None, color=None, placeholder_color=None, selection_color=None, icon_color=None, align_x=None, ) ``` A controlled text field: it shows `value` and notifies you of changes through `on_input`. For the themed field use `ui.input`. | Property | Default | Description | | --- | --- | --- | | `value` | *required* | Current text. | | `placeholder` | `""` | Hint text shown when empty. | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `size` | `None` | Font size. | | `font` | `None` | Font family. | | `padding` | `None` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `secure` | `False` | Hides the text (passwords). | | `icon` | `None` | Icon character (use `pyzed.icons.glyph(name)`). | | `icon_size` | `None` | Icon size. | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `border_color` | `None` | Border color. | | `hover_border_color` | `None` | Border color on hover. | | `focus_border_color` | `None` | Border color when focused. | | `border_width` | `None` | Border width in pixels. | | `radius` | `None` | Corner radius: a number or `(top_left, top_right, bottom_right, bottom_left)`. | | `color` | `None` | Text or icon color. | | `placeholder_color` | `None` | Placeholder color. | | `selection_color` | `None` | Selection color. | | `icon_color` | `None` | Icon color. | | `align_x` | `None` | Horizontal alignment: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | Events: `on_input`, `on_submit` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#text_input) #### Related - [Search input](https://albakore.github.io/pyzed/library/search-input/): An input with a magnifying glass, for filtering. - [Field](https://albakore.github.io/pyzed/library/field/): A settings row: title and description left, control right. - [Number field](https://albakore.github.io/pyzed/library/number-field/): A number with −/+ buttons, limits, step and suffix. ### Kbd > Keyboard shortcuts drawn as keys, with symbols on macOS. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/kbd/ :::tip[In plain words] Draws keyboard keys, so people learn the shortcut: Ctrl + S. ::: Draws a shortcut written as a string (`"ctrl-shift-p"`, chords like `"cmd-k cmd-s"`) as key caps. On macOS it uses symbols (⌘⇧P) and words elsewhere; `pyzed.ui.base.KEY_STYLE` forces `"symbols"` or `"words"`. `ui.format_keys` returns the labels if you draw them yourself. `ui.kbd` · Category: [Display](https://albakore.github.io/pyzed/library/#display) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#kbd) **Code:** ```python pz.column([ ui.kbd("ctrl-shift-p"), ui.kbd("ctrl-k ctrl-s"), pz.row([ui.kbd("escape", size="xs"), ui.kbd("enter", size="xs"), ui.kbd("tab", size="xs")], spacing=8), ], spacing=12, align="center") ``` #### When to use - In menus, tooltips, palettes and help screens, to teach shortcuts. - Most components already accept `keys=` and draw it for you (button, menu item, tooltip). #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ ui.kbd("ctrl-shift-p"), ui.kbd("ctrl-k ctrl-s"), pz.row([ui.kbd("escape", size="xs"), ui.kbd("enter", size="xs"), ui.kbd("tab", size="xs")], spacing=8), ], spacing=12, align="center") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Keys Write shortcuts as `"ctrl-s"`; on macOS `ctrl` becomes ⌃ and `cmd` ⌘ automatically. ```python ui.kbd("enter") ``` ```python ui.kbd("ctrl-shift-p") ``` ```python ui.kbd("ctrl-k ctrl-s") ``` ```python pz.row([ui.kbd("escape"), ui.kbd("tab"), ui.kbd("backspace"), ui.kbd("up")], spacing=6) ``` ##### Size ```python ui.kbd("ctrl-p", size="xs") ``` ```python ui.kbd("ctrl-p", size="sm") ``` #### Events Kbd has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.kbd` ```python kbd(keys, *, size='sm', theme=None) ``` Draws a shortcut as keys: `"ctrl-shift-p"`, `"cmd-k cmd-s"`. Uses symbols on macOS (⌘⇧P) and words elsewhere. | Property | Default | Description | | --- | --- | --- | | `keys` | *required* | The shortcut. | | `size` | `"sm"` | `"xs"` or `"sm"`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#kbd) ##### `ui.format_keys` ```python format_keys(keys) ``` Splits a shortcut into labels per chord: `format_keys("ctrl-shift-p") == [["Ctrl", "Shift", "P"]]`. The style is controlled by `pyzed.ui.base.KEY_STYLE` (`"auto"`, `"symbols"` or `"words"`). | Property | Default | Description | | --- | --- | --- | | `keys` | *required* | The shortcut. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#format_keys) ##### `ui.keys_text` ```python keys_text(keys) ``` A shortcut as plain text for a label or a status line: `keys_text("ctrl-shift-p") == "Ctrl+Shift+P"` (`"⌃⇧P"` with macOS symbols), chords separated by a space. Tooltips, buttons and menus take `keys=` instead and draw key caps, so never write the keys into their text. | Property | Default | Description | | --- | --- | --- | | `keys` | *required* | The shortcut. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#keys_text) #### Related - [Keybinding hint](https://albakore.github.io/pyzed/library/keybinding-hint/): A muted “Open file Ctrl P” hint. - [Shortcuts](https://albakore.github.io/pyzed/library/shortcuts/): A searchable list of key bindings, grouped. - [Tooltip](https://albakore.github.io/pyzed/library/tooltip/): A hint shown on hover, optionally with its shortcut. ### Keybinding hint > A muted “Open file Ctrl P” hint. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/keybinding-hint/ :::tip[In plain words] A quiet line that teaches a shortcut: “Open file Ctrl P”. ::: A muted hint that pairs an action with its shortcut, for footers and empty states. `ui.keybinding_hint` · Category: [Feedback](https://albakore.github.io/pyzed/library/#feedback) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#keybinding_hint) **Code:** ```python pz.column([ ui.keybinding_hint("Open file", "ctrl-p"), ui.keybinding_hint("Command palette", "ctrl-shift-p"), ui.keybinding_hint("Toggle terminal", "ctrl-j"), ], spacing=10, align="center") ``` #### When to use - In empty editors and footers, to teach the shortcuts of the most useful actions. #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ ui.keybinding_hint("Open file", "ctrl-p"), ui.keybinding_hint("Command palette", "ctrl-shift-p"), ui.keybinding_hint("Toggle terminal", "ctrl-j"), ], spacing=10, align="center") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Examples ```python ui.keybinding_hint("Search", "/") ``` ```python ui.keybinding_hint("Command palette", "ctrl-shift-p") ``` ```python ui.keybinding_hint("Keyboard shortcuts", "ctrl-k ctrl-s") ``` #### Events Keybinding hint has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.keybinding_hint` ```python keybinding_hint(text, keys, *, theme=None) ``` A muted hint like “Open file Ctrl P” for footers and empty states. | Property | Default | Description | | --- | --- | --- | | `text` | *required* | Text. | | `keys` | *required* | Shortcut shown at the end, written as a string: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#keybinding_hint) #### Related - [Kbd](https://albakore.github.io/pyzed/library/kbd/): Keyboard shortcuts drawn as keys, with symbols on macOS. - [Empty state](https://albakore.github.io/pyzed/library/empty-state/): A centered placeholder for empty lists and searches. - [Shortcuts](https://albakore.github.io/pyzed/library/shortcuts/): A searchable list of key bindings, grouped. ### Label > Text with a theme color, size and weight; `muted` for secondary text. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/label/ :::tip[In plain words] Plain text on the screen, in the colors of the current theme. ::: Text that takes its color from the theme. `color` is a theme token (`"text"`, `"accent"`, `"error"`…) or a hex color, and `size` is a named step (`"xs"` to `"2xl"`) or a number. `ui.muted(...)` is a label with the `"text_muted"` color, for secondary text. `ui.label` · Category: [Display](https://albakore.github.io/pyzed/library/#display) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#label) **Code:** ```python pz.column([ ui.label("Pyzed draws native UIs with Iced."), ui.muted("Secondary text uses the muted color."), ui.label("Accent and semibold", color="accent", weight="semibold"), ui.label("def mono(): ...", mono=True, size="sm"), ], spacing=8) ``` #### When to use - For any text in a themed UI: it follows theme changes, unlike a hard-coded `pz.text`. - Use `ui.muted` for descriptions, hints and metadata. - For titles use [Heading](https://albakore.github.io/pyzed/library/heading/). #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ ui.label("Pyzed draws native UIs with Iced."), ui.muted("Secondary text uses the muted color."), ui.label("Accent and semibold", color="accent", weight="semibold"), ui.label("def mono(): ...", mono=True, size="sm"), ], spacing=8) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Size Named sizes follow the theme's font size; a number is a size in pixels. ```python ui.label("Extra small", size="xs") ``` ```python ui.label("Small", size="sm") ``` ```python ui.label("Medium", size="md") ``` ```python ui.label("Large", size="lg") ``` ```python ui.label("Extra large", size="xl") ``` ```python ui.label("Huge", size="2xl") ``` ##### Color ```python ui.label("Text") ``` ```python ui.muted("Muted text") ``` ```python ui.label("Accent", color="accent") ``` ```python ui.label("Success", color="success") ``` ```python ui.label("Warning", color="warning") ``` ```python ui.label("Error", color="error") ``` ```python ui.label("Any hex color", color="#c678dd") ``` ##### Weight and style ```python ui.label("Normal", weight="normal", size="lg") ``` ```python ui.label("Medium", weight="medium", size="lg") ``` ```python ui.label("Semibold", weight="semibold", size="lg") ``` ```python ui.label("Bold", weight="bold", size="lg") ``` ```python ui.label("Italic", italic=True, size="lg") ``` ```python ui.label("def mono(): ...", mono=True) ``` #### Events Label has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.label` ```python label( content, *, color='text', size='md', weight=None, mono=False, italic=False, wrap=True, theme=None, **kwargs, ) ``` Text with a theme color. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `color` | `"text"` | Theme token or hex color. Defaults to `"text"`. | | `size` | `"md"` | `"xs"` (11), `"sm"` (12), `"md"` (13.5), `"lg"` (16), `"xl"` (20), `"2xl"` (26) or a number. | | `weight` | `None` | Weight: `"medium"`, `"semibold"`, `"bold"`… | | `mono` | `False` | Uses the monospace font (IBM Plex Mono). | | `italic` | `False` | Italic. | | `wrap` | `True` | Line wrapping. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | | `**kwargs` | — | Passed to `pyzed.text`. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#label) ##### `ui.muted` ```python muted(content, **kwargs) ``` A `label` with the `"text_muted"` color. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `**kwargs` | — | Same as `label`. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#muted) #### Related - [Heading](https://albakore.github.io/pyzed/library/heading/): Semibold titles in three levels. - [Kbd](https://albakore.github.io/pyzed/library/kbd/): Keyboard shortcuts drawn as keys, with symbols on macOS. - [Badge](https://albakore.github.io/pyzed/library/badge/): A small pill for statuses, versions and counters. ### Layer > Draws an overlay over your view; clicking outside dismisses it. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/layer/ :::tip[In plain words] Puts something on top of the screen, like a search box, and closes it if you click outside. ::: Stacks `overlay` on top of `base`. Clicking outside the overlay sends `on_dismiss`, and `backdrop` dims the view behind it. With `overlay=None` it returns `base` unchanged, so it can always wrap your view. `align_x`, `align_y` and `offset` place the overlay (by default centered at the top, like a command palette). `ui.layer` · Category: [Overlays](https://albakore.github.io/pyzed/library/#overlays) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#layer) **Code:** ```python ui.layer( main_view(), ui.picker("", on_query, [(ui.PaletteItem("notes.py", on_open, icon="file-code"), []), (ui.PaletteItem("README.md", on_open, icon="file-text"), [])], placeholder="Search files…", width=420), on_dismiss=on_close, backdrop="#00000055", offset=40, ) ``` #### When to use - To show palettes, pickers, modals and menus above everything else. - For dialogs, [Dialog layer](https://albakore.github.io/pyzed/library/dialog-layer/) already centers them over a dimmed backdrop. #### Usage ```python import pyzed as pz from pyzed import ui class App(pz.App): def __init__(self): self.finder_open = False def view(self): overlay = self.finder() if self.finder_open else None return ui.layer(self.main_view(), overlay, on_dismiss=lambda: setattr(self, "finder_open", False)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Placement ```python ui.layer(main_view(), ui.card(ui.label("Overlay"), width=200), on_dismiss=on_close) ``` ```python ui.layer(main_view(), ui.card(ui.label("Overlay"), width=200), align_y="center", on_dismiss=on_close) ``` ```python ui.layer(main_view(), ui.card(ui.label("Overlay"), width=200), align_x="end", align_y="end", offset=16, on_dismiss=on_close) ``` ##### Backdrop With `overlay=None` the layer returns your view unchanged. ```python ui.layer(main_view(), ui.card(ui.label("Overlay"), width=200), backdrop="#00000088", align_y="center", on_dismiss=on_close) ``` ```python ui.layer(main_view(), None) ``` #### Events Layer has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_dismiss` | `()` | Message or callable sent on close. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class App(pz.App): def __init__(self): self.finder_open = True def view(self): finder = ui.card(ui.muted("Search files…"), width=320) return ui.layer(main_view(), finder if self.finder_open else None, on_dismiss=lambda: setattr(self, "finder_open", False)) # click outside ``` **Declarative:** ```python class App(pz.App): finder_open = True def update(self, message): match message: case "open finder": self.finder_open = True case "close finder": self.finder_open = False def view(self): overlay = ui.card(ui.muted("Search files…"), width=320) if self.finder_open else None return ui.layer(main_view(), overlay, on_dismiss="close finder") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: finder_open: bool = True def view(state, set_state): overlay = ui.card(ui.muted("Search files…"), width=320) if state.finder_open else None return ui.layer(main_view(), overlay, on_dismiss=lambda: set_state(finder_open=False)) ``` #### Properties and API ##### `ui.layer` ```python layer( base, overlay=None, *, on_dismiss=None, backdrop=None, align_x='center', align_y='start', offset=72, ) ``` Stacks `overlay` on top of `base`. Clicking outside sends `on_dismiss`. With `overlay=None` it returns `base` unchanged. | Property | Default | Description | | --- | --- | --- | | `base` | *required* | The UI underneath. | | `overlay` | `None` | What floats on top (modal, palette, menu). | | `backdrop` | `None` | Color of the backdrop that dims the UI. | | `align_x` | `"center"` | Horizontal alignment: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `align_y` | `"start"` | Vertical alignment: `"start"`/`"top"`, `"center"`, `"end"`/`"bottom"`. | | `offset` | `72` | Distance from the top when `align_y` is `"start"`. | Events: `on_dismiss` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#layer) #### Related - [Modal](https://albakore.github.io/pyzed/library/modal/): A dialog surface with title, content and footer buttons. - [Popover](https://albakore.github.io/pyzed/library/popover/): Floating content at a point: context menus, color pickers. - [Dialog layer](https://albakore.github.io/pyzed/library/dialog-layer/): Centers a dialog over your view with a dimmed backdrop. ### List header > A small section title for lists, with an end slot. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/list-header/ :::tip[In plain words] A small title above a group of rows. ::: A muted section header for lists, with an optional widget at the end (usually an icon button). `ui.list_header` · Category: [Lists and menus](https://albakore.github.io/pyzed/library/#lists-and-menus) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#list_header) **Code:** ```python pz.column([ ui.list_header("RECENT", end=ui.icon_button("plus", on_new, size="xs")), ui.list_item("main.py", on_open, icon="file-code", inset=True), ui.list_item("app.py", on_open, icon="file-code", inset=True), ui.list_header("PINNED"), ui.list_item("README.md", on_open, icon="file-text", inset=True), ], width=300) ``` #### When to use - To split a list into groups (RECENT, PINNED) or to add an action to a list. #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ ui.list_header("RECENT", end=ui.icon_button("plus", on_new, size="xs")), ui.list_item("main.py", on_open, icon="file-code", inset=True), ui.list_item("app.py", on_open, icon="file-code", inset=True), ui.list_header("PINNED"), ui.list_item("README.md", on_open, icon="file-text", inset=True), ], width=300) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python pz.container(ui.list_header("RECENT"), width=240) ``` ```python pz.container(ui.list_header("PINNED", end=ui.icon_button("plus", on_new, size="xs")), width=240) ``` ```python pz.container(ui.list_header("CHANGES", end=ui.count_badge(4)), width=240) ``` #### Events List header has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.list_header` ```python list_header(title, *, end=None, theme=None) ``` A section header for lists. | Property | Default | Description | | --- | --- | --- | | `title` | *required* | Title. | | `end` | `None` | Widgets at the end (right). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#list_header) #### Related - [List item](https://albakore.github.io/pyzed/library/list-item/): A list row with icon, text and an end slot. - [Panel header](https://albakore.github.io/pyzed/library/panel-header/): Icon, title and actions on top of a dock. ### List item > A list row with icon, text and an end slot. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/list-item/ :::tip[In plain words] One row of a list: an icon, a name and maybe something on the right. ::: A list row: an icon, the text and `start`/`end` slots for any widget. It supports `selected`, `focused` (focus border), `disabled`, nesting with `indent`, and `on_right_press` for a context menu. `ui.list_item` · Category: [Lists and menus](https://albakore.github.io/pyzed/library/#lists-and-menus) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#list_item) **Code:** ```python pz.column([ ui.list_item("main.py", on_open, icon="file-code", icon_color="#3572a5", selected=True, inset=True), ui.list_item("README.md", on_open, icon="file-text", inset=True, end=ui.badge("M", tint="warning")), ui.list_item("Cargo.toml", on_open, icon="file-cog", inset=True, end=ui.kbd("ctrl-1", size="xs")), ui.list_item("old.txt", on_open, icon="file", inset=True, disabled=True), ], width=320) ``` #### When to use - For any vertical list: recent files, search results, settings categories, people. - Use `inset=True` inside panels so the highlight has a margin. #### Usage ```python import pyzed as pz from pyzed import ui class Recent(pz.App): def __init__(self): self.files = ["main.py", "app.py", "README.md"] self.current = "main.py" def view(self): return pz.column([ ui.list_item(f, lambda f=f: setattr(self, "current", f), icon="file", selected=f == self.current, inset=True) for f in self.files ]) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content ```python pz.container(ui.list_item("README.md", on_open), width=240) ``` ```python pz.container(ui.list_item("README.md", on_open, icon="file-text"), width=240) ``` ```python pz.container(ui.list_item("main.py", on_open, icon="file-code", icon_color="#3572a5"), width=240) ``` ```python pz.container(ui.list_item("app.py", on_open, icon="file-code", end=ui.badge("M", tint="warning")), width=240) ``` ```python pz.container(ui.list_item("Buy milk", on_open, start=ui.checkbox(True, on_toggle)), width=240) ``` ```python pz.container(ui.list_item("Delete project", on_open, icon="trash-2", color="error"), width=240) ``` ##### State ```python pz.container(ui.list_item("main.py", on_open, icon="file-code", selected=True), width=240) ``` ```python pz.container(ui.list_item("main.py", on_open, icon="file-code", focused=True), width=240) ``` ```python pz.container(ui.list_item("old.txt", on_open, icon="file", disabled=True), width=240) ``` ```python pz.container(ui.list_item("main.py", on_open, icon="file-code", selected=True, inset=True), width=240) ``` ```python pz.container(ui.list_item("button.rs", on_open, icon="file-code", indent=24), width=240) ``` ##### States and events `on_right_press` opens a context menu and `tooltip=` shows on hover. ```python pz.container(ui.list_item("main.py", on_press, icon="file", description="src/app"), width=300) ``` ```python pz.container(ui.list_item("Rename me", on_press, icon="pencil", on_double_press=on_rename), width=300) ``` #### Events List item has **3 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_press` | `()` | Message or callable sent on press. `None` disables the control. | | `on_right_press` | `()` | Right click. | | `on_double_press` | `()` | Sent on a double click (open, rename…). | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Recent(pz.App): def __init__(self): self.files = ["main.py", "app.py", "README.md"] self.current = "main.py" self.opened = None def view(self): return pz.column([ ui.list_item(f, lambda f=f: setattr(self, "current", f), icon="file", # on_press selected=f == self.current, inset=True, on_double_press=lambda f=f: setattr(self, "opened", f), on_right_press=lambda f=f: print("menu for", f)) for f in self.files ], width=280) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Select: path: str @dataclass class Open: path: str class Recent(pz.App): files = ["main.py", "app.py", "README.md"] current, opened = "main.py", None def update(self, message): match message: case Select(path): self.current = path case Open(path): self.opened = path def view(self): return pz.column([ui.list_item(f, Select(f), icon="file", selected=f == self.current, on_double_press=Open(f)) for f in self.files], width=280) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: files: tuple = ("main.py", "app.py", "README.md") current: str = "main.py" def file_row(name, selected, select): return ui.list_item(name, select, icon="file", selected=selected, inset=True) def view(state, set_state): return pz.column([file_row(f, f == state.current, lambda f=f: set_state(current=f)) for f in state.files], width=280) ``` #### Properties and API ##### `ui.list_item` ```python list_item( content, on_press=None, *, icon=None, icon_color=None, start=None, end=None, selected=False, focused=False, indent=0, height=26, color=None, inset=False, disabled=False, on_right_press=None, on_double_press=None, description=None, tooltip=None, theme=None, ) ``` A list row: icon, text and a slot at the end. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Text or widget. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `icon_color` | `None` | Icon color: a theme token or a hex color. | | `start` | `None` | Widgets at the start (left). | | `end` | `None` | Widgets at the end (right). | | `selected` | `False` | Marks it as selected. | | `focused` | `False` | Draws a focus border. | | `indent` | `0` | Left indentation, in pixels. | | `height` | `26` | Row height. | | `color` | `None` | Text color. | | `inset` | `False` | With margin and rounded corners (menus, palettes). | | `disabled` | `False` | Disables it (ignores clicks). | | `description` | `None` | A muted second line (the row grows to 40 px unless `height` is set). | | `tooltip` | `None` | Text shown on hover. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_press`, `on_right_press`, `on_double_press` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#list_item) #### Related - [List header](https://albakore.github.io/pyzed/library/list-header/): A small section title for lists, with an end slot. - [Tree](https://albakore.github.io/pyzed/library/tree/): A collapsible file tree with icons and git colors. - [Menu](https://albakore.github.io/pyzed/library/menu/): A context menu with icons, shortcuts and checks. ### Markdown > Renders Markdown with the theme's colors. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/markdown/ :::tip[In plain words] Shows formatted text (titles, lists, links) written in the simple Markdown format. ::: Renders Markdown (headings, lists, code blocks, quotes, tables, links) with the theme's text, link and code colors. `on_link(url)` receives clicked links. `pz.markdown` is the base widget with explicit colors. `ui.markdown` · Category: [Code and tools](https://albakore.github.io/pyzed/library/#code-and-tools) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#markdown) **Code:** ```python ui.markdown('''## Release notes Pyzed **0.4** adds the *component designer*, `pyzed build` and [styles](https://iced.rs). - Track grids and flex - Ready-made dialogs ''', width=460, on_link=on_link) ``` #### When to use - For READMEs, release notes, help pages and chat messages. #### Usage ```python from pyzed import ui ui.markdown('''## Release notes Pyzed **0.4** adds the *component designer*, `pyzed build` and [styles](https://iced.rs). - Track grids and flex - Ready-made dialogs ''', width=460, on_link=on_link) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Elements ```python ui.markdown("## Title\nSome **bold** and *italic* text.", width=320) ``` ```python ui.markdown("- Fast\n- Native\n- Pythonic\n\n1. Install\n2. Run", width=320) ``` ```python ui.markdown("Run `pyzed new`:\n\n```python\nimport pyzed as pz\n```", width=320) ``` ```python ui.markdown("> Desktop UIs in Python.\n\nSee [the docs](https://albakore.github.io/pyzed/).", width=320, on_link=on_link) ``` ```python ui.markdown("| Key | Action |\n| --- | --- |\n| ctrl-s | Save |\n| ctrl-p | Open |", width=320) ``` #### Events Markdown has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_link` | `ui.markdown`, `pz.markdown` | `(url)` | Callable that receives the URL when a link is clicked. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python import webbrowser class Notes(pz.App): def __init__(self): self.last = None def follow(self, url): # on_link: the URL of the clicked link self.last = url webbrowser.open(url) def view(self): return ui.markdown("Read the [docs](https://albakore.github.io/pyzed/) or the " "[changelog](https://github.com/albakore/pyzed).", on_link=self.follow) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Link: url: str class Notes(pz.App): def update(self, message): match message: case Link(url) if url.startswith("app:"): print("internal page", url[4:]) # links can drive the app, too case Link(url): print("open", url) def view(self): return ui.markdown("Go to [settings](app:settings) or the [docs](https://albakore.github.io/pyzed/).", on_link=Link) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: page: str = "home" PAGES = {"home": "Welcome. See [about](about).", "about": "Made with Pyzed. Back [home](home)."} def view(state, set_state): text = PAGES.get(state.page, "Not found. Back [home](home).") return ui.markdown(text, on_link=lambda url: set_state(page=url)) ``` #### Properties and API ##### `ui.markdown` ```python markdown(source, *, size=None, on_link=None, width=None, selectable=None, theme=None) ``` `pz.markdown` with the theme's text, link and code colors. | Property | Default | Description | | --- | --- | --- | | `source` | *required* | The Markdown text. | | `size` | `None` | Size. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `selectable` | `None` | Same as in `pz.markdown`; the selection uses the theme's `selection` color. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_link` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#markdown) ##### `pz.markdown` ```python markdown( source, *, size=14.0, color=None, link_color=None, code_color=None, code_bg=None, on_link=None, width=None, selectable=None, selection_color=None, ) ``` Renders Markdown (headings, lists, code blocks, quotes, tables, links…). For the theme colors use `ui.markdown`. | Property | Default | Description | | --- | --- | --- | | `source` | *required* | The Markdown text. | | `size` | `14.0` | Size. | | `color` | `None` | Text or icon color. | | `link_color` | `None` | Link color. | | `code_color` | `None` | Code color. | | `code_bg` | `None` | Code background. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `selectable` | `None` | `True` lets the mouse select the text (drag, double-click a word, triple-click a block) and Ctrl+C copy it. A string names a selection group: the texts and Markdown of one group form a single selection, in the order they are drawn (the lines of a log, the messages of a chat). | | `selection_color` | `None` | Color behind the selected text. | Events: `on_link` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#markdown) #### Related - [Rich text](https://albakore.github.io/pyzed/library/rich-text/): One block of text mixing colors, weights and links. - [Text](https://albakore.github.io/pyzed/library/text/): A piece of text with size, color, font and weight. ### Menu > A context menu with icons, shortcuts and checks. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/menu/ :::tip[In plain words] The list of options that appears when you right-click or press a “more” button. ::: A floating context menu. Items are `ui.MenuItem(label, on_press, icon=, keys=, checked=, danger=, disabled=)`; `ui.SEPARATOR` draws a line between groups and a plain string is a header. Show it with a [Popover](https://albakore.github.io/pyzed/library/popover/) or a [Layer](https://albakore.github.io/pyzed/library/layer/). `ui.menu` · Category: [Lists and menus](https://albakore.github.io/pyzed/library/#lists-and-menus) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#menu) **Code:** ```python ui.menu([ ui.MenuItem("New file", on_new, icon="file-plus", keys="ctrl-n"), ui.MenuItem("New folder", on_new_folder, icon="folder-plus"), ui.SEPARATOR, "EDIT", ui.MenuItem("Copy path", on_copy, keys="ctrl-alt-c"), ui.MenuItem("Soft wrap", on_wrap, checked=True), ui.SEPARATOR, ui.MenuItem("Delete", on_delete, icon="trash-2", danger=True), ]) ``` #### When to use - For right-click menus and the options of a split button or an ellipsis button. - Mark destructive actions with `danger=True` and put them last. #### Usage ```python from pyzed import ui ui.menu([ ui.MenuItem("New file", on_new, icon="file-plus", keys="ctrl-n"), ui.MenuItem("New folder", on_new_folder, icon="folder-plus"), ui.SEPARATOR, "EDIT", ui.MenuItem("Copy path", on_copy, keys="ctrl-alt-c"), ui.MenuItem("Soft wrap", on_wrap, checked=True), ui.SEPARATOR, ui.MenuItem("Delete", on_delete, icon="trash-2", danger=True), ]) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Items ```python ui.menu([ui.MenuItem("Open", on_open), ui.MenuItem("Rename", on_rename), ui.MenuItem("Duplicate", on_duplicate)], width=200) ``` ```python ui.menu([ui.MenuItem("Copy", on_copy, icon="copy", keys="ctrl-c"), ui.MenuItem("Paste", on_paste, icon="clipboard", keys="ctrl-v")], width=220) ``` ```python ui.menu([ui.MenuItem("Soft wrap", on_wrap, checked=True), ui.MenuItem("Minimap", on_minimap, checked=False)], width=200) ``` ```python ui.menu([ui.MenuItem("Rename", on_rename), ui.SEPARATOR, ui.MenuItem("Delete", on_delete, icon="trash-2", danger=True)], width=200) ``` ```python ui.menu([ui.MenuItem("Undo", on_undo, keys="ctrl-z"), ui.MenuItem("Redo", on_redo, keys="ctrl-shift-z", disabled=True)], width=220) ``` ##### Groups ```python ui.menu(["FILE", ui.MenuItem("New", on_new, keys="ctrl-n"), ui.SEPARATOR, "EDIT", ui.MenuItem("Find", on_find, keys="ctrl-f")], width=220) ``` ##### States and events ```python ui.menu([ui.MenuItem("Share", on_press, icon="share", submenu=True), ui.MenuItem("Rename", on_press, description="F2 also works")]) ``` ```python ui.menu([ui.MenuItem(f"Recent {i}", on_press) for i in range(10)], max_height=140) ``` #### Events Menu has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_press` | `()` | Message or callable sent on press. `None` disables the control. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Files(pz.App): def __init__(self): self.wrap = True self.log = [] def view(self): return ui.menu([ ui.MenuItem("New file", lambda: self.log.append("new"), icon="file-plus", keys="ctrl-n"), ui.MenuItem("Soft wrap", lambda: setattr(self, "wrap", not self.wrap), checked=self.wrap), ui.SEPARATOR, ui.MenuItem("Delete", lambda: self.log.append("delete"), icon="trash-2", danger=True), ]) ``` **Declarative:** ```python class Files(pz.App): wrap = True def update(self, message): # each item's on_press is a message match message: case "new": print("new file") case "wrap": self.wrap = not self.wrap def view(self): return ui.menu([ui.MenuItem("New file", "new", icon="file-plus", keys="ctrl-n"), ui.MenuItem("Soft wrap", "wrap", checked=self.wrap)]) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: wrap: bool = True def view(state, set_state): return ui.menu([ ui.MenuItem("Soft wrap", lambda: set_state(wrap=not state.wrap), checked=state.wrap), ui.MenuItem("Delete", None, icon="trash-2", danger=True, disabled=True), ]) ``` #### Properties and API ##### `ui.menu` ```python menu(items, *, width=240, max_height=None, theme=None) ``` A floating context menu. Use `ui.SEPARATOR` between groups; a plain string is a header. | Property | Default | Description | | --- | --- | --- | | `items` | *required* | List of `MenuItem`, `ui.SEPARATOR` or strings. | | `width` | `240` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `max_height` | `None` | Scrolls a menu taller than this. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#menu) ##### `ui.MenuItem` ```python @dataclass class MenuItem(label, on_press=None, icon=None, keys=None, disabled=False, danger=False, checked=None, description=None, submenu=False) ``` An entry of `ui.menu`. | Property | Default | Description | | --- | --- | --- | | `label` | *required* | Label text. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `keys` | `None` | Shortcut shown at the end, written as a string: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `disabled` | `False` | Disables it (ignores clicks). | | `danger` | `False` | In red (destructive actions). | | `checked` | `None` | Shows a check mark (`True`/`False`) or nothing (`None`). | | `description` | `None` | A muted second line. | | `submenu` | `False` | Ends the row with a chevron; open the next menu beside it in `on_press`. | Events: `on_press` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#menuitem) ##### `ui.SEPARATOR` Separator for `ui.menu`. [Full reference →](https://albakore.github.io/pyzed/reference/ui/#separator) #### Related - [Popover](https://albakore.github.io/pyzed/library/popover/): Floating content at a point: context menus, color pickers. - [Split button](https://albakore.github.io/pyzed/library/split-button/): A main action plus an arrow that opens more options. - [List item](https://albakore.github.io/pyzed/library/list-item/): A list row with icon, text and an end slot. ### Modal > A dialog surface with title, content and footer buttons. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/modal/ :::tip[In plain words] A window inside the window that asks for your attention before you go on. ::: A dialog surface with a title, a close button, your content and a footer of buttons. Use it as the overlay of `ui.layer` or `ui.dialog_layer`. The ready-made [dialogs](https://albakore.github.io/pyzed/library/confirm/) are built on it. `ui.modal` · Category: [Overlays](https://albakore.github.io/pyzed/library/#overlays) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#modal) **Code:** ```python ui.modal( ui.muted("notes.py has unsaved changes. Save them before closing?"), title="Save changes?", on_close=on_close, footer=[ui.button("Don't save", on_discard, variant="ghost"), ui.button("Cancel", on_close), ui.button("Save", on_save, variant="solid")], width=440, ) ``` #### When to use - For dialogs with your own content: a form, settings, an About box. #### Usage ```python from pyzed import ui ui.modal( ui.muted("notes.py has unsaved changes. Save them before closing?"), title="Save changes?", on_close=on_close, footer=[ui.button("Don't save", on_discard, variant="ghost"), ui.button("Cancel", on_close), ui.button("Save", on_save, variant="solid")], width=440, ) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content ```python ui.modal(ui.muted("Your trial ends in 3 days."), width=320) ``` ```python ui.modal(ui.muted("Your trial ends in 3 days."), title="Trial", on_close=on_close, width=320) ``` ```python ui.modal(ui.muted("Discard the draft?"), title="Draft", on_close=on_close, width=320, footer=[ui.button("Cancel", on_close), ui.button("Discard", on_discard, variant="error")]) ``` ##### States and events `max_height=` scrolls long content, and Escape sends `on_close` while the dialog has focus. ```python ui.modal(ui.input("notes.md", on_input), title="Rename", description="Choose a new name", icon="pencil", on_close=on_close, width="sm", footer=[ui.button("Rename", on_press, variant="solid")]) ``` #### Examples ##### Over the app Wrap the view with `ui.dialog_layer` to center it over a dimmed backdrop. **Code:** ```python ui.dialog_layer(main_view(), ui.modal( pz.column([ui.input("", on_input, placeholder="Project name"), ui.checkbox(True, on_toggle, label="Create a git repository")], spacing=10), title="New project", on_close=on_close, footer=[ui.button("Cancel", on_close), ui.button("Create", on_create, variant="solid")], width=380, ), on_dismiss=on_close) ``` #### Events Modal has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_close` | `()` | The close button; also sent by Escape while the dialog has focus. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Notes(pz.App): def __init__(self): self.asking = True def save(self): self.asking = False def view(self): dialog = ui.modal(ui.muted("Save your changes before closing?"), title="Save changes?", on_close=lambda: setattr(self, "asking", False), # ✕ and Escape footer=[ui.button("Save", self.save, variant="solid")], width=420) return ui.dialog_layer(main_view(), dialog if self.asking else None) ``` **Declarative:** ```python class Notes(pz.App): asking = True def update(self, message): if message in ("save", "close"): self.asking = False def view(self): dialog = ui.modal(ui.muted("Save your changes?"), title="Save changes?", on_close="close", footer=[ui.button("Save", "save", variant="solid")]) return ui.dialog_layer(main_view(), dialog if self.asking else None) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: asking: bool = True def view(state, set_state): close = lambda: set_state(asking=False) dialog = ui.modal(ui.muted("Save your changes?"), title="Save changes?", on_close=close, footer=[ui.button("Save", close, variant="solid")]) return ui.dialog_layer(main_view(), dialog if state.asking else None) ``` #### Properties and API ##### `ui.modal` ```python modal( content, *, title=None, on_close=None, width=480, footer=(), description=None, icon=None, max_height=None, theme=None, ) ``` A dialog. Use it as the `overlay` of `ui.layer`. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `title` | `None` | Title. | | `width` | `480` | Pixels or `"sm"` (360), `"md"` (480), `"lg"` (640) or `"xl"` (820). | | `footer` | `()` | Footer buttons, aligned to the right. | | `description` | `None` | Muted line under the title. | | `icon` | `None` | Icon before the title. | | `max_height` | `None` | Scrolls content taller than this, keeping the header and footer in view. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_close` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#modal) #### Related - [Dialog layer](https://albakore.github.io/pyzed/library/dialog-layer/): Centers a dialog over your view with a dimmed backdrop. - [Layer](https://albakore.github.io/pyzed/library/layer/): Draws an overlay over your view; clicking outside dismisses it. - [Confirm](https://albakore.github.io/pyzed/library/confirm/): A yes/no question, red for destructive actions. ### Mouse area > Makes any widget react to clicks, hover, drags and the wheel. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/mouse-area/ :::tip[In plain words] An invisible layer that lets anything (a picture, a card, a drawing) notice clicks and the mouse. ::: `pz.mouse_area` wraps any widget and reports what the mouse does over it: presses (left, right, middle, double click), entering and leaving, moving (`on_move(x, y)`) and the wheel (`on_scroll(dx, dy)`); `cursor` changes the pointer. `pz.hover(base, top)` shows `top` over `base` only while the pointer is over it, and `pz.opaque` stops clicks from reaching what is under a widget in a `stack`. `pz.mouse_area` · Category: [Actions](https://albakore.github.io/pyzed/library/#actions) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#mouse_area) **Code:** ```python pz.mouse_area( ui.card(pz.row([pz.icon("folder", size=20), ui.label("Open project…")], spacing=10, align="center"), width=280), on_press=on_open, on_right_press=on_menu, cursor="pointer", ) ``` #### When to use - To make a card, an image or a custom drawing clickable without turning it into a button. - For right-click menus (`on_right_press`), drag gestures (`on_move`) and zoom with the wheel. - `pz.hover` for actions that appear on hover, like the close button of a tab. #### Usage ```python import pyzed as pz class Board(pz.App): def __init__(self): self.pointer = (0, 0) self.zoom = 1.0 def view(self): return pz.mouse_area( self.drawing(), on_move=lambda x, y: setattr(self, "pointer", (x, y)), on_scroll=lambda dx, dy: setattr(self, "zoom", max(0.1, self.zoom + dy * 0.1)), on_double_click=self.reset, cursor="crosshair", ) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Cursor The pointer shape is only visible with a real mouse; the shot shows the wrapped widget. ```python pz.mouse_area(ui.card(ui.label("Click me"), width=180), on_press=on_open, cursor="pointer") ``` ```python pz.mouse_area(ui.card(ui.label("Drag me"), width=180), on_move=on_drag, cursor="grab") ``` ```python pz.mouse_area(pz.canvas([draw.circle(60, 40, 24, fill="#74ade8")], width=120, height=80), on_press=on_pick, cursor="crosshair") ``` ##### Hover content `pz.hover` only draws its `top` while the pointer is over it: here, the close button of a row. ```python pz.hover(ui.list_item("notes.py", on_open, icon="file-text"), pz.container(ui.icon_button("x", on_close, size="xs"), align_x="end", width="fill")) ``` ```python pz.hover(ui.list_item("notes.py", on_open, icon="file-text"), pz.container(ui.icon_button("x", on_close, size="xs"), align_x="end", width="fill")) ``` #### Events Mouse area has **10 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_press` | `()` | Left click (on press). | | `on_release` | `()` | On releasing the left button. | | `on_right_press` | `()` | Right click (context menus). | | `on_double_click` | `()` | Double click. | | `on_enter` | `()` | The mouse enters. | | `on_exit` | `()` | The mouse leaves. | | `on_move` | `(x, y)` | Called with `(x, y)` when the mouse moves. | | `on_scroll` | `(dx, dy)` | Callable that receives `(dx, dy)` in lines when the wheel is used. | | `on_middle_press` | `()` | Middle-button click. | | `on_middle_release` | `()` | Middle button released (pan gestures, for example). | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Board(pz.App): def __init__(self): self.pointer, self.hovering, self.zoom = (0, 0), False, 1.0 def view(self): return pz.mouse_area( ui.card(ui.label(f"{self.pointer} · zoom {self.zoom:.1f}"), width=320), on_enter=lambda: setattr(self, "hovering", True), on_exit=lambda: setattr(self, "hovering", False), on_move=lambda x, y: setattr(self, "pointer", (round(x), round(y))), on_scroll=lambda dx, dy: setattr(self, "zoom", max(0.1, self.zoom + dy * 0.1)), on_double_click=lambda: setattr(self, "zoom", 1.0), on_right_press=lambda: print("context menu"), cursor="crosshair", ) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Moved: x: float y: float class Board(pz.App): pointer = (0, 0) def update(self, message): match message: case Moved(x, y): self.pointer = (round(x), round(y)) case "reset": self.pointer = (0, 0) def view(self): return pz.mouse_area(ui.card(ui.label(str(self.pointer)), width=320), on_move=Moved, on_double_click="reset") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: hovering: bool = False def view(state, set_state): return pz.mouse_area(ui.card(ui.label("Hovering" if state.hovering else "Point at me"), width=320), on_enter=lambda: set_state(hovering=True), on_exit=lambda: set_state(hovering=False)) ``` #### Tips :::tip Hover and pointer states only exist with a real mouse, so the previews draw them as they look; see the animated clips in [Showcase](https://albakore.github.io/pyzed/showcase/) for the real thing. ::: #### Properties and API ##### `pz.mouse_area` ```python mouse_area( content, *, on_press=None, on_release=None, on_right_press=None, on_double_click=None, on_enter=None, on_exit=None, on_move=None, on_scroll=None, on_middle_press=None, on_middle_release=None, cursor=None, ) ``` Detects mouse events over its content. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `cursor` | `None` | `"pointer"`, `"text"`, `"grab"`, `"grabbing"`, `"crosshair"`, `"not-allowed"`, `"resize-horizontal"`, `"resize-vertical"`, `"move"` or `"default"`. | Events: `on_press`, `on_release`, `on_right_press`, `on_double_click`, `on_enter`, `on_exit`, `on_move`, `on_scroll`, `on_middle_press`, `on_middle_release` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#mouse_area) ##### `pz.hover` ```python hover(base, top) ``` Shows `top` over `base` only while the mouse is over it (for example, a tab's close button). | Property | Default | Description | | --- | --- | --- | | `base` | *required* | Widget underneath. | | `top` | *required* | Widget that appears on hover. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#hover) ##### `pz.opaque` ```python opaque(content) ``` Makes a widget block the mouse for whatever is below it in a `stack`. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#opaque) #### Related - [Button](https://albakore.github.io/pyzed/library/button/): The themed button: variants, sizes, icons and shortcuts. - [Popover](https://albakore.github.io/pyzed/library/popover/): Floating content at a point: context menus, color pickers. - [Stack](https://albakore.github.io/pyzed/library/stack/): Layers children on top of each other; `pin` places one at x, y. ### Number field > A number with −/+ buttons, limits, step and suffix. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/number-field/ :::tip[In plain words] A box with a number and − / + buttons to make it smaller or bigger. ::: A numeric field with − and + buttons. You can also type a value: `on_change` receives the number already clamped to `min`/`max`, and text that is not a number is ignored. `ui.number_field` · Category: [Forms](https://albakore.github.io/pyzed/library/#forms) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#number_field) **Code:** ```python pz.row([ ui.number_field(14, on_change, min=8, max=32, suffix="px"), ui.number_field(4, on_change, min=1, max=8), ui.number_field(0.5, on_change, step=0.1, suffix="s", width=140), ], spacing=12) ``` #### When to use - For exact values with sensible limits: font size, tab width, port, retries. - Use `suffix` for units (`"px"`, `"ms"`, `"%"`). #### Usage ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.font_size = 14 def view(self): return ui.number_field(self.font_size, lambda v: setattr(self, "font_size", v), min=8, max=32, suffix="px") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python ui.number_field(4, on_change, min=1, max=8) ``` ```python ui.number_field(14, on_change, min=8, max=32, suffix="px") ``` ```python ui.number_field(0.5, on_change, step=0.1, suffix="s", width=140) ``` ```python ui.number_field(8080, on_change, min=1, max=65535, align="start", width=140) ``` ##### Limits At a limit its button is dimmed and the value can't go further. ```python ui.number_field(1, on_change, min=1, max=8) ``` ```python ui.number_field(8, on_change, min=1, max=8) ``` ##### States and events Once clicked, ↑/↓ step it (Shift: ten steps) and Home/End jump to `min`/`max`. ```python ui.number_field(9.5, on_change, step=0.5, prefix="$", decimals=2, width=150) ``` ```python ui.number_field(70000, on_change, invalid=True, width=150) ``` ```python ui.number_field(4, on_change, min=1, max=8, disabled=True) ``` #### Events Number field has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_change` | `(value)` | Callable that receives the new value. | | `on_submit` | `()` | Sent on Enter. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Settings(pz.App): def __init__(self): self.font_size = 14 self.applied = 14 def view(self): return pz.row([ ui.number_field(self.font_size, lambda v: setattr(self, "font_size", v), # on_change min=8, max=32, suffix="px", on_submit=lambda: setattr(self, "applied", self.font_size)), # Enter pz.text("Preview", size=self.applied), ], spacing=12, align="center") ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class FontSize: value: float class Settings(pz.App): font_size = 14 def update(self, message): match message: case FontSize(value): self.font_size = value def view(self): return ui.number_field(self.font_size, FontSize, min=8, max=32, suffix="px") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: font_size: float = 14 def view(state, set_state): return pz.row([ui.number_field(state.font_size, lambda v: set_state(font_size=v), min=8, max=32), pz.text("Preview", size=state.font_size)], spacing=12, align="center") ``` #### Properties and API ##### `ui.number_field` ```python number_field( value, on_change=None, *, min=None, max=None, step=1, width=120, suffix=None, prefix=None, align='center', decimals=None, disabled=False, invalid=False, id=None, on_submit=None, steppers=True, drag=True, drag_step=None, theme=None, ) ``` A numeric field with −/+ buttons that you can also drag, like Figma's. `on_change(value)` receives the number already clamped to `min`/`max`; anything that can't be parsed as a number is ignored. Press and move the mouse sideways over the number to change it (it keeps going outside the window); double-click it to type a value, and Enter, Escape or a click outside to finish. Once clicked, ↑/↓ step it (Shift: ten steps) and Home/End jump to `min`/`max`. | Property | Default | Description | | --- | --- | --- | | `value` | *required* | Current value. | | `min` | `None` | Minimum. | | `max` | `None` | Maximum. | | `step` | `1` | Button step. | | `width` | `120` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `suffix` | `None` | Unit shown at the end (`"px"`, `"%"`…). | | `prefix` | `None` | Text before the number (`"$"`…). | | `align` | `"center"` | Where the number sits between the buttons: `"center"` (default), `"start"` or `"end"`. | | `decimals` | `None` | Decimals shown (by default, as many as `step`). | | `disabled` | `False` | Greys it out. | | `invalid` | `False` | Paints the border in the error color. | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `steppers` | `True` | Shows the −/+ buttons (`False`: a compact field, as in a property grid). | | `drag` | `True` | Lets the mouse change it by dragging (`False`: a plain text field). | | `drag_step` | `None` | How much each pixel dragged adds (by default `step`, up to 1; steps under 1 take three pixels each). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_change`, `on_submit` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#number_field) #### Related - [Slider](https://albakore.github.io/pyzed/library/slider/): Pick a number in a range by dragging. - [Input](https://albakore.github.io/pyzed/library/input/): A controlled text field with icon, placeholder and password mode. - [Field](https://albakore.github.io/pyzed/library/field/): A settings row: title and description left, control right. ### Panel > A dockable, resizable side panel. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/panel/ :::tip[In plain words] A side area of the window, like the file list on the left, that you can make wider or narrower. ::: A dockable panel with an optional header. `side` decides which edge gets the border line; with a numeric `width` (`height` for the bottom) that edge can be dragged to resize it, and `on_resize` reports the new size. `ui.panel` · Category: [Workspace](https://albakore.github.io/pyzed/library/#workspace) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#panel) **Code:** ```python pz.row([ ui.panel( pz.column([ui.list_item("Inbox", on_open, icon="inbox", selected=True, inset=True), ui.list_item("Archive", on_open, icon="archive", inset=True)]), header=ui.panel_header("Mail", [ui.icon_button("plus", on_new, size="xs")], icon="mail"), width=220, side="left", ), pz.container(ui.muted("Content"), center=True, width="fill", height="fill"), ], height="fill") ``` #### When to use - When you build your own layout instead of `ui.workspace`: sidebars, inspectors, outlines. #### Usage ```python import pyzed as pz from pyzed import ui pz.row([ ui.panel( pz.column([ui.list_item("Inbox", on_open, icon="inbox", selected=True, inset=True), ui.list_item("Archive", on_open, icon="archive", inset=True)]), header=ui.panel_header("Mail", [ui.icon_button("plus", on_new, size="xs")], icon="mail"), width=220, side="left", ), pz.container(ui.muted("Content"), center=True, width="fill", height="fill"), ], height="fill") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Side ```python pz.row([ui.panel(ui.list_item("Inbox", icon="inbox", inset=True), header=ui.panel_header("Mail"), width=160, side="left"), pz.container(width="fill")], height="fill") ``` ```python pz.row([pz.container(width="fill"), ui.panel(ui.muted("Properties"), header=ui.panel_header("Inspector"), width=160, side="right")], height="fill") ``` ```python pz.column([pz.container(height="fill"), ui.panel(ui.terminal("$ ls"), header=ui.panel_header("Terminal"), height=100, side="bottom")], width="fill") ``` #### Events Panel has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_resize` | `(size)` | Called with the new size when a drag ends. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Mail(pz.App): def __init__(self): self.width = 220 def view(self): return pz.row([ ui.panel(ui.muted("Inbox"), header=ui.panel_header("Mail"), width=self.width, on_resize=lambda size: setattr(self, "width", size)), # when the drag ends main_view(), ], height="fill") ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Width: size: float class Mail(pz.App): width = 220 def update(self, message): match message: case Width(size): self.width = size def view(self): return pz.row([ui.panel(ui.muted("Inbox"), width=self.width, on_resize=Width), main_view()], height="fill") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: width: float = 220 def view(state, set_state): return pz.row([ui.panel(ui.muted("Inbox"), width=state.width, on_resize=lambda s: set_state(width=s)), main_view()], height="fill") ``` #### Properties and API ##### `ui.panel` ```python panel( content, *, header=None, width=None, height=None, side='left', resizable=True, on_resize=None, min_size=120, theme=None, ) ``` A dockable panel. `side` decides which edge gets the border line; with a numeric `width` (`height` for the bottom) that border can be dragged to resize it. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `header` | `None` | Header (`ui.panel_header`). | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `side` | `"left"` | `"left"`, `"right"` or `"bottom"`. | | `resizable` | `True` | `False` keeps the size fixed. | | `min_size` | `120` | Smallest size when resizing. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_resize` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#panel) #### Related - [Panel header](https://albakore.github.io/pyzed/library/panel-header/): Icon, title and actions on top of a dock. - [Workspace](https://albakore.github.io/pyzed/library/workspace/): The layout of an editor-style window: title bar, docks, center, status bar. - [Split](https://albakore.github.io/pyzed/library/split/): Two panes with a draggable divider. ### Panel header > Icon, title and actions on top of a dock. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/panel-header/ :::tip[In plain words] The title row of a side panel, with its buttons. ::: The header of a dock or panel: an optional icon, the title and action buttons on the right. `ui.panel_header` · Category: [Workspace](https://albakore.github.io/pyzed/library/#workspace) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#panel_header) **Code:** ```python pz.column([ ui.panel_header("Project", [ui.icon_button("plus", on_new, size="xs"), ui.icon_button("ellipsis", on_menu, size="xs")]), ui.panel_header("Terminal", [ui.icon_button("x", on_close, size="xs")], icon="terminal"), ], spacing=12, width=320) ``` #### When to use - At the top of every dock (Project, Outline, Terminal) to name it and hold its actions. #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ ui.panel_header("Project", [ui.icon_button("plus", on_new, size="xs"), ui.icon_button("ellipsis", on_menu, size="xs")]), ui.panel_header("Terminal", [ui.icon_button("x", on_close, size="xs")], icon="terminal"), ], spacing=12, width=320) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content ```python pz.container(ui.panel_header("Project"), width=280) ``` ```python pz.container(ui.panel_header("Terminal", icon="terminal"), width=280) ``` ```python pz.container(ui.panel_header("Project", [ui.icon_button("plus", on_new, size="xs"), ui.icon_button("ellipsis", on_menu, size="xs")]), width=280) ``` ```python pz.container(ui.panel_header("Outline", height=40), width=280) ``` #### Events Panel header has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.panel_header` ```python panel_header(title, actions=(), *, icon=None, height=32, theme=None) ``` A panel header: icon, title and actions. | Property | Default | Description | | --- | --- | --- | | `title` | *required* | Title. | | `actions` | `()` | Extra widgets (usually buttons) shown as actions. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `height` | `32` | Height. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#panel_header) #### Related - [Panel](https://albakore.github.io/pyzed/library/panel/): A dockable, resizable side panel. - [List header](https://albakore.github.io/pyzed/library/list-header/): A small section title for lists, with an end slot. ### Picker > A searchable list on a floating surface, with fuzzy matching. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/picker/ :::tip[In plain words] A floating search box with a list of results that narrows as you type. ::: A search box over a list of results on a floating surface: the basis of the command palette and the file finder. `ui.fuzzy_filter(query, items, key=)` ranks items (best match first) and returns `[(item, positions), ...]`; pass those pairs to `picker` to highlight the matches. `ui.fuzzy_match` scores one string and `ui.highlighted` draws one with its matches. `ui.picker` · Category: [Overlays](https://albakore.github.io/pyzed/library/#overlays) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#picker) **Code:** ```python files = ["src/app.rs", "src/render.rs", "python/pyzed/app.py", "python/pyzed/theme.py", "README.md"] items = ui.fuzzy_filter("app", [ui.PaletteItem(f, on_open, icon="file-code") for f in files], key=lambda item: item.label) ui.picker("app", on_query, items, selected=0, placeholder="Search files…", width=480) ``` #### When to use - For your own finders: files, symbols, branches, projects, emoji. #### Usage ```python import pyzed as pz from pyzed import ui class Finder(pz.App): def __init__(self): self.query, self.selected = "", 0 self.files = ["src/app.rs", "src/render.rs", "README.md"] def view(self): matches = ui.fuzzy_filter(self.query, self.files) items = [(ui.PaletteItem(f, lambda f=f: self.open(f), icon="file"), positions) for f, positions in matches[:50]] return ui.picker(self.query, lambda q: setattr(self, "query", q), items, selected=self.selected, placeholder="Search files…") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Results ```python files = ["src/app.rs", "src/render.rs", "app.py"] items = ui.fuzzy_filter("app", [ui.PaletteItem(f, on_open, icon="file-code") for f in files], key=lambda item: item.label) ui.picker("app", on_query, items, width=380) ``` ```python ui.picker("zzz", on_query, [], empty_text="No files match", width=380) ``` ```python ui.picker("", on_query, [(ui.PaletteItem("main", on_open, icon="git-branch", detail="2 hours ago"), []), (ui.PaletteItem("docs", on_open, icon="git-branch", detail="yesterday"), [])], width=380) ``` ##### Highlight ```python t = pz.theme.current() score, positions = ui.fuzzy_match("tgsw", "editor: toggle soft wrap") ui.highlighted("editor: toggle soft wrap", positions, color=t.text, match_color=t.accent, size=15) ``` #### Examples ##### Highlighting matches `fuzzy_match` returns `(score, positions)` or `None`; `highlighted` paints those positions. **Code:** ```python t = pz.theme.current() score, positions = ui.fuzzy_match("tgsw", "editor: toggle soft wrap") ui.highlighted("editor: toggle soft wrap", positions, color=t.text, match_color=t.accent, size=16) ``` #### Events Picker has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_query` | `(text)` | Callable that receives the new text. | | `on_submit` | `()` | Message or callable sent when Enter is pressed. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Finder(pz.App): def __init__(self): self.query, self.opened = "", None self.files = ["src/app.rs", "src/render.rs", "README.md"] def matches(self): return ui.fuzzy_filter(self.query, self.files) def open_first(self): # on_submit: Enter found = self.matches() self.opened = found[0][0] if found else None def view(self): items = [(ui.PaletteItem(f, lambda f=f: setattr(self, "opened", f), icon="file"), positions) for f, positions in self.matches()] return ui.picker(self.query, lambda q: setattr(self, "query", q), items, # on_query on_submit=self.open_first, placeholder="Search files…") ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Query: text: str @dataclass class Open: path: str class Finder(pz.App): query, opened = "", None files = ["src/app.rs", "src/render.rs", "README.md"] def update(self, message): match message: case Query(text): self.query = text case Open(path): self.opened = path def view(self): items = [(ui.PaletteItem(f, Open(f), icon="file"), p) for f, p in ui.fuzzy_filter(self.query, self.files)] return ui.picker(self.query, Query, items, placeholder="Search files…") ``` **Functional:** ```python from dataclasses import dataclass FILES = ["src/app.rs", "src/render.rs", "README.md"] @dataclass(frozen=True) class State: query: str = "" opened: str = "" def view(state, set_state): items = [(ui.PaletteItem(f, lambda f=f: set_state(opened=f), icon="file"), p) for f, p in ui.fuzzy_filter(state.query, FILES)] return ui.picker(state.query, lambda q: set_state(query=q), items, placeholder="Search files…") ``` #### Properties and API ##### `ui.picker` ```python picker( query, on_query, items, *, selected=0, placeholder='Search…', input_id='picker', on_submit=None, width=560, max_height=340, empty_text='No matches', theme=None, ) ``` A searchable list on a floating surface. The basis of the palette and the file finder. | Property | Default | Description | | --- | --- | --- | | `query` | *required* | Search text. | | `items` | *required* | `(PaletteItem, positions)` pairs, for example from `fuzzy_filter`. | | `selected` | `0` | Highlighted index. | | `placeholder` | `"Search…"` | Hint text shown when empty. | | `input_id` | `"picker"` | Field id. | | `width` | `560` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `max_height` | `340` | Maximum height in pixels. | | `empty_text` | `"No matches"` | Text shown when there are no results. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_query`, `on_submit` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#picker) ##### `ui.fuzzy_filter` ```python fuzzy_filter(query, items, key=) ``` Filters and sorts `items` by fuzzy score. Returns `[(item, positions), ...]`. | Property | Default | Description | | --- | --- | --- | | `query` | *required* | Search text. | | `items` | *required* | Items. | | `key` | `` | Function that returns the text of each item. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#fuzzy_filter) ##### `ui.fuzzy_match` ```python fuzzy_match(query, candidate) ``` Returns `(score, positions)` if every letter of `query` appears in order in `candidate`; otherwise `None`. Rewards word starts and consecutive letters. | Property | Default | Description | | --- | --- | --- | | `query` | *required* | Search text. | | `candidate` | *required* | Candidate text. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#fuzzy_match) ##### `ui.highlighted` ```python highlighted(text, positions, *, color, match_color, size=13.5, weight=None) ``` Text with the characters at `positions` highlighted (fuzzy search matches). | Property | Default | Description | | --- | --- | --- | | `text` | *required* | Text. | | `positions` | *required* | Indices to highlight. | | `color` | *required* | Text or icon color. | | `match_color` | *required* | Color of the matches. | | `size` | `13.5` | Size. | | `weight` | `None` | Text weight. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#highlighted) #### Related - [Command palette](https://albakore.github.io/pyzed/library/command-palette/): A command palette with fuzzy search and shortcuts. - [Search input](https://albakore.github.io/pyzed/library/search-input/): An input with a magnifying glass, for filtering. - [Layer](https://albakore.github.io/pyzed/library/layer/): Draws an overlay over your view; clicking outside dismisses it. ### Plan > An agent's plan as a checklist that folds. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/plan/ :::tip[In plain words] A to-do list an AI assistant writes for itself and ticks off as it works. ::: `ui.Plan` is a component that shows an AI agent's plan as a checklist: each step is pending (an empty circle), in progress (a dot, or a spinner with `running=True`) or done (a green check, with its text dimmed), and the header counts `done/total`. Clicking the header folds and unfolds the steps. `steps` are dicts with `step` (the text) and `status`: `"pending"`, `"in_progress"` or `"done"`. `ui.Plan` · Category: [AI agents](https://albakore.github.io/pyzed/library/#ai-agents) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#plan) **Code:** ```python pz.container(ui.Plan(steps=[ {"step": "Read the form and its handlers", "status": "done"}, {"step": "Add the email field", "status": "done"}, {"step": "Validate the email as you type", "status": "in_progress"}, {"step": "Run the tests", "status": "pending"}, ], running=True), width=420) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - Above an agent's chat, while it works through a task of several steps. - For any list of steps that updates as the work goes on, such as an install or a migration. #### Usage ```python import pyzed as pz from pyzed import ui class Agent(pz.App): def __init__(self): self.steps = [{"step": "Add the form", "status": "done"}, {"step": "Wire the events", "status": "in_progress"}] self.working = True def view(self): return ui.Plan(steps=self.steps, running=self.working) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### State Without `running` the step in progress shows a dot; folded is drawn as after a click on the header. ```python pz.container(ui.Plan(steps=[{"step": "Add the form", "status": "done"}, {"step": "Wire the events", "status": "in_progress"}, {"step": "Run the tests", "status": "pending"}], running=True), width=300) ``` ```python pz.container(ui.Plan(steps=[{"step": "Add the form", "status": "done"}, {"step": "Wire the events", "status": "in_progress"}, {"step": "Run the tests", "status": "pending"}]), width=300) ``` ```python pz.container(ui.Plan(steps=[{"step": "Add the form", "status": "pending"}, {"step": "Run the tests", "status": "pending"}]), width=300) ``` ```python pz.container(ui.Plan(steps=[{"step": "Add the form", "status": "done"}, {"step": "Run the tests", "status": "done"}]), width=300) ``` ```python pz.container(ui.Plan(steps=[{"step": "Add the form", "status": "done"}, {"step": "Run the tests", "status": "pending"}]), width=300) ``` #### Events Plan has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. #### Properties and API ##### `ui.Plan` ```python class Plan ``` A component for an agent's plan: a checklist with `done/total` in its header, which folds it. | Property | Default | Description | | --- | --- | --- | | `steps` | `None` | `{"step": text, "status": "pending" \| "in_progress" \| "done"}` dicts. | | `running` | `False` | Spins the step in progress. | | `theme` | `None` | Theme. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#plan) #### Related - [Tool call](https://albakore.github.io/pyzed/library/tool-call/): One step of an AI agent: what it did, its result, and Allow or Reject. - [Progress](https://albakore.github.io/pyzed/library/progress/): A thin progress bar in a semantic color. - [Edits bar](https://albakore.github.io/pyzed/library/edits-bar/): The files an agent changed, with Keep all and Reject all. ### Popover > Floating content at a point: context menus, color pickers. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/popover/ :::tip[In plain words] A small floating box that appears next to where you clicked, like a right-click menu. ::: Shows `content` (a menu, a card…) over `base` at `(x, y)`. Clicking anywhere else calls `on_dismiss`; with `content=None` only `base` is shown. For a right-click menu, store the pointer position with `pz.mouse_area(on_move=...)` and open it from `on_right_press`. `ui.popover` · Category: [Overlays](https://albakore.github.io/pyzed/library/#overlays) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#popover) **Code:** ```python ui.popover( main_view(), ui.menu([ ui.MenuItem("Copy", on_copy, icon="copy", keys="ctrl-c"), ui.MenuItem("Paste", on_paste, icon="clipboard", keys="ctrl-v"), ui.SEPARATOR, ui.MenuItem("Delete", on_delete, icon="trash-2", danger=True), ], width=220), x=180, y=70, on_dismiss=on_close, ) ``` #### When to use - For context menus, dropdown panels and pickers anchored to a point or a button. #### Usage ```python import pyzed as pz from pyzed import ui class Canvas(pz.App): def __init__(self): self.pointer, self.menu_at = (0, 0), None def view(self): base = pz.mouse_area(self.content(), on_move=lambda x, y: setattr(self, "pointer", (x, y)), on_right_press=lambda: setattr(self, "menu_at", self.pointer)) menu = ui.menu([ui.MenuItem("Copy", self.copy), ui.MenuItem("Paste", self.paste)]) x, y = self.menu_at or (0, 0) return ui.popover(base, menu if self.menu_at else None, x=x, y=y, on_dismiss=lambda: setattr(self, "menu_at", None)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content ```python ui.popover(main_view(), ui.menu([ui.MenuItem("Copy", on_copy, icon="copy"), ui.MenuItem("Paste", on_paste, icon="clipboard")], width=180), x=60, y=40, on_dismiss=on_close) ``` ```python ui.popover(main_view(), ui.card(ui.muted("Any widget fits here."), title="Popover", width=220), x=120, y=40, on_dismiss=on_close) ``` #### Events Popover has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_dismiss` | `()` | Message or callable sent on close. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Canvas(pz.App): def __init__(self): self.pointer, self.menu_at = (0, 0), None def view(self): base = pz.mouse_area(main_view(), on_move=lambda x, y: setattr(self, "pointer", (x, y)), on_right_press=lambda: setattr(self, "menu_at", self.pointer)) menu = ui.menu([ui.MenuItem("Copy", lambda: setattr(self, "menu_at", None))]) x, y = self.menu_at or (0, 0) return ui.popover(base, menu if self.menu_at else None, x=x, y=y, on_dismiss=lambda: setattr(self, "menu_at", None)) # click outside ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class MenuAt: x: float y: float class Canvas(pz.App): menu_at = None def update(self, message): match message: case MenuAt(x, y): self.menu_at = (x, y) case "close": self.menu_at = None def view(self): base = pz.canvas([], width=400, height=200, on_right_press=MenuAt) x, y = self.menu_at or (0, 0) return ui.popover(base, ui.menu([ui.MenuItem("Copy", "close")]) if self.menu_at else None, x=x, y=y, on_dismiss="close") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: menu_at: tuple = () def view(state, set_state): base = pz.canvas([], width=400, height=200, on_right_press=lambda x, y: set_state(menu_at=(x, y))) menu = ui.menu([ui.MenuItem("Copy", lambda: set_state(menu_at=()))]) if state.menu_at else None x, y = state.menu_at or (0, 0) return ui.popover(base, menu, x=x, y=y, on_dismiss=lambda: set_state(menu_at=())) ``` #### Properties and API ##### `ui.popover` ```python popover(base, content=None, *, x=0, y=0, on_dismiss=None, align_x='start') ``` Shows `content` (a `ui.menu`, a card…) over `base` at `(x, y)`. Clicking anywhere else calls `on_dismiss`. For a right-click menu, store the mouse position with `mouse_area(on_move=...)` and open the popover from `on_right_press` (see `examples/context_menu.py`). | Property | Default | Description | | --- | --- | --- | | `base` | *required* | The UI underneath. | | `content` | `None` | What floats (or `None`). | | `x` | `0` | Horizontal position. | | `y` | `0` | Vertical position. | | `align_x` | `"start"` | Horizontal alignment: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | Events: `on_dismiss` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#popover) #### Related - [Menu](https://albakore.github.io/pyzed/library/menu/): A context menu with icons, shortcuts and checks. - [Layer](https://albakore.github.io/pyzed/library/layer/): Draws an overlay over your view; clicking outside dismisses it. - [Split button](https://albakore.github.io/pyzed/library/split-button/): A main action plus an arrow that opens more options. ### Progress > A thin progress bar in a semantic color. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/progress/ :::tip[In plain words] A bar that fills up to show how much of a task is done. ::: A thin bar that fills from `0` to `max` (1.0 by default). `tint` changes its color and `height` its thickness. `ui.progress` · Category: [Feedback](https://albakore.github.io/pyzed/library/#feedback) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#progress) **Code:** ```python pz.column([ ui.progress(0.25), ui.progress(0.6, tint="success", height=6), ui.progress(37, max=40, tint="warning", height=8), ], spacing=18, width=360) ``` #### When to use - For work whose progress you can measure: downloads, builds, imports. - When you can't measure it, show a [Spinner](https://albakore.github.io/pyzed/library/spinner/). #### Usage ```python import pyzed as pz from pyzed import ui class Download(pz.App): def __init__(self): self.done, self.total = 0, 1 def update(self, message): self.done, self.total = message # posted from a worker thread with pz.post((done, total)) def view(self): return ui.progress(self.done, max=self.total) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Value ```python ui.progress(0.1, width=240) ``` ```python ui.progress(0.5, width=240) ``` ```python ui.progress(1.0, width=240) ``` ```python ui.progress(37, max=40, width=240) ``` ##### Tint ```python ui.progress(0.7, tint="success", width=240) ``` ```python ui.progress(0.7, tint="warning", width=240) ``` ```python ui.progress(0.7, tint="error", width=240) ``` ##### Thickness ```python ui.progress(0.6, height=2, width=240) ``` ```python ui.progress(0.6, height=4, width=240) ``` ```python ui.progress(0.6, height=10, width=240) ``` ##### States and events ```python pz.container(ui.progress(0.42, label="Downloading", show_value=True), width=300) ``` ```python pz.container(ui.progress(None, label="Indexing"), width=300) ``` ##### Base widget The `pz.*` widget underneath: no theme, every color and size is yours. ```python pz.progress_bar(64, max=100, width=240, height=12, accent="#c678dd", bg="#c678dd33", radius=6) ``` #### Events Progress has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.progress` ```python progress( value, *, max=1.0, tint='accent', height=4, width='fill', label=None, show_value=False, theme=None, ) ``` A thin progress bar. | Property | Default | Description | | --- | --- | --- | | `value` | *required* | Progress, or `None` for an indeterminate bar (keep a `pz.every(1/30, ...)` subscription while it shows). | | `max` | `1.0` | Maximum value. | | `tint` | `"accent"` | Semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` or a token. | | `height` | `4` | Thickness. | | `width` | `"fill"` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `label` | `None` | Text above the bar. | | `show_value` | `False` | Adds the percentage at its right. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#progress) ##### `pz.progress_bar` ```python progress_bar( value, *, min=0.0, max=1.0, width=None, height=None, accent=None, bg=None, radius=2.0, ) ``` A progress bar. | Property | Default | Description | | --- | --- | --- | | `value` | *required* | Current value. | | `min` | `0.0` | Minimum. | | `max` | `1.0` | Maximum. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `accent` | `None` | Accent color (active part). | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `radius` | `2.0` | Corner radius. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#progress_bar) #### Related - [Spinner](https://albakore.github.io/pyzed/library/spinner/): A spinning arc for loading states. - [Slider](https://albakore.github.io/pyzed/library/slider/): Pick a number in a range by dragging. - [Stat](https://albakore.github.io/pyzed/library/stat/): A dashboard number with change and sparkline. ### Prompt > Asks for a line of text. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/prompt/ :::tip[In plain words] A small box that asks you to type something, like a new name for a file. ::: Asks for a line of text: a controlled input plus Cancel and OK. Enter or the button calls `on_submit`. `ui.prompt` · Category: [Dialogs](https://albakore.github.io/pyzed/library/#dialogs) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#prompt) **Code:** ```python ui.dialog_layer(main_view(), ui.prompt( "Rename", "notes.py", on_input, on_submit, on_cancel, message="New name for the file:", width=360, )) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - For quick names: rename a file, name a new branch, save a view. #### Usage ```python import pyzed as pz from pyzed import ui class Files(pz.App): def __init__(self): self.new_name = None def view(self): dialog = None if self.new_name is not None: dialog = ui.prompt("Rename", self.new_name, lambda t: setattr(self, "new_name", t), self.rename, lambda: setattr(self, "new_name", None)) return ui.dialog_layer(self.main_view(), dialog) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content ```python ui.prompt("New branch", "", on_input, on_submit, on_cancel, placeholder="feature/…", width=340) ``` ```python ui.prompt("Rename", "notes.py", on_input, on_submit, on_cancel, message="New name for the file:", width=340) ``` ```python ui.prompt("Save view", "My view", on_input, on_submit, on_cancel, submit_label="Save", width=340) ``` #### Events Prompt has **3 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_input` | `(text)` | Receives each edit. | | `on_submit` | `()` | Enter or the submit button. | | `on_cancel` | `()` | Cancel, the close icon. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Files(pz.App): def __init__(self): self.name = "notes.py" self.new_name = "notes.py" # None while the dialog is closed def rename(self): # on_submit: Enter or the button self.name, self.new_name = self.new_name, None def view(self): dialog = None if self.new_name is not None: dialog = ui.prompt("Rename", self.new_name, lambda text: setattr(self, "new_name", text), # on_input self.rename, lambda: setattr(self, "new_name", None), # on_cancel message="New name for the file:") return ui.dialog_layer(main_view(), dialog) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Typed: text: str class Files(pz.App): name, new_name = "notes.py", "notes.py" def update(self, message): match message: case Typed(text): self.new_name = text case "rename": self.name, self.new_name = self.new_name, None case "cancel": self.new_name = None def view(self): dialog = ui.prompt("Rename", self.new_name, Typed, "rename", "cancel") \ if self.new_name is not None else None return ui.dialog_layer(main_view(), dialog) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: name: str = "notes.py" draft: str = "notes.py" renaming: bool = True def view(state, set_state): if not state.renaming: return ui.label(state.name) return ui.dialog_layer(main_view(), ui.prompt( "Rename", state.draft, lambda text: set_state(draft=text), lambda: set_state(name=state.draft, renaming=False), lambda: set_state(renaming=False))) ``` #### Properties and API ##### `ui.prompt` ```python prompt( title, value, on_input, on_submit, on_cancel, *, message=None, placeholder='', submit_label=None, cancel_label=None, id=None, width=440, theme=None, ) ``` Asks for a line of text. | Property | Default | Description | | --- | --- | --- | | `title` | *required* | Title. | | `value` | *required* | Current text. | | `message` | `None` | Text above the field. | | `placeholder` | `""` | Hint text shown when empty. | | `submit_label` | `None` | Submit button text. | | `cancel_label` | `None` | Cancel button text. | | `id` | `None` | Field id (for `Command.focus`). | | `width` | `440` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_input`, `on_submit`, `on_cancel` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#prompt) #### Related - [Confirm](https://albakore.github.io/pyzed/library/confirm/): A yes/no question, red for destructive actions. - [Input](https://albakore.github.io/pyzed/library/input/): A controlled text field with icon, placeholder and password mode. - [Dialog layer](https://albakore.github.io/pyzed/library/dialog-layer/): Centers a dialog over your view with a dimmed backdrop. ### QR code > A QR code for any text or URL. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/qr-code/ :::tip[In plain words] A square barcode a phone camera can read, for sharing a link. ::: Draws a QR code that encodes `data`. `cell_size` sets the size of each module; `color` and `bg` change its colors. `pz.qr_code` · Category: [Content](https://albakore.github.io/pyzed/library/#content) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#qr_code) **Code:** ```python pz.row([ pz.qr_code("https://albakore.github.io/pyzed/", cell_size=5), pz.qr_code("pyzed", cell_size=5, color="#74ade8", bg="#282c33"), ], spacing=24, align="center") ``` #### When to use - To pass a link, a Wi-Fi password or a pairing code to a phone. #### Usage ```python import pyzed as pz pz.row([ pz.qr_code("https://albakore.github.io/pyzed/", cell_size=5), pz.qr_code("pyzed", cell_size=5, color="#74ade8", bg="#282c33"), ], spacing=24, align="center") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python pz.qr_code("https://albakore.github.io/pyzed/", cell_size=3) ``` ```python pz.qr_code("pyzed", cell_size=5) ``` ```python pz.qr_code("pyzed", cell_size=5, color="#74ade8", bg="#1e2228") ``` #### Events QR code has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.qr_code` ```python qr_code(data, *, cell_size=4.0, color=None, bg=None) ``` A QR code that encodes `data`. | Property | Default | Description | | --- | --- | --- | | `data` | *required* | The text or URL to encode. | | `cell_size` | `4.0` | Size of each module in pixels. | | `color` | `None` | Text or icon color. | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#qr_code) #### Related - [Image](https://albakore.github.io/pyzed/library/image/): Bitmaps with fit, opacity, radius; a pan-and-zoom viewer. - [Canvas](https://albakore.github.io/pyzed/library/canvas/): 2D drawing with shapes, paths, text and mouse events. ### Radio group > Mutually exclusive options as radio buttons. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/radio-group/ :::tip[In plain words] A few round options where picking one unpicks the others. ::: A set of radio buttons where one option is selected. `options` are strings or `(value, label)` pairs; `on_select` receives the value. `horizontal=True` lays them out in a row. `ui.radio_group` · Category: [Forms](https://albakore.github.io/pyzed/library/#forms) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#radio_group) **Code:** ```python pz.row([ ui.radio_group(["Spaces", "Tabs"], "Spaces", on_select), ui.radio_group([("lf", "LF"), ("crlf", "CRLF"), ("cr", "CR")], "lf", on_select, horizontal=True), ], spacing=40, align="center") ``` #### When to use - When the options need a label each and should all be visible (indentation: spaces or tabs). - For many options use a [Select](https://albakore.github.io/pyzed/library/select/). #### Usage ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.indent = "spaces" def view(self): return ui.radio_group([("spaces", "Spaces"), ("tabs", "Tabs")], self.indent, lambda value: setattr(self, "indent", value)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Direction ```python ui.radio_group(["Spaces", "Tabs"], "Spaces", on_select) ``` ```python ui.radio_group(["LF", "CRLF", "CR"], "LF", on_select, horizontal=True) ``` ```python ui.radio_group(["Small", "Medium", "Large"], "Medium", on_select, horizontal=True, spacing=16) ``` ##### Options ```python ui.radio_group([("lf", "Unix (LF)"), ("crlf", "Windows (CRLF)")], "crlf", on_select) ``` ```python ui.radio_group(["Yes", "No"], None, on_select, horizontal=True) ``` ##### States and events ```python ui.radio_group(["LF", "CRLF", "CR"], "LF", on_select, horizontal=True, disabled={"CR"}) ``` ```python ui.radio_group(["Spaces", "Tabs"], "Spaces", on_select, descriptions={"Spaces": "Indent with 4 spaces", "Tabs": "One tab per level"}) ``` ##### Base widget The `pz.*` widget underneath: no theme, every color and size is yours. ```python pz.row([pz.radio("Cats", True, on_select, accent="#e5c07b"), pz.radio("Dogs", False, on_select, accent="#e5c07b")], spacing=16) ``` #### Events Radio group has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_select` | `ui.radio_group`, `pz.radio` | `(value)` | Callable that receives the chosen value. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Settings(pz.App): def __init__(self): self.indent = "spaces" def set_indent(self, value): # on_select gets the value, not the label self.indent = value def view(self): return ui.radio_group([("spaces", "Spaces"), ("tabs", "Tabs")], self.indent, self.set_indent) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Indent: value: str class Settings(pz.App): indent = "spaces" def update(self, message): match message: case Indent(value): self.indent = value def view(self): return ui.radio_group([("spaces", "Spaces"), ("tabs", "Tabs")], self.indent, Indent) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: indent: str = "spaces" def view(state, set_state): return ui.radio_group([("spaces", "Spaces"), ("tabs", "Tabs")], state.indent, lambda value: set_state(indent=value), horizontal=True) ``` #### Tips :::tip Once clicked or reached with Tab the group has the keyboard focus: the arrows pick the previous or next choice (skipping disabled ones) and Home/End the first and last. ::: #### Properties and API ##### `ui.radio_group` ```python radio_group( options, selected, on_select=None, *, horizontal=False, spacing=8, disabled=False, descriptions=None, id=None, theme=None, ) ``` Mutually exclusive options as radio buttons. | Property | Default | Description | | --- | --- | --- | | `options` | *required* | Strings or `(value, label)` pairs. | | `selected` | *required* | Current value. | | `horizontal` | `False` | In a row instead of a column. | | `spacing` | `8` | Space between children, in pixels. | | `disabled` | `False` | `True`, or the values to disable. | | `descriptions` | `None` | Maps values to a muted line under their label. | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_select` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#radio_group) ##### `pz.radio` ```python radio( label, selected, on_select=None, *, size=None, text_size=None, spacing=None, accent=None, bg=None, border_color=None, color=None, ) ``` A radio button. Group several yourself (or use `ui.radio_group`); `on_select` is sent when it is clicked. | Property | Default | Description | | --- | --- | --- | | `label` | *required* | Label text. | | `selected` | *required* | Whether it is checked. | | `size` | `None` | Size. | | `text_size` | `None` | Text size. | | `spacing` | `None` | Space between the circle and the text. | | `accent` | `None` | Accent color (active part). | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `border_color` | `None` | Border color. | | `color` | `None` | Text or icon color. | Events: `on_select` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#radio) #### Related - [Segmented](https://albakore.github.io/pyzed/library/segmented/): A group of mutually exclusive text buttons. - [Select](https://albakore.github.io/pyzed/library/select/): A dropdown to choose one option. - [Checkbox](https://albakore.github.io/pyzed/library/checkbox/): A checkbox with an optional label. ### Reference browser > Built-in, searchable docs like Godot's: a class list next to the open page. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/reference-browser/ :::tip[In plain words] A help window with the documentation inside the app, searchable and offline. ::: The built-in reference: a searchable class list (the library, your components and any section you `reference.register`) next to the page that is open, with back and forward, internal links and an Open online button. Keep a `pyzed.reference.Browser` in your state; the browser changes it. Links that leave the reference go to `on_open_url`. See [Built-in reference](https://albakore.github.io/pyzed/editor/reference/). `ui.reference_browser` · Category: [Code and tools](https://albakore.github.io/pyzed/library/#code-and-tools) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#reference_browser) **Code:** ```python from pyzed import reference help = reference.Browser("pyzed.ui.button") ui.reference_browser(help, on_open_url=open_in_browser, sidebar_width=230) ``` #### When to use - In a Help tab or window of an app built on Pyzed, so its users can read the API offline. - With your own `reference.Provider` sections, as your app's help. #### Usage ```python from pyzed import ui from pyzed import reference help = reference.Browser("pyzed.ui.button") ui.reference_browser(help, on_open_url=open_in_browser, sidebar_width=230) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Pages ```python from pyzed import reference ui.reference_browser(reference.Browser("pyzed.ui.badge"), sidebar_width=200) ``` ```python from pyzed import reference ui.reference_browser(reference.Browser("pyzed.row"), sidebar_width=200) ``` #### Events Reference browser has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_open_url` | `(url)` | Called with the URL of a link that leaves the reference (open it in the browser). | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python import webbrowser from pyzed import reference class Help(pz.App): def __init__(self): self.browser = reference.Browser("pyzed.ui.button") def view(self): return ui.reference_browser(self.browser, on_open_url=webbrowser.open) # links to the web ``` **Declarative:** ```python from dataclasses import dataclass from pyzed import reference @dataclass class OpenUrl: url: str class Help(pz.App): browser = reference.Browser("pyzed.ui.button") def update(self, message): match message: case OpenUrl(url): print("open", url) def view(self): return ui.reference_browser(self.browser, on_open_url=OpenUrl) ``` **Functional:** ```python from dataclasses import dataclass from pyzed import reference BROWSER = reference.Browser("pyzed.ui.button") @dataclass(frozen=True) class State: last_url: str = "" def view(state, set_state): return ui.reference_browser(BROWSER, on_open_url=lambda url: set_state(last_url=url)) ``` #### Properties and API ##### `ui.reference_browser` ```python reference_browser( browser, *, on_open_url=None, sidebar_width=270, width='fill', height='fill', theme=None, ) ``` The built-in reference, like Godot's: a searchable class list next to the page that is open. Events: `on_open_url` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#reference_browser) #### Related - [Shortcuts](https://albakore.github.io/pyzed/library/shortcuts/): A searchable list of key bindings, grouped. - [About](https://albakore.github.io/pyzed/library/about/): An About box with icon, version, details and links. - [Markdown](https://albakore.github.io/pyzed/library/markdown/): Renders Markdown with the theme's colors. ### Resizable > A panel whose edge can be dragged to resize it. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/resizable/ :::tip[In plain words] A panel with an edge you can drag to make it bigger or smaller. ::: A panel whose `edge` (`"left"`, `"right"`, `"top"` or `"bottom"`) can be dragged to resize it, between `min_size` and `max_size`. It keeps the size the user drags it to; `on_resize(size)` is called when the drag ends so you can save it. `pz.resizable` · Category: [Layout](https://albakore.github.io/pyzed/library/#layout) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#resizable) **Code:** ```python t = pz.theme.current() pz.row([ pz.resizable(pz.container(ui.label("Drag my right edge"), padding=12, width="fill", height="fill", bg=t.panel), edge="right", size=260, min_size=160, color=t.border, hover_color=t.accent), pz.container(ui.muted("Content"), center=True, width="fill", height="fill"), ], height="fill") ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - For one resizable sidebar or bottom panel next to fixed content. #### Usage ```python import pyzed as pz from pyzed import ui t = pz.theme.current() pz.row([ pz.resizable(pz.container(ui.label("Drag my right edge"), padding=12, width="fill", height="fill", bg=t.panel), edge="right", size=260, min_size=160, color=t.border, hover_color=t.accent), pz.container(ui.muted("Content"), center=True, width="fill", height="fill"), ], height="fill") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Edge ```python t = pz.theme.current() pz.row([pz.resizable(pz.container(ui.label("Sidebar"), padding=10, width="fill", height="fill", bg=t.panel), edge="right", size=150, color=t.border), pz.container(width="fill")], height="fill") ``` ```python t = pz.theme.current() pz.row([pz.container(width="fill"), pz.resizable(pz.container(ui.label("Inspector"), padding=10, width="fill", height="fill", bg=t.panel), edge="left", size=150, color=t.border)], height="fill") ``` ```python t = pz.theme.current() pz.column([pz.container(height="fill"), pz.resizable(pz.container(ui.label("Console"), padding=10, width="fill", height="fill", bg=t.panel), edge="top", size=80, color=t.border)], width="fill") ``` #### Events Resizable has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_resize` | `(size)` | Called with the new size when a drag ends. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Mail(pz.App): def __init__(self): self.width = 260 def view(self): return pz.row([ pz.resizable(ui.muted(f"{self.width:.0f} px"), edge="right", size=self.width, min_size=160, on_resize=lambda size: setattr(self, "width", size)), # when the drag ends main_view(), ], height="fill") ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Width: size: float class Mail(pz.App): width = 260 def update(self, message): match message: case Width(size): self.width = size def view(self): return pz.row([pz.resizable(ui.muted("Folders"), size=self.width, on_resize=Width), main_view()], height="fill") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: width: float = 260 def view(state, set_state): return pz.row([pz.resizable(ui.muted("Folders"), size=state.width, on_resize=lambda size: set_state(width=size)), main_view()], height="fill") ``` #### Properties and API ##### `pz.resizable` ```python resizable( content, *, edge='right', size=240.0, min_size=80.0, max_size=None, on_resize=None, color=None, hover_color=None, thickness=1.0, handle=8.0, width=None, height=None, ) ``` A panel whose `edge` can be dragged to resize it. It keeps the size the user drags it to; `on_resize(size)` is called when a drag ends so you can save it. Changing `size` later resets it. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | The panel's content. | | `edge` | `"right"` | The edge with the handle: `"right"` (a left sidebar), `"left"`, `"top"` (a bottom panel) or `"bottom"`. | | `size` | `240.0` | Starting width (height for top/bottom edges). | | `min_size` | `80.0` | Smallest size. | | `max_size` | `None` | Largest size. | | `color` | `None` | Line color. | | `hover_color` | `None` | Line color while hovered or dragged. | | `thickness` | `1.0` | Line thickness. | | `handle` | `8.0` | Width of the area that can be grabbed. | | `width` | `None` | Width for top/bottom edges. | | `height` | `None` | Height for left/right edges. | Events: `on_resize` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#resizable) #### Related - [Split](https://albakore.github.io/pyzed/library/split/): Two panes with a draggable divider. - [Panel](https://albakore.github.io/pyzed/library/panel/): A dockable, resizable side panel. ### Responsive > Builds its content from the space actually available. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/responsive/ :::tip[In plain words] Changes the layout depending on how much room there is, like a phone versus a monitor. ::: Calls `function(width, height)` with the space it really gets and shows what it returns, so the layout can adapt: hide a sidebar when the window is narrow, change the number of columns… `pz.responsive` · Category: [Layout](https://albakore.github.io/pyzed/library/#layout) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#responsive) **Code:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.responsive(lambda w, h: pz.center(pz.column([ ui.muted(f"{w:.0f} × {h:.0f} px available"), pz.row([box("1"), box("2"), box("3"), box("4")], spacing=8) if w > 400 else pz.column([box("1"), box("2")], spacing=8), ], spacing=10, align="center"))), width=480, height=150) ``` #### When to use - For layouts that must work both in a small and a large window. #### Usage ```python import pyzed as pz from pyzed import ui box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.responsive(lambda w, h: pz.center(pz.column([ ui.muted(f"{w:.0f} × {h:.0f} px available"), pz.row([box("1"), box("2"), box("3"), box("4")], spacing=8) if w > 400 else pz.column([box("1"), box("2")], spacing=8), ], spacing=10, align="center"))), width=480, height=150) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Available width The same code, given two widths: the layout changes by itself. ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.responsive(lambda w, h: pz.row([box("1"), box("2"), box("3")], spacing=8) if w > 300 else pz.column([box("1"), box("2")], spacing=8)), width=200, height=110) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.responsive(lambda w, h: pz.row([box("1"), box("2"), box("3")], spacing=8) if w > 300 else pz.column([box("1"), box("2")], spacing=8)), width=400, height=60) ``` #### Events Responsive has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.responsive` ```python responsive(function) ``` Builds its content with `function(width, height)` using the space actually available, so the layout can adapt (for example, hiding a sidebar when the window is narrow). | Property | Default | Description | | --- | --- | --- | | `function` | *required* | Function that receives `(width, height)` and returns a widget. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#responsive) #### Related - [Grid](https://albakore.github.io/pyzed/library/grid/): Equal columns, or CSS-like tracks with spans. - [Flex](https://albakore.github.io/pyzed/library/flex/): CSS flexbox: justify, align, gap, wrap and grow. ### Rich text > One block of text mixing colors, weights and links. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/rich-text/ :::tip[In plain words] A sentence where some words are bold, colored or links. ::: Combines `pz.span(text, color=, size=, font=, weight=, italic=, bg=, underline=, strikethrough=, link=)` fragments into a single block of text that wraps as a whole. Clicking a span with `link` sends that value. `pz.rich_text` · Category: [Content](https://albakore.github.io/pyzed/library/#content) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#rich_text) **Code:** ```python pz.rich_text([ pz.span("rich_text "), pz.span("mixes ", color="#e5c07b"), pz.span("styles", weight="bold", underline=True), pz.span(" on one line, with "), pz.span(" code ", font="IBM Plex Mono", bg="#3b414d", radius=4), pz.span(" and "), pz.span("links", color="#74ade8", underline=True, link="https://iced.rs"), ], size=18, width=460) ``` #### When to use - For inline highlights, mentions, search matches and short formatted messages. #### Usage ```python import pyzed as pz pz.rich_text([ pz.span("rich_text "), pz.span("mixes ", color="#e5c07b"), pz.span("styles", weight="bold", underline=True), pz.span(" on one line, with "), pz.span(" code ", font="IBM Plex Mono", bg="#3b414d", radius=4), pz.span(" and "), pz.span("links", color="#74ade8", underline=True, link="https://iced.rs"), ], size=18, width=460) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Span styles ```python pz.rich_text([pz.span("Some "), pz.span("bold", weight="bold"), pz.span(" text")], size=16) ``` ```python pz.rich_text([pz.span("Some "), pz.span("italic", italic=True), pz.span(" text")], size=16) ``` ```python pz.rich_text([pz.span("Some "), pz.span("colored", color="#e5c07b"), pz.span(" text")], size=16) ``` ```python pz.rich_text([pz.span("Some "), pz.span("underlined", underline=True), pz.span(" text")], size=16) ``` ```python pz.rich_text([pz.span("Some "), pz.span("struck", strikethrough=True), pz.span(" text")], size=16) ``` ```python pz.rich_text([pz.span("Run "), pz.span(" pyzed new ", font="mono", bg="#3b414d", color="#e6e6e6", radius=4)], size=16) ``` ```python pz.rich_text([pz.span("Read "), pz.span("the docs", color="#74ade8", underline=True, link="https://iced.rs")], size=16) ``` ```python pz.rich_text([pz.span("small ", size=12), pz.span("big", size=24, weight="bold")]) ``` #### Events Rich text has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.rich_text` ```python rich_text( spans, *, size=None, color=None, font=None, line_height=None, width=None, align_x=None, wrap=True, ) ``` Text made of spans with different styles. Plain strings are unstyled spans. | Property | Default | Description | | --- | --- | --- | | `spans` | *required* | List of `span(...)` or strings. | | `size` | `None` | Size. | | `color` | `None` | Text or icon color. | | `font` | `None` | Font of the span or text (family name, e.g. `"IBM Plex Mono"`). | | `line_height` | `None` | Line height. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `align_x` | `None` | Horizontal alignment: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `wrap` | `True` | Line wrapping / moves children to a new row when they don't fit. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#rich_text) ##### `pz.span` ```python span( text, *, color=None, size=None, font=None, weight=None, italic=False, bg=None, radius=0.0, padding=None, underline=False, strikethrough=False, link=None, ) ``` A styled span for `rich_text`. With `link`, clicking it sends (or calls) that value. | Property | Default | Description | | --- | --- | --- | | `text` | *required* | The text. | | `color` | `None` | Text or icon color. | | `size` | `None` | Size. | | `font` | `None` | Font of the span or text (family name, e.g. `"IBM Plex Mono"`). | | `weight` | `None` | Font weight: `"normal"`, `"medium"`, `"semibold"` or `"bold"`. | | `italic` | `False` | If `True`, the span is drawn in italics. | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `radius` | `0.0` | Background radius. | | `padding` | `None` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `underline` | `False` | Underline. | | `strikethrough` | `False` | Strikethrough. | | `link` | `None` | Message or callable on click. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#span) #### Related - [Text](https://albakore.github.io/pyzed/library/text/): A piece of text with size, color, font and weight. - [Markdown](https://albakore.github.io/pyzed/library/markdown/): Renders Markdown with the theme's colors. ### Row > Lays out children horizontally. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/row/ :::tip[In plain words] Puts things side by side, from left to right. ::: Lays out its children horizontally, with `spacing` between them and `align` on the cross axis. `wrap=True` moves children that don't fit to the next line. Lists are flattened and `None`/`False` are skipped, so `cond and widget` works. `pz.row` · Category: [Layout](https://albakore.github.io/pyzed/library/#layout) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#row) **Code:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([ pz.row([box("A"), box("B"), box("C")], spacing=8), pz.row([box("Start"), pz.hspace(), box("End")], spacing=8, width=420), pz.row([box("tall", h=70), box("center"), box("end")], spacing=8, align="center"), ], spacing=14) ``` #### When to use - For toolbars, button groups, form rows: anything side by side. - Push items apart with `pz.hspace()` between them. #### Usage ```python import pyzed as pz from pyzed import ui box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([ pz.row([box("A"), box("B"), box("C")], spacing=8), pz.row([box("Start"), pz.hspace(), box("End")], spacing=8, width=420), pz.row([box("tall", h=70), box("center"), box("end")], spacing=8, align="center"), ], spacing=14) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Spacing ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("A"), box("B"), box("C")]) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("A"), box("B"), box("C")], spacing=8) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("A"), box("B"), box("C")], spacing=24) ``` ##### Cross-axis alignment ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("tall", h=70), box("B"), box("C")], spacing=8, align="start") ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("tall", h=70), box("B"), box("C")], spacing=8, align="center") ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("tall", h=70), box("B"), box("C")], spacing=8, align="end") ``` ##### Options ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("Start"), pz.hspace(), box("End")], width=320) ``` ```python pz.container(pz.row([ui.chip(t) for t in ["python", "rust", "iced", "desktop", "gui", "async", "themes"]], spacing=6, wrap=True, line_spacing=6), width=260) ``` #### Examples ##### Wrapping With `wrap=True` children flow onto new lines, like tags. **Code:** ```python pz.container(pz.row([ui.chip(t) for t in ["python", "rust", "iced", "zed", "desktop", "gui", "async", "themes", "canvas", "markdown"]], spacing=6, wrap=True, line_spacing=6), width=360) ``` #### Events Row has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.row` ```python row( children=None, *, spacing=0.0, padding=None, width=None, height=None, align=None, clip=False, wrap=False, line_spacing=None, ) ``` Lays out its children horizontally. | Property | Default | Description | | --- | --- | --- | | `children` | `None` | List of children. Nested lists are flattened and `None` and `False` are ignored, so you can write `cond and widget`. | | `spacing` | `0.0` | Space between children, in pixels. | | `padding` | `None` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `align` | `None` | Alignment of the children on the cross axis: `"start"`, `"center"` or `"end"`. | | `clip` | `False` | Clips content that overflows the bounds. | | `wrap` | `False` | Line wrapping / moves children to a new row when they don't fit. | | `line_spacing` | `None` | Space between rows when `wrap=True`. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#row) #### Related - [Column](https://albakore.github.io/pyzed/library/column/): Stacks children vertically. - [Flex](https://albakore.github.io/pyzed/library/flex/): CSS flexbox: justify, align, gap, wrap and grow. - [Space](https://albakore.github.io/pyzed/library/spacing/): Fixed gaps and flexible spacers. ### Scrollable > Scrolls content that doesn't fit. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/scrollable/ :::tip[In plain words] Lets you scroll content that doesn't fit, with a scroll bar. ::: Makes its content scrollable vertically, horizontally or `"both"`. Give it an `id` to scroll from code (`pz.Command.scroll_to`), `anchor_bottom=True` to stick to the end (chats, logs), and style the rail and scroller with the theme or your own colors. `pz.scrollable` · Category: [Layout](https://albakore.github.io/pyzed/library/#layout) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#scrollable) **Code:** ```python pz.container(pz.scrollable( pz.column([ui.list_item(f"Item {i}", on_open, icon="file", inset=True) for i in range(1, 30)]), height=220, ), width=300) ``` #### When to use - For long lists, documents and any content taller than its space. #### Usage ```python import pyzed as pz from pyzed import ui pz.container(pz.scrollable( pz.column([ui.list_item(f"Item {i}", on_open, icon="file", inset=True) for i in range(1, 30)]), height=220, ), width=300) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Direction ```python pz.container(pz.scrollable(pz.column([ui.list_item(f"Item {i}", icon="file", inset=True) for i in range(1, 30)]), height=130), width=240) ``` ```python pz.container(pz.scrollable(pz.container(pz.row([ui.chip(f"tag {i}") for i in range(1, 25)], spacing=6), padding=(0, 0, 14, 0)), direction="horizontal"), width=300) ``` #### Events Scrollable has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_scroll` | `(info)` | Callable that receives a dict with `x`, `y`, `relative_x`, `relative_y`, `width`, `height`, `content_width` and `content_height`. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Feed(pz.App): def __init__(self): self.items = [f"Post {i}" for i in range(1, 30)] self.at_end = False def scrolled(self, info): # on_scroll: x, y, relative_x, relative_y, sizes self.at_end = info["relative_y"] > 0.95 def view(self): return pz.column([ pz.scrollable(pz.column([ui.list_item(p, icon="file") for p in self.items]), height=200, on_scroll=self.scrolled), ui.muted("You reached the end" if self.at_end else "Scroll down"), ], spacing=6, width=300) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Scrolled: info: dict class Feed(pz.App): items = [f"Post {i}" for i in range(1, 30)] def update(self, message): match message: case Scrolled(info) if info["relative_y"] > 0.95: self.items = self.items + [f"Post {len(self.items) + 1}"] # load more def view(self): return pz.scrollable(pz.column([ui.list_item(p) for p in self.items]), height=200, on_scroll=Scrolled) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: progress: float = 0.0 def view(state, set_state): return pz.column([ ui.progress(state.progress), pz.scrollable(pz.column([ui.list_item(f"Post {i}") for i in range(30)]), height=200, on_scroll=lambda info: set_state(progress=info["relative_y"])), ], spacing=6, width=300) ``` #### Properties and API ##### `pz.scrollable` ```python scrollable( content, *, direction='vertical', width=None, height=None, padding=None, id=None, anchor_bottom=False, rail=None, scroller=None, scroller_hover=None, scrollbar_width=8.0, scroller_width=None, margin=0.0, on_scroll=None, ) ``` Makes its content scrollable. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `direction` | `"vertical"` | `"vertical"`, `"horizontal"` or `"both"`. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `padding` | `None` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `anchor_bottom` | `False` | Keeps the scroll pinned to the end (logs, terminals, chats). | | `rail` | `None` | Scrollbar rail color. | | `scroller` | `None` | Scroller color. | | `scroller_hover` | `None` | Scroller color on hover. | | `scrollbar_width` | `8.0` | Scrollbar width. | | `scroller_width` | `None` | Scroller width (defaults to the scrollbar's). | | `margin` | `0.0` | Margin between the scrollbar and the content. | Events: `on_scroll` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#scrollable) #### Related - [Column](https://albakore.github.io/pyzed/library/column/): Stacks children vertically. - [List item](https://albakore.github.io/pyzed/library/list-item/): A list row with icon, text and an end slot. ### Search input > An input with a magnifying glass, for filtering. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/search-input/ :::tip[In plain words] A text box with a magnifying glass, for typing what you are looking for. ::: An `input` with a magnifying glass icon and `"Search…"` as placeholder. It accepts every other parameter of [`ui.input`](https://albakore.github.io/pyzed/library/input/). `ui.search_input` · Category: [Forms](https://albakore.github.io/pyzed/library/#forms) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#search_input) **Code:** ```python pz.column([ ui.search_input("", on_query), ui.search_input("button", on_query, placeholder="Search components…"), ], spacing=10, width=360) ``` #### When to use - At the top of a list, tree or table to filter it as you type. - Pair it with `ui.fuzzy_filter` to rank the best matches first. #### Usage ```python import pyzed as pz from pyzed import ui class Finder(pz.App): def __init__(self): self.query = "" self.files = ["main.py", "app.py", "theme.py"] def view(self): matches = ui.fuzzy_filter(self.query, self.files) return pz.column([ ui.search_input(self.query, lambda q: setattr(self, "query", q)), *[ui.list_item(name, icon="file") for name, _ in matches], ], spacing=6) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content ```python ui.search_input("", on_query, width=260) ``` ```python ui.search_input("button", on_query, width=260) ``` ```python ui.search_input("", on_query, placeholder="Search components…", width=260) ``` ##### Size ```python ui.search_input("", on_query, size="sm", width=260) ``` ```python ui.search_input("", on_query, size="lg", width=260) ``` ```python ui.search_input("", on_query, borderless=True, width=260) ``` #### Events Search input has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_input` | `(text)` | Callable that receives the new text on every keystroke. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Finder(pz.App): def __init__(self): self.query = "" self.files = ["main.py", "app.py", "theme.py"] def search(self, text): # on_input: every keystroke self.query = text def view(self): matches = ui.fuzzy_filter(self.query, self.files) return pz.column([ui.search_input(self.query, self.search), *[ui.list_item(name, icon="file") for name, _ in matches]], spacing=6) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Search: query: str class Finder(pz.App): query = "" files = ["main.py", "app.py", "theme.py"] def update(self, message): match message: case Search(query): self.query = query def view(self): matches = ui.fuzzy_filter(self.query, self.files) return pz.column([ui.search_input(self.query, Search), *[ui.list_item(name, icon="file") for name, _ in matches]], spacing=6) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: query: str = "" files: tuple = ("main.py", "app.py", "theme.py") def view(state, set_state): matches = ui.fuzzy_filter(state.query, list(state.files)) return pz.column([ui.search_input(state.query, lambda q: set_state(query=q)), *[ui.list_item(name, icon="file") for name, _ in matches]], spacing=6) ``` #### Properties and API ##### `ui.search_input` ```python search_input(value, on_input=None, *, placeholder='Search…', **kwargs) ``` An `input` with a magnifying glass icon. | Property | Default | Description | | --- | --- | --- | | `value` | *required* | Current text. | | `placeholder` | `"Search…"` | Hint text shown when empty. | | `**kwargs` | — | Same as `input`. | Events: `on_input` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#search_input) #### Related - [Input](https://albakore.github.io/pyzed/library/input/): A controlled text field with icon, placeholder and password mode. - [Picker](https://albakore.github.io/pyzed/library/picker/): A searchable list on a floating surface, with fuzzy matching. - [Command palette](https://albakore.github.io/pyzed/library/command-palette/): A command palette with fuzzy search and shortcuts. ### Segmented > A group of mutually exclusive text buttons. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/segmented/ :::tip[In plain words] A row of joined buttons where exactly one is chosen, like Day / Week / Month in a calendar. ::: A row of joined buttons where one is selected. `on_select` receives the chosen option. `ui.segmented` · Category: [Forms](https://albakore.github.io/pyzed/library/#forms) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#segmented) **Code:** ```python pz.column([ ui.segmented(["Editor", "Terminal", "Both"], "Editor", on_select), ui.segmented(["Day", "Week", "Month"], "Week", on_select), ], spacing=14, align="center") ``` #### When to use - To switch between two to five views or modes (Day / Week / Month). - For icons only, use a [Toggle group](https://albakore.github.io/pyzed/library/toggle-group/). #### Usage ```python import pyzed as pz from pyzed import ui class Calendar(pz.App): def __init__(self): self.mode = "Week" def view(self): return ui.segmented(["Day", "Week", "Month"], self.mode, lambda m: setattr(self, "mode", m)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python ui.segmented(["Code", "Preview"], "Code", on_select) ``` ```python ui.segmented(["Day", "Week", "Month"], "Week", on_select) ``` ```python ui.segmented(["XS", "S", "M", "L", "XL"], "M", on_select) ``` ##### Selection ```python ui.segmented(["Editor", "Terminal", "Both"], "Editor", on_select) ``` ```python ui.segmented(["Editor", "Terminal", "Both"], "Both", on_select) ``` ##### States and events ```python ui.segmented([("list", "List", "list"), ("grid", "Grid", "layout-grid")], "grid", on_select) ``` ```python ui.segmented(["Day", "Week", "Month"], "Week", on_select, width=320) ``` ```python ui.segmented(["Day", "Week", "Month"], "Day", on_select, disabled={"Month"}) ``` ```python ui.segmented(["Code", "Preview"], "Code", on_select, size="sm") ``` #### Events Segmented has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_select` | `(item)` | Callable that receives the chosen item. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Calendar(pz.App): def __init__(self): self.mode = "Week" def view(self): return pz.column([ ui.segmented(["Day", "Week", "Month"], self.mode, lambda m: setattr(self, "mode", m)), ui.label(f"Showing a {self.mode.lower()}"), ], spacing=8, align="center") ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Mode: name: str class Calendar(pz.App): mode = "Week" def update(self, message): match message: case Mode(name): self.mode = name def view(self): return ui.segmented(["Day", "Week", "Month"], self.mode, Mode) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: mode: str = "Week" def view(state, set_state): return ui.segmented(["Day", "Week", "Month"], state.mode, lambda m: set_state(mode=m)) ``` #### Tips :::tip Once clicked or reached with Tab it has the keyboard focus: ←/→ pick the previous or next option (skipping disabled ones) and Home/End the first and last. ::: #### Properties and API ##### `ui.segmented` ```python segmented( options, selected, on_select=None, *, size='md', width=None, disabled=False, id=None, theme=None, ) ``` A group of mutually exclusive buttons. | Property | Default | Description | | --- | --- | --- | | `options` | *required* | Strings, `(value, label)` or `(value, label, icon)`. | | `selected` | *required* | The active value. | | `size` | `"md"` | `"sm"`, `"md"` or `"lg"`. | | `width` | `None` | `"fill"` stretches the buttons evenly. | | `disabled` | `False` | `True`, or the values to disable. | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_select` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#segmented) #### Related - [Toggle group](https://albakore.github.io/pyzed/library/toggle-group/): An icon-only segmented control. - [Radio group](https://albakore.github.io/pyzed/library/radio-group/): Mutually exclusive options as radio buttons. - [Tab bar](https://albakore.github.io/pyzed/library/tab-bar/): Editor tabs with icons, dirty dots and close buttons. ### Select > A dropdown to choose one option. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/select/ :::tip[In plain words] A box that opens a list so you can pick one option from it. ::: A dropdown in the theme style. Options are shown with `str()`, so strings, enums or dataclasses all work, and `on_select` receives the chosen object. `ui.select` · Category: [Forms](https://albakore.github.io/pyzed/library/#forms) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#select) **Code:** ```python pz.row([ ui.select(["Python", "Rust", "TypeScript"], "Rust", on_select, width=180), ui.select(["UTF-8", "Latin-1"], None, on_select, placeholder="Encoding…", width=180), ], spacing=12) ``` #### When to use - To pick one of many options when there is no room to show them all (language, theme, encoding). - With two to five short options, [Segmented](https://albakore.github.io/pyzed/library/segmented/) or a [Radio group](https://albakore.github.io/pyzed/library/radio-group/) shows them at a glance. #### Usage ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.language = "Python" def view(self): return ui.select(["Python", "Rust", "TypeScript"], self.language, lambda value: setattr(self, "language", value), width=180) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### State ```python ui.select(["Python", "Rust", "TypeScript"], "Rust", on_select, width=200) ``` ```python ui.select(["UTF-8", "Latin-1"], None, on_select, placeholder="Encoding…", width=200) ``` ```python ui.select(["One Dark"], "One Dark", width=200) ``` ##### Size ```python ui.select(["Python", "Rust"], "Python", on_select, size="sm", width=200) ``` ```python ui.select(["Python", "Rust"], "Python", on_select, size="md", width=200) ``` ```python ui.select(["Python", "Rust"], "Python", on_select, size="lg", width=200) ``` ##### States and events `on_open` and `on_close` run when the menu opens and closes. ```python ui.select(["One Dark"], "One Dark", on_select, disabled=True, width=200) ``` ```python ui.select(["UTF-8", "Latin-1"], None, on_select, placeholder="Encoding…", invalid=True, width=200) ``` ##### Base widget The `pz.*` widget underneath: no theme, every color and size is yours. ```python pz.pick_list(["Small", "Medium", "Large"], "Medium", on_select, width=200, bg="#2b2140", color="#e9d5ff", border_color="#c678dd", radius=8) ``` #### Events Select has **3 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_select` | `ui.select`, `pz.pick_list` | `(item)` | Callable that receives the chosen item. | | `on_open` | `ui.select`, `pz.pick_list` | `()` | Sent when the menu opens. | | `on_close` | `ui.select`, `pz.pick_list` | `()` | Sent when the menu closes. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Settings(pz.App): def __init__(self): self.language = "Python" self.open = False def view(self): return pz.column([ ui.select(["Python", "Rust", "TypeScript"], self.language, lambda value: setattr(self, "language", value), # on_select: the option on_open=lambda: setattr(self, "open", True), on_close=lambda: setattr(self, "open", False), width=180), ui.muted("Choosing…" if self.open else f"Language: {self.language}"), ], spacing=8) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Language: name: str class Settings(pz.App): language = "Python" def update(self, message): match message: case Language(name): self.language = name def view(self): return ui.select(["Python", "Rust", "TypeScript"], self.language, Language, width=180) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: language: str = "Python" def view(state, set_state): return ui.select(["Python", "Rust", "TypeScript"], state.language, lambda value: set_state(language=value), width=180) ``` #### Tips :::tip It takes the keyboard focus like a text field (a click or Tab) and keeps it after you pick an option: then ↓/↑ choose the next or previous option without opening the menu, Home/End the first and last, and typing jumps to the option that starts with those letters. Enter, Space, Alt+↓ or F4 open the menu; there ↑/↓ move, Enter picks and Esc closes. Pass `id=` to focus it with `pz.Command.focus(id)`. ::: #### Properties and API ##### `ui.select` ```python select( options, selected=None, on_select=None, *, placeholder='Select…', width=None, size='md', disabled=False, invalid=False, on_open=None, on_close=None, id=None, theme=None, ) ``` A dropdown with the theme style. Options are displayed with `str()`, so strings, enums, dataclasses or database models all work; `on_select` receives the chosen object, not its label. | Property | Default | Description | | --- | --- | --- | | `options` | *required* | List of options. | | `selected` | `None` | Current option. | | `placeholder` | `"Select…"` | Hint text shown when empty. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `size` | `"md"` | `"sm"`, `"md"` or `"lg"`. | | `disabled` | `False` | Greys it out. | | `invalid` | `False` | Paints the border in the error color. | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_select`, `on_open`, `on_close` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#select) ##### `pz.pick_list` ```python pick_list( options, selected=None, on_select=None, *, on_open=None, on_close=None, placeholder=None, width=None, padding=None, text_size=None, font=None, handle=None, handle_size=12.0, menu_height=None, bg=None, color=None, placeholder_color=None, handle_color=None, border_color=None, hover_border_color=None, radius=None, menu_bg=None, menu_color=None, menu_border_color=None, selected_bg=None, selected_color=None, focus_border_color=None, id=None, ) ``` A dropdown to choose one of `options`. `on_select(option)` receives the chosen one. For the themed dropdown use `ui.select`. It takes the keyboard focus like a text field (a click, Tab or `Command.focus(id)`) and keeps it after a choice: ↑/↓ then pick the previous or next option without opening it, Home/End the first and last and typing jumps to an option; Enter, Space, Alt+↓ or F4 open the menu, where ↑/↓ move, Enter picks and Esc closes. | Property | Default | Description | | --- | --- | --- | | `options` | *required* | List of options. | | `selected` | `None` | Current option. | | `placeholder` | `None` | Hint text shown when empty. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `padding` | `None` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `text_size` | `None` | Text size. | | `font` | `None` | Font family. | | `handle` | `None` | Arrow icon character. | | `handle_size` | `12.0` | Arrow size. | | `menu_height` | `None` | Maximum menu height. | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `color` | `None` | Text or icon color. | | `placeholder_color` | `None` | Placeholder color. | | `handle_color` | `None` | Arrow color. | | `border_color` | `None` | Border color. | | `hover_border_color` | `None` | Border color on hover. | | `radius` | `None` | Corner radius: a number or `(top_left, top_right, bottom_right, bottom_left)`. | | `menu_bg` | `None` | Menu background. | | `menu_color` | `None` | Menu text. | | `menu_border_color` | `None` | Menu border. | | `selected_bg` | `None` | Background of the highlighted option. | | `selected_color` | `None` | Text of the highlighted option. | | `focus_border_color` | `None` | Border color while it has the keyboard focus. | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | Events: `on_select`, `on_open`, `on_close` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#pick_list) #### Related - [Segmented](https://albakore.github.io/pyzed/library/segmented/): A group of mutually exclusive text buttons. - [Radio group](https://albakore.github.io/pyzed/library/radio-group/): Mutually exclusive options as radio buttons. - [Picker](https://albakore.github.io/pyzed/library/picker/): A searchable list on a floating surface, with fuzzy matching. ### Service row > A settings row for an MCP server, agent or skill: status, switch and menu. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/service-row/ :::tip[In plain words] One line of a settings page for a tool the assistant uses: whether it works, a switch to turn it on or off, and a menu with more options. ::: `ui.ServiceRow` is a component for one thing an AI agent uses (an MCP server, an external agent, a skill) on a settings page: an icon, the name with its `version` and a status dot, the `detail` (its command or URL, in a monospace font) and a `summary`; then a spinner while `busy`, a main button (`primary`, such as Update or Install), an on/off switch (`enabled`; `None` hides it) and a `⋯` menu with the `actions` (`ui.MenuItem`). `status` colors the dot: `running` and `installed` green, `starting` yellow, `update` in the accent color, `error` red, `off` and `missing` gray. `status_text` is the dot's tooltip, and it shows under the name for `error`, `missing` and `update`. `ui.ServiceRow` · Category: [AI agents](https://albakore.github.io/pyzed/library/#ai-agents) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#servicerow) **Code:** ```python pz.column([ ui.ServiceRow(name="github", detail="npx -y @modelcontextprotocol/server-github", icon_name="github", status="running", enabled=True, on_toggle=on_toggle, actions=[ui.MenuItem("Edit", on_edit, icon="pencil")]), ui.ServiceRow(name="filesystem", detail="uvx mcp-server-filesystem ~/notes", status="error", status_text="Exited with code 1", enabled=True, on_toggle=on_toggle), ui.ServiceRow(name="Gemini CLI", version="0.64.0", icon_name="bot", status="update", status_text="0.65.0 is available", primary="Update", on_primary=on_update), ], spacing=4, width=560) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - In the settings of an app with AI agents: one row per MCP server, agent or skill. - For any list of services that can be turned on and off, updated or removed. #### Usage ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.on = True def view(self): return ui.ServiceRow(name="github", detail="npx -y @modelcontextprotocol/server-github", status="running" if self.on else "off", enabled=self.on, on_toggle=lambda e: setattr(self, "on", e.value), actions=[ui.MenuItem("Remove", self.remove, icon="trash-2", danger=True)]) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### State `enabled=False` also dims the name and the icon. ```python pz.container(ui.ServiceRow(name="github", detail="npx -y server-github", status="running", enabled=True), width=420) ``` ```python pz.container(ui.ServiceRow(name="github", detail="npx -y server-github", status="starting", enabled=True), width=420) ``` ```python pz.container(ui.ServiceRow(name="github", detail="npx -y server-github", status="error", status_text="Exited with code 1", enabled=True), width=420) ``` ```python pz.container(ui.ServiceRow(name="github", detail="npx -y server-github", status="off", enabled=False), width=420) ``` ```python pz.container(ui.ServiceRow(name="Gemini CLI", version="0.64.0", icon_name="bot", status="installed"), width=420) ``` ```python pz.container(ui.ServiceRow(name="Codex CLI", icon_name="bot", status="missing", status_text="Not installed yet", primary="Install"), width=420) ``` ```python pz.container(ui.ServiceRow(name="Gemini CLI", version="0.64.0", icon_name="bot", status="update", status_text="0.65.0 is available", primary="Update"), width=420) ``` ##### Options The `⋯` menu is drawn open, as after a click on it. ```python pz.container(ui.ServiceRow(name="notes", icon_name="book-open", status="running", summary="Reads and writes the notes folder."), width=420) ``` ```python pz.container(ui.ServiceRow(name="Gemini CLI", version="0.64.0", icon_name="bot", busy=True, primary="Update"), width=420) ``` ```python pz.container(ui.ServiceRow(name="github", detail="npx -y server-github", status="running", enabled=True, actions=[ui.MenuItem("Edit", on_edit, icon="pencil"), ui.MenuItem("Remove", on_remove, icon="trash-2", danger=True)]), width=420) ``` #### Events Service row has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. | Event | Handler gets | When | | --- | --- | --- | | `on_toggle` | `(event)` | The switch was flipped. | | `on_primary` | `(event)` | The main button was pressed. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Settings(pz.App): def __init__(self): self.on = True self.updates = 0 def view(self): return ui.ServiceRow(name="github", status="running" if self.on else "off", enabled=self.on, primary="Update", on_toggle=lambda e: setattr(self, "on", e.value), # the switch on_primary=lambda e: setattr(self, "updates", self.updates + 1)) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Enable: on: bool class Settings(pz.App): on = True def update(self, message): match message: case Enable(on): self.on = on def view(self): return ui.ServiceRow(name="github", enabled=self.on, status="running" if self.on else "off", on_toggle=lambda e: Enable(e.value)) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: on: bool = True def view(state, set_state): return ui.ServiceRow(name="github", enabled=state.on, status="running" if state.on else "off", on_toggle=lambda e: set_state(on=e.value)) ``` #### Properties and API ##### `ui.ServiceRow` ```python class ServiceRow ``` A component for one thing an agent uses (an MCP server, an external agent, a skill), like the rows of Zed's agent settings: a status dot, the name and command, an on/off switch, a main button and a `⋯` menu. | Property | Default | Description | | --- | --- | --- | | `name` | `""` | Name. | | `detail` | `""` | The command or URL, in a monospace font. | | `summary` | `""` | A line about it. | | `icon_name` | `"plug"` | The icon. | | `status` | `""` | `running`, `starting`, `error`, `off`, `installed`, `missing` or `update` (the dot's color). | | `status_text` | `""` | The dot's tooltip; shown under the name for errors and updates. | | `version` | `""` | A version or a source. | | `enabled` | `None` | The switch (`None` hides it). | | `busy` | `False` | Shows a spinner. | | `primary` | `""` | Text of the main button (Update, Install…). | | `actions` | `None` | `ui.MenuItem` entries of the `⋯` menu. | | `theme` | `None` | Theme. | Events: `on_toggle`, `on_primary` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#servicerow) #### Related - [Catalog](https://albakore.github.io/pyzed/library/catalog/): Things to install, with a search box and Install or Update. - [Switch](https://albakore.github.io/pyzed/library/switch/): An on/off switch for settings that apply at once. - [Menu](https://albakore.github.io/pyzed/library/menu/): A context menu with icons, shortcuts and checks. - [Settings group](https://albakore.github.io/pyzed/library/settings-group/): A titled block of field rows, for settings screens. ### Settings group > A titled block of field rows, for settings screens. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/settings-group/ :::tip[In plain words] A titled block of settings, like a section of a preferences window. ::: A titled block of `ui.field` rows separated by lines, with an optional description, like a settings screen. `ui.settings_group` · Category: [Data](https://albakore.github.io/pyzed/library/#data) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#settings_group) **Code:** ```python pz.container(ui.settings_group("Editor", [ ui.field("Font size", ui.number_field(14, on_change, suffix="px")), ui.field("Soft wrap", ui.switch(True, on_toggle)), ui.field("Theme", ui.select(["One Dark", "One Light"], "One Dark", on_select, width=150)), ], description="Editor preferences."), width=460) ``` #### When to use - To organize a preferences screen into sections (Editor, Files, Git…). #### Usage ```python import pyzed as pz from pyzed import ui pz.container(ui.settings_group("Editor", [ ui.field("Font size", ui.number_field(14, on_change, suffix="px")), ui.field("Soft wrap", ui.switch(True, on_toggle)), ui.field("Theme", ui.select(["One Dark", "One Light"], "One Dark", on_select, width=150)), ], description="Editor preferences."), width=460) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python pz.container(ui.settings_group("Files", [ ui.field("Autosave", ui.switch(True, on_toggle)), ui.field("Trim whitespace", ui.checkbox(False, on_toggle)), ]), width=380) ``` ```python pz.container(ui.settings_group("Git", [ ui.field("Inline blame", ui.switch(False, on_toggle)), ], description="Shown in the editor gutter."), width=380) ``` ##### States and events ```python pz.container(ui.settings_group("Editor", [ui.field("Wrap lines", ui.switch(True, on_toggle))], end=ui.button("Reset", on_press, size="sm", variant="ghost")), width=420) ``` #### Events Settings group has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.settings_group` ```python settings_group(title, rows, *, description=None, end=None, theme=None) ``` A titled block of `ui.field` rows separated by lines, like a settings screen. | Property | Default | Description | | --- | --- | --- | | `title` | *required* | Group title. | | `rows` | *required* | The rows (usually `ui.field`). | | `description` | `None` | Secondary text. | | `end` | `None` | Widget at the right of the title (a Reset button…). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#settings_group) #### Related - [Field](https://albakore.github.io/pyzed/library/field/): A settings row: title and description left, control right. - [Accordion](https://albakore.github.io/pyzed/library/accordion/): Stacked collapsible sections, like the panels of a settings screen. - [Card](https://albakore.github.io/pyzed/library/card/): A bordered surface with an optional title and actions. ### Shader > A widget painted on the GPU by your own WGSL code. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/shader/ :::tip[In plain words] A surface painted by the graphics card, for moving backgrounds and visual effects. ::: `pz.shader` runs a function you write in WGSL, `fn shade(uv, px, size, time) -> vec4`, for every pixel of the widget, every frame, on the GPU: animated backgrounds, plasma, fire or a radar sweep cost Python nothing. Each entry of `uniforms` becomes a WGSL function of the same name, and `mouse()` gives the pointer. Without a GPU the widget shows `fallback`. See the [Shaders guide](https://albakore.github.io/pyzed/shading/shaders/) and the [showcase](https://albakore.github.io/pyzed/showcase/). `pz.shader` · Category: [Content](https://albakore.github.io/pyzed/library/#content) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#shader) **Code:** ```python pz.shader(""" fn shade(uv: vec2, px: vec2, size: vec2, time: f32) -> vec4 { let wave = 0.5 + 0.5 * sin(uv.x * 12.0 + uv.y * 4.0 + time * speed()); return vec4(mix(vec3(0.08, 0.09, 0.2), tint().rgb, wave), 1.0); } """, uniforms={"speed": 2.0, "tint": "#c678dd"}, width=640, height=240, time=0.5, fallback=pz.Gradient(["#14172f", "#c678dd"], angle=0)) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - For animated backgrounds behind glass panels, menus and HUDs. - For game visuals and effects that would be too slow to draw from Python every frame. #### Usage ```python import pyzed as pz pz.shader(""" fn shade(uv: vec2, px: vec2, size: vec2, time: f32) -> vec4 { let wave = 0.5 + 0.5 * sin(uv.x * 12.0 + uv.y * 4.0 + time * speed()); return vec4(mix(vec3(0.08, 0.09, 0.2), tint().rgb, wave), 1.0); } """, uniforms={"speed": 2.0, "tint": "#c678dd"}, width=640, height=240, time=0.5, fallback=pz.Gradient(["#14172f", "#c678dd"], angle=0)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Uniforms Each capture is one frame at `time=0.5`; run the example to see it move. Without a GPU the widget draws its `fallback`. ```python pz.shader(""" fn shade(uv: vec2, px: vec2, size: vec2, time: f32) -> vec4 { let wave = 0.5 + 0.5 * sin(uv.x * 12.0 + uv.y * 4.0 + time * 2.0); return vec4(mix(vec3(0.08, 0.09, 0.2), tint().rgb, wave), 1.0); } """, uniforms={"tint": "#c678dd"}, width=260, height=110, time=0.5, fallback=pz.Gradient(["#14172f", "#c678dd"], angle=0)) ``` ```python pz.shader(""" fn shade(uv: vec2, px: vec2, size: vec2, time: f32) -> vec4 { let wave = 0.5 + 0.5 * sin(uv.x * 12.0 + uv.y * 4.0 + time * 2.0); return vec4(mix(vec3(0.08, 0.09, 0.2), tint().rgb, wave), 1.0); } """, uniforms={"tint": "#56b6c2"}, width=260, height=110, time=0.5, fallback=pz.Gradient(["#14172f", "#56b6c2"], angle=0)) ``` ```python pz.shader(""" fn shade(uv: vec2, px: vec2, size: vec2, time: f32) -> vec4 { let wave = 0.5 + 0.5 * sin(uv.x * 12.0 + uv.y * 4.0 + time * 2.0); return vec4(mix(vec3(0.08, 0.09, 0.2), tint().rgb, wave), 1.0); } """, uniforms={"tint": "#e5c07b"}, width=260, height=110, time=0.5, fallback=pz.Gradient(["#14172f", "#e5c07b"], angle=0)) ``` #### Events Shader has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Tips :::tip Shaders need the GPU renderer. Always pass a `fallback` that looks right on its own: it is what the software renderer and machines without a GPU draw. ::: #### Properties and API ##### `pz.shader` ```python shader( code, *, uniforms=None, textures=None, width=None, height=None, animate=True, time=None, fallback=None, ) ``` A widget painted on the GPU by your WGSL `fn shade(uv, px, size, time) -> vec4`, which returns each pixel's color, or by a shader graph made of nodes (a `.pzshad` path or a [`pyzed.shading`](https://albakore.github.io/pyzed/reference/shading/) `Graph` or `Shader`). Each entry of `uniforms` becomes a function of the same name (`{"speed": 2}` -> `speed()`); `mouse()` gives the pointer. Without a GPU the widget shows `fallback`. Raises `ValueError` with the compiler's message for invalid code. See the [shaders](https://albakore.github.io/pyzed/shading/shaders/) guide. | Property | Default | Description | | --- | --- | --- | | `code` | *required* | WGSL source that defines `shade` (and any helper functions), or a `.pzshad` path, `Graph` or `Shader`. | | `uniforms` | `None` | Up to 32 named values: a number, 2 to 4 numbers or a color. With a `.pzshad`, the shader's mutable variables by name. | | `textures` | `None` | Up to 8 images, `{name: path}`: each becomes `name(uv)`, the image's color at `uv`. | | `width` | `None` | Width (`"fill"` by default). | | `height` | `None` | Height (`"fill"` by default). | | `animate` | `True` | Redraw every frame so `time` moves (`True`). | | `time` | `None` | A fixed `time` in seconds, for a still frame. | | `fallback` | `None` | Color or gradient shown without a GPU. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#shader) #### Related - [Effects](https://albakore.github.io/pyzed/library/effects/): Glow, bloom, glass and opacity as style properties. - [Canvas](https://albakore.github.io/pyzed/library/canvas/): 2D drawing with shapes, paths, text and mouse events. - [Animation](https://albakore.github.io/pyzed/library/animation/): Presets, keyframes and transitions for any widget. ### Shortcuts > A searchable list of key bindings, grouped. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/shortcuts/ :::tip[In plain words] A searchable list of all the keyboard shortcuts of an app. ::: A searchable, grouped list of key bindings for a Keyboard shortcuts dialog. `groups` are `(title, [(action, keys), ...])` pairs. `ui.shortcuts` · Category: [Code and tools](https://albakore.github.io/pyzed/library/#code-and-tools) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#shortcuts) **Code:** ```python pz.container(ui.shortcuts([ ("File", [("Save", "ctrl-s"), ("Open file", "ctrl-p")]), ("View", [("Command palette", "ctrl-shift-p"), ("Toggle left dock", "ctrl-b")]), ], on_query=on_query, height=240), width=420) ``` #### When to use - Inside a `ui.modal` opened from a Help menu or with Ctrl+K Ctrl+S. #### Usage ```python import pyzed as pz from pyzed import ui pz.container(ui.shortcuts([ ("File", [("Save", "ctrl-s"), ("Open file", "ctrl-p")]), ("View", [("Command palette", "ctrl-shift-p"), ("Toggle left dock", "ctrl-b")]), ], on_query=on_query, height=240), width=420) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python pz.container(ui.shortcuts([("File", [("Save", "ctrl-s"), ("Open", "ctrl-o")])], on_query=on_query, height=130), width=360) ``` ```python pz.container(ui.shortcuts([("View", [("Command palette", "ctrl-shift-p"), ("Zoom in", "ctrl-=")])], query="pal", on_query=on_query, height=110), width=360) ``` #### Events Shortcuts has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_query` | `(text)` | Adds a search box. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Help(pz.App): def __init__(self): self.query = "" def view(self): return ui.shortcuts([("File", [("Save", "ctrl-s"), ("Open file", "ctrl-p")])], query=self.query, on_query=lambda q: setattr(self, "query", q)) # adds search ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Search: query: str class Help(pz.App): query = "" def update(self, message): match message: case Search(query): self.query = query def view(self): return ui.shortcuts([("File", [("Save", "ctrl-s")])], query=self.query, on_query=Search) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: query: str = "" def view(state, set_state): return ui.shortcuts([("File", [("Save", "ctrl-s"), ("Open file", "ctrl-p")])], query=state.query, on_query=lambda q: set_state(query=q)) ``` #### Properties and API ##### `ui.shortcuts` ```python shortcuts(groups, *, query='', on_query=None, height=380, theme=None) ``` A searchable list of key bindings, grouped (for a Keyboard shortcuts dialog). | Property | Default | Description | | --- | --- | --- | | `groups` | *required* | `[(title, [(label, keys), ...]), ...]`, keys like `"ctrl-s"`. | | `query` | `""` | Filter text. | | `height` | `380` | Height of the list. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_query` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#shortcuts) #### Related - [Kbd](https://albakore.github.io/pyzed/library/kbd/): Keyboard shortcuts drawn as keys, with symbols on macOS. - [Keybinding hint](https://albakore.github.io/pyzed/library/keybinding-hint/): A muted “Open file Ctrl P” hint. - [About](https://albakore.github.io/pyzed/library/about/): An About box with icon, version, details and links. ### Sign-in prompt > Asks to sign in, then shows the browser step, a code and Cancel. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/auth-prompt/ :::tip[In plain words] A box that asks you to sign in, with a button for each way to do it. ::: `ui.AuthPrompt` is a component that asks to sign in, for an agent or a service that needs it. It shows a `title`, an optional `detail` and one button per way to sign in: `methods` are dicts with `id`, `name` and optional `description` (shown as a tooltip) and `icon`, and the first one is the main button. Picking one reports `on_pick` with its id. While `waiting` holds that id it shows a spinner and what is happening (`waiting_text`, or "Waiting for the browser…"), the `code` to type with a copy button when there is one, Open the page again when `url` is set, and Cancel. `error` shows a message in red. `ui.AuthPrompt` · Category: [AI agents](https://albakore.github.io/pyzed/library/#ai-agents) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#authprompt) **Code:** ```python pz.container(ui.AuthPrompt( title="Sign in to use the agent", detail="Your account is used to run the model.", methods=[{"id": "browser", "name": "Log in with the browser", "icon": "globe"}, {"id": "key", "name": "Use an API key", "icon": "key-round"}], on_pick=on_pick, ), width=440) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - Where an agent or a connected service needs the user to sign in before it can work. - For a device sign-in: show the `code` the user types on the page that opened. #### Usage ```python import pyzed as pz from pyzed import ui class Agent(pz.App): def __init__(self): self.waiting, self.code = "", "" def sign_in(self, method): self.waiting = method self.code = start_device_login() # opens the browser and returns the code to type def view(self): return ui.AuthPrompt(title="Sign in to use the agent", waiting=self.waiting, code=self.code, methods=[{"id": "browser", "name": "Log in with the browser"}], on_pick=lambda e: self.sign_in(e.value), on_cancel=lambda e: setattr(self, "waiting", ""), on_copy=lambda e: pz.Command.copy(e.value)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Ways to sign in The first method is the main (solid) button. ```python pz.container(ui.AuthPrompt(title="Sign in to continue", methods=[{"id": "browser", "name": "Log in with the browser"}]), width=380) ``` ```python pz.container(ui.AuthPrompt(title="Sign in to continue", methods=[ {"id": "browser", "name": "Log in with the browser", "icon": "globe"}, {"id": "key", "name": "Use an API key", "icon": "key-round"}, ]), width=380) ``` ```python pz.container(ui.AuthPrompt(title="Sign in to continue", detail="The agent needs your account.", methods=[{"id": "browser", "name": "Log in with the browser"}]), width=380) ``` ```python pz.container(ui.AuthPrompt(title="Sign in to continue", error="The sign-in was cancelled.", methods=[{"id": "browser", "name": "Try again"}]), width=380) ``` ##### Waiting ```python pz.container(ui.AuthPrompt(title="Sign in to continue", waiting="browser"), width=380) ``` ```python pz.container(ui.AuthPrompt(title="Sign in to continue", waiting="cli", waiting_text="Finish signing in in the terminal."), width=380) ``` ```python pz.container(ui.AuthPrompt(title="Sign in to continue", waiting="device", code="WDJB-MJHT", url="https://example.com/device"), width=380) ``` #### Events Sign-in prompt has **4 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. | Event | Handler gets | When | | --- | --- | --- | | `on_pick` | `(event)` | A method was picked (`event.value` is its id). | | `on_cancel` | `(event)` | Cancel. | | `on_open_url` | `(event)` | Open the page again. | | `on_copy` | `(event)` | The code was copied. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Agent(pz.App): def __init__(self): self.waiting, self.code = "", "" def sign_in(self, method): # on_pick: event.value is the method's id self.waiting, self.code = method, "ABCD-1234" def view(self): return ui.AuthPrompt(title="Sign in to use the agent", waiting=self.waiting, code=self.code, methods=[{"id": "browser", "name": "Log in with the browser"}], on_pick=lambda e: self.sign_in(e.value), on_cancel=lambda e: setattr(self, "waiting", ""), on_copy=lambda e: pz.Command.copy(e.value)) # the code ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Picked: method: str class Agent(pz.App): waiting = "" def update(self, message): match message: case Picked(method): self.waiting = method case "cancel": self.waiting = "" def view(self): return ui.AuthPrompt(title="Sign in", waiting=self.waiting, methods=[{"id": "key", "name": "Use an API key"}], on_pick=lambda e: Picked(e.value), on_cancel="cancel") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: waiting: str = "" def view(state, set_state): return ui.AuthPrompt(title="Sign in", waiting=state.waiting, methods=[{"id": "browser", "name": "Log in with the browser"}], on_pick=lambda e: set_state(waiting=e.value), on_cancel=lambda e: set_state(waiting="")) ``` #### Tips :::tip The copy button only reports `on_copy` with the code in `event.value`; return `pz.Command.copy(e.value)` from it to put the code on the clipboard. ::: #### Properties and API ##### `ui.AuthPrompt` ```python class AuthPrompt ``` A component that asks to sign in, like Zed when an agent needs it: one button per way to sign in, then, while one runs, what is happening (the browser or a terminal is open), a code to type when there is one, Open the page again and Cancel. | Property | Default | Description | | --- | --- | --- | | `title` | `""` | The heading. | | `detail` | `""` | A line under it. | | `methods` | `None` | Dicts with `id`, `name`, optional `description` and `icon`; the first is the main one. | | `waiting` | `""` | The id of the method in progress (`""` when none). | | `waiting_text` | `""` | What to show while waiting. | | `code` | `""` | A code to type (device sign-in). | | `url` | `""` | The page that was opened. | | `error` | `""` | An error to show. | | `icon_name` | `"key-round"` | The icon. | | `theme` | `None` | Theme. | Events: `on_pick`, `on_cancel`, `on_open_url`, `on_copy` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#authprompt) #### Related - [Callout](https://albakore.github.io/pyzed/library/callout/): An inline notice with a title, text and actions. - [Button](https://albakore.github.io/pyzed/library/button/): The themed button: variants, sizes, icons and shortcuts. - [Service row](https://albakore.github.io/pyzed/library/service-row/): A settings row for an MCP server, agent or skill: status, switch and menu. ### Slider > Pick a number in a range by dragging. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/slider/ :::tip[In plain words] A bar with a knob you drag to pick an amount, like a volume control. ::: A themed slider between `min` and `max` (0 to 1 by default) that moves in `step` increments. `on_change` receives the new value while dragging. `ui.slider` · Category: [Forms](https://albakore.github.io/pyzed/library/#forms) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#slider) **Code:** ```python pz.column([ ui.slider(0.35, on_change), ui.slider(70, on_change, min=0, max=100, step=1), ], spacing=18, width=360) ``` #### When to use - For values where the exact number matters less than the feel: volume, opacity, zoom. - When users need to type an exact value, use a [Number field](https://albakore.github.io/pyzed/library/number-field/). #### Usage ```python import pyzed as pz from pyzed import ui class Player(pz.App): def __init__(self): self.volume = 70 def view(self): return pz.row([ ui.slider(self.volume, lambda v: setattr(self, "volume", v), min=0, max=100, step=1), ui.label(f"{self.volume:.0f}%"), ], spacing=12, align="center") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Value ```python ui.slider(0.0, on_change, width=240) ``` ```python ui.slider(0.35, on_change, width=240) ``` ```python ui.slider(1.0, on_change, width=240) ``` ##### Range and step ```python ui.slider(70, on_change, min=0, max=100, step=1, width=240) ``` ```python pz.column([ui.slider(40, on_change, min=0, max=100, step=10, width=240), ui.muted("40 %")], spacing=6, align="center") ``` ##### States and events `on_release` runs when the drag ends: save or apply there instead of on every move. ```python ui.slider(70, on_change, min=0, max=100, step=1, show_value=True, width=240) ``` ```python ui.slider(0.35, on_change, show_value="{:.0%}", width=240) ``` ```python ui.slider(6, on_change, min=0, max=10, step=1, vertical=True, height=110, show_value=True) ``` ```python ui.slider(0.6, on_change, disabled=True, width=240) ``` ##### Base widget The `pz.*` widget underneath: no theme, every color and size is yours. ```python pz.slider(0, 100, 60, on_change=on_change, width=240, accent="#e5c07b", knob_color="#ffffff") ``` ```python pz.vertical_slider(0, 100, 60, on_change=on_change, height=120, accent="#56b6c2") ``` #### Events Slider has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_change` | `ui.slider`, `pz.slider` | `(value)` | Callable that receives the new value. | | `on_release` | `ui.slider`, `pz.slider` | `()` | Sent when the drag ends (save or apply there). | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Player(pz.App): def __init__(self): self.volume = 70 self.saved = 70 def view(self): return pz.row([ ui.slider(self.volume, lambda v: setattr(self, "volume", v), # on_change: while dragging min=0, max=100, step=1, on_release=lambda: setattr(self, "saved", self.volume)), # on_release: once ui.label(f"{self.volume:.0f}% (saved {self.saved:.0f}%)"), ], spacing=12, align="center", width=420) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Volume: value: float class Player(pz.App): volume = 70 saved = 70 def update(self, message): match message: case Volume(value): self.volume = value case "save": self.saved = self.volume def view(self): return ui.slider(self.volume, Volume, min=0, max=100, step=1, on_release="save") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: volume: float = 70 def view(state, set_state): return pz.row([ ui.slider(state.volume, lambda v: set_state(volume=v), min=0, max=100, step=1), ui.label(f"{state.volume:.0f}%"), ], spacing=12, align="center", width=420) ``` #### Tips :::tip Once clicked or reached with Tab it has the keyboard focus: the arrows move it one `step`, with Shift (or Page Up/Down) ten, and Home/End jump to `min`/`max`. ::: #### Properties and API ##### `ui.slider` ```python slider( value, on_change=None, *, min=0.0, max=1.0, step=0.01, on_release=None, vertical=False, width=None, height=None, disabled=False, show_value=False, id=None, theme=None, ) ``` A slider. | Property | Default | Description | | --- | --- | --- | | `value` | *required* | Current value. | | `min` | `0.0` | Minimum. | | `max` | `1.0` | Maximum. | | `step` | `0.01` | Step. | | `vertical` | `False` | Stands it up. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `disabled` | `False` | Greys it out. | | `show_value` | `False` | Shows the number next to it: `True`, or a format such as `"{:.0%}"`. | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_change`, `on_release` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#slider) ##### `pz.slider` ```python slider( min, max, value, *, step=1.0, on_change=None, on_release=None, width=None, height=None, vertical=False, accent=None, bg=None, knob_color=None, border_color=None, ) ``` A slider between `min` and `max`. | Property | Default | Description | | --- | --- | --- | | `min` | *required* | Minimum. | | `max` | *required* | Maximum. | | `value` | *required* | Current value. | | `step` | `1.0` | Step. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `vertical` | `False` | Vertical orientation. | | `accent` | `None` | Accent color (active part). | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `knob_color` | `None` | Knob color. | | `border_color` | `None` | Border color. | Events: `on_change`, `on_release` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#slider) ##### `pz.vertical_slider` ```python vertical_slider(min, max, value, **kwargs) ``` A vertical slider (`slider(..., vertical=True)`). | Property | Default | Description | | --- | --- | --- | | `min` | *required* | Minimum. | | `max` | *required* | Maximum. | | `value` | *required* | Current value. | | `**kwargs` | — | Same as `slider`. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#vertical_slider) #### Related - [Number field](https://albakore.github.io/pyzed/library/number-field/): A number with −/+ buttons, limits, step and suffix. - [Progress](https://albakore.github.io/pyzed/library/progress/): A thin progress bar in a semantic color. ### Space > Fixed gaps and flexible spacers. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/spacing/ :::tip[In plain words] Empty space: a fixed gap, or a spring that pushes things apart. ::: `pz.space(width, height)` is an empty box of a given size. `pz.hspace()` and `pz.vspace()` are flexible gaps that take the free space, pushing their siblings to the edges of a row or column. `pz.space` · Category: [Layout](https://albakore.github.io/pyzed/library/#layout) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#space) **Code:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([ pz.row([box("left"), pz.hspace(), box("right")], width=440), pz.row([box("a"), pz.space(width=60), box("60 px later", w=140)]), ], spacing=16) ``` #### When to use - To push actions to the right of a toolbar, or a footer to the bottom of a panel. #### Usage ```python import pyzed as pz from pyzed import ui box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([ pz.row([box("left"), pz.hspace(), box("right")], width=440), pz.row([box("a"), pz.space(width=60), box("60 px later", w=140)]), ], spacing=16) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("left"), pz.hspace(), box("right")], width=320) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("a"), pz.space(width=60), box("b")]) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([box("top"), pz.vspace(), box("bottom")], height=150) ``` #### Events Space has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.space` ```python space(width=None, height=None) ``` Empty space of a given size. | Property | Default | Description | | --- | --- | --- | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#space) ##### `pz.hspace` ```python hspace(width='fill') ``` Flexible horizontal gap: pushes the siblings in a row towards the edges. | Property | Default | Description | | --- | --- | --- | | `width` | `"fill"` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#hspace) ##### `pz.vspace` ```python vspace(height='fill') ``` Flexible vertical gap: pushes the siblings in a column towards the edges. | Property | Default | Description | | --- | --- | --- | | `height` | `"fill"` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#vspace) #### Related - [Row](https://albakore.github.io/pyzed/library/row/): Lays out children horizontally. - [Column](https://albakore.github.io/pyzed/library/column/): Stacks children vertically. - [Divider](https://albakore.github.io/pyzed/library/divider/): A thin line in the theme's border color. ### Sparkline > A tiny line chart for trends. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/sparkline/ :::tip[In plain words] A tiny line chart that shows if something is going up or down. ::: A small line chart, filled below the line by default, that shows the shape of a series. `ui.sparkline` · Category: [Data](https://albakore.github.io/pyzed/library/#data) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#sparkline) **Code:** ```python pz.column([ ui.sparkline([1, 4, 2, 6, 3, 7, 5, 8], width=240, height=48), ui.sparkline([8, 6, 7, 4, 5, 3, 2, 1], width=240, height=48, tint="error", fill=False), ], spacing=16) ``` #### When to use - Inline next to a number or in a table cell, where the trend matters more than the values. #### Usage ```python import pyzed as pz from pyzed import ui pz.column([ ui.sparkline([1, 4, 2, 6, 3, 7, 5, 8], width=240, height=48), ui.sparkline([8, 6, 7, 4, 5, 3, 2, 1], width=240, height=48, tint="error", fill=False), ], spacing=16) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python ui.sparkline([1, 4, 2, 6, 3, 7, 5, 8], width=200, height=40) ``` ```python ui.sparkline([1, 4, 2, 6, 3, 7, 5, 8], width=200, height=40, fill=False) ``` ```python ui.sparkline([8, 6, 7, 4, 5, 3, 2, 1], width=200, height=40, tint="error") ``` ```python ui.sparkline([2, 3, 3, 5, 6, 8, 9, 12], width=200, height=40, tint="success") ``` ##### Size ```python ui.sparkline([3, 5, 2, 6, 4, 7], width=80, height=20) ``` ```python ui.sparkline([3, 5, 2, 6, 4, 7, 5, 9, 6, 8], width=280, height=64) ``` #### Events Sparkline has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.sparkline` ```python sparkline(values, *, width=120, height=32, tint='accent', fill=True, theme=None) ``` A tiny line chart for trends. | Property | Default | Description | | --- | --- | --- | | `values` | *required* | Values. | | `width` | `120` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `32` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `tint` | `"accent"` | Semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` or a token. | | `fill` | `True` | Fills the area under the line. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#sparkline) #### Related - [Stat](https://albakore.github.io/pyzed/library/stat/): A dashboard number with change and sparkline. - [Bar chart](https://albakore.github.io/pyzed/library/bar-chart/): A vertical bar chart with labels. ### Spinner > A spinning arc for loading states. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/spinner/ :::tip[In plain words] A spinning circle that says “wait, I'm working on it”. ::: A spinning arc. Its angle comes from the app clock, so keep a subscription such as `pz.every(1 / 30, ...)` active while it is shown or it will not move. `ui.spinner` · Category: [Feedback](https://albakore.github.io/pyzed/library/#feedback) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#spinner) **Code:** ```python pz.row([ ui.spinner(), ui.spinner(size=24, tint="success"), pz.row([ui.spinner(size=14), ui.muted("Indexing project…")], spacing=8, align="center"), ], spacing=24, align="center") ``` #### When to use - For short waits with an unknown duration: connecting, searching, loading a file. - For long work with known progress use [Progress](https://albakore.github.io/pyzed/library/progress/). #### Usage ```python import pyzed as pz from pyzed import ui class Loader(pz.App): def __init__(self): self.loading = True def subscription(self): return pz.every(1 / 30, lambda: None) if self.loading else None def view(self): return ui.spinner() if self.loading else ui.label("Done") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Size ```python ui.spinner(size=12) ``` ```python ui.spinner() ``` ```python ui.spinner(size=24) ``` ```python ui.spinner(size=36) ``` ##### Tint ```python ui.spinner(size=24, tint="success") ``` ```python ui.spinner(size=24, tint="warning") ``` ```python ui.spinner(size=24, tint="text_muted") ``` ##### With text ```python pz.row([ui.spinner(size=14), ui.muted("Indexing project…")], spacing=8, align="center") ``` ```python ui.button(pz.row([ui.spinner(size=12, tint="text"), pz.text("Saving")], spacing=6, align="center")) ``` #### Events Spinner has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.spinner` ```python spinner(*, size=16, tint='accent', speed=1.0, theme=None) ``` A spinning arc for loading states. Its angle comes from the clock, so keep a `pz.every(1/30, ...)` subscription active while it is shown. | Property | Default | Description | | --- | --- | --- | | `size` | `16` | Size. | | `tint` | `"accent"` | Semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` or a token. | | `speed` | `1.0` | Turns per second. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#spinner) #### Related - [Progress](https://albakore.github.io/pyzed/library/progress/): A thin progress bar in a semantic color. - [Empty state](https://albakore.github.io/pyzed/library/empty-state/): A centered placeholder for empty lists and searches. ### Split > Two panes with a draggable divider. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/split/ :::tip[In plain words] Two areas side by side (or one above the other) with a line you drag to resize them. ::: Separates two widgets with a draggable divider in the theme's border color (accent while dragging). The `ratio` is yours: store it in `on_resize(ratio)`. `vertical=True` stacks the panes. `pz.split` is the base version with configurable `color`, `hover_color`, `thickness` and `handle`. `ui.split` · Category: [Workspace](https://albakore.github.io/pyzed/library/#workspace) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#split) **Code:** ```python t = pz.theme.current() pane = lambda title, bg: pz.container(ui.label(title), padding=12, width="fill", height="fill", bg=bg) ui.split(pane("Left", t.panel), ui.split(pane("Top", t.editor), pane("Bottom", t.background), vertical=True), ratio=0.4, on_resize=on_resize) ``` #### When to use - For side-by-side editors, a preview next to its source, or an editor above a terminal. - For one resizable side panel, [Resizable](https://albakore.github.io/pyzed/library/resizable/) is simpler. #### Usage ```python import pyzed as pz from pyzed import ui class Panes(pz.App): def __init__(self): self.ratio = 0.3 def view(self): return ui.split(self.sidebar(), self.editor(), ratio=self.ratio, on_resize=lambda r: setattr(self, "ratio", r)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Direction ```python t = pz.theme.current() ui.split(pz.container(ui.label("A"), padding=10, width="fill", height="fill", bg=t.panel), pz.container(ui.label("B"), padding=10, width="fill", height="fill"), on_resize=on_resize) ``` ```python t = pz.theme.current() ui.split(pz.container(ui.label("Top"), padding=10, width="fill", height="fill", bg=t.panel), pz.container(ui.label("Bottom"), padding=10, width="fill", height="fill"), vertical=True) ``` ```python t = pz.theme.current() ui.split(pz.container(ui.label("25%"), padding=10, width="fill", height="fill", bg=t.panel), pz.container(ui.label("75%"), padding=10, width="fill", height="fill"), ratio=0.25) ``` #### Events Split has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_resize` | `ui.split`, `pz.split` | `(ratio)` | Callable that receives the new ratio. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Panes(pz.App): def __init__(self): self.ratio = 0.3 def view(self): return ui.split(ui.muted(f"Sidebar ({self.ratio:.0%})"), main_view(), ratio=self.ratio, on_resize=lambda r: setattr(self, "ratio", r)) # the share of the first pane ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Ratio: value: float class Panes(pz.App): ratio = 0.3 def update(self, message): match message: case Ratio(value): self.ratio = value def view(self): return ui.split(ui.muted("Sidebar"), main_view(), ratio=self.ratio, on_resize=Ratio) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: ratio: float = 0.3 def view(state, set_state): return ui.split(ui.muted("Sidebar"), main_view(), ratio=state.ratio, on_resize=lambda r: set_state(ratio=r)) ``` #### Properties and API ##### `ui.split` ```python split( first, second, *, ratio=0.5, on_resize=None, vertical=False, min_size=80, theme=None, **kwargs, ) ``` `pz.split` with the divider in the theme's border color (accent while dragging). | Property | Default | Description | | --- | --- | --- | | `first` | *required* | First pane. | | `second` | *required* | Second pane. | | `ratio` | `0.5` | Proportion of the first pane (0–1). | | `vertical` | `False` | Vertical orientation. | | `min_size` | `80` | Minimum size of each pane. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | | `**kwargs` | — | Same as `pz.split`. | Events: `on_resize` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#split) ##### `pz.split` ```python split( first, second, *, ratio=0.5, on_resize=None, vertical=False, min_size=80.0, min_size_second=None, color=None, hover_color=None, thickness=1.0, handle=8.0, width=None, height=None, ) ``` Two panes with a draggable divider. For the theme style use `ui.split`. | Property | Default | Description | | --- | --- | --- | | `first` | *required* | First pane. | | `second` | *required* | Second pane. | | `ratio` | `0.5` | Proportion of the first pane (0–1). The split keeps the ratio the user drags to until this changes. | | `vertical` | `False` | Stacks the panes top/bottom. | | `min_size` | `80.0` | Minimum size of the first pane. | | `min_size_second` | `None` | Minimum size of the second pane. | | `color` | `None` | Divider color. | | `hover_color` | `None` | Divider color on hover or while dragging. | | `thickness` | `1.0` | Line thickness. | | `handle` | `8.0` | Width of the draggable area. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | Events: `on_resize` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#split) #### Related - [Resizable](https://albakore.github.io/pyzed/library/resizable/): A panel whose edge can be dragged to resize it. - [Panel](https://albakore.github.io/pyzed/library/panel/): A dockable, resizable side panel. - [Workspace](https://albakore.github.io/pyzed/library/workspace/): The layout of an editor-style window: title bar, docks, center, status bar. ### Split button > A main action plus an arrow that opens more options. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/split-button/ :::tip[In plain words] A button with two parts: the big part does the usual thing, the little arrow shows other ways to do it. ::: A button with a separate chevron on the right. `on_press` runs the main action and `on_menu` usually opens a `ui.menu` inside a `ui.popover` with the alternatives. `ui.split_button` · Category: [Actions](https://albakore.github.io/pyzed/library/#actions) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#split_button) **Code:** ```python pz.row([ ui.split_button("Run", on_run, on_menu, icon="play"), ui.split_button("Deploy", on_deploy, on_menu, variant="solid"), ], spacing=12) ``` #### When to use - When one action is the usual one but there are a few variations (Run / Run with arguments). - Avoid it when the options are equally important: use a [Select](https://albakore.github.io/pyzed/library/select/). #### Usage ```python import pyzed as pz from pyzed import ui class Runner(pz.App): def __init__(self): self.menu_open = False def view(self): button = ui.split_button("Run", self.run, lambda: setattr(self, "menu_open", True), icon="play") menu = ui.menu([ui.MenuItem("Run with arguments…", self.run_args), ui.MenuItem("Debug", self.debug, icon="bug")]) if self.menu_open else None return ui.popover(button, menu, y=32, on_dismiss=lambda: setattr(self, "menu_open", False)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Variant ```python ui.split_button("Run", on_run, on_menu, icon="play") ``` ```python ui.split_button("Run", on_run, on_menu, icon="play", variant="ghost") ``` ```python ui.split_button("Run", on_run, on_menu, icon="play", variant="outline") ``` ```python ui.split_button("Deploy", on_deploy, on_menu, icon="rocket", variant="solid") ``` ##### Size ```python ui.split_button("Build", on_build, on_menu, size="sm") ``` ```python ui.split_button("Build", on_build, on_menu, size="md") ``` ```python ui.split_button("Build", on_build, on_menu, size="lg") ``` ##### Content ```python ui.split_button("Merge", on_merge, on_menu) ``` ```python ui.split_button("Merge", on_merge, on_menu, icon="git-merge") ``` ##### States and events ```python ui.split_button("Deploy", on_press, on_menu, tint="success") ``` ```python ui.split_button("Run", on_press, on_menu, disabled=True) ``` #### Events Split button has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_press` | `()` | Message or callable sent on press. `None` disables the control. | | `on_menu` | `()` | Message or callable for the menu button. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Runner(pz.App): def __init__(self): self.menu_open = False self.last = "nothing yet" def run(self, how="run"): self.last, self.menu_open = how, False def view(self): button = ui.split_button("Run", self.run, # on_press: the main action lambda: setattr(self, "menu_open", True), # on_menu: the arrow icon="play") menu = ui.menu([ui.MenuItem("Debug", lambda: self.run("debug"), icon="bug")]) return pz.column([ ui.popover(button, menu if self.menu_open else None, y=32, on_dismiss=lambda: setattr(self, "menu_open", False)), ui.label(f"Last: {self.last}"), ], spacing=8) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Run: how: str = "run" class Runner(pz.App): menu_open = False last = "nothing yet" def update(self, message): match message: case Run(how): self.last, self.menu_open = how, False case "menu": self.menu_open = True case "dismiss": self.menu_open = False def view(self): menu = ui.menu([ui.MenuItem("Debug", Run("debug"), icon="bug")]) return ui.popover(ui.split_button("Run", Run(), "menu", icon="play"), menu if self.menu_open else None, y=32, on_dismiss="dismiss") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: menu_open: bool = False last: str = "nothing yet" def view(state, set_state): run = lambda how: set_state(last=how, menu_open=False) menu = ui.menu([ui.MenuItem("Debug", lambda: run("debug"), icon="bug")]) return ui.popover( ui.split_button("Run", lambda: run("run"), lambda: set_state(menu_open=True), icon="play"), menu if state.menu_open else None, y=32, on_dismiss=lambda: set_state(menu_open=False)) ``` #### Properties and API ##### `ui.split_button` ```python split_button( content, on_press=None, on_menu=None, *, icon=None, variant='filled', tint=None, size='md', disabled=False, tooltip=None, theme=None, ) ``` A button with a separate arrow for more options. `on_press` runs the main action and `on_menu` usually opens a `ui.menu` in a `ui.popover`. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Button text. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `variant` | `"filled"` | As in `ui.button`. | | `tint` | `None` | As in `ui.button`. | | `size` | `"md"` | `"xs"`, `"sm"`, `"md"` or `"lg"`. | | `disabled` | `False` | Disables both halves. | | `tooltip` | `None` | Tooltip of the main half. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_press`, `on_menu` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#split_button) #### Related - [Button](https://albakore.github.io/pyzed/library/button/): The themed button: variants, sizes, icons and shortcuts. - [Menu](https://albakore.github.io/pyzed/library/menu/): A context menu with icons, shortcuts and checks. - [Popover](https://albakore.github.io/pyzed/library/popover/): Floating content at a point: context menus, color pickers. ### Stack > Layers children on top of each other; `pin` places one at x, y. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/stack/ :::tip[In plain words] Puts things on top of each other, like sheets of paper. ::: Layers its children on top of each other, the last one on top: the basis of layers, modals and notifications. `pz.pin(child, x=, y=)` places a child at a position inside the stack. `pz.stack` · Category: [Layout](https://albakore.github.io/pyzed/library/#layout) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#stack) **Code:** ```python t = pz.theme.current() pz.stack([ pz.container(width=320, height=180, bg=t.element, radius=10), pz.pin(pz.container(ui.label("pinned at 24, 24"), padding=10, bg=t.panel, radius=6, border_color=t.border, border_width=1), x=24, y=24), pz.pin(ui.count_badge(3), x=290, y=8), pz.pin(pz.container(ui.label("on top", color="#ffffff"), padding=10, bg=t.accent, radius=6), x=150, y=110), ], width=320, height=180) ``` #### When to use - For badges over icons, overlays, and positioning things freely. #### Usage ```python import pyzed as pz from pyzed import ui t = pz.theme.current() pz.stack([ pz.container(width=320, height=180, bg=t.element, radius=10), pz.pin(pz.container(ui.label("pinned at 24, 24"), padding=10, bg=t.panel, radius=6, border_color=t.border, border_width=1), x=24, y=24), pz.pin(ui.count_badge(3), x=290, y=8), pz.pin(pz.container(ui.label("on top", color="#ffffff"), padding=10, bg=t.accent, radius=6), x=150, y=110), ], width=320, height=180) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python t = pz.theme.current() pz.stack([pz.container(width=180, height=100, bg=t.element, radius=10), pz.container(ui.label("on top"), center=True, width=180, height=100)]) ``` ```python t = pz.theme.current() pz.stack([pz.container(width=180, height=100, bg=t.element, radius=10), pz.pin(ui.count_badge(3), x=150, y=8)], width=180, height=100) ``` ```python pz.stack([pz.container(ui.icon_button("bell", on_press, size="lg"), padding=6), pz.pin(ui.indicator("error", size=8), x=24, y=6)], width=40, height=40) ``` #### Events Stack has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.stack` ```python stack(children=None, *, width=None, height=None) ``` Layers its children on top of each other, the last one on top. The basis for layers, modals and notifications. | Property | Default | Description | | --- | --- | --- | | `children` | `None` | List of children. Nested lists are flattened and `None` and `False` are ignored, so you can write `cond and widget`. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#stack) ##### `pz.pin` ```python pin(content, *, x=0.0, y=0.0, width=None, height=None) ``` Places `content` at `(x, y)` inside a box. Use it in a `stack` to position popovers and context menus. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `x` | `0.0` | Horizontal position. | | `y` | `0.0` | Vertical position. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#pin) #### Related - [Layer](https://albakore.github.io/pyzed/library/layer/): Draws an overlay over your view; clicking outside dismisses it. - [Popover](https://albakore.github.io/pyzed/library/popover/): Floating content at a point: context menus, color pickers. - [Container](https://albakore.github.io/pyzed/library/container/): Size, padding, alignment, background, border and shadow for one child. ### Stat > A dashboard number with change and sparkline. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/stat/ :::tip[In plain words] A big number with its name and whether it went up or down, like on a dashboard. ::: A dashboard tile: a title, a large value, an optional change (`delta`, shown as `+4.2%` in green or red) and an optional `trend` drawn as a sparkline. `ui.stat` · Category: [Data](https://albakore.github.io/pyzed/library/#data) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#stat) **Code:** ```python pz.row([ ui.stat("Users", "1,284", delta=12.5, trend=[3, 5, 4, 6, 8, 7, 9], width=200), ui.stat("Errors", "37", delta=-8.0, width=200), ui.stat("Build time", "1m 12s", width=200), ], spacing=12) ``` #### When to use - For key numbers at the top of a dashboard or report. #### Usage ```python import pyzed as pz from pyzed import ui pz.row([ ui.stat("Users", "1,284", delta=12.5, trend=[3, 5, 4, 6, 8, 7, 9], width=200), ui.stat("Errors", "37", delta=-8.0, width=200), ui.stat("Build time", "1m 12s", width=200), ], spacing=12) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content ```python ui.stat("Build time", "1m 12s", width=190) ``` ```python ui.stat("Users", "1,284", delta=12.5, width=190) ``` ```python ui.stat("Errors", "37", delta=-8.0, width=190) ``` ```python ui.stat("Revenue", "$9.4k", delta=4.2, trend=[3, 5, 4, 6, 8, 7, 9], width=190) ``` ##### States and events With `inverse=True` a drop is good news (green), for errors or latency. ```python ui.stat("Errors", "12", delta=-8.0, inverse=True, icon="bug", width=180) ``` ```python ui.stat("Users", "1,204", delta=4.2, icon="users", on_press=on_press, width=180) ``` #### Events Stat has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_press` | `()` | Makes the card clickable. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Dashboard(pz.App): def __init__(self): self.detail = None def view(self): return pz.column([ pz.row([ui.stat("Users", "1,284", delta=12.5, width=200, on_press=lambda: setattr(self, "detail", "Users")), # the whole card ui.stat("Errors", "37", delta=-8.0, width=200, on_press=lambda: setattr(self, "detail", "Errors"))], spacing=12), ui.muted(f"Details of {self.detail}" if self.detail else "Click a number"), ], spacing=8) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Open: metric: str class Dashboard(pz.App): detail = None def update(self, message): match message: case Open(metric): self.detail = metric def view(self): return pz.row([ui.stat(m, v, width=200, on_press=Open(m)) for m, v in (("Users", "1,284"), ("Errors", "37"))], spacing=12) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: detail: str = "" def view(state, set_state): return pz.row([ui.stat(m, v, width=200, on_press=lambda m=m: set_state(detail=m)) for m, v in (("Users", "1,284"), ("Errors", "37"))], spacing=12) ``` #### Properties and API ##### `ui.stat` ```python stat( title, value, *, delta=None, trend=None, width=None, icon=None, inverse=False, on_press=None, theme=None, ) ``` A dashboard number: title, large value, optional change (`+4.2%`) and sparkline. | Property | Default | Description | | --- | --- | --- | | `title` | *required* | Title. | | `value` | *required* | Current value. | | `delta` | `None` | Change in %; green if positive, red otherwise. | | `trend` | `None` | Values for the sparkline. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `icon` | `None` | Icon before the title. | | `inverse` | `False` | Colors a drop green and a rise red (errors, latency…). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_press` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#stat) #### Related - [Sparkline](https://albakore.github.io/pyzed/library/sparkline/): A tiny line chart for trends. - [Bar chart](https://albakore.github.io/pyzed/library/bar-chart/): A vertical bar chart with labels. - [Donut](https://albakore.github.io/pyzed/library/donut/): A donut chart with a label in the middle. ### Status bar > The bottom bar: diagnostics, cursor position, language. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/status-bar/ :::tip[In plain words] The thin bar at the bottom of the window with small facts: errors, line number, language. ::: The bottom bar with items at the `start` and `end`. `ui.status_item` shows an icon and/or text, can be pressed, `selected` (a dock that is open) and carry a `tooltip` with `keys`. `ui.status_bar` · Category: [Workspace](https://albakore.github.io/pyzed/library/#workspace) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#status_bar) **Code:** ```python ui.status_bar( [ui.status_item(icon="panel-left", on_press=on_toggle, selected=True, tooltip="Project panel", keys="ctrl-b"), ui.status_item("0", icon="circle-x"), ui.status_item("2", icon="triangle-alert", icon_color="warning")], [ui.status_item("Ln 12, Col 8"), ui.status_item("UTF-8"), ui.status_item("Python", icon="file-code")], ) ``` #### When to use - For always-visible state: errors and warnings, cursor position, language, git branch. #### Usage ```python from pyzed import ui ui.status_bar( [ui.status_item(icon="panel-left", on_press=on_toggle, selected=True, tooltip="Project panel", keys="ctrl-b"), ui.status_item("0", icon="circle-x"), ui.status_item("2", icon="triangle-alert", icon_color="warning")], [ui.status_item("Ln 12, Col 8"), ui.status_item("UTF-8"), ui.status_item("Python", icon="file-code")], ) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Items ```python ui.status_item("Ln 12, Col 8") ``` ```python ui.status_item("Python", icon="file-code") ``` ```python ui.status_item("2", icon="triangle-alert", icon_color="warning") ``` ```python ui.status_item(icon="panel-left", on_press=on_toggle, selected=True) ``` ```python ui.status_item("main", icon="git-branch", color="accent") ``` ##### Bar ```python pz.container(ui.status_bar([ui.status_item("0", icon="circle-x")], [ui.status_item("UTF-8"), ui.status_item("Python")]), width=420) ``` #### Events Status bar has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_press` | `()` | Message or callable sent on press. `None` disables the control. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Ide(pz.App): def __init__(self): self.show_left = True def view(self): return ui.status_bar( [ui.status_item(icon="panel-left", selected=self.show_left, tooltip="Project panel", on_press=lambda: setattr(self, "show_left", not self.show_left))], [ui.status_item("Python", icon="file-code")], ) ``` **Declarative:** ```python class Ide(pz.App): show_left = True def update(self, message): if message == "toggle left": self.show_left = not self.show_left def view(self): return ui.status_bar([ui.status_item(icon="panel-left", on_press="toggle left", selected=self.show_left)]) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: show_left: bool = True def view(state, set_state): return ui.status_bar([ui.status_item(icon="panel-left", selected=state.show_left, on_press=lambda: set_state(show_left=not state.show_left))]) ``` #### Properties and API ##### `ui.status_bar` ```python status_bar(start=(), end=(), *, height=28, theme=None) ``` The bottom status bar. | Property | Default | Description | | --- | --- | --- | | `start` | `()` | Widgets at the start (left). | | `end` | `()` | Widgets at the end (right). | | `height` | `28` | Height. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#status_bar) ##### `ui.status_item` ```python status_item( content=None, on_press=None, *, icon=None, color=None, icon_color=None, selected=False, tooltip=None, keys=None, theme=None, ) ``` An icon and/or text in the status bar (diagnostics, cursor position, language…). | Property | Default | Description | | --- | --- | --- | | `content` | `None` | Text. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `color` | `None` | Text or icon color. | | `icon_color` | `None` | Icon color: a theme token or a hex color. | | `selected` | `False` | Marks it as selected. | | `tooltip` | `None` | Tooltip text. | | `keys` | `None` | Shortcut shown at the end, written as a string: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_press` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#status_item) #### Related - [Workspace](https://albakore.github.io/pyzed/library/workspace/): The layout of an editor-style window: title bar, docks, center, status bar. - [Title bar](https://albakore.github.io/pyzed/library/title-bar/): The top bar with project and branch pickers. - [Count badge](https://albakore.github.io/pyzed/library/count-badge/): A round counter that shows 99+ above its max. ### Switch > An on/off switch for settings that apply at once. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/switch/ :::tip[In plain words] An on/off switch, like a light switch, for settings that change right away. ::: An on/off toggle in the theme's accent color. `on_toggle` receives the new `bool`. `ui.switch` · Category: [Forms](https://albakore.github.io/pyzed/library/#forms) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#switch) **Code:** ```python pz.column([ ui.switch(True, on_toggle, label="Autosave"), ui.switch(False, on_toggle, label="Vim mode"), ], spacing=12) ``` #### When to use - For settings that take effect immediately (Wi-Fi, autosave, dark mode). - Put it on the right of a [Field](https://albakore.github.io/pyzed/library/field/) to build settings screens. #### Usage ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.autosave = True def view(self): return ui.switch(self.autosave, lambda on: setattr(self, "autosave", on), label="Autosave") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### State ```python ui.switch(True, on_toggle, label="Autosave") ``` ```python ui.switch(False, on_toggle, label="Autosave") ``` ```python ui.switch(True, label="Sync (managed)") ``` ```python ui.switch(True, on_toggle) ``` ##### States and events ```python ui.switch(True, on_toggle, label="Locked", disabled=True) ``` ```python ui.switch(True, on_toggle, label="Small", size="sm") ``` ```python ui.switch(True, on_toggle, label="Large", size="lg") ``` ##### Base widget The `pz.*` widget underneath: no theme, every color and size is yours. ```python pz.toggler(True, "pz.toggler", on_toggle=on_toggle, accent="#a1c181", size=22) ``` #### Events Switch has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_toggle` | `ui.switch`, `pz.toggler` | `(checked)` | Callable that receives the new `bool` (or a message to send). | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Settings(pz.App): def __init__(self): self.autosave = True def set_autosave(self, on): # on_toggle: the new state self.autosave = on def view(self): return pz.column([ ui.switch(self.autosave, self.set_autosave, label="Autosave"), ui.label("Saving as you type" if self.autosave else "Save with Ctrl+S"), ], spacing=8) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class SetAutosave: on: bool class Settings(pz.App): autosave = True def update(self, message): match message: case SetAutosave(on): self.autosave = on def view(self): return ui.switch(self.autosave, SetAutosave, label="Autosave") ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: autosave: bool = True def view(state, set_state): return ui.switch(state.autosave, lambda on: set_state(autosave=on), label="Autosave") ``` #### Tips :::tip Once clicked or reached with Tab it has the keyboard focus (a ring shows when you use the keys): Space or Enter flips it. ::: #### Properties and API ##### `ui.switch` ```python switch( checked, on_toggle=None, *, label=None, size='md', disabled=False, id=None, theme=None, ) ``` An on/off switch. | Property | Default | Description | | --- | --- | --- | | `checked` | *required* | Current state. | | `label` | `None` | Label text. | | `size` | `"md"` | `"sm"`, `"md"` or `"lg"`. | | `disabled` | `False` | Greys it out and ignores clicks. | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_toggle` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#switch) ##### `pz.toggler` ```python toggler( checked, label=None, *, on_toggle=None, size=None, text_size=None, accent=None, bg=None, border_color=None, knob_color=None, color=None, ) ``` An on/off switch. | Property | Default | Description | | --- | --- | --- | | `checked` | *required* | Current state. | | `label` | `None` | Label text. | | `size` | `None` | Switch height. | | `text_size` | `None` | Text size. | | `accent` | `None` | Accent color (active part). | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `border_color` | `None` | Border color. | | `knob_color` | `None` | Knob color. | | `color` | `None` | Text or icon color. | Events: `on_toggle` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#toggler) #### Related - [Checkbox](https://albakore.github.io/pyzed/library/checkbox/): A checkbox with an optional label. - [Field](https://albakore.github.io/pyzed/library/field/): A settings row: title and description left, control right. - [Settings group](https://albakore.github.io/pyzed/library/settings-group/): A titled block of field rows, for settings screens. ### Tab bar > Editor tabs with icons, dirty dots and close buttons. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/tab-bar/ :::tip[In plain words] The tabs at the top of an editor, one per open document. ::: A row of editor tabs with optional controls at the start and end. `ui.Tab.for_file(path)` creates a tab with the file's name, icon and path as id. `dirty=True` shows a dot that turns into the close button on hover, `preview=True` draws the name in italics, and `status` tints it with a git color. `on_select(id)` and `on_close(id)` receive the tab id. `ui.tab_bar` · Category: [Navigation](https://albakore.github.io/pyzed/library/#navigation) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#tab_bar) **Code:** ```python ui.tab_bar( [ui.Tab.for_file("src/main.rs"), ui.Tab.for_file("src/app.rs", dirty=True, status="modified"), ui.Tab.for_file("README.md", preview=True)], "src/main.rs", on_select=on_select, on_close=on_close, start=[ui.icon_button("arrow-left", on_back, size="sm"), ui.icon_button("arrow-right", on_forward, size="sm")], end=[ui.icon_button("plus", on_new, size="sm"), ui.icon_button("columns-2", on_split, size="sm")], ) ``` #### When to use - For open documents in an editor, or any set of views the user opens and closes. #### Usage ```python import pyzed as pz from pyzed import ui class Editor(pz.App): def __init__(self): self.open, self.active = ["main.py", "app.py"], "main.py" def close(self, path): i = self.open.index(path) self.open.remove(path) if self.active == path: self.active = self.open[min(i, len(self.open) - 1)] if self.open else None def view(self): return ui.tab_bar([ui.Tab.for_file(p) for p in self.open], self.active, on_select=lambda p: setattr(self, "active", p), on_close=self.close) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Tabs ```python pz.container(ui.tab_bar([ui.Tab.for_file("main.rs"), ui.Tab.for_file("app.rs")], "main.rs", on_close=on_close), width=400) ``` ```python pz.container(ui.tab_bar([ui.Tab.for_file("main.rs"), ui.Tab.for_file("notes.md", dirty=True)], "main.rs", on_close=on_close), width=400) ``` ```python pz.container(ui.tab_bar([ui.Tab.for_file("README.md", preview=True)], "README.md", on_close=on_close), width=400) ``` ```python pz.container(ui.tab_bar([ui.Tab.for_file("app.py", status="modified"), ui.Tab.for_file("new.py", status="created")], "app.py", on_close=on_close), width=400) ``` ```python pz.container(ui.tab_bar([ui.Tab("home", "Home", icon="house"), ui.Tab("logs", "Logs", icon="scroll-text")], "home", on_select=on_select), width=400) ``` ##### Controls ```python pz.container(ui.tab_bar([ui.Tab.for_file("main.rs")], "main.rs", on_close=on_close, start=[ui.icon_button("arrow-left", on_back, size="sm")], end=[ui.icon_button("plus", on_new, size="sm")]), width=400) ``` ```python pz.row([ui.tab(ui.Tab.for_file("main.rs"), active=True), ui.tab(ui.Tab.for_file("app.rs"), active=False, dimmed=True)]) ``` ##### States and events A middle click closes a tab, `on_context(id)` runs on a right click and `on_new` on a double click of the empty space. ```python ui.tab_bar([ui.Tab("app", "app.py", icon="file", pinned=True), ui.Tab("ui", "ui.py", icon="file")], "ui", on_close=on_close, on_context=on_context, on_new=on_new) ``` #### Events Tab bar has **7 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_select` | `ui.tab_bar`, `ui.tab` | `(tab_id)` | Callable that receives the chosen item. | | `on_close` | `ui.tab_bar`, `ui.tab` | `(tab_id)` | Message or callable sent when the X is pressed. | | `on_drag` | `ui.tab_bar` | `(tab_id)` | Called with the tab id when a tab starts being dragged. | | `on_drop` | `ui.tab_bar` | `(tab_id, event)` | Called with the tab id and the `Event` when the dragged tab is released: `event.window_x`/`window_y` (outside the window if it was dropped outside) and `event.screen`. | | `on_drag_over` | `ui.tab_bar` | `(index)` | While `dragging` is set: called with the insertion index under the pointer, or `None` when it leaves the bar. | | `on_context` | `ui.tab_bar` | `(tab_id)` | Called with the id of a tab that is right-clicked. | | `on_new` | `ui.tab_bar` | `()` | Sent on a double click of the empty space after the tabs. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Editor(pz.App): def __init__(self): self.open, self.active = ["main.py", "app.py"], "main.py" self.untitled = 0 def close(self, path): # on_close: the id of the tab i = self.open.index(path) self.open.remove(path) if self.active == path: self.active = self.open[min(i, len(self.open) - 1)] if self.open else None def new(self): # on_new: double click after the tabs self.untitled += 1 self.open.append(f"untitled-{self.untitled}") self.active = self.open[-1] def view(self): return ui.tab_bar([ui.Tab.for_file(p) for p in self.open], self.active, on_select=lambda p: setattr(self, "active", p), on_close=self.close, on_new=self.new, on_context=lambda p: print("menu for", p)) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Activate: path: str @dataclass class Close: path: str class Editor(pz.App): open, active = ["main.py", "app.py"], "main.py" def update(self, message): match message: case Activate(path): self.active = path case Close(path): self.open = [p for p in self.open if p != path] self.active = self.open[0] if self.open else None def view(self): return ui.tab_bar([ui.Tab.for_file(p) for p in self.open], self.active, on_select=Activate, on_close=Close) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: open: tuple = ("main.py", "app.py") active: str = "main.py" def view(state, set_state): def close(path): rest = tuple(p for p in state.open if p != path) set_state(open=rest, active=state.active if state.active != path else (rest[0] if rest else "")) return ui.tab_bar([ui.Tab.for_file(p) for p in state.open], state.active, on_select=lambda p: set_state(active=p), on_close=close) ``` #### Properties and API ##### `ui.tab_bar` ```python tab_bar( tabs, active, *, on_select=None, on_close=None, start=(), end=(), height=32, on_drag=None, on_drop=None, on_drag_over=None, dragging=None, drop_index=None, on_context=None, on_new=None, theme=None, ) ``` A row of tabs with optional controls at the start and end (navigation, split, +). The close button appears on the active tab and on hover. Tabs can be dragged to reorder them or out of the window: see [Draggable tabs](https://albakore.github.io/pyzed/guides/windows/#draggable-tabs-and-detachable-windows). | Property | Default | Description | | --- | --- | --- | | `tabs` | *required* | List of `Tab`. | | `active` | *required* | Id of the active tab. | | `start` | `()` | Widgets at the start (left). | | `end` | `()` | Widgets at the end (right). | | `height` | `32` | Height. | | `dragging` | `None` | Id of the tab being dragged (dims it and turns on `on_drag_over`). | | `drop_index` | `None` | Draws the insertion mark before the tab at this index (`len(tabs)`: after the last one). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_select`, `on_close`, `on_drag`, `on_drop`, `on_drag_over`, `on_context`, `on_new` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#tab_bar) ##### `ui.Tab` ```python @dataclass class Tab(id, label, icon=None, icon_color=None, dirty=False, preview=False, status=None, pinned=False, tooltip=None) ``` A tab. `Tab.for_file(path, **kwargs)` creates it with the file's name and icon. | Property | Default | Description | | --- | --- | --- | | `id` | *required* | Stable identifier used to focus or scroll it with a `Command`. | | `label` | *required* | Label text. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `icon_color` | `None` | Icon color: a theme token or a hex color. | | `dirty` | `False` | Unsaved changes (shows a dot). | | `preview` | `False` | Preview tab (in italics). | | `status` | `None` | Git status (tints the name). | | `pinned` | `False` | Shows only the icon, has no close button and goes first in `tab_bar`. | | `tooltip` | `None` | Text shown on hover (the full path…). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#tab) ##### `ui.tab` ```python tab(item, *, active, on_select=None, on_close=None, height=32, dimmed=False, theme=None) ``` A single tab. You normally use `tab_bar`. | Property | Default | Description | | --- | --- | --- | | `item` | *required* | The `Tab`. | | `active` | *required* | Whether it is the active one. | | `height` | `32` | Height. | | `dimmed` | `False` | Mutes the label (the tab being dragged). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_select`, `on_close` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#tab-1) #### Related - [Toolbar](https://albakore.github.io/pyzed/library/toolbar/): The bar under the tabs: breadcrumbs and buffer actions. - [Breadcrumbs](https://albakore.github.io/pyzed/library/breadcrumbs/): `src › main.rs › fn main`, optionally clickable. - [Segmented](https://albakore.github.io/pyzed/library/segmented/): A group of mutually exclusive text buttons. ### Table > A data table with a fixed header, sorting and selection. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/table/ :::tip[In plain words] Rows and columns of data, like a spreadsheet you can sort by clicking a column. ::: A data table with a fixed header, optional sorting and row selection. Columns are `ui.Column` objects whose `key` is a dict key, an attribute name or a function of the row; `render` turns a value into any widget. The table sorts the rows itself by the column whose **title** is `sort_by`: you only store that title and the direction. `ui.table` · Category: [Data](https://albakore.github.io/pyzed/library/#data) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#table) **Code:** ```python rows = [ {"name": "render.rs", "lang": "Rust", "lines": 1840}, {"name": "app.py", "lang": "Python", "lines": 512}, {"name": "theme.py", "lang": "Python", "lines": 730}, {"name": "docs.yml", "lang": "YAML", "lines": 64}, ] pz.container(ui.table( [ui.Column("File", "name"), ui.Column("Language", "lang", width=120), ui.Column("Lines", "lines", width=80, align="end")], rows, sort_by="Lines", descending=True, on_sort=on_sort, selected=rows[1], striped=True, ), width=520) ``` #### When to use - For records with several comparable fields: files, users, processes, results. - For a single column of items, a list of [List items](https://albakore.github.io/pyzed/library/list-item/) is lighter. #### Usage ```python import pyzed as pz from pyzed import ui class Files(pz.App): def __init__(self): self.rows = [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}] self.sort_by, self.descending, self.selected = "Lines", True, None def sort(self, title): self.descending = not self.descending if self.sort_by == title else False self.sort_by = title def view(self): return ui.table( [ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], self.rows, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort, on_row_press=lambda row: setattr(self, "selected", row), selected=self.selected, ) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}]), width=320) ``` ```python pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}], striped=True), width=320) ``` ```python rows = [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}] pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], rows, selected=rows[1]), width=320) ``` ```python pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}], sort_by="Lines", descending=True, on_sort=on_sort), width=320) ``` ```python pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}], row_height=24), width=320) ``` ##### Cells ```python rows = [{"job": "build", "state": "passed"}, {"job": "lint", "state": "failed"}] tint = {"passed": "success", "failed": "error"} pz.container(ui.table([ui.Column("Job", "job"), ui.Column("State", "state", width=110, render=lambda s: ui.badge(s, tint=tint[s]))], rows), width=320) ``` ```python people = [("Ana", 31), ("Luis", 27)] pz.container(ui.table([ui.Column("Name", lambda p: p[0]), ui.Column("Age", lambda p: p[1], width=70, align="end")], people), width=320) ``` ##### States and events `on_row_double_press(row)` opens a row and `on_row_context(row)` shows a context menu. ```python rows = [{"name": "app.py", "size": 4}, {"name": "ui.py", "size": 12}, {"name": "README.md", "size": 2}] ui.table(["name", "size"], rows, selected=[rows[0], rows[2]], on_row_press=on_press, on_row_double_press=on_open, on_row_context=on_context, height=130) ``` ```python ui.table(["name", "size"], [], empty="No files yet", height=100) ``` #### Examples ##### Custom cells `render` receives the value and returns a widget, here a badge. **Code:** ```python rows = [{"job": "build", "state": "passed"}, {"job": "lint", "state": "failed"}, {"job": "docs", "state": "running"}] tint = {"passed": "success", "failed": "error", "running": "warning"} pz.container(ui.table( [ui.Column("Job", "job"), ui.Column("State", "state", width=120, render=lambda s: ui.badge(s, tint=tint[s]))], rows, ), width=400) ``` #### Events Table has **4 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_sort` | `(title)` | Callable that receives the clicked title. | | `on_row_press` | `(row)` | Callable that receives the clicked row. | | `on_row_double_press` | `(row)` | Called with the row on a double click (open, edit…). | | `on_row_context` | `(row)` | Called with the row on a right click (a context menu). | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Files(pz.App): def __init__(self): self.rows = [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}] self.sort_by, self.descending = "Lines", True self.selected, self.opened = None, None def sort(self, title): # on_sort: the clicked column title self.descending = not self.descending if self.sort_by == title else False self.sort_by = title def view(self): key = "name" if self.sort_by == "File" else "lines" rows = sorted(self.rows, key=lambda r: r[key], reverse=self.descending) return ui.table( [ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], rows, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort, selected=self.selected, on_row_press=lambda row: setattr(self, "selected", row), # click on_row_double_press=lambda row: setattr(self, "opened", row), # double click on_row_context=lambda row: print("menu for", row["name"]), # right click ) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Sort: title: str @dataclass class Select: row: dict class Files(pz.App): rows = [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}] sort_by, descending, selected = "Lines", True, None def update(self, message): match message: case Sort(title): self.descending = not self.descending if title == self.sort_by else False self.sort_by = title case Select(row): self.selected = row def view(self): return ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80)], self.rows, sort_by=self.sort_by, descending=self.descending, selected=self.selected, on_sort=Sort, on_row_press=Select) ``` **Functional:** ```python from dataclasses import dataclass ROWS = ({"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}) @dataclass(frozen=True) class State: sort_by: str = "Lines" descending: bool = True selected: str = "" def view(state, set_state): key = "name" if state.sort_by == "File" else "lines" rows = sorted(ROWS, key=lambda r: r[key], reverse=state.descending) return ui.table( [ui.Column("File", "name"), ui.Column("Lines", "lines", width=80)], rows, sort_by=state.sort_by, descending=state.descending, selected=next((r for r in rows if r["name"] == state.selected), None), on_sort=lambda t: set_state(sort_by=t, descending=not state.descending if t == state.sort_by else False), on_row_press=lambda row: set_state(selected=row["name"])) ``` #### Properties and API ##### `ui.table` ```python table( columns, rows, *, sort_by=None, descending=False, on_sort=None, on_row_press=None, selected=None, striped=False, row_height=30, height=None, on_row_double_press=None, on_row_context=None, empty=None, theme=None, ) ``` A data table with a fixed header, optional sorting and row selection. Sorting happens here when `sort_by` names a column: clicking a header calls `on_sort(title)` and you update `sort_by`/`descending` in your state. Rows are virtualized: only the ones in view are built, so hundreds of thousands of rows scroll smoothly. | Property | Default | Description | | --- | --- | --- | | `columns` | *required* | `ui.Column` or plain keys. | | `rows` | *required* | Dicts or objects. | | `sort_by` | `None` | Title of the column to sort by. | | `descending` | `False` | Descending order. | | `selected` | `None` | The row to highlight, or a list or set of rows (multiple selection). | | `striped` | `False` | Shaded alternating rows. | | `row_height` | `30` | Height of each row. | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `empty` | `None` | Text or widget shown in place of the rows when there are none. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_sort`, `on_row_press`, `on_row_double_press`, `on_row_context` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#table) ##### `ui.Column` ```python @dataclass class Column(title, key, width='fill', align='start', render=None, sortable=True) ``` A column of `ui.table`. `key` is a dict key, an attribute name or a function of the row; `render` (optional) turns the value into a widget. `width` is `"fill"`, a number or a portion such as `"fill:2"`. | Property | Default | Description | | --- | --- | --- | | `title` | *required* | Header (and name used for sorting). | | `key` | *required* | How to get the value from each row. | | `width` | `"fill"` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `align` | `"start"` | `"start"`, `"center"` or `"end"`. | | `render` | `None` | Function value → widget. | | `sortable` | `True` | Whether the header can be clicked to sort. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#column) #### Related - [List item](https://albakore.github.io/pyzed/library/list-item/): A list row with icon, text and an end slot. - [Virtual list](https://albakore.github.io/pyzed/library/virtual-list/): A list that only builds the rows in view, for millions of items. - [Stat](https://albakore.github.io/pyzed/library/stat/): A dashboard number with change and sparkline. - [Settings group](https://albakore.github.io/pyzed/library/settings-group/): A titled block of field rows, for settings screens. ### Terminal > A read-only terminal view with ANSI colors. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/terminal/ :::tip[In plain words] Shows the text a program prints, with its colors, like a terminal window. ::: A read-only view that understands ANSI colors and stays scrolled to the end. Feed it the output of a process you run (for example with `subprocess` in a thread and `pz.post`). It also accepts lines of `(text, color)` segments; `ui.ansi(text)` converts ANSI text into those segments. `ui.terminal` · Category: [Code and tools](https://albakore.github.io/pyzed/library/#code-and-tools) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#terminal) **Code:** ```python out = ("$ pytest -q\n\x1b[32m........................\x1b[0m\n\x1b[32m24 passed\x1b[0m in 1.21s\n" "$ maturin develop --release\n\x1b[2m Compiling pyzed v0.4.0\x1b[0m\n" "\x1b[32m✓\x1b[0m Installed pyzed-0.4.0\n$ ") pz.container(ui.terminal(out), width=560, height=190) ``` #### When to use - To show build output, test runs, server logs or any command's output. #### Usage ```python import pyzed as pz from pyzed import ui out = ("$ pytest -q\n\x1b[32m........................\x1b[0m\n\x1b[32m24 passed\x1b[0m in 1.21s\n" "$ maturin develop --release\n\x1b[2m Compiling pyzed v0.4.0\x1b[0m\n" "\x1b[32m✓\x1b[0m Installed pyzed-0.4.0\n$ ") pz.container(ui.terminal(out), width=560, height=190) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content ```python pz.container(ui.terminal("$ ls\nmain.py notes.md pyproject.toml\n$ "), width=380, height=80) ``` ```python pz.container(ui.terminal("\x1b[32m✓\x1b[0m 24 passed \x1b[33m!\x1b[0m 1 warning \x1b[31m✗\x1b[0m 0 failed"), width=380, height=50) ``` ```python pz.container(ui.terminal("$ python main.py\nHello", font_size=16), width=380, height=70) ``` ```python pz.container(ui.terminal([[("ok ", "#a1c181"), ("build", None)], [("err ", "#d07277"), ("lint", None)]]), width=380, height=60) ``` #### Events Terminal has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.terminal` ```python terminal(lines, *, font_size=13, id=None, theme=None) ``` A read-only terminal view with ANSI colors. It stays scrolled to the end. | Property | Default | Description | | --- | --- | --- | | `lines` | *required* | Text with ANSI escapes, or a list of lines of `(text, color)` segments. | | `font_size` | `13` | Font size. | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#terminal) ##### `ui.ansi` ```python ansi(text) ``` Converts text with ANSI colors into lines of `(text, color_name)` segments. | Property | Default | Description | | --- | --- | --- | | `text` | *required* | Text with ANSI escapes. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#ansi) #### Related - [Console](https://albakore.github.io/pyzed/library/console/): A dev-tools style log with levels, filter, copy and clear. - [Editor](https://albakore.github.io/pyzed/library/editor/): A full code editor: highlighting, folding, minimap, git markers. ### Text > A piece of text with size, color, font and weight. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/text/ :::tip[In plain words] Text with any size, color and font you choose. ::: The base text widget: `size`, `color`, `font`, `weight`, `italic`, `line_height`, alignment and wrapping. In a themed UI prefer [Label](https://albakore.github.io/pyzed/library/label/), which takes its color from the theme. `pz.text` · Category: [Content](https://albakore.github.io/pyzed/library/#content) · [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#text) **Code:** ```python pz.column([ pz.text("Pyzed", size=36, weight="bold", color="#74ade8"), pz.text("Fast, modern desktop UIs in Python.", size=16, color="#a9afbc"), pz.text("IBM Plex Mono, italic", font="IBM Plex Mono", italic=True, color="#e5c07b"), ], spacing=6, align="center") ``` #### When to use - For text with explicit styles, or custom fonts loaded with `pz.load_font`. #### Usage ```python import pyzed as pz pz.column([ pz.text("Pyzed", size=36, weight="bold", color="#74ade8"), pz.text("Fast, modern desktop UIs in Python.", size=16, color="#a9afbc"), pz.text("IBM Plex Mono, italic", font="IBM Plex Mono", italic=True, color="#e5c07b"), ], spacing=6, align="center") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Size ```python pz.text("Size 12", size=12) ``` ```python pz.text("Size 18", size=18) ``` ```python pz.text("Size 32", size=32) ``` ##### Font and weight ```python pz.text("Bold", size=18, weight="bold") ``` ```python pz.text("Light", size=18, weight="light") ``` ```python pz.text("Italic", size=18, italic=True) ``` ```python pz.text("Monospace 0123", size=16, font="mono") ``` ##### Color and wrapping ```python pz.text("Golden text", size=18, color="#e5c07b") ``` ```python pz.container(pz.text("A long sentence that wraps over several lines, centered.", align_x="center"), width=220) ``` ```python pz.container(pz.text("Taller lines are easier to read in long paragraphs.", line_height=1.8), width=220) ``` #### Events Text has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `pz.text` ```python text( content, *, size=None, color=None, font=None, weight=None, italic=False, line_height=None, align_x=None, align_y=None, width=None, height=None, wrap=True, selectable=None, selection_color=None, ) ``` A piece of text. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `size` | `None` | Font size. | | `color` | `None` | Text or icon color. | | `font` | `None` | `"sans"` (IBM Plex Sans), `"mono"`, `"icons"`, `"serif"` or the name of a family loaded with `load_font`. | | `weight` | `None` | `"thin"`, `"light"`, `"normal"`, `"medium"`, `"semibold"`, `"bold"`, `"black"` or a number (100–900). | | `italic` | `False` | Italic. | | `line_height` | `None` | Line height relative to the size (1.0, 1.5…). | | `align_x` | `None` | Horizontal alignment: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `align_y` | `None` | Vertical alignment: `"start"`/`"top"`, `"center"`, `"end"`/`"bottom"`. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `wrap` | `True` | Line wrapping. With `False` the text doesn't wrap. | | `selectable` | `None` | `True` lets the mouse select the text (drag, double-click a word, triple-click a block) and Ctrl+C copy it. A string names a selection group: the texts and Markdown of one group form a single selection, in the order they are drawn (the lines of a log, the messages of a chat). | | `selection_color` | `None` | Color behind the selected text. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#text) #### Related - [Label](https://albakore.github.io/pyzed/library/label/): Text with a theme color, size and weight; `muted` for secondary text. - [Rich text](https://albakore.github.io/pyzed/library/rich-text/): One block of text mixing colors, weights and links. - [Icon](https://albakore.github.io/pyzed/library/icon/): Lucide icons by name, and SVG images. ### Thread history > Past conversations, searchable and grouped by day. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/thread-history/ :::tip[In plain words] The list of your earlier chats, sorted by day, with a box to search them. ::: `ui.ThreadHistory` is a component for past conversations: a search box and the threads grouped by when they were last updated (Today, Yesterday, This week, This month, Older). Each row shows the title, an optional `detail` (the model or the agent) and a delete button. `threads` are dicts or objects with `id`, `title`, `updated` (a `time.time()` timestamp) and `detail`; `selected` marks the open one. Picking a thread reports `on_open` and the trash button `on_delete`, both with the thread's id in `event.value`. The component keeps the search text itself. `ui.ThreadHistory` · Category: [AI agents](https://albakore.github.io/pyzed/library/#ai-agents) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#threadhistory) **Code:** ```python import time now = time.time() pz.container(ui.ThreadHistory(threads=[ {"id": "1", "title": "Add a login form", "updated": now - 60, "detail": "Claude"}, {"id": "2", "title": "Fix the failing tests", "updated": now - 900, "detail": "Claude"}, {"id": "3", "title": "Dark theme for the settings", "updated": now - 86400, "detail": "Gemini CLI"}, {"id": "4", "title": "Export the table to CSV", "updated": now - 4 * 86400}, {"id": "5", "title": "First steps", "updated": now - 60 * 86400}, ], selected="1", on_open=on_open, on_delete=on_delete), width=380, height=320) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - In a side panel or a popover of a chat app, to reopen an earlier conversation. #### Usage ```python import pyzed as pz from pyzed import ui class Chats(pz.App): def __init__(self): self.threads = load_threads() # [{"id", "title", "updated", "detail"}, ...] self.open_id = "" def delete(self, ident): self.threads = [t for t in self.threads if t["id"] != ident] def view(self): return ui.ThreadHistory(threads=self.threads, selected=self.open_id, on_open=lambda e: setattr(self, "open_id", e.value), on_delete=lambda e: self.delete(e.value)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### State The search ones are drawn with text typed in the box (the component keeps it). ```python import time now = time.time() threads = [ {"id": "1", "title": "Add a login form", "updated": now - 60, "detail": "Claude"}, {"id": "2", "title": "Fix the failing tests", "updated": now - 86400}, {"id": "3", "title": "Export the table to CSV", "updated": now - 4 * 86400}, ] pz.container(ui.ThreadHistory(threads=threads), width=320, height=260) ``` ```python import time now = time.time() threads = [ {"id": "1", "title": "Add a login form", "updated": now - 60, "detail": "Claude"}, {"id": "2", "title": "Fix the failing tests", "updated": now - 86400}, {"id": "3", "title": "Export the table to CSV", "updated": now - 4 * 86400}, ] pz.container(ui.ThreadHistory(threads=threads, selected="2"), width=320, height=260) ``` ```python pz.container(ui.ThreadHistory(threads=[]), width=320, height=110) ``` ```python import time now = time.time() threads = [ {"id": "1", "title": "Add a login form", "updated": now - 60, "detail": "Claude"}, {"id": "2", "title": "Fix the failing tests", "updated": now - 86400}, {"id": "3", "title": "Export the table to CSV", "updated": now - 4 * 86400}, ] pz.container(ui.ThreadHistory(threads=threads), width=320, height=160) ``` ```python import time now = time.time() threads = [ {"id": "1", "title": "Add a login form", "updated": now - 60, "detail": "Claude"}, {"id": "2", "title": "Fix the failing tests", "updated": now - 86400}, {"id": "3", "title": "Export the table to CSV", "updated": now - 4 * 86400}, ] pz.container(ui.ThreadHistory(threads=threads), width=320, height=110) ``` #### Events Thread history has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. | Event | Handler gets | When | | --- | --- | --- | | `on_open` | `(event)` | A thread was picked (`event.value` is its id). | | `on_delete` | `(event)` | Delete was pressed on a thread. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python import time class Chats(pz.App): def __init__(self): now = time.time() self.threads = [{"id": "1", "title": "Add a login form", "updated": now - 60}, {"id": "2", "title": "Fix the tests", "updated": now - 900}] self.open_id = "1" def view(self): # event.value is the thread's id return ui.ThreadHistory(threads=self.threads, selected=self.open_id, on_open=lambda e: setattr(self, "open_id", e.value), on_delete=lambda e: self.delete(e.value)) def delete(self, ident): self.threads = [t for t in self.threads if t["id"] != ident] ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class OpenThread: ident: str @dataclass class DeleteThread: ident: str class Chats(pz.App): threads = [{"id": "1", "title": "Add a login form", "updated": 0}] open_id = "" def update(self, message): match message: case OpenThread(ident): self.open_id = ident case DeleteThread(ident): self.threads = [t for t in self.threads if t["id"] != ident] def view(self): return ui.ThreadHistory(threads=self.threads, selected=self.open_id, on_open=lambda e: OpenThread(e.value), on_delete=lambda e: DeleteThread(e.value)) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: threads: tuple = ({"id": "1", "title": "Add a login form", "updated": 0},) open_id: str = "" def view(state, set_state): return ui.ThreadHistory( threads=list(state.threads), selected=state.open_id, on_open=lambda e: set_state(open_id=e.value), on_delete=lambda e: set_state(threads=tuple(t for t in state.threads if t["id"] != e.value))) ``` #### Properties and API ##### `ui.ThreadHistory` ```python class ThreadHistory ``` A component for past conversations: a search box and the threads grouped by day (Today, Yesterday, This week, This month, Older), each with a delete button. | Property | Default | Description | | --- | --- | --- | | `threads` | `None` | Dicts or objects with `id`, `title`, `updated` (a timestamp) and an optional `detail`. | | `selected` | `""` | The id of the open thread. | | `theme` | `None` | Theme. | Events: `on_open`, `on_delete` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#threadhistory) #### Related - [List item](https://albakore.github.io/pyzed/library/list-item/): A list row with icon, text and an end slot. - [Search input](https://albakore.github.io/pyzed/library/search-input/): An input with a magnifying glass, for filtering. - [Virtual list](https://albakore.github.io/pyzed/library/virtual-list/): A list that only builds the rows in view, for millions of items. ### Time picker > A time field with columns of hours and minutes, in 24 or 12 hours. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/time-picker/ :::tip[In plain words] A box for a time of day, with lists of hours and minutes to choose from. ::: `ui.TimePicker` is a field for a time of day. Its popup has a column of hours, one of minutes (every `minute_step` minutes), seconds when the format has `ss` and AM/PM when it has `A` or `h`. ↑ and ↓ move it by one step from the keyboard. `on_change` receives a `datetime.time`. `ui.TimePicker` · Category: [Dates and times](https://albakore.github.io/pyzed/library/#dates-and-times) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#timepicker) **Code:** ```python pz.row([ pz.container(ui.TimePicker(value="14:30", on_change=on_change), width=140, height=290, align_y="start"), pz.container(ui.TimePicker(value="14:30", format="h:mm A", minute_step=15, on_change=on_change), width=140, height=290, align_y="start"), ], spacing=24) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - For alarms, opening hours, meetings and reminders. - `format="HH:mm"` is 24 hours; `"h:mm A"` is 12 hours with AM/PM; `"HH:mm:ss"` adds seconds. #### Usage ```python import pyzed as pz from pyzed import ui import datetime class Alarm(pz.App): def __init__(self): self.at = datetime.time(7, 30) def view(self): return ui.field("Wake me at", ui.TimePicker(value=self.at, format="h:mm A", on_change=lambda e: setattr(self, "at", e.value))) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Format ```python ui.TimePicker(value="14:30", on_change=on_change) ``` ```python ui.TimePicker(value="14:30", format="h:mm A", on_change=on_change) ``` ```python ui.TimePicker(value="14:30:15", format="HH:mm:ss", on_change=on_change) ``` ##### Open ```python ui.TimePicker(value="06:15:30", format="HH:mm:ss", on_change=on_change) ``` ```python ui.TimePicker(value="14:30", format="h:mm A", minute_step=15, on_change=on_change) ``` #### Events Time picker has **3 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. | Event | Handler gets | When | | --- | --- | --- | | `on_change` | `(event)` | Receives `event.value`: a `datetime.time` or `None`. | | `on_open` | `(event)` | The list opened. | | `on_close` | `(event)` | The list closed. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python import datetime class Alarm(pz.App): def __init__(self): self.at = datetime.time(7, 30) def changed(self, event): # on_change: event.value is a datetime.time self.at = event.value def view(self): return pz.column([ ui.TimePicker(value=self.at, format="h:mm A", on_change=self.changed), ui.label(f"Alarm at {ui.format_time(self.at, 'HH:mm')}"), ], spacing=8, width=200) ``` **Declarative:** ```python import datetime from dataclasses import dataclass @dataclass class At: time: datetime.time class Alarm(pz.App): at = datetime.time(7, 30) def update(self, message): match message: case At(time): self.at = time def view(self): return ui.TimePicker(value=self.at, on_change=lambda e: At(e.value)) ``` **Functional:** ```python import datetime from dataclasses import dataclass @dataclass(frozen=True) class State: at: datetime.time = datetime.time(7, 30) def view(state, set_state): return ui.TimePicker(value=state.at, minute_step=15, on_change=lambda e: set_state(at=e.value)) ``` #### Properties and API ##### `ui.TimePicker` ```python class TimePicker ``` A component: a time field with columns of hours and minutes (and seconds, and AM/PM with a 12-hour `format`) in a popup. Once clicked, ↑/↓ change it by `minute_step`. See the [Time picker](https://albakore.github.io/pyzed/library/time-picker/) page. | Property | Default | Description | | --- | --- | --- | | `value` | `None` | The time (`datetime.time`, `"14:30"` or `None`). | | `format` | `"HH:mm"` | `"HH:mm"`, `"HH:mm:ss"`, `"h:mm A"`... | | `placeholder` | `""` | Hint while empty. | | `minute_step` | `5` | Minutes between the ones offered. | | `min` | `None` | The earliest time. | | `max` | `None` | The latest time. | | `clearable` | `True` | Shows an x that empties it. | | `disabled` | `False` | Greys it out. | | `invalid` | `False` | Paints the border in the error color. | Events: `on_change`, `on_open`, `on_close` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#timepicker) ##### `ui.format_time` ```python format_time(value, fmt='HH:mm') ``` Writes a time (or `"14:30"`) with a pattern such as `"h:mm A"`. | Property | Default | Description | | --- | --- | --- | | `value` | *required* | A `datetime.time` or text. | | `fmt` | `"HH:mm"` | The pattern. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#format_time) ##### `ui.parse_time` ```python parse_time(text, fmt='HH:mm') ``` Reads a time: `"14:30"`, `"2:30 PM"`, `"14:30:05"`, with `fmt` first. `None` when it is not one. | Property | Default | Description | | --- | --- | --- | | `text` | *required* | The text. | | `fmt` | `"HH:mm"` | The pattern. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#parse_time) #### Related - [Date picker](https://albakore.github.io/pyzed/library/date-picker/): A date field with a calendar that drops down, in the format you choose. - [Number field](https://albakore.github.io/pyzed/library/number-field/): A number with −/+ buttons, limits, step and suffix. ### Title bar > The top bar with project and branch pickers. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/title-bar/ :::tip[In plain words] The bar at the very top of the window, with the project name and quick buttons. ::: The top bar of the window: widgets at the `start` (project and branch pickers), an optional `center` and widgets at the `end` (account, notifications). `ui.title_button` is the compact button it uses (`pyzed ▾`, `main`), with `chevron` and `muted` styles. `ui.title_bar` · Category: [Workspace](https://albakore.github.io/pyzed/library/#workspace) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#title_bar) **Code:** ```python ui.title_bar( [ui.title_button("pyzed", on_projects, chevron=True), ui.title_button("main", on_branches, icon="git-branch", muted=True)], [ui.facepile(["Ana", "Luis"]), ui.icon_button("bell", on_bell, size="sm"), ui.avatar("KV")], center=ui.muted("notes.py"), ) ``` #### When to use - At the top of a `ui.workspace`, or of any window with `decorations=False`. #### Usage ```python from pyzed import ui ui.title_bar( [ui.title_button("pyzed", on_projects, chevron=True), ui.title_button("main", on_branches, icon="git-branch", muted=True)], [ui.facepile(["Ana", "Luis"]), ui.icon_button("bell", on_bell, size="sm"), ui.avatar("KV")], center=ui.muted("notes.py"), ) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Title buttons ```python ui.title_button("pyzed", on_projects) ``` ```python ui.title_button("pyzed", on_projects, chevron=True) ``` ```python ui.title_button("main", on_branches, icon="git-branch", muted=True) ``` ##### Bar ```python pz.container(ui.title_bar([ui.title_button("pyzed", chevron=True)]), width=420) ``` ```python pz.container(ui.title_bar([], [], center=ui.muted("notes.py — Notes")), width=420) ``` ```python pz.container(ui.title_bar([], [ui.icon_button("bell", on_bell, size="sm"), ui.avatar("KV")]), width=420) ``` #### Events Title bar has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_press` | `()` | Message or callable sent on press. `None` disables the control. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Ide(pz.App): def __init__(self): self.picking = None def view(self): return ui.title_bar([ ui.title_button("pyzed", lambda: setattr(self, "picking", "project"), chevron=True), ui.title_button("main", lambda: setattr(self, "picking", "branch"), icon="git-branch", muted=True), ], center=ui.muted(f"Pick a {self.picking}" if self.picking else "notes.py")) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Pick: what: str class Ide(pz.App): picking = None def update(self, message): match message: case Pick(what): self.picking = what def view(self): return ui.title_bar([ui.title_button("pyzed", Pick("project"), chevron=True), ui.title_button("main", Pick("branch"), icon="git-branch", muted=True)]) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: project: str = "pyzed" projects: tuple = ("pyzed", "notes") def view(state, set_state): nxt = state.projects[(state.projects.index(state.project) + 1) % len(state.projects)] return ui.title_bar([ui.title_button(state.project, lambda: set_state(project=nxt), chevron=True)]) ``` #### Properties and API ##### `ui.title_bar` ```python title_bar(start=(), end=(), *, center=None, height=34, theme=None) ``` The top bar: project and branch pickers on the left, account and actions on the right. | Property | Default | Description | | --- | --- | --- | | `start` | `()` | Widgets at the start (left). | | `end` | `()` | Widgets at the end (right). | | `center` | `None` | Centered widget. | | `height` | `34` | Height. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#title_bar) ##### `ui.title_button` ```python title_button( content, on_press=None, *, icon=None, muted=False, chevron=False, theme=None, ) ``` A compact button for the title bar (`pyzed ▾`, `main`…). | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Text. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `muted` | `False` | Muted text. | | `chevron` | `False` | Adds a ▾. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_press` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#title_button) #### Related - [Workspace](https://albakore.github.io/pyzed/library/workspace/): The layout of an editor-style window: title bar, docks, center, status bar. - [Status bar](https://albakore.github.io/pyzed/library/status-bar/): The bottom bar: diagnostics, cursor position, language. - [Facepile](https://albakore.github.io/pyzed/library/facepile/): Overlapping collaborator avatars with +N. - [Window frame](https://albakore.github.io/pyzed/library/window-frame/): Your own title bar, window buttons and menus. ### Toast > Notifications stacked in the bottom-right corner. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/toast/ :::tip[In plain words] A small notification that pops up in a corner and goes away by itself. ::: `ui.toast` draws a notification with a title, a message, actions and a close button; `kind` is `"info"`, `"success"`, `"warning"` or `"error"`. `ui.toasts(base, items)` stacks them in the bottom-right corner over your view. `ui.toast` · Category: [Overlays](https://albakore.github.io/pyzed/library/#overlays) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#toast) **Code:** ```python pz.column([ ui.toast("Saved", "main.py was saved.", kind="success", on_dismiss=on_dismiss), ui.toast("Update available", "Pyzed 0.4 is on PyPI.", kind="info", actions=[ui.button("What's new", on_open, size="sm")], on_dismiss=on_dismiss), ui.toast("Could not connect", kind="error", on_dismiss=on_dismiss), ], spacing=8) ``` #### When to use - To confirm something that happened in the background (saved, copied, connected). - Messages that need an answer belong in a [Dialog](https://albakore.github.io/pyzed/library/confirm/). #### Usage ```python import pyzed as pz from pyzed import ui import threading class Notes(pz.App): def __init__(self): self.messages = [] def notify(self, title): self.messages.append(title) threading.Timer(3, pz.post, args=(("expire", title),)).start() # hide it after 3 s def update(self, message): if message[0] == "expire": self.messages.remove(message[1]) def view(self): items = [ui.toast(m, kind="success", on_dismiss=lambda m=m: self.messages.remove(m)) for m in self.messages] return ui.toasts(self.main_view(), items) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Kind ```python ui.toast("Update available", "Pyzed 0.5 is on PyPI.") ``` ```python ui.toast("Saved", "main.py was saved.", kind="success") ``` ```python ui.toast("Low disk space", "2 GB left.", kind="warning") ``` ```python ui.toast("Could not connect", "Check your network.", kind="error") ``` ##### Content ```python ui.toast("Copied to clipboard", kind="success") ``` ```python ui.toast("Update available", "Restart to install it.", actions=[ui.button("Restart", on_restart, size="sm")], on_dismiss=on_dismiss) ``` ```python ui.toast("Now playing", "Lo-fi beats", icon="music", width=280) ``` ##### States and events ```python ui.toast("Uploading", "notes.md", kind="info", progress=0.6, on_dismiss=on_close) ``` ```python ui.toast("Build failed", "Click to see the log", kind="error", on_press=on_press) ``` #### Events Toast has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_dismiss` | `()` | Message or callable sent on close. | | `on_press` | `()` | Makes the card clickable. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Notes(pz.App): def __init__(self): self.messages = ["Saved"] self.opened = 0 def view(self): items = [ui.toast(m, kind="success", on_dismiss=lambda m=m: self.messages.remove(m), # its ✕ on_press=lambda: setattr(self, "opened", self.opened + 1)) # the card for m in self.messages] return ui.toasts(main_view(), items) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Dismiss: text: str class Notes(pz.App): messages = ["Saved"] def update(self, message): match message: case Dismiss(text): self.messages = [m for m in self.messages if m != text] def view(self): return ui.toasts(main_view(), [ui.toast(m, kind="success", on_dismiss=Dismiss(m)) for m in self.messages]) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: messages: tuple = ("Saved",) def view(state, set_state): close = lambda m: set_state(messages=tuple(x for x in state.messages if x != m)) return ui.toasts(main_view(), [ui.toast(m, kind="success", on_dismiss=lambda m=m: close(m)) for m in state.messages]) ``` #### Properties and API ##### `ui.toast` ```python toast( title, message=None, *, kind='info', icon=None, on_dismiss=None, actions=(), width=340, progress=None, on_press=None, theme=None, ) ``` A notification. | Property | Default | Description | | --- | --- | --- | | `title` | *required* | Title. | | `message` | `None` | Secondary text. | | `kind` | `"info"` | `"info"`, `"success"`, `"warning"` or `"error"`. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `actions` | `()` | Extra widgets (usually buttons) shown as actions. | | `width` | `340` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `progress` | `None` | 0 to 1: a thin bar at the bottom (time left, a download). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_dismiss`, `on_press` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#toast) ##### `ui.toasts` ```python toasts(base, items, *, spacing=8) ``` Shows `items` (usually `toast(...)`) stacked in the bottom-right corner over `base`. | Property | Default | Description | | --- | --- | --- | | `base` | *required* | The UI underneath. | | `items` | *required* | List of toasts. | | `spacing` | `8` | Space between children, in pixels. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#toasts) #### Related - [Callout](https://albakore.github.io/pyzed/library/callout/): An inline notice with a title, text and actions. - [Layer](https://albakore.github.io/pyzed/library/layer/): Draws an overlay over your view; clicking outside dismisses it. - [Alert](https://albakore.github.io/pyzed/library/alert/): A message with one button. ### Toggle group > An icon-only segmented control. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/toggle-group/ :::tip[In plain words] A row of icon buttons where only one can be on, like choosing left, center or right alignment. ::: A row of icon buttons where exactly one is selected, like text alignment in an editor. `options` are `(value, icon)` pairs and `on_select` receives the chosen value. `ui.toggle_group` · Category: [Actions](https://albakore.github.io/pyzed/library/#actions) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#toggle_group) **Code:** ```python pz.row([ ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], "center", on_align), ui.toggle_group([("list", "list"), ("grid", "layout-grid")], "grid", on_layout), ], spacing=16) ``` #### When to use - For a small set of mutually exclusive modes that have clear icons. - With text labels, use [Segmented](https://albakore.github.io/pyzed/library/segmented/) instead. #### Usage ```python import pyzed as pz from pyzed import ui class Toolbar(pz.App): def __init__(self): self.align = "left" def view(self): return ui.toggle_group( [("left", "align-left"), ("center", "align-center"), ("right", "align-right")], self.align, lambda value: setattr(self, "align", value), ) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python ui.toggle_group([("list", "list"), ("grid", "layout-grid")], "list", on_select) ``` ```python ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], "center", on_select) ``` ```python ui.toggle_group([("b", "bold"), ("i", "italic"), ("u", "underline"), ("s", "strikethrough")], "b", on_select) ``` ##### Selection ```python ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], "left", on_select) ``` ```python ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], "right", on_select) ``` ```python ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], None, on_select) ``` ##### States and events A third item in an option adds a tooltip: `(value, icon, tooltip)`. ```python ui.toggle_group([("b", "bold"), ("i", "italic"), ("u", "underline")], {"b", "u"}, on_select, multiple=True) ``` ```python ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], "left", on_select, disabled={"right"}) ``` #### Events Toggle group has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_select` | `(value)` | Callable that receives the chosen item. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Toolbar(pz.App): def __init__(self): self.align = "left" def set_align(self, value): # on_select gets the value of the pressed option self.align = value def view(self): return ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], self.align, self.set_align) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Align: value: str class Toolbar(pz.App): align = "left" def update(self, message): match message: case Align(value): self.align = value def view(self): return ui.toggle_group([("left", "align-left"), ("center", "align-center")], self.align, Align) # the class builds the message from the value ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: align: str = "left" def view(state, set_state): return ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], state.align, lambda value: set_state(align=value)) ``` #### Properties and API ##### `ui.toggle_group` ```python toggle_group( options, selected, on_select=None, *, disabled=False, multiple=False, theme=None, ) ``` An icon-only segmented control. | Property | Default | Description | | --- | --- | --- | | `options` | *required* | `(value, icon_name)` or `(value, icon_name, tooltip)`. | | `selected` | *required* | Current value (a collection with `multiple`). | | `disabled` | `False` | `True`, or the values to disable. | | `multiple` | `False` | Each button toggles its value on or off. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_select` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#toggle_group) #### Related - [Segmented](https://albakore.github.io/pyzed/library/segmented/): A group of mutually exclusive text buttons. - [Radio group](https://albakore.github.io/pyzed/library/radio-group/): Mutually exclusive options as radio buttons. - [Icon button](https://albakore.github.io/pyzed/library/icon-button/): A square button with just an icon and an optional tooltip. ### Tool call > One step of an AI agent: what it did, its result, and Allow or Reject. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/tool-call/ :::tip[In plain words] One thing an AI assistant did, like reading or changing a file. Click it to see the details; if it needs your permission, it asks with Allow and Reject buttons. ::: `ui.ToolCall` is a component for one action of an AI agent (reading a file, editing it, running a command), drawn like in Zed: a line with its status, the `title` and what it works on (`detail`), and a click unfolds the `input` and the `output` (plus any `images`). Edits, commands (`kind` `edit`, `delete`, `move` or `execute`) and calls that wait are cards instead. `diffs` show the edits it made as colored diffs, and while it runs `progress` shows the last lines a command printed. While `status` is `"waiting"` it asks right on the card: Always allow, Allow or Reject, reported through `on_allow`, `on_allow_always` and `on_deny`. The component remembers whether it is unfolded; your app only keeps the data. `ui.ToolCall` · Category: [AI agents](https://albakore.github.io/pyzed/library/#ai-agents) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#toolcall) **Code:** ```python pz.column([ ui.ToolCall(title="Read file", detail="app/storage.py", kind="read", output="def save(path): ..."), ui.ToolCall(title="Search", detail="def save", kind="search", output="app/storage.py:12"), ui.ToolCall(title="Edit file", detail="app/storage.py", kind="edit", status="waiting", diffs=[{"path": "app/storage.py", "old": "def save(path):\n write(path)\n", "new": "def save(path, data):\n write(path, data)\n"}], on_allow=on_allow, on_allow_always=on_allow_always, on_deny=on_deny), ui.ToolCall(title="Run", detail="pytest -q", kind="execute", status="running", progress="collected 24 items\n........................"), ], spacing=4, width=560) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - In an agent's chat, one per step the agent takes. - With `status="waiting"`, to ask before a risky step such as writing files or running a command. - For an edit on its own, outside a tool call, use [Diff view](https://albakore.github.io/pyzed/library/diff-view/). #### Usage ```python import pyzed as pz from pyzed import ui class Chat(pz.App): def __init__(self): self.status = "waiting" def answer(self, allowed): self.status = "done" if allowed else "rejected" def view(self): return ui.ToolCall(title="Run", detail="pytest -q", kind="execute", status=self.status, input={"command": "pytest -q"}, output="24 passed", on_allow=lambda e: self.answer(True), on_deny=lambda e: self.answer(False)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### State Pending and running show a spinner, waiting asks with Always allow, Allow and Reject, failed and rejected say so in red. ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="pending"), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="running"), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="done"), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="waiting", on_allow=on_allow, on_allow_always=on_allow_always, on_deny=on_deny), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="failed"), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="rejected"), width=360) ``` ##### Kind The kind picks the icon of a finished call. ```python pz.container(ui.ToolCall(title="Read file", detail="app.py", kind="read"), width=300) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit"), width=300) ``` ```python pz.container(ui.ToolCall(title="Delete file", detail="old.py", kind="delete"), width=300) ``` ```python pz.container(ui.ToolCall(title="Move file", detail="a.py → b.py", kind="move"), width=300) ``` ```python pz.container(ui.ToolCall(title="Search", detail="def save", kind="search"), width=300) ``` ```python pz.container(ui.ToolCall(title="Run", detail="pytest -q", kind="execute"), width=300) ``` ```python pz.container(ui.ToolCall(title="Think", kind="think"), width=300) ``` ```python pz.container(ui.ToolCall(title="Fetch", detail="https://example.com", kind="fetch"), width=300) ``` ```python pz.container(ui.ToolCall(title="Switch mode", detail="plan", kind="switch_mode"), width=300) ``` ```python pz.container(ui.ToolCall(title="Tool", detail="custom", kind="other"), width=300) ``` ##### Content Unfolded ones are drawn as they look after a click on the header. ```python pz.container(ui.ToolCall(title="Run", detail="pytest -q", kind="execute", status="running", progress="collected 24 items\n............\n............"), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="running", diffs=[{"path": "app.py", "old": "x = 1\ny = 2\n", "new": "x = 10\ny = 2\n"}]), width=360) ``` ```python pz.container(ui.ToolCall(title="Run", detail="rm -rf build", kind="execute", status="waiting", allow_always=False, on_allow=on_allow, on_deny=on_deny), width=360) ``` ```python pz.container(ui.ToolCall(title="Read file", detail="app.py", kind="read", input={"path": "app.py"}, output="import pyzed as pz\n\npz.run(App)"), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", diffs=[{"path": "app.py", "old": "x = 1\ny = 2\n", "new": "x = 10\ny = 2\n"}]), width=360) ``` #### Events Tool call has **3 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). It is a `pz.Component`, so it also raises `on_mount` and `on_unmount`, and its handlers get a `pz.Event` whose `value` holds the data. | Event | Handler gets | When | | --- | --- | --- | | `on_allow` | `(event)` | Allow was pressed. | | `on_allow_always` | `(event)` | Always allow was pressed. | | `on_deny` | `(event)` | Reject was pressed. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Chat(pz.App): def __init__(self): self.status = "waiting" def answer(self, status): self.status = status def view(self): # a pz.Component: handlers get a pz.Event return ui.ToolCall(title="Run", detail="pytest -q", kind="execute", status=self.status, input={"command": "pytest -q"}, on_allow=lambda e: self.answer("done"), on_allow_always=lambda e: self.answer("done"), on_deny=lambda e: self.answer("rejected")) ``` **Declarative:** ```python class Chat(pz.App): status = "waiting" def update(self, message): match message: case "allow": self.status = "done" case "deny": self.status = "rejected" def view(self): return ui.ToolCall(title="Run", detail="pytest -q", kind="execute", status=self.status, on_allow="allow", on_deny="deny") # messages work for components too ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: status: str = "waiting" def view(state, set_state): return ui.ToolCall(title="Run", detail="pytest -q", kind="execute", status=state.status, on_allow=lambda e: set_state(status="done"), on_deny=lambda e: set_state(status="rejected")) ``` #### Tips :::tip `status` is yours: set it from what the agent reports (`pending`, then `running`, then `done` or `failed`; `rejected` after a Reject). `kind` only picks the icon: `read`, `edit`, `delete`, `move`, `search`, `execute`, `think`, `fetch`, `switch_mode` or `other`. The header only unfolds when there is something to show (`input`, `output`, `images` or `diffs`). ::: #### Properties and API ##### `ui.ToolCall` ```python class ToolCall ``` A component for one action of an AI agent, drawn like in Zed's Agent panel: a line with its icon, what it does and where (it unfolds to show the input and the output), or a card for edits and commands, with the diff or the output. While `status` is `"waiting"` it asks right on the card: Always allow, Allow or Reject. See [AI agents](https://albakore.github.io/pyzed/editor/ai-agents/). | Property | Default | Description | | --- | --- | --- | | `title` | `""` | What it does ("Edit file"). | | `detail` | `""` | What it works on, in a monospace font. | | `kind` | `""` | ACP's kind: `read`, `edit`, `delete`, `move`, `search`, `execute`, `think`, `fetch` or `other` (its icon). | | `status` | `"done"` | `pending`, `running`, `waiting`, `done`, `failed` or `rejected`. | | `input` | `None` | The arguments (a dict or text). | | `output` | `None` | The answer (text or a dict). | | `images` | `None` | PNG bytes shown under the output. | | `diffs` | `None` | The edits it made: `{"path", "old", "new"}` dicts, shown as colored diffs (see `ui.diff_view`). | | `progress` | `""` | What it printed so far (a command's output); its last lines show under the header while it runs. | | `allow_always` | `True` | Shows Always allow. | | `theme` | `None` | Theme. | Events: `on_allow`, `on_allow_always`, `on_deny` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#toolcall) #### Related - [Diff view](https://albakore.github.io/pyzed/library/diff-view/): An edit as colored lines: added in green, removed in red. - [Plan](https://albakore.github.io/pyzed/library/plan/): An agent's plan as a checklist that folds. - [Edits bar](https://albakore.github.io/pyzed/library/edits-bar/): The files an agent changed, with Keep all and Reject all. - [Context meter](https://albakore.github.io/pyzed/library/context-meter/): How full a model's context window is: a ring and `48k / 200k`. ### Toolbar > The bar under the tabs: breadcrumbs and buffer actions. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/toolbar/ :::tip[In plain words] A thin bar with the actions of what you are looking at. ::: The bar below the tabs of a code editor: widgets at the `start` (usually breadcrumbs) and at the `end` (buffer actions). `ui.toolbar` · Category: [Navigation](https://albakore.github.io/pyzed/library/#navigation) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#toolbar) **Code:** ```python ui.toolbar( [ui.breadcrumbs([("file-code", "src"), "main.rs", "fn main"])], [ui.icon_button("search", on_search, size="sm"), ui.icon_button("sparkles", on_assist, size="sm")], ) ``` #### When to use - Between the tab bar and the editor, or at the top of any content view. #### Usage ```python from pyzed import ui ui.toolbar( [ui.breadcrumbs([("file-code", "src"), "main.rs", "fn main"])], [ui.icon_button("search", on_search, size="sm"), ui.icon_button("sparkles", on_assist, size="sm")], ) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Content ```python pz.container(ui.toolbar([ui.breadcrumbs(["src", "main.rs"])]), width=400) ``` ```python pz.container(ui.toolbar([], [ui.icon_button("search", on_search, size="sm"), ui.icon_button("sparkles", on_assist, size="sm")]), width=400) ``` ```python pz.container(ui.toolbar([ui.label("Preview")], [ui.button("Publish", on_publish, size="sm", variant="solid")], height=40), width=400) ``` ##### States and events ```python pz.container(ui.toolbar([ui.breadcrumbs(["src", "app.py"])], [ui.icon_button("play", on_press)], center=ui.muted("Running…", size="sm")), width=440) ``` #### Events Toolbar has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Properties and API ##### `ui.toolbar` ```python toolbar(start=(), end=(), *, center=None, height=32, theme=None) ``` The bar below the tabs: breadcrumbs on the left and buffer actions on the right. | Property | Default | Description | | --- | --- | --- | | `start` | `()` | Widgets at the start (left). | | `end` | `()` | Widgets at the end (right). | | `center` | `None` | Kept in the middle of the bar whatever the widths of both sides. | | `height` | `32` | Height. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#toolbar) #### Related - [Tab bar](https://albakore.github.io/pyzed/library/tab-bar/): Editor tabs with icons, dirty dots and close buttons. - [Breadcrumbs](https://albakore.github.io/pyzed/library/breadcrumbs/): `src › main.rs › fn main`, optionally clickable. - [Icon button](https://albakore.github.io/pyzed/library/icon-button/): A square button with just an icon and an optional tooltip. ### Tooltip > A hint shown on hover, optionally with its shortcut. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/tooltip/ :::tip[In plain words] A little box with a hint that appears when you rest the mouse on something. ::: Wraps any widget and shows a small themed box next to it while the mouse is over it. `keys=` adds the shortcut and `position` is `"top"`, `"bottom"` (default), `"left"` or `"right"`. `pz.tooltip` is the unstyled base version. `ui.tooltip` · Category: [Display](https://albakore.github.io/pyzed/library/#display) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#tooltip) **Code:** ```python pz.row([ ui.tooltip(ui.icon_button("save", on_save), "Save", keys="ctrl-s"), ui.icon_button("settings", on_settings, tooltip="Settings"), ], spacing=8) ``` #### When to use - On icon buttons and compact controls whose meaning is not obvious. - Don't hide essential information in a tooltip: it is invisible on touch and keyboard use. #### Usage ```python import pyzed as pz from pyzed import ui pz.row([ ui.tooltip(ui.icon_button("save", on_save), "Save", keys="ctrl-s"), ui.icon_button("settings", on_settings, tooltip="Settings"), ], spacing=8) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Position Drawn as the pointer shows it: tooltips only open on hover. ```python ui.tooltip(ui.icon_button("save", on_save), "Save") ``` ```python ui.tooltip(ui.icon_button("save", on_save), "Save", position="top") ``` ```python ui.tooltip(ui.icon_button("save", on_save), "Save", position="right") ``` ```python ui.tooltip(ui.icon_button("save", on_save), "Save", position="left") ``` ##### Content Drawn as the pointer shows it: tooltips only open on hover. ```python ui.tooltip(ui.button("Save", on_save), "Save the file", keys="ctrl-s") ``` ```python ui.icon_button("settings", on_settings, tooltip="Settings") ``` #### Events Tooltip has no events of its own: it shows what you give it. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). #### Tips :::tip Tooltips only appear on hover, so the preview shows the buttons. `ui.icon_button(..., tooltip="Text")` is a shortcut for wrapping it in `ui.tooltip`. ::: #### Properties and API ##### `ui.tooltip` ```python tooltip(content, text, *, keys=None, position='bottom', theme=None) ``` Wraps `content` with a themed tooltip, optionally with its shortcut. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `text` | *required* | Tooltip text. | | `keys` | `None` | Shortcut shown at the end, written as a string: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `position` | `"bottom"` | `"top"`, `"bottom"`, `"left"`, `"right"` or `"cursor"`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#tooltip) ##### `pz.tooltip` ```python tooltip( content, tip, *, position='bottom', gap=4.0, bg=None, color=None, border_color=None, border_width=0.0, radius=None, shadow=None, ) ``` Shows `tip` next to `content` on hover. `ui.tooltip` adds the theme's style. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | Content: a `Widget`, a string or a number (converted to text). | | `tip` | *required* | Tooltip content. | | `position` | `"bottom"` | `"top"`, `"bottom"`, `"left"`, `"right"` or `"cursor"`. | | `gap` | `4.0` | Distance from the content. | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `color` | `None` | Text or icon color. | | `border_color` | `None` | Border color. | | `border_width` | `0.0` | Border width in pixels. | | `radius` | `None` | Corner radius: a number or `(top_left, top_right, bottom_right, bottom_left)`. | | `shadow` | `None` | Shadow: `(color, (dx, dy), blur)`. | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#tooltip) #### Related - [Icon button](https://albakore.github.io/pyzed/library/icon-button/): A square button with just an icon and an optional tooltip. - [Kbd](https://albakore.github.io/pyzed/library/kbd/): Keyboard shortcuts drawn as keys, with symbols on macOS. - [Popover](https://albakore.github.io/pyzed/library/popover/): Floating content at a point: context menus, color pickers. ### Tree > A collapsible file tree with icons and git colors. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/tree/ :::tip[In plain words] A list of folders you can open and close, like the files of a project. ::: A project panel: collapsible folders, an icon per file type and git status colors. `TreeNode.from_paths(paths, status=...)` builds the nodes, puts folders first and propagates the git status to the folders that contain changes. Each node's id is its path; you own the `expanded` set and the `selected` id. `ui.tree` · Category: [Lists and menus](https://albakore.github.io/pyzed/library/#lists-and-menus) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#tree) **Code:** ```python nodes = ui.TreeNode.from_paths( ["src/main.rs", "src/app.rs", "src/ui/button.rs", "src/ui/mod.rs", "tests/app.rs", "Cargo.toml"], status={"src/app.rs": "modified", "src/ui/button.rs": "created"}, ) pz.container(ui.tree(nodes, expanded={"src", "src/ui"}, selected="src/app.rs", on_toggle=on_toggle, on_select=on_select), width=280) ``` #### When to use - For files and any hierarchy: outlines, categories, settings sections. - `status` is `"modified"`, `"created"`, `"deleted"`, `"conflict"` or `"ignored"`. #### Usage ```python import pyzed as pz from pyzed import ui class Explorer(pz.App): def __init__(self): self.nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "Cargo.toml"]) self.expanded = {"src"} self.selected = None def toggle(self, folder): self.expanded ^= {folder} def view(self): return ui.tree(self.nodes, expanded=self.expanded, selected=self.selected, on_toggle=self.toggle, on_select=lambda path: setattr(self, "selected", path)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Expanded ```python nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"]) pz.container(ui.tree(nodes, expanded=set()), width=240) ``` ```python nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"]) pz.container(ui.tree(nodes, expanded={"src"}), width=240) ``` ```python nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"]) pz.container(ui.tree(nodes, expanded={"src", "src/ui"}, selected="src/ui/button.rs"), width=240) ``` ##### Git status ```python nodes = ui.TreeNode.from_paths(["src/app.rs", "src/new.rs", "src/old.rs", "build.log"], status={"src/app.rs": "modified", "src/new.rs": "created", "src/old.rs": "deleted", "build.log": "ignored"}) pz.container(ui.tree(nodes, expanded={"src"}), width=240) ``` ```python nodes = ui.TreeNode.from_paths(["merge.py", "ok.py"], status={"merge.py": "conflict"}) pz.container(ui.tree(nodes, expanded=set()), width=240) ``` ##### Density ```python nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"]) pz.container(ui.tree(nodes, expanded={"src", "src/ui"}, item_height=20, indent=10), width=240) ``` ```python nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"]) pz.container(ui.tree(nodes, expanded={"src"}, item_height=30, indent=20), width=240) ``` ##### States and events `on_open(id)` runs on a double click of a file and `on_context(id)` on a right click. ```python pz.container(ui.tree(ui.TreeNode.from_paths(["src/app.py", "src/ui.py", "README.md"]), expanded={"src"}, selected={"src/app.py", "README.md"}, focused="src/ui.py", on_open=on_open, on_context=on_context), width=300) ``` #### Examples ##### A real folder Build the paths with `pathlib` and pass them to `from_paths`. ```python from pathlib import Path root = Path(".") paths = [p.relative_to(root).as_posix() for p in root.rglob("*") if p.is_file() and ".git" not in p.parts] nodes = ui.TreeNode.from_paths(paths) ``` ##### Nodes by hand Folders are nodes with `children`. **Code:** ```python nodes = [ ui.TreeNode("Inbox", children=[ui.TreeNode("Welcome", id="inbox/1", icon="mail")], id="inbox"), ui.TreeNode("Archive", children=[], id="archive"), ui.TreeNode("Notes.md", id="notes", icon="notebook-pen"), ] pz.container(ui.tree(nodes, expanded={"inbox"}, selected="inbox/1"), width=260) ``` #### Events Tree has **4 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_toggle` | `(id)` | Called with the id of the clicked folder. | | `on_select` | `(id)` | Called with the id of the clicked file. | | `on_open` | `(id)` | Called with the id of a file that is double-clicked. | | `on_context` | `(id)` | Called with the id of a row that is right-clicked (show a `ui.menu` there). | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Explorer(pz.App): def __init__(self): self.nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "Cargo.toml"]) self.expanded = {"src"} self.selected = None def toggle(self, folder): # on_toggle: the folder's id (its path) self.expanded ^= {folder} def view(self): return ui.tree(self.nodes, expanded=self.expanded, selected=self.selected, on_toggle=self.toggle, on_select=lambda path: setattr(self, "selected", path), # click on a file on_open=lambda path: print("open", path), # double click on_context=lambda path: print("menu for", path)) # right click ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Toggle: folder: str @dataclass class Select: path: str class Explorer(pz.App): nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "Cargo.toml"]) expanded = frozenset({"src"}) selected = None def update(self, message): match message: case Toggle(folder): self.expanded = self.expanded ^ {folder} case Select(path): self.selected = path def view(self): return ui.tree(self.nodes, expanded=self.expanded, selected=self.selected, on_toggle=Toggle, on_select=Select) ``` **Functional:** ```python from dataclasses import dataclass NODES = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "Cargo.toml"]) @dataclass(frozen=True) class State: expanded: frozenset = frozenset({"src"}) selected: str = "" def view(state, set_state): return ui.tree(NODES, expanded=state.expanded, selected=state.selected, on_toggle=lambda folder: set_state(expanded=state.expanded ^ {folder}), on_select=lambda path: set_state(selected=path)) ``` #### Properties and API ##### `ui.tree` ```python tree( nodes, *, expanded, selected=None, on_toggle=None, on_select=None, on_open=None, on_context=None, focused=None, indent=14, item_height=24, theme=None, ) ``` A collapsible file tree, like a code editor's project panel. | Property | Default | Description | | --- | --- | --- | | `nodes` | *required* | List of `TreeNode`. | | `expanded` | *required* | Set of open folder ids. | | `selected` | `None` | One id or a collection of ids (multiple selection). | | `focused` | `None` | Outlines the row the keyboard is on. | | `indent` | `14` | Left indentation, in pixels. | | `item_height` | `24` | Height of each row. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_toggle`, `on_select`, `on_open`, `on_context` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#tree) ##### `ui.TreeNode` ```python @dataclass class TreeNode(name, children=None, id=None, status=None, icon=None, icon_color=None) ``` A file or folder for `ui.tree`. Folders have `children` (a list). `status` is `"modified"`, `"created"`, `"deleted"`, `"conflict"` or `"ignored"`. `TreeNode.from_paths(paths, status=None)` builds the tree from paths like `"src/main.rs"`, with folders first and the git status propagated to folders. | Property | Default | Description | | --- | --- | --- | | `name` | *required* | Display name. | | `children` | `None` | Children (`None` for a file). | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `status` | `None` | Git status. | | `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). | | `icon_color` | `None` | Icon color: a theme token or a hex color. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#treenode) #### Related - [List item](https://albakore.github.io/pyzed/library/list-item/): A list row with icon, text and an end slot. - [Panel](https://albakore.github.io/pyzed/library/panel/): A dockable, resizable side panel. - [Workspace](https://albakore.github.io/pyzed/library/workspace/): The layout of an editor-style window: title bar, docks, center, status bar. ### Unsaved changes > “Save your changes before closing?”. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/unsaved-changes/ :::tip[In plain words] The “Save your changes before closing?” question. ::: “Save your changes to *name* before closing?” with Don't save, Cancel and Save. `ui.unsaved_changes` · Category: [Dialogs](https://albakore.github.io/pyzed/library/#dialogs) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#unsaved_changes) **Code:** ```python ui.dialog_layer(main_view(), ui.unsaved_changes("notes.py", on_save, on_discard, on_cancel)) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - When closing a tab or a window with unsaved edits. #### Usage ```python from pyzed import ui ui.dialog_layer(main_view(), ui.unsaved_changes("notes.py", on_save, on_discard, on_cancel)) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python ui.unsaved_changes("notes.py", on_save, on_discard, on_cancel) ``` ```python ui.unsaved_changes("report.pzform", on_save, on_discard, on_cancel, width=360) ``` #### Events Unsaved changes has **3 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_save` | `()` | Save. | | `on_discard` | `()` | Don't save. | | `on_cancel` | `()` | Cancel. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Notes(pz.App): def __init__(self): self.closing = True def save(self): self.closing = False # write the file, then close def view(self): dialog = ui.unsaved_changes("notes.py", self.save, lambda: setattr(self, "closing", False), # on_discard lambda: setattr(self, "closing", False)) # on_cancel return ui.dialog_layer(main_view(), dialog if self.closing else None) ``` **Declarative:** ```python class Notes(pz.App): closing = True def update(self, message): match message: case "save" | "discard": self.closing = False case "cancel": self.closing = False def view(self): dialog = ui.unsaved_changes("notes.py", "save", "discard", "cancel") return ui.dialog_layer(main_view(), dialog if self.closing else None) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: closing: bool = True saved: bool = False def view(state, set_state): dialog = ui.unsaved_changes("notes.py", lambda: set_state(saved=True, closing=False), lambda: set_state(closing=False), lambda: set_state(closing=False)) return ui.dialog_layer(main_view(), dialog if state.closing else None) ``` #### Properties and API ##### `ui.unsaved_changes` ```python unsaved_changes(name, on_save, on_discard, on_cancel, *, width=440, theme=None) ``` "Save your changes to *name* before closing?" with Don't save, Cancel and Save. | Property | Default | Description | | --- | --- | --- | | `name` | *required* | File or document name. | | `width` | `440` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_save`, `on_discard`, `on_cancel` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#unsaved_changes) #### Related - [Confirm](https://albakore.github.io/pyzed/library/confirm/): A yes/no question, red for destructive actions. - [Choice](https://albakore.github.io/pyzed/library/choice/): A message with your own buttons. - [Tab bar](https://albakore.github.io/pyzed/library/tab-bar/): Editor tabs with icons, dirty dots and close buttons. ### Virtual list > A list that only builds the rows in view, for millions of items. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/virtual-list/ :::tip[In plain words] A list that stays fast even with millions of rows, because it only draws what you see. ::: A scrolling list for thousands or millions of items. Only the rows in view are built: `render(item)` is called when an item scrolls into view, so the list costs the same whatever its length. Every row has the same `item_height`. `pz.virtual_column` is the building block underneath (rows by index, inside any `scrollable`), and [Table](https://albakore.github.io/pyzed/library/table/) uses it for its rows too. `ui.virtual_list` · Category: [Lists and menus](https://albakore.github.io/pyzed/library/#lists-and-menus) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#virtual_list) **Code:** ```python files = [f"src/module_{i:06}.py" for i in range(1_000_000)] pz.container(ui.virtual_list( files, lambda f: ui.list_item(f, icon="file-code", selected=f.endswith("02.py")), height=240, ), width=420) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - For logs, search results, file lists or any list that can grow past a few hundred rows. - For short lists, a `pz.column` of [List items](https://albakore.github.io/pyzed/library/list-item/) is simpler. #### Usage ```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") ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Options ```python pz.container(ui.virtual_list([f"row {i:,}" for i in range(1_000_000)], lambda r: ui.list_item(r, icon="file"), height=150), width=260) ``` ```python pz.container(ui.virtual_list([f"[{i:06}] request handled" for i in range(100_000)], lambda line: pz.text(line, font="mono", size=12), item_height=20, height=150), width=260) ``` ```python pz.container(ui.virtual_list(list(range(10_000)), lambda i: ui.card(ui.label(f"Card {i}"), padding=8), item_height=40, spacing=6, height=150), width=260) ``` #### Examples ##### Rows by index `pz.virtual_column(count, item, item_height=...)` calls `item(index)`, handy when the rows come from an array, a database cursor or a computed value. Scroll it like any `scrollable`: row `i` starts at `i * (item_height + spacing)`. **Code:** ```python pz.container(pz.scrollable( pz.virtual_column(10_000_000, lambda i: pz.text(f"{i:>10,} {i * i:>22,}", font="mono", size=12), item_height=22), height=150, ), width=420) ``` #### Events Virtual list has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_scroll` | `(info)` | Callable that receives the scroll viewport, as in `pz.scrollable`. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Logs(pz.App): def __init__(self): self.lines = [f"[{i:06}] request handled" for i in range(100_000)] self.top = 0 def scrolled(self, info): # on_scroll: offsets and sizes of the viewport self.top = int(info["y"] // 20) def view(self): return pz.column([ ui.label(f"Line {self.top + 1} of {len(self.lines)}"), ui.virtual_list(self.lines, lambda line: pz.text(line, font="mono", size=12), item_height=20, height=200, on_scroll=self.scrolled), ], spacing=6) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Scrolled: info: dict class Logs(pz.App): lines = [f"[{i:06}] request handled" for i in range(100_000)] top = 0 def update(self, message): match message: case Scrolled(info): self.top = int(info["y"] // 20) def view(self): return ui.virtual_list(self.lines, lambda line: pz.text(line, size=12), item_height=20, height=200, on_scroll=Scrolled) ``` **Functional:** ```python from dataclasses import dataclass LINES = [f"[{i:06}] request handled" for i in range(100_000)] @dataclass(frozen=True) class State: top: int = 0 def view(state, set_state): return pz.column([ ui.label(f"Line {state.top + 1}"), ui.virtual_list(LINES, lambda line: pz.text(line, size=12), item_height=20, height=200, on_scroll=lambda info: set_state(top=int(info["y"] // 20))), ], spacing=6) ``` #### Tips :::tip - Rows are built again when they scroll back into view, so keep `render` cheap or wrap it in `@pz.memo`. - `items` should be a list or another sequence: other iterables are copied into a list on every `view()`. ::: #### Properties and API ##### `ui.virtual_list` ```python virtual_list( items, render, *, item_height=26, spacing=0, width='fill', height='fill', padding=None, overscan=4, id=None, on_scroll=None, theme=None, ) ``` A scrolling list that only builds the rows in view, for thousands or millions of items. `render(item)` returns the widget for one item and is called when it scrolls into view. Every row has the same height. | Property | Default | Description | | --- | --- | --- | | `items` | *required* | The items: a list or any sequence (other iterables are turned into a list). | | `render` | *required* | Function that receives an item and returns its widget. | | `item_height` | `26` | Height of every row, in pixels. | | `spacing` | `0` | Space between children, in pixels. | | `width` | `"fill"` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `"fill"` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `padding` | `None` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `overscan` | `4` | Extra rows built above and below the visible ones. | | `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_scroll` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#virtual_list) ##### `pz.virtual_column` ```python virtual_column(count, item, *, item_height, spacing=0.0, overscan=4, width=None) ``` A column of `count` rows of the same height that only builds the rows in view: `item(index)` is called when row `index` scrolls into view, and rows that leave it are dropped. A column of a million rows costs the same as one of fifty. Put it in a `scrollable` (it virtualizes against the visible area of the closest one, or the window). | Property | Default | Description | | --- | --- | --- | | `count` | *required* | Number of rows. | | `item` | *required* | Function that receives a row index and returns its widget. | | `item_height` | *required* | Height of every row, in pixels. | | `spacing` | `0.0` | Space between rows, in pixels. | | `overscan` | `4` | Extra rows built above and below the visible ones, so fast scrolling shows no gaps. | | `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | [Full reference →](https://albakore.github.io/pyzed/reference/widgets/#virtual_column) #### Related - [List item](https://albakore.github.io/pyzed/library/list-item/): A list row with icon, text and an end slot. - [Table](https://albakore.github.io/pyzed/library/table/): A data table with a fixed header, sorting and selection. ### Window frame > Your own title bar, window buttons and menus. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API. Source: https://albakore.github.io/pyzed/library/window-frame/ :::tip[In plain words] Draws the window's own top bar, with its menus and minimize / maximize / close buttons. ::: Draws the whole frame of a window opened without system decorations (`frame = "custom"`): a title bar that moves the window (double click maximizes), the minimize, maximize and close buttons in the style of a platform, menus on the left (`ui.Menu`) and borders that resize the window. `ui.window_title_bar` is the bar alone, `ui.window_controls` the three buttons and `ui.system_platform()` says which style this system uses. `ui.window_frame` · Category: [Workspace](https://albakore.github.io/pyzed/library/#workspace) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#window_frame) **Code:** ```python ui.window_frame( ui.editor("frame-main", "import pyzed as pz\n\npz.run(App)\n", path="main.py"), title="notes.py", menus=[ui.Menu("File", [ui.MenuItem("Open…", on_open, keys="ctrl-o")]), ui.Menu("Edit", [ui.MenuItem("Undo", on_undo, keys="ctrl-z")])], start=[ui.title_button("pyzed", on_projects, chevron=True)], style="windows", ) ``` :::note[New] This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues). ::: #### When to use - For apps that want a modern editor-like window: menus and pickers in the title bar instead of the system's frame. See [Window frames](https://albakore.github.io/pyzed/guides/windows/#window-frames-your-own-title-bar). - With no callbacks, `ui.window_title_bar` and `ui.window_controls` only show: use them to preview a window in the frame of each platform. #### Usage ```python import pyzed as pz from pyzed import ui class Notes(pz.App): title = "Notes" frame = "custom" # or "macos", "windows", "gnome", "ubuntu", "kde" def view(self): return ui.window_frame( self.editor(), menus=[ui.Menu("File", [ui.MenuItem("Open…", self.open, keys="ctrl-o"), ui.SEPARATOR, ui.MenuItem("Quit", self.close)])], start=[ui.title_button("pyzed", chevron=True)], end=[ui.button("Run", self.run, icon="play", size="sm")], ) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Platform style ```python pz.container(ui.window_title_bar("notes.py", style="windows"), width=420) ``` ```python pz.container(ui.window_title_bar("notes.py", style="macos"), width=420) ``` ```python pz.container(ui.window_title_bar("notes.py", style="gnome"), width=420) ``` ```python pz.container(ui.window_title_bar("notes.py", style="kde"), width=420) ``` ```python pz.container(ui.window_title_bar("notes.py", style="ubuntu"), width=420) ``` ##### State ```python pz.container(ui.window_title_bar("notes.py", style="windows", focused=False), width=420) ``` ```python ui.window_controls("windows", maximized=True) ``` ```python ui.window_controls("macos") ``` #### Events Window frame has **5 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Of | Handler gets | When | | --- | --- | --- | --- | | `on_drag` | `ui.window_title_bar` | `()` | Called when the empty part of the bar is dragged (return `Command.drag_window()`). | | `on_minimize` | `ui.window_title_bar`, `ui.window_controls` | `()` | Minimize button. | | `on_maximize` | `ui.window_title_bar`, `ui.window_controls` | `()` | Maximize button and double click on the bar. | | `on_close` | `ui.window_title_bar`, `ui.window_controls` | `()` | Close button. | | `on_menu` | `ui.window_title_bar` | `()` | Right click on the bar. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Notes(pz.App): frame = "custom" # Pyzed draws the title bar def view(self): bar = ui.window_title_bar( "notes.py", on_drag=lambda: self.drag(), # the empty part of the bar on_minimize=lambda: self.minimize(), on_maximize=lambda: self.toggle_maximize(), # also a double click on the bar on_close=lambda: self.close(), on_menu=lambda: print("window menu"), # right click on the bar maximized=self.window_maximized, ) return pz.column([bar, main_view()]) ``` **Declarative:** ```python class Notes(pz.App): frame = "custom" def update(self, message): # the commands are returned from update match message: case "drag": return self.drag() case "minimize": return self.minimize() case "maximize": return self.toggle_maximize() case "close": return self.close() def view(self): return pz.column([ui.window_title_bar("notes.py", on_drag="drag", on_minimize="minimize", on_maximize="maximize", on_close="close"), main_view()]) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: title: str = "notes.py" def view(state, set_state): # A handler can return a command: Pyzed runs it. return pz.column([ui.window_title_bar(state.title, on_drag=lambda: pz.Command.drag_window(), on_close=lambda: pz.Command.exit()), main_view()]) ``` #### Tips :::tip With only menus, define `frame_menus()` on the window and skip `ui.window_frame`: the runtime frames the view for you. `style` defaults to the window's `frame`, or this system's style. ::: #### Properties and API ##### `ui.window_frame` ```python window_frame( content, *, title=None, menus=None, menu_button=True, start=(), center=None, end=(), style=None, window=None, height=None, resize_border=6.0, resize_edges=True, bg=None, theme=None, ) ``` The whole frame of a window opened with `frame="custom"`: a title bar that moves the window (double click maximizes, right click opens the system menu on Windows), the minimize, maximize and close buttons in the style of a platform, menus on the left, and borders that resize the window. | Property | Default | Description | | --- | --- | --- | | `content` | *required* | The window's content, under the title bar. | | `title` | `None` | Title shown in the bar. By default the window's `title`. | | `menus` | `None` | Menus on the left of the bar: a list of `ui.Menu`. | | `menu_button` | `True` | Shows the menus behind a ☰ button (`False` always shows their names). | | `start` | `()` | Items after the menus (project, branch…). | | `center` | `None` | Your own widget in the middle instead of the title (a search field, Run and Stop…); the bar still drags on both sides of it. | | `end` | `()` | Items before the window buttons. | | `style` | `None` | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"` or `"kde"`. By default the window's `frame`, or this system's. | | `window` | `None` | The window the buttons act on. By default the one whose `view()` is running. | | `height` | `None` | Height of the bar. By default the platform's. | | `resize_border` | `6.0` | Width in pixels of the borders that resize the window. | | `resize_edges` | `True` | `False` leaves the resize borders to you (`pz.window_edges`), to put them over menus and dialogs. | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#window_frame) ##### `ui.Menu` ```python @dataclass class Menu(label, items=[]) ``` A menu of a window frame's menu bar: a name and its items (`ui.MenuItem`, `ui.SEPARATOR` or a header string). | Property | Default | Description | | --- | --- | --- | | `label` | *required* | The menu's name. | | `items` | `[]` | Its entries: `ui.MenuItem`, `ui.SEPARATOR` or a header string. | [Full reference →](https://albakore.github.io/pyzed/reference/ui/#menu-1) ##### `ui.window_title_bar` ```python window_title_bar( title=None, *, style=None, start=(), center=None, end=(), on_drag=None, on_minimize=None, on_maximize=None, on_close=None, on_menu=None, maximized=False, focused=True, width='fill', height=None, bg=None, border=True, theme=None, ) ``` A window's title bar alone: window buttons in the style of `style` (on the left for macOS), `start` items, the title and `end` items. Without callbacks it only shows, which is how Pyzed Design previews a form in the frame of each platform. | Property | Default | Description | | --- | --- | --- | | `title` | `None` | Title (centered, or on the left on Windows). | | `style` | `None` | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"` or `"kde"` (`None`: this system's). | | `start` | `()` | Items at the start. | | `center` | `None` | Widget in the middle instead of the title; the bar still drags on both sides of it. | | `end` | `()` | Items at the end. | | `maximized` | `False` | Shows the restore symbol. | | `focused` | `True` | `False` dims it like a window in the background. | | `width` | `"fill"` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | `None` | Height. By default the platform's. | | `bg` | `None` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `border` | `True` | Line under the bar. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_drag`, `on_minimize`, `on_maximize`, `on_close`, `on_menu` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#window_title_bar) ##### `ui.window_controls` ```python window_controls( style=None, *, on_minimize=None, on_maximize=None, on_close=None, maximized=False, focused=True, height=None, theme=None, ) ``` The minimize, maximize and close buttons in the style of a platform. A button without a callback is drawn but does nothing. | Property | Default | Description | | --- | --- | --- | | `style` | `None` | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"` or `"kde"` (`None`: this system's). | | `maximized` | `False` | Shows the restore symbol. | | `focused` | `True` | `False` dims them. | | `height` | `None` | Height. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_minimize`, `on_maximize`, `on_close` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#window_controls) ##### `ui.system_platform` ```python system_platform() ``` The style of this system's window buttons: `"macos"`, `"windows"`, `"kde"`, `"ubuntu"` or `"gnome"` (other Linux desktops). Under WSL it is `"windows"`. [Full reference →](https://albakore.github.io/pyzed/reference/ui/#system_platform) #### Related - [Title bar](https://albakore.github.io/pyzed/library/title-bar/): The top bar with project and branch pickers. - [Workspace](https://albakore.github.io/pyzed/library/workspace/): The layout of an editor-style window: title bar, docks, center, status bar. - [Menu](https://albakore.github.io/pyzed/library/menu/): A context menu with icons, shortcuts and checks. ### Workspace > The layout of an editor-style window: title bar, docks, center, status bar. A Pyzed component for Python desktop GUIs, with examples and its API. Source: https://albakore.github.io/pyzed/library/workspace/ :::tip[In plain words] The whole layout of an editor-like window: bar on top, side panels, the main area and a bar at the bottom. ::: Builds the frame of an editor-style window: the title bar on top, docks on the left, right and bottom, the center and the status bar. Pass `None` to hide a dock (that is how Ctrl+B works); `left_width`, `right_width` and `bottom_height` set their size and `on_resize` reports drags. `ui.workspace` · Category: [Workspace](https://albakore.github.io/pyzed/library/#workspace) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#workspace-1) **Code:** ```python files = ui.TreeNode.from_paths(["src/main.py", "src/ui.py", "README.md"], status={"src/ui.py": "modified"}) ui.workspace( pz.column([ ui.tab_bar([ui.Tab.for_file("src/main.py")], "src/main.py", on_close=on_close), ui.editor("ws-main", "import pyzed as pz\n\n\ndef main():\n pz.run(App)\n", path="main.py"), ]), title_bar=ui.title_bar([ui.title_button("pyzed", chevron=True), ui.title_button("main", icon="git-branch", muted=True)], [ui.icon_button("bell", on_bell, size="sm"), ui.avatar("KV")]), left=pz.column([ui.panel_header("Project", [ui.icon_button("plus", on_new, size="xs")]), ui.tree(files, expanded={"src"}, selected="src/main.py")]), bottom=pz.column([ui.panel_header("Terminal", icon="terminal"), ui.terminal("$ python main.py\n\x1b[32m✓\x1b[0m done")]), status_bar=ui.status_bar([ui.status_item("0", icon="circle-x"), ui.status_item("1", icon="triangle-alert")], [ui.status_item("Ln 4, Col 8"), ui.status_item("Python")]), left_width=200, bottom_height=110, ) ``` #### When to use - As the root of editor-like apps: IDEs, note apps, database clients, file managers. #### Usage ```python import pyzed as pz from pyzed import ui class Ide(pz.App): def __init__(self): self.show_left = True def on_key(self, combo): if combo == "ctrl-b": self.show_left = not self.show_left def view(self): return ui.workspace( self.editor_area(), title_bar=ui.title_bar([ui.title_button("my-project", chevron=True)]), left=self.project_panel() if self.show_left else None, status_bar=ui.status_bar([], [ui.status_item("Python")]), ) ``` #### Variants :::note[About these captures] Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks. ::: ##### Docks ```python ui.workspace(main_view(), status_bar=ui.status_bar([], [ui.status_item("Python")])) ``` ```python ui.workspace(main_view(), left=pz.column([ui.panel_header("Project"), ui.list_item("main.py", icon="file-code", selected=True, inset=True)]), left_width=150) ``` ```python ui.workspace(main_view(), bottom=pz.column([ui.panel_header("Terminal", icon="terminal"), ui.terminal("$ python main.py")]), bottom_height=90) ``` ```python ui.workspace(main_view(), right=pz.column([ui.panel_header("Outline"), ui.list_item("class Notes", icon="box")]), right_width=150) ``` ```python ui.workspace(main_view(), title_bar=ui.title_bar([ui.title_button("pyzed", chevron=True)], [ui.avatar("KV")])) ``` #### Events Workspace has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/). | Event | Handler gets | When | | --- | --- | --- | | `on_resize` | `(side, size)` | Called with `(side, size)` when the user finishes resizing a dock by dragging its inner border. | **Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`. ##### Handling the events The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/). **Imperative:** ```python class Ide(pz.App): def __init__(self): self.left_width, self.bottom_height = 220, 160 def resized(self, side, size): # on_resize: which dock and its new size if side == "left": self.left_width = size elif side == "bottom": self.bottom_height = size def view(self): return ui.workspace(main_view(), left=ui.panel_header("Project"), bottom=ui.panel_header("Terminal"), left_width=self.left_width, bottom_height=self.bottom_height, on_resize=self.resized) ``` **Declarative:** ```python from dataclasses import dataclass @dataclass class Resized: side: str size: float class Ide(pz.App): sizes = {"left": 220, "bottom": 160} def update(self, message): match message: case Resized(side, size): self.sizes = {**self.sizes, side: size} def view(self): return ui.workspace(main_view(), left=ui.panel_header("Project"), bottom=ui.panel_header("Terminal"), left_width=self.sizes["left"], bottom_height=self.sizes["bottom"], on_resize=Resized) ``` **Functional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: left: float = 220 bottom: float = 160 def view(state, set_state): return ui.workspace(main_view(), left=ui.panel_header("Project"), bottom=ui.panel_header("Terminal"), left_width=state.left, bottom_height=state.bottom, on_resize=lambda side, size: set_state(**{side: size})) ``` #### Properties and API ##### `ui.workspace` ```python workspace( center, *, title_bar=None, left=None, right=None, bottom=None, status_bar=None, left_width=240, right_width=320, bottom_height=220, on_resize=None, theme=None, ) ``` The layout of an editor-style window: title bar, left, right and bottom docks, center and status bar. Pass `None` to hide a dock. | Property | Default | Description | | --- | --- | --- | | `center` | *required* | Central content (usually tabs + editor). | | `title_bar` | `None` | Title bar (`ui.title_bar`). | | `left` | `None` | Left dock. | | `right` | `None` | Right dock. | | `bottom` | `None` | Bottom dock. | | `status_bar` | `None` | Status bar (`ui.status_bar`). | | `left_width` | `240` | Width of the left dock. | | `right_width` | `320` | Width of the right dock. | | `bottom_height` | `220` | Height of the bottom dock. | | `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). | Events: `on_resize` ([events](#events)). [Full reference →](https://albakore.github.io/pyzed/reference/ui/#workspace-1) #### Related - [Title bar](https://albakore.github.io/pyzed/library/title-bar/): The top bar with project and branch pickers. - [Status bar](https://albakore.github.io/pyzed/library/status-bar/): The bottom bar: diagnostics, cursor position, language. - [Panel](https://albakore.github.io/pyzed/library/panel/): A dockable, resizable side panel. - [Split](https://albakore.github.io/pyzed/library/split/): Two panes with a draggable divider. ## Blocks ### Blocks > Ready-made Python desktop UI screens to copy: login, dashboard, settings, data table, chat, IDE layout, onboarding wizard, music player and kanban, built with Pyzed components. Source: https://albakore.github.io/pyzed/blocks/ Blocks are **complete screens** built from Pyzed's [components](https://albakore.github.io/pyzed/library/), in the spirit of shadcn/ui's blocks: open one, run it, copy the code into your app and change what you need. Each block is a single runnable file in [`examples/blocks/`](https://github.com/albakore/pyzed/tree/main/examples/blocks), tested in CI in light and dark themes, so the code on these pages always works with the current Pyzed. #### How to use a block 1. Open its page and look at the screenshot in light and dark (switch the site's theme). 2. Run it: `python examples/blocks/.py`, or copy the **Code** section into a file. 3. Keep what you need. Blocks are ordinary `pz.App` classes: their pieces are methods (`sidebar()`, `header()`...) that return widgets, so you can lift one into your own app as is. #### Make them yours - **Theme.** Set `theme = "Gruvbox Dark"` (or any [theme](https://albakore.github.io/pyzed/theming/themes/)) on the app, or derive one with your brand color: `pz.themes.get("One Dark").with_accent("#e11d48")`. Every block uses theme tokens (`"accent"`, `"panel"`, `"text_muted"`...), never fixed colors for its surfaces. - **Data.** The sample data is at the top of each file. Replace it with a database query, an HTTP call or files; slow work goes in a coroutine so the window never blocks (see [Async](https://albakore.github.io/pyzed/guides/async/)). - **Components.** When a piece grows its own state, turn it into a [`pz.Component`](https://albakore.github.io/pyzed/guides/components/) so it shows up in Pyzed Design's toolbox too. - **AI.** With the [MCP server](https://albakore.github.io/pyzed/ai/mcp/) connected, ask your agent: *"Start from the dashboard block and make it show our GitHub issues"*. It can read every block with `read_example("blocks/dashboard")`. ### Login > A split sign-in screen for Python desktop apps: brand panel with a gradient, SSO and passkey buttons, email and password fields with validation. Source: https://albakore.github.io/pyzed/blocks/login/ A two-column sign-in screen: a gradient brand panel with features and a testimonial on a translucent card, and a form with single sign-on, passkey, email and password, *remember me* and inline validation. The screenshot follows the site's theme: switch between light and dark at the top of the page to see the block in One Light and One Dark. #### Run it From the repository root: ```bash python examples/blocks/login.py ``` #### What it shows - Gradient background with `pz.Gradient` and a translucent glass-like card. - Validation on submit: a `ui.callout` shows the error, or the success. - Text links built with `.on(click=..., cursor="pointer")`. - Enter in the password field signs in. #### Components used | Component | Used for | | --- | --- | | [Input](https://albakore.github.io/pyzed/library/input/) | Email and password | | [Button](https://albakore.github.io/pyzed/library/button/) | Sign in, SSO and passkey | | [Checkbox](https://albakore.github.io/pyzed/library/checkbox/) | Keep me signed in | | [Callout](https://albakore.github.io/pyzed/library/callout/) | Errors and success | | [Avatar](https://albakore.github.io/pyzed/library/avatar/) | Testimonial author | | [Divider](https://albakore.github.io/pyzed/library/divider/) | The “or” separator | | [Heading](https://albakore.github.io/pyzed/library/heading/) | Title | | [Label](https://albakore.github.io/pyzed/library/label/) | Labels and links | #### Code The whole block, ready to copy. It only needs `pip install pyzed-ui`. ```python title="examples/blocks/login.py" """Login block: a split sign-in screen with a brand panel, single sign-on, passkey and an email form.""" import pyzed as pz from pyzed import ui def link(text, action): """A text that behaves like a link.""" return ui.label(text, color="accent", size="sm", weight="medium").on(click=action, cursor="pointer") FEATURES = [ ("rocket", "Deploy in seconds", "Push to main and your app is live worldwide."), ("shield-check", "Secure by default", "SSO, audit logs and encrypted secrets."), ("activity", "Observability built in", "Traces, metrics and logs in one place."), ] BRAND = pz.Gradient([(0.0, "#6366f1"), (0.55, "#8b5cf6"), (1.0, "#ec4899")], angle=135) class Login(pz.App): title = "Sign in" size = (1200, 760) def __init__(self): self.email = "ada@lovelace.dev" self.password = "analytical-engine" self.remember = True self.error = "" self.signed_in = False # -- actions --------------------------------------------------------------------------- def sign_in(self): if "@" not in self.email: self.error = "Enter a valid email address." elif len(self.password) < 8: self.error = "The password needs at least 8 characters." else: self.error, self.signed_in = "", True def forgot(self): self.error = "" if "@" in self.email else "Enter your email first." def sign_up(self): self.signed_in = False def set_email(self, value): self.email, self.error = value, "" def set_password(self, value): self.password, self.error = value, "" # -- view ------------------------------------------------------------------------------ def brand(self): quote = pz.column([ pz.text("“We replaced a 4,000-line Tkinter app with 900 lines of Pyzed. " "It starts faster and finally looks like it belongs in 2026.”", size=17, color="#ffffff", line_height=1.45), pz.row([ ui.avatar("ML", color="#f59e0b", size=34), pz.column([pz.text("Maya López", size=14, weight="semibold", color="#ffffff"), pz.text("Staff engineer, Northwind Labs", size=12.5, color="#e0e7ff")], spacing=2), ], spacing=10, align="center"), ], spacing=18) return pz.container( pz.column([ pz.row([ pz.container(pz.icon("zap", size=18, color="#ffffff"), padding=7, radius=9, bg="#ffffff33"), pz.text("Acme Cloud", size=17, weight="semibold", color="#ffffff"), ], spacing=10, align="center"), pz.vspace(), pz.column([self.feature(*f) for f in FEATURES], spacing=22), pz.vspace(), pz.container(quote, padding=22, radius=12, bg="#ffffff1c", border_color="#ffffff38", border_width=1), ]), width="fill", height="fill", padding=44, ).style(background=BRAND, radius=14) def feature(self, icon, title, text): return pz.row([ pz.container(pz.icon(icon, size=18, color="#ffffff"), padding=9, radius=10, bg="#ffffff26"), pz.column([pz.text(title, size=15, weight="semibold", color="#ffffff"), pz.text(text, size=13.5, color="#e0e7ff")], spacing=3), ], spacing=14, align="center") def form(self): social = pz.row([ ui.button("Single sign-on", self.sign_in, icon="building-2", size="lg", full_width=True), ui.button("Passkey", self.sign_in, icon="fingerprint", size="lg", full_width=True), ], spacing=10) separator = pz.row([ui.divider(), ui.muted("or continue with email", size="sm", wrap=False), ui.divider()], spacing=10, align="center") password_label = pz.row([ui.label("Password", size="sm", weight="medium"), pz.hspace(), link("Forgot password?", self.forgot)], align="center") fields = pz.column([ pz.column([ui.label("Email", size="sm", weight="medium"), ui.input(self.email, self.set_email, placeholder="you@company.com", icon="mail", size="lg", id="email")], spacing=6), pz.column([password_label, ui.input(self.password, self.set_password, placeholder="••••••••", icon="lock", size="lg", secure=True, on_submit=self.sign_in, id="password")], spacing=6), ui.checkbox(self.remember, lambda v: setattr(self, "remember", v), label="Keep me signed in"), ], spacing=16) status = [] if self.error: status = [ui.callout(self.error, tint="error", icon="circle-alert")] elif self.signed_in: status = [ui.callout("Signed in", f"Welcome back, {self.email}.", tint="success", icon="circle-check")] return pz.container( pz.column([ pz.column([ui.heading("Welcome back"), ui.muted("Sign in to your workspace to continue.")], spacing=6), social, separator, fields, *status, ui.button("Sign in", self.sign_in, variant="solid", size="lg", full_width=True, icon="log-in"), pz.row([ui.muted("Don't have an account?", size="sm"), link("Create one", self.sign_up)], spacing=6, align="center"), ], spacing=22, max_width=380), width="fill", height="fill", center=True, padding=32, ) def view(self): return pz.row([ pz.container(self.brand(), width="fill", height="fill", padding=12), self.form(), ], height="fill").style(background="background") if __name__ == "__main__": pz.run(Login) ``` #### Make it yours Change `theme` (or `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), replace the sample data with your own and split pieces into [components](https://albakore.github.io/pyzed/guides/components/) when they grow. An AI agent with the [MCP server](https://albakore.github.io/pyzed/ai/mcp/) can read this block with `read_example("blocks/login")` and adapt it for you. ### Dashboard > An analytics dashboard in Python: sidebar navigation, KPI cards with sparklines, a revenue bar chart with ranges, a traffic donut and a sortable orders table. Source: https://albakore.github.io/pyzed/blocks/dashboard/ An admin dashboard: sidebar navigation with an upgrade card, a header with search and notifications, four KPI cards with trend lines, a revenue chart with a 7 days / 30 days / 12 months switch, traffic sources with a donut and a monthly goal, and a sortable table of recent orders. The screenshot follows the site's theme: switch between light and dark at the top of the page to see the block in One Light and One Dark. #### Run it From the repository root: ```bash python examples/blocks/dashboard.py ``` #### What it shows - `ui.stat` cards: value, change and sparkline in one call. - `ui.bar_chart` and `ui.donut` take plain lists of numbers; colors come from the theme. - Custom cells in `ui.table`: avatars and status badges through `render=`. - Click a column header to sort; the search filters the orders as you type. #### Components used | Component | Used for | | --- | --- | | [Stat](https://albakore.github.io/pyzed/library/stat/) | KPI cards | | [Bar chart](https://albakore.github.io/pyzed/library/bar-chart/) | Revenue | | [Donut](https://albakore.github.io/pyzed/library/donut/) | Traffic sources | | [Table](https://albakore.github.io/pyzed/library/table/) | Recent orders | | [Card](https://albakore.github.io/pyzed/library/card/) | Every panel | | [Segmented](https://albakore.github.io/pyzed/library/segmented/) | Date range | | [List item](https://albakore.github.io/pyzed/library/list-item/) | Navigation | | [Search input](https://albakore.github.io/pyzed/library/search-input/) | Search | | [Progress](https://albakore.github.io/pyzed/library/progress/) | Monthly goal | | [Badge](https://albakore.github.io/pyzed/library/badge/) | Statuses | | [Avatar](https://albakore.github.io/pyzed/library/avatar/) | Customers | | [Sparkline](https://albakore.github.io/pyzed/library/sparkline/) | Trends | #### Code The whole block, ready to copy. It only needs `pip install pyzed-ui`. ```python title="examples/blocks/dashboard.py" """Dashboard block: sidebar navigation, KPI cards, a revenue chart, traffic sources and recent orders.""" import pyzed as pz from pyzed import ui NAV = [ ("layout-dashboard", "Overview"), ("chart-line", "Analytics"), ("shopping-cart", "Orders"), ("users", "Customers"), ("box", "Products"), ("credit-card", "Billing"), ] STATS = [ ("Revenue", "$48,290", 12.4, [18, 22, 19, 25, 27, 24, 31, 35]), ("Orders", "1,284", 8.1, [40, 38, 44, 47, 45, 52, 55, 58]), ("Customers", "9,431", 3.6, [70, 72, 71, 74, 76, 77, 79, 82]), ("Refund rate", "1.9%", -0.4, [3.1, 2.8, 2.9, 2.5, 2.4, 2.2, 2.0, 1.9]), ] REVENUE = { "7 days": ([5.2, 6.8, 6.1, 7.4, 8.9, 9.6, 8.2], ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]), "30 days": ([28, 31, 26, 35, 39, 42], ["W1", "W2", "W3", "W4", "W5", "W6"]), "12 months": ([31, 29, 36, 40, 38, 45, 51, 49, 55, 58, 62, 70], ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"]), } TRAFFIC = [("Organic search", 46, "accent"), ("Direct", 24, "success"), ("Social", 18, "warning"), ("Referral", 12, "error")] ORDERS = [ {"id": "#3210", "customer": "Olivia Martin", "product": "Pro plan · yearly", "status": "Paid", "amount": 1999.00}, {"id": "#3209", "customer": "Jackson Lee", "product": "Team plan · 5 seats", "status": "Pending", "amount": 450.00}, {"id": "#3208", "customer": "Isabella Nguyen", "product": "Starter plan", "status": "Paid", "amount": 99.00}, {"id": "#3207", "customer": "William Kim", "product": "Add-on · Analytics", "status": "Refunded", "amount": 39.00}, {"id": "#3206", "customer": "Sofia Davis", "product": "Pro plan · monthly", "status": "Paid", "amount": 199.00}, ] STATUS_TINT = {"Paid": "success", "Pending": "warning", "Refunded": "error"} class Dashboard(pz.App): title = "Dashboard" size = (1200, 760) def __init__(self): self.page = "Overview" self.range = "7 days" self.query = "" self.sort_by, self.descending = "Order", True # a column title def sort(self, key): self.descending = not self.descending if key == self.sort_by else False self.sort_by = key # -- pieces ---------------------------------------------------------------------------- def sidebar(self): items = [ui.list_item(label, lambda label=label: setattr(self, "page", label), icon=icon, selected=label == self.page, height=32) for icon, label in NAV] return pz.container( pz.column([ pz.row([pz.container(pz.icon("zap", size=16, color="#ffffff"), padding=6).style( background="accent", radius=8), ui.label("Acme", size="lg", weight="semibold")], spacing=10, align="center"), pz.column(items, spacing=2), pz.vspace(), ui.card(pz.column([ ui.label("Upgrade to Pro", weight="semibold"), ui.muted("Unlock forecasting and unlimited seats.", size="sm"), ui.button("Upgrade", lambda: setattr(self, "page", "Billing"), variant="solid", size="sm", full_width=True), ], spacing=8), padding=12), ui.list_item("Settings", None, icon="settings", height=32), ], spacing=20, height="fill"), width=220, height="fill", padding=(18, 12), ).style(background="panel") def header(self): return pz.row([ pz.column([ui.heading(self.page, level=2), ui.muted("Here is what happened in your store.")], spacing=2), pz.hspace(), pz.container(ui.search_input(self.query, lambda v: setattr(self, "query", v), placeholder="Search orders…"), width=240), ui.icon_button("bell", None, tooltip="Notifications"), ui.avatar("KV", size=30), ], spacing=12, align="center") def revenue(self): values, labels = REVENUE[self.range] return ui.card( pz.column([ pz.row([pz.text("$48.3k", size=22, weight="semibold"), ui.badge("+12.4%", tint="success", icon="trending-up")], spacing=10, align="center"), ui.bar_chart(values, labels=labels, width=470, height=140), ], spacing=12), title="Revenue", actions=[ui.segmented(list(REVENUE), self.range, lambda v: setattr(self, "range", v))], width="fill", ) def traffic(self): legend = [pz.row([ui.indicator(color, size=8), ui.label(name, size="sm"), pz.hspace(), ui.label(f"{share}%", size="sm", weight="medium")], spacing=8, align="center") for name, share, color in TRAFFIC] colors = [self.resolved_theme().color(c) for _, _, c in TRAFFIC] return ui.card( pz.column([ pz.row([ui.donut([s for _, s, _ in TRAFFIC], colors=colors, size=150, thickness=20, label="12.8k"), pz.column(legend, spacing=12, width="fill")], spacing=20, align="center"), ui.divider(), pz.column([pz.row([ui.label("Monthly goal", size="sm"), pz.hspace(), ui.muted("$48.3k of $60k", size="sm")]), ui.progress(0.805, height=6)], spacing=8), ], spacing=16), title="Traffic sources", width="fill", ) def orders(self): rows = [o for o in ORDERS if self.query.lower() in (o["customer"] + o["product"]).lower()] columns = [ ui.Column("Order", "id", width=80), ui.Column("Customer", "customer", render=lambda name: pz.row( [ui.avatar("".join(p[0] for p in name.split()), size=22, index=len(name)), ui.label(name)], spacing=8, align="center")), ui.Column("Product", "product"), ui.Column("Status", "status", width=110, render=lambda s: ui.badge(s, tint=STATUS_TINT[s])), ui.Column("Amount", "amount", width=100, align="end", render=lambda a: ui.label(f"${a:,.2f}")), ] return ui.card( ui.table(columns, rows, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort, row_height=36), title="Recent orders", actions=[ui.button("View all", None, variant="ghost", size="sm", icon="arrow-right", icon_position="end")], width="fill", ) def view(self): stats = [ui.stat(title, value, delta=delta, trend=trend, width="fill") for title, value, delta, trend in STATS] body = pz.column([ self.header(), pz.row(stats, spacing=16), pz.row([pz.container(self.revenue(), width="fill:3"), pz.container(self.traffic(), width="fill:2")], spacing=16), self.orders(), ], spacing=20, padding=24) return pz.row([self.sidebar(), ui.divider(vertical=True), pz.scrollable(body, height="fill")], height="fill").style( background="background") if __name__ == "__main__": pz.run(Dashboard) ``` #### Make it yours Change `theme` (or `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), replace the sample data with your own and split pieces into [components](https://albakore.github.io/pyzed/guides/components/) when they grow. An AI agent with the [MCP server](https://albakore.github.io/pyzed/ai/mcp/) can read this block with `read_example("blocks/dashboard")` and adapt it for you. ### Settings > A settings page for Python desktop apps: sectioned navigation, profile fields, theme and accent pickers, notification switches, a save bar and a danger zone with confirmation. Source: https://albakore.github.io/pyzed/blocks/settings/ A settings screen in the style of modern desktop apps: a section list on the left and grouped rows on the right, with profile fields, a language select, light / dark / system theme, accent swatches, a font size slider, notification switches and a danger zone that asks before deleting. The screenshot follows the site's theme: switch between light and dark at the top of the page to see the block in One Light and One Dark. #### Run it From the repository root: ```bash python examples/blocks/settings.py ``` #### What it shows - `ui.settings_group` + `ui.field`: titled groups of rows with descriptions. - A sticky footer with *unsaved changes* and Save / Discard. - `ui.confirm` inside `ui.dialog_layer` for the destructive action. - Accent swatches with a ring on the selected one. #### Components used | Component | Used for | | --- | --- | | [Settings group](https://albakore.github.io/pyzed/library/settings-group/) | Groups of rows | | [Field](https://albakore.github.io/pyzed/library/field/) | Each setting | | [Input](https://albakore.github.io/pyzed/library/input/) | Name and email | | [Select](https://albakore.github.io/pyzed/library/select/) | Language | | [Segmented](https://albakore.github.io/pyzed/library/segmented/) | Theme | | [Color picker](https://albakore.github.io/pyzed/library/color-picker/) | Accent swatches | | [Slider](https://albakore.github.io/pyzed/library/slider/) | Font size | | [Switch](https://albakore.github.io/pyzed/library/switch/) | Notifications | | [Confirm](https://albakore.github.io/pyzed/library/confirm/) | Delete account | | [Dialog layer](https://albakore.github.io/pyzed/library/dialog-layer/) | The dialog over the page | | [List item](https://albakore.github.io/pyzed/library/list-item/) | Sections | #### Code The whole block, ready to copy. It only needs `pip install pyzed-ui`. ```python title="examples/blocks/settings.py" """Settings block: a sectioned settings page with profile, appearance, notifications and a danger zone.""" import pyzed as pz from pyzed import ui SECTIONS = [ ("user", "Profile"), ("palette", "Appearance"), ("bell", "Notifications"), ("shield", "Security"), ("credit-card", "Billing"), ("plug", "Integrations"), ] LANGUAGES = ["English", "Español", "Português", "Deutsch", "日本語"] ACCENTS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f97316", "#22c55e", "#14b8a6"] class Settings(pz.App): title = "Settings" size = (1200, 760) def __init__(self): self.section = "Profile" self.name = "Ada Lovelace" self.email = "ada@lovelace.dev" self.bio = "Writing the first programs for engines that don't exist yet." self.mode = "System" self.accent = ACCENTS[1] self.font_size = 14.0 self.language = "English" self.notify = {"mentions": True, "weekly": True, "product": False} self.saved = False self.confirm_delete = False def toggle(self, key): self.notify[key] = not self.notify[key] self.saved = False def discard(self): self.__init__() # back to the saved values def save(self): self.saved = True # -- pieces ---------------------------------------------------------------------------- def nav(self): items = [ui.list_item(label, lambda label=label: setattr(self, "section", label), icon=icon, selected=label == self.section, height=32) for icon, label in SECTIONS] return pz.column([ ui.label("Settings", size="xl", weight="semibold"), ui.search_input("", None, placeholder="Search settings…"), pz.column(items, spacing=2), ], spacing=16, width=230) def profile(self): return ui.settings_group("Profile", [ pz.row([ ui.avatar("AL", size=56), pz.column([ui.label(self.name, weight="semibold"), ui.muted(self.email, size="sm")], spacing=2, width="fill"), ui.button("Change photo", self.save, icon="upload", size="sm"), ], spacing=14, align="center"), ui.field("Display name", pz.container(ui.input(self.name, lambda v: setattr(self, "name", v)), width=320)), ui.field("Email", pz.container(ui.input(self.email, lambda v: setattr(self, "email", v), icon="mail"), width=320), description="Used for sign-in and receipts."), ui.field("Language", ui.select(LANGUAGES, self.language, lambda v: setattr(self, "language", v), width=320)), ], description="How other people see you across the workspace.") def appearance(self): swatches = pz.row([ pz.container(ui.color_swatch(color, size=22, on_press=lambda color=color: setattr(self, "accent", color)), padding=2).style(border=(2, "text" if color == self.accent else "#00000000"), radius=6) for color in ACCENTS], spacing=8) return ui.settings_group("Appearance", [ ui.field("Theme", ui.segmented(["Light", "Dark", "System"], self.mode, lambda v: setattr(self, "mode", v))), ui.field("Accent color", swatches, description="Buttons, links and focus rings."), ui.field("Interface font size", pz.row([ pz.container(ui.slider(self.font_size, lambda v: setattr(self, "font_size", round(v)), min=11, max=20, step=1), width=200), ui.label(f"{self.font_size:.0f} px", size="sm", mono=True), ], spacing=12, align="center")), ]) def notifications(self): rows = [ ("mentions", "Mentions and replies", "When someone mentions you or answers your comment."), ("weekly", "Weekly summary", "A digest of your projects every Monday."), ("product", "Product news", "New features and tips, about once a month."), ] return ui.settings_group("Notifications", [ ui.field(title, ui.switch(self.notify[key], lambda _=None, key=key: self.toggle(key)), description=text) for key, title, text in rows]) def danger(self): return ui.settings_group("Danger zone", [ ui.field("Delete account", ui.button("Delete account", lambda: setattr(self, "confirm_delete", True), variant="error", icon="trash-2"), description="Deletes your workspaces and data. This can't be undone."), ]) def view(self): footer = pz.row([ ui.muted("All changes saved" if self.saved else "You have unsaved changes", size="sm"), pz.hspace(), ui.button("Discard", self.discard, variant="ghost"), ui.button("Save changes", self.save, variant="solid", icon="check"), ], spacing=8, align="center") content = pz.column([ self.profile(), self.appearance(), self.notifications(), self.danger(), ], spacing=28, padding=(0, 24, 24, 0), max_width=760) page = pz.column([ pz.scrollable(content, height="fill"), ui.divider(), pz.container(footer, padding=(12, 0), max_width=736), ], width=780, height="fill") base = pz.row([self.nav(), page], spacing=40, padding=(28, 32, 12, 32), width="fill", height="fill").style( background="background") dialog = None if self.confirm_delete: dialog = ui.confirm("Your workspaces and data will be deleted for good.", lambda: setattr(self, "confirm_delete", False), lambda: setattr(self, "confirm_delete", False), title="Delete your account?", confirm_label="Delete", danger=True) return ui.dialog_layer(base, dialog, on_dismiss=lambda: setattr(self, "confirm_delete", False)) if __name__ == "__main__": pz.run(Settings) ``` #### Make it yours Change `theme` (or `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), replace the sample data with your own and split pieces into [components](https://albakore.github.io/pyzed/guides/components/) when they grow. An AI agent with the [MCP server](https://albakore.github.io/pyzed/ai/mcp/) can read this block with `read_example("blocks/settings")` and adapt it for you. ### Data table > A data table with filters in Python: search, status chips, a role select, sortable columns, row selection, pagination, CSV export and an invite dialog. Source: https://albakore.github.io/pyzed/blocks/data-table/ A team members table: filter by name or email, toggle statuses with chips, pick a role, sort by any column, select rows with checkboxes, page through results, copy them as CSV and invite people from a dialog. The screenshot follows the site's theme: switch between light and dark at the top of the page to see the block in One Light and One Dark. #### Run it From the repository root: ```bash python examples/blocks/data_table.py ``` #### What it shows - Filtering, sorting and paging done in plain Python, so it is easy to swap in a database query. - Two-line cells (avatar, name and email) through `ui.Column(render=...)`. - `pz.Command.copy` puts the CSV on the clipboard. - `ui.prompt` for the invite, with Enter to send. #### Components used | Component | Used for | | --- | --- | | [Table](https://albakore.github.io/pyzed/library/table/) | The table | | [Chip](https://albakore.github.io/pyzed/library/chip/) | Status filters | | [Search input](https://albakore.github.io/pyzed/library/search-input/) | Text filter | | [Select](https://albakore.github.io/pyzed/library/select/) | Role filter | | [Checkbox](https://albakore.github.io/pyzed/library/checkbox/) | Row selection | | [Badge](https://albakore.github.io/pyzed/library/badge/) | Statuses | | [Avatar](https://albakore.github.io/pyzed/library/avatar/) | Members | | [Facepile](https://albakore.github.io/pyzed/library/facepile/) | Team | | [Prompt](https://albakore.github.io/pyzed/library/prompt/) | Invite dialog | | [Icon button](https://albakore.github.io/pyzed/library/icon-button/) | Pagination and row menu | #### Code The whole block, ready to copy. It only needs `pip install pyzed-ui`. ```python title="examples/blocks/data_table.py" """Data table block: a team members table with search, status filters, sorting, selection and paging.""" import pyzed as pz from pyzed import ui PEOPLE = [ ("Olivia Martin", "olivia@acme.dev", "Owner", "Active", "Just now"), ("Jackson Lee", "jackson@acme.dev", "Admin", "Active", "5 min ago"), ("Isabella Nguyen", "isabella@acme.dev", "Editor", "Active", "1 hour ago"), ("William Kim", "will@acme.dev", "Viewer", "Invited", "—"), ("Sofia Davis", "sofia@acme.dev", "Editor", "Active", "Yesterday"), ("Liam Johnson", "liam@acme.dev", "Editor", "Suspended", "3 weeks ago"), ("Emma Wilson", "emma@acme.dev", "Admin", "Active", "2 hours ago"), ("Noah Brown", "noah@acme.dev", "Viewer", "Active", "4 days ago"), ("Ava Garcia", "ava@acme.dev", "Editor", "Invited", "—"), ("Lucas Rossi", "lucas@acme.dev", "Viewer", "Active", "Today"), ("Mia Fischer", "mia@acme.dev", "Editor", "Active", "Last week"), ("Ethan Clark", "ethan@acme.dev", "Viewer", "Suspended", "2 months ago"), ("Chloe Dubois", "chloe@acme.dev", "Admin", "Active", "Today"), ("Mateo Silva", "mateo@acme.dev", "Editor", "Active", "Yesterday"), ] ROWS = [{"id": i, "name": n, "email": e, "role": r, "status": s, "seen": seen} for i, (n, e, r, s, seen) in enumerate(PEOPLE)] BY_NAME = {r["name"]: r for r in ROWS} STATUS_TINT = {"Active": "success", "Invited": "info", "Suspended": "error"} ROLES = ["All roles", "Owner", "Admin", "Editor", "Viewer"] PAGE_SIZE = 9 SORT_KEYS = {"Member": "name", "Role": "role", "Status": "status", "Last active": "seen"} class DataTable(pz.App): title = "Team members" size = (1200, 760) def __init__(self): self.query = "" self.statuses = {"Active", "Invited"} self.role = "All roles" self.sort_by, self.descending = "Member", False # a column title self.selected = {1, 4} self.page = 0 self.notice = "" self.inviting, self.invite_email = False, "" # -- data ------------------------------------------------------------------------------ def visible(self): q = self.query.lower() rows = [r for r in ROWS if r["status"] in self.statuses and (self.role == "All roles" or r["role"] == self.role) and (q in r["name"].lower() or q in r["email"].lower())] return rows # -- actions --------------------------------------------------------------------------- def toggle_status(self, status): self.statuses ^= {status} self.page = 0 def toggle_row(self, row_id): self.selected ^= {row_id} def sort(self, key): self.descending = not self.descending if key == self.sort_by else False self.sort_by = key def export(self): rows = self.visible() self.notice = f"Copied {len(rows)} rows as CSV" lines = ["name,email,role,status"] + [f"{r['name']},{r['email']},{r['role']},{r['status']}" for r in rows] return pz.Command.copy("\n".join(lines)) def invite(self): if "@" in self.invite_email: self.notice = f"Invitation sent to {self.invite_email}" self.inviting, self.invite_email = False, "" def set_query(self, value): self.query, self.page = value, 0 # -- view ------------------------------------------------------------------------------ def person(self, row): return pz.row([ ui.avatar("".join(p[0] for p in row["name"].split()), size=28, index=row["id"]), pz.column([ui.label(row["name"], weight="medium"), ui.muted(row["email"], size="sm")], spacing=1), ], spacing=10, align="center") def toolbar(self): chips = [ui.chip(f"{status} {sum(r['status'] == status for r in ROWS)}", on_press=lambda status=status: self.toggle_status(status), selected=status in self.statuses, icon="circle-dot" if status in self.statuses else None) for status in STATUS_TINT] return pz.row([ pz.container(ui.search_input(self.query, self.set_query, placeholder="Filter by name or email…"), width=300), *chips, ui.select(ROLES, self.role, lambda v: setattr(self, "role", v), width=140), pz.hspace(), ui.button("Export", self.export, icon="download", variant="ghost"), ui.button("Invite member", lambda: setattr(self, "inviting", True), icon="user-plus", variant="solid"), ], spacing=8, align="center") def table(self, rows): columns = [ ui.Column("", "id", width=36, sortable=False, render=lambda i: ui.checkbox(i in self.selected, lambda _=None, i=i: self.toggle_row(i))), ui.Column("Member", "name", width=("fill", 3), render=lambda name: self.person(BY_NAME[name])), ui.Column("Role", "role", width=("fill", 1), render=lambda role: ui.label(role, color="text_muted")), ui.Column("Status", "status", width=("fill", 1), render=lambda s: ui.badge(s, tint=STATUS_TINT[s])), ui.Column("Last active", "seen", width=("fill", 1), render=lambda s: ui.muted(s)), ui.Column("", "id", width=44, sortable=False, render=lambda i: ui.icon_button("ellipsis", None, size="sm", tooltip="More")), ] return ui.table(columns, rows, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort, row_height=52) def view(self): rows = self.visible() pages = max(1, -(-len(rows) // PAGE_SIZE)) page_rows = sorted(rows, key=lambda r: r[SORT_KEYS[self.sort_by]], reverse=self.descending) page_rows = page_rows[self.page * PAGE_SIZE:(self.page + 1) * PAGE_SIZE] start = self.page * PAGE_SIZE + 1 if rows else 0 footer = pz.row([ ui.muted(self.notice or f"{len(self.selected)} selected · showing {start}–{start + len(page_rows) - 1 if rows else 0} " f"of {len(rows)}", size="sm"), pz.hspace(), ui.icon_button("chevron-left", lambda: setattr(self, "page", max(0, self.page - 1)), disabled=self.page == 0, variant="filled"), *[ui.button(str(n + 1), lambda n=n: setattr(self, "page", n), size="sm", variant="solid" if n == self.page else "ghost") for n in range(pages)], ui.icon_button("chevron-right", lambda: setattr(self, "page", min(pages - 1, self.page + 1)), disabled=self.page >= pages - 1, variant="filled"), ], spacing=6, align="center") page = pz.column([ pz.row([ pz.column([ui.heading("Team members", level=2), ui.muted("Manage who can access the Acme workspace and what they can do.")], spacing=4), pz.hspace(), ui.facepile(["OM", "JL", "IN", "WK", "SD", "LJ"], size=28), ], align="center"), self.toolbar(), ui.card(pz.column([self.table(page_rows), ui.divider(), pz.container(footer, padding=(4, 4, 0, 4))], spacing=8), padding=8), ], spacing=20, padding=28, height="fill").style(background="background") dialog = None if self.inviting: dialog = ui.prompt("Invite a member", self.invite_email, lambda v: setattr(self, "invite_email", v), self.invite, lambda: setattr(self, "inviting", False), message="They get an email with a link to join the workspace.", placeholder="name@company.com", submit_label="Send invite", id="invite") return ui.dialog_layer(page, dialog, on_dismiss=lambda: setattr(self, "inviting", False)) if __name__ == "__main__": pz.run(DataTable) ``` #### Make it yours Change `theme` (or `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), replace the sample data with your own and split pieces into [components](https://albakore.github.io/pyzed/guides/components/) when they grow. An AI agent with the [MCP server](https://albakore.github.io/pyzed/ai/mcp/) can read this block with `read_example("blocks/data_table")` and adapt it for you. ### Chat > A team chat UI in Python: channels with unread counts, direct messages with presence, grouped messages with Markdown, attachments, reactions and a rich composer. Source: https://albakore.github.io/pyzed/blocks/chat/ A messenger in the style of Slack or Discord: channels with unread counters, direct messages with presence dots, a thread that groups consecutive messages, Markdown, file attachments, reactions and a composer with formatting buttons. For an AI chat that streams answers see [Building AI apps](https://albakore.github.io/pyzed/ai/ai-apps/). The screenshot follows the site's theme: switch between light and dark at the top of the page to see the block in One Light and One Dark. #### Run it From the repository root: ```bash python examples/blocks/chat.py ``` #### What it shows - Consecutive messages from the same person are grouped under one avatar. - `ui.markdown` renders bold, code and links in messages. - `pz.scrollable(anchor_bottom=True)` keeps the newest message in view. - Enter sends and scrolls to the end with `pz.Command.scroll_to_end`. #### Components used | Component | Used for | | --- | --- | | [Markdown](https://albakore.github.io/pyzed/library/markdown/) | Message text | | [Avatar](https://albakore.github.io/pyzed/library/avatar/) | People | | [Indicator](https://albakore.github.io/pyzed/library/indicator/) | Presence | | [Count badge](https://albakore.github.io/pyzed/library/count-badge/) | Unread counts | | [List item](https://albakore.github.io/pyzed/library/list-item/) | Channels and people | | [List header](https://albakore.github.io/pyzed/library/list-header/) | Sidebar sections | | [Chip](https://albakore.github.io/pyzed/library/chip/) | Reactions | | [Input](https://albakore.github.io/pyzed/library/input/) | Composer | | [Facepile](https://albakore.github.io/pyzed/library/facepile/) | Channel members | | [Scrollable](https://albakore.github.io/pyzed/library/scrollable/) | The thread | #### Code The whole block, ready to copy. It only needs `pip install pyzed-ui`. ```python title="examples/blocks/chat.py" """Chat block: a team messenger with channels, direct messages, a message thread and a composer.""" import pyzed as pz from pyzed import ui CHANNELS = [("general", 0), ("design", 3), ("engineering", 12), ("releases", 0), ("random", 1)] PEOPLE = [("Maya López", "online"), ("Jackson Lee", "online"), ("Isabella Nguyen", "away"), ("William Kim", "offline")] PRESENCE = {"online": "success", "away": "warning", "offline": "text_disabled"} MESSAGES = [ ("Maya López", "09:12", "Morning! The new onboarding flow is on staging. Can someone give it a spin?", None), ("Jackson Lee", "09:14", "On it. First impression: **much** faster than the old one.", None), ("Jackson Lee", "09:15", "One nit: the progress bar jumps from step 2 to 4 when you skip the team invite.", None), ("Isabella Nguyen", "09:21", "Here's the build size before and after the switch to Pyzed:", ("build-report.pdf", "2.4 MB")), ("Maya López", "09:24", "Good catch, I'll fix the skip logic. Shipping it with `v2.3.1` this afternoon.", None), ] def initials(name): return "".join(part[0] for part in name.split()) class Chat(pz.App): title = "Messages" size = (1200, 760) def __init__(self): self.channel = "design" self.draft = "Looks great! Let's demo it in Friday's review" self.messages = list(MESSAGES) self.reactions = {2: {"👍": 3, "🎯": 1}, 4: {"🚀": 4}} def send(self): if self.draft.strip(): self.messages.append(("Kevin Vargas", "09:30", self.draft.strip(), None)) self.draft = "" return pz.Command.scroll_to_end("chat-thread") # -- sidebar --------------------------------------------------------------------------- def sidebar(self): channels = [ui.list_item(f"# {name}", lambda name=name: setattr(self, "channel", name), selected=name == self.channel, height=30, end=ui.count_badge(unread) if unread else None) for name, unread in CHANNELS] people = [ui.list_item(name, None, height=30, start=pz.stack([ ui.avatar(initials(name), size=20, index=i), pz.container(ui.indicator(PRESENCE[status], size=7), width=22, height=22, align_x="end", align_y="end")])) for i, (name, status) in enumerate(PEOPLE)] return pz.column([ pz.row([ui.label("Acme", size="lg", weight="semibold"), pz.hspace(), ui.icon_button("square-pen", None, tooltip="New message")], align="center"), ui.search_input("", None, placeholder="Jump to…"), pz.column([ui.list_header("Channels", end=ui.icon_button("plus", None, size="sm")), *channels], spacing=2), pz.column([ui.list_header("Direct messages"), *people], spacing=2), ], spacing=16, width=250, height="fill", padding=(16, 12)).style(background="panel") # -- thread ---------------------------------------------------------------------------- def message(self, index, author, time, text, attachment): previous = self.messages[index - 1][0] if index else None body = [ui.markdown(text)] if attachment: name, size = attachment body.append(pz.row([ pz.container(pz.icon("file-text", size=18, color="#ffffff"), padding=8).style( background="error", radius=8), pz.column([ui.label(name, weight="medium"), ui.muted(f"PDF · {size}", size="sm")], spacing=1), pz.hspace(width=24), ui.icon_button("download", None, tooltip="Download"), ], spacing=10, align="center").style(background="surface", border=(1, "border_variant"), radius=10, padding=10, width="shrink")) reactions = self.reactions.get(index) if reactions: body.append(pz.row([ui.chip(f"{emoji} {count}", on_press=None) for emoji, count in reactions.items()], spacing=6)) if author == previous: # a follow-up message: no avatar or name return pz.row([pz.space(width=36), pz.column(body, spacing=6, width="fill")], spacing=12, padding=(2, 20)) names = [a for a, _ in PEOPLE] header = pz.row([ui.label(author, weight="semibold"), ui.muted(time, size="sm")], spacing=8, align="end") return pz.row([ ui.avatar(initials(author), size=36, index=names.index(author) if author in names else 5), pz.column([header, *body], spacing=6, width="fill"), ], spacing=12, padding=(10, 20, 2, 20)) def composer(self): return pz.container( pz.column([ ui.input(self.draft, lambda v: setattr(self, "draft", v), placeholder=f"Message #{self.channel}", on_submit=self.send, borderless=True, id="composer"), pz.row([ ui.icon_button("plus", None, tooltip="Attach"), ui.icon_button("bold", None), ui.icon_button("italic", None), ui.icon_button("code", None), ui.icon_button("smile", None, tooltip="Emoji"), ui.icon_button("at-sign", None, tooltip="Mention"), pz.hspace(), ui.muted("Enter to send", size="sm"), ui.icon_button("send-horizontal", self.send, variant="solid" if self.draft else "ghost", tooltip="Send"), ], spacing=2, align="center"), ], spacing=6), padding=10, width="fill", ).style(background="surface", border=(1, "border"), radius=12) def view(self): header = pz.row([ ui.label(f"# {self.channel}", size="lg", weight="semibold"), ui.muted("Product design, reviews and handoff", size="sm"), pz.hspace(), ui.facepile(["ML", "JL", "IN", "WK", "KV", "SD", "AB"], size=24, max_shown=4), ui.icon_button("phone", None, tooltip="Call"), ui.icon_button("pin", None, tooltip="Pinned"), ui.icon_button("panel-right", None, tooltip="Details"), ], spacing=10, align="center", padding=(12, 20)) divider = pz.row([ui.divider(), ui.badge("Today", tint="accent", variant="outline"), ui.divider()], spacing=10, align="center", padding=(8, 20)) thread = pz.scrollable( pz.column([divider, *[self.message(i, *m) for i, m in enumerate(self.messages)]], spacing=4), height="fill", id="chat-thread", anchor_bottom=True) main = pz.column([header, ui.divider(), thread, pz.container(self.composer(), padding=(8, 20, 20, 20))], width="fill", height="fill").style(background="editor") return pz.row([self.sidebar(), ui.divider(vertical=True), main], height="fill") if __name__ == "__main__": pz.run(Chat) ``` #### Make it yours Change `theme` (or `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), replace the sample data with your own and split pieces into [components](https://albakore.github.io/pyzed/guides/components/) when they grow. An AI agent with the [MCP server](https://albakore.github.io/pyzed/ai/mcp/) can read this block with `read_example("blocks/chat")` and adapt it for you. ### File explorer and IDE > An IDE layout in Python, like a modern code editor: project tree, editor tabs, breadcrumbs, a code editor with minimap and git markers, terminal tabs, an outline and a status bar. Source: https://albakore.github.io/pyzed/blocks/file-explorer/ A code editor window built with `ui.workspace`: a title bar with project and branch, a filterable project tree with git status colors, editor tabs and breadcrumbs, the code editor with syntax highlighting, git markers and a minimap, a terminal with tabs, an outline panel and a status bar. The screenshot follows the site's theme: switch between light and dark at the top of the page to see the block in One Light and One Dark. #### Run it From the repository root: ```bash python examples/blocks/file_explorer.py ``` #### What it shows - `ui.workspace` places the docks; they can be resized by dragging. - `ui.TreeNode.from_paths` builds the tree from a list of paths, with git statuses. - `ui.Tab.for_file` picks the icon and color of each file type. - `ui.terminal` understands ANSI colors. #### Components used | Component | Used for | | --- | --- | | [Workspace](https://albakore.github.io/pyzed/library/workspace/) | The window layout | | [Tree](https://albakore.github.io/pyzed/library/tree/) | Project files | | [Tab bar](https://albakore.github.io/pyzed/library/tab-bar/) | Open files | | [Breadcrumbs](https://albakore.github.io/pyzed/library/breadcrumbs/) | Path | | [Editor](https://albakore.github.io/pyzed/library/editor/) | Code | | [Terminal](https://albakore.github.io/pyzed/library/terminal/) | Terminal | | [Title bar](https://albakore.github.io/pyzed/library/title-bar/) | Title bar | | [Status bar](https://albakore.github.io/pyzed/library/status-bar/) | Status bar | | [Toolbar](https://albakore.github.io/pyzed/library/toolbar/) | Editor toolbar | | [Panel header](https://albakore.github.io/pyzed/library/panel-header/) | Panel titles | | [List item](https://albakore.github.io/pyzed/library/list-item/) | Outline | #### Code The whole block, ready to copy. It only needs `pip install pyzed-ui`. ```python title="examples/blocks/file_explorer.py" """IDE layout block: a project tree, editor tabs with breadcrumbs, a terminal, an outline panel and a status bar.""" import pyzed as pz from pyzed import ui FILES = { "src/app/main.py": '''"""Acme Cloud desktop client.""" import pyzed as pz from pyzed import ui from .api import Client from .views import Sidebar, Deployments class AcmeApp(pz.App): title = "Acme Cloud" size = (1280, 800) def __init__(self): self.client = Client.from_env() self.deployments = [] self.selected = None def on_start(self): return self.refresh() async def refresh(self): return ("loaded", await self.client.deployments()) def update(self, message): kind, data = message if kind == "loaded": self.deployments = data def view(self): return ui.workspace( Deployments(items=self.deployments), left=Sidebar(selected=self.selected), ) if __name__ == "__main__": pz.run(AcmeApp) ''', "src/app/api.py": "import httpx\n\n\nclass Client:\n ...\n", "src/app/views.py": "import pyzed as pz\n", "src/app/__init__.py": "", "tests/test_app.py": "def test_refresh():\n ...\n", "pyproject.toml": "[project]\nname = \"acme-cloud\"\n", "README.md": "# Acme Cloud\n", ".gitignore": "__pycache__/\n", } STATUS = {"src/app/main.py": "modified", "src/app/views.py": "created", "README.md": "modified"} OUTLINE = [("class", "AcmeApp", 0), ("variable", "title", 1), ("variable", "size", 1), ("method", "__init__", 1), ("method", "on_start", 1), ("method", "refresh", 1), ("method", "update", 1), ("method", "view", 1)] OUTLINE_ICON = {"class": ("box", "warning"), "method": ("square-function", "accent"), "variable": ("variable", "info")} TERMINAL = ("$ uv run pytest -q\n" "\x1b[32m........................................\x1b[0m [100%]\n" "\x1b[32m40 passed\x1b[0m in 1.84s\n" "$ pyzed dev src/app/main.py\n" "\x1b[36m●\x1b[0m watching src/ for changes · hot reload on\n") class FileExplorer(pz.App): title = "acme-cloud — main.py" size = (1200, 760) def __init__(self): self.tree = ui.TreeNode.from_paths(list(FILES), status=STATUS) self.expanded = {"src", "src/app", "tests"} self.open = ["src/app/main.py", "src/app/api.py", "README.md"] self.active = "src/app/main.py" self.texts = dict(FILES) self.cursor = (24, 18) self.query = "" # -- actions --------------------------------------------------------------------------- def toggle(self, node_id): self.expanded ^= {node_id} def select(self, node_id): if node_id in FILES: if node_id not in self.open: self.open.append(node_id) self.active = node_id def close(self, path): self.open.remove(path) if self.active == path: self.active = self.open[-1] if self.open else None def edit(self, text): self.texts[self.active] = text # -- view ------------------------------------------------------------------------------ def project(self): return pz.column([ ui.panel_header("acme-cloud", [ui.icon_button("file-plus", None, size="xs", tooltip="New file"), ui.icon_button("folder-plus", None, size="xs", tooltip="New folder"), ui.icon_button("list-collapse", lambda: setattr(self, "expanded", set()), size="xs", tooltip="Collapse all")]), pz.container(ui.search_input(self.query, lambda v: setattr(self, "query", v), placeholder="Filter files…", size="sm"), padding=(4, 8)), ui.tree(self.tree, expanded=self.expanded, selected=self.active, on_toggle=self.toggle, on_select=self.select), ]) def outline(self): rows = [] for kind, name, depth in OUTLINE: icon, color = OUTLINE_ICON[kind] rows.append(ui.list_item(name, None, icon=icon, icon_color=self.resolved_theme().color(color), indent=depth * 14, height=26, selected=name == "refresh")) return pz.column([ui.panel_header("Outline", [ui.icon_button("arrow-down-a-z", None, size="xs")], icon="list-tree"), pz.column(rows, padding=(4, 4))]) def editor(self): if self.active is None: return ui.empty_state("No open files", "Pick a file in the project panel.", icon="file") tabs = [ui.Tab.for_file(p, dirty=p == "src/app/main.py", status=STATUS.get(p)) for p in self.open] parts = self.active.split("/") return pz.column([ ui.tab_bar(tabs, self.active, on_select=self.select, on_close=self.close, end=[ui.icon_button("columns-2", None, size="sm", tooltip="Split right")]), ui.toolbar([ui.breadcrumbs([("folder", parts[0]), *parts[1:], "AcmeApp", "refresh"])], [ui.icon_button("play", None, size="sm", tooltip="Run"), ui.icon_button("sparkles", None, size="sm", tooltip="Inline assist")]), ui.editor("ide-editor", self.texts[self.active], path=self.active, on_change=self.edit, markers={19: "modified", 20: "modified", 24: "created"}, minimap=True), ]) def view(self): line, col = self.cursor return ui.workspace( self.editor(), title_bar=ui.title_bar( [ui.title_button("acme-cloud", chevron=True), ui.title_button("feat/deployments", icon="git-branch", muted=True)], [ui.icon_button("bell", None, size="sm"), ui.avatar("KV")], center=ui.title_button("Search files…", icon="search", muted=True)), left=self.project(), right=self.outline(), bottom=pz.column([ ui.tab_bar([ui.Tab("zsh", "zsh", icon="terminal"), ui.Tab("tests", "tests", icon="flask-conical")], "zsh", end=[ui.icon_button("plus", None, size="sm"), ui.icon_button("x", None, size="sm")]), ui.terminal(TERMINAL), ]), status_bar=ui.status_bar( [ui.status_item("feat/deployments", icon="git-branch"), ui.status_item("0", icon="circle-x"), ui.status_item("2", icon="triangle-alert", icon_color=self.resolved_theme().warning)], [ui.status_item(f"Ln {line}, Col {col}"), ui.status_item("UTF-8"), ui.status_item("Python 3.13"), ui.status_item(None, icon="sparkles", tooltip="AI agent")]), left_width=250, right_width=220, bottom_height=172, ) if __name__ == "__main__": pz.run(FileExplorer) ``` #### Make it yours Change `theme` (or `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), replace the sample data with your own and split pieces into [components](https://albakore.github.io/pyzed/guides/components/) when they grow. An AI agent with the [MCP server](https://albakore.github.io/pyzed/ai/mcp/) can read this block with `read_example("blocks/file_explorer")` and adapt it for you. ### Onboarding wizard > A multi-step onboarding wizard in Python: a vertical stepper, profile form, template cards, team invites, a summary and validation between steps. Source: https://albakore.github.io/pyzed/blocks/onboarding/ A four-step setup wizard: a vertical stepper with progress, a profile form, a workspace name with selectable template cards, team invites and a summary before launching. Each step validates before moving on. The screenshot follows the site's theme: switch between light and dark at the top of the page to see the block in One Light and One Dark. #### Run it From the repository root: ```bash python examples/blocks/onboarding.py ``` #### What it shows - The current step is plain state (`self.step`); each step is a method that returns its view. - Selectable cards built from `pz.container(...).style(...).on(click=...)`. - The stepper is drawn with icons, `pz.rule` and theme tokens, so it follows any theme. - Back / Continue / Skip buttons and an error callout. #### Components used | Component | Used for | | --- | --- | | [Progress](https://albakore.github.io/pyzed/library/progress/) | Overall progress | | [Input](https://albakore.github.io/pyzed/library/input/) | Text fields | | [Select](https://albakore.github.io/pyzed/library/select/) | Role | | [Badge](https://albakore.github.io/pyzed/library/badge/) | Step number | | [Card](https://albakore.github.io/pyzed/library/card/) | Invites and summary | | [List item](https://albakore.github.io/pyzed/library/list-item/) | Invited people | | [Empty state](https://albakore.github.io/pyzed/library/empty-state/) | No invites yet | | [Callout](https://albakore.github.io/pyzed/library/callout/) | Validation errors | | [Button](https://albakore.github.io/pyzed/library/button/) | Navigation | #### Code The whole block, ready to copy. It only needs `pip install pyzed-ui`. ```python title="examples/blocks/onboarding.py" """Onboarding block: a multi-step setup wizard with a stepper, choice cards, validation and a summary.""" import pyzed as pz from pyzed import ui STEPS = [ ("user", "Your profile", "Tell us who you are"), ("layout-template", "Workspace", "Pick a template"), ("users", "Invite your team", "Work together from day one"), ("rocket", "All set", "Review and launch"), ] TEMPLATES = [ ("layout-dashboard", "Dashboard", "KPIs, charts and tables for internal tools."), ("bot", "AI assistant", "A streaming chat app wired to any LLM."), ("gamepad-2", "Game", "A canvas, a game loop and keyboard input."), ("code", "Developer tool", "A code editor with docks and a terminal."), ] ROLES = ["Developer", "Designer", "Data scientist", "Student", "Founder"] class Onboarding(pz.App): title = "Welcome" size = (1200, 760) def __init__(self): self.step = 1 self.name = "Ada Lovelace" self.role = "Developer" self.template = "AI assistant" self.workspace = "analytical-engine" self.invites = ["charles@babbage.dev"] self.invite = "" self.error = "" # -- actions --------------------------------------------------------------------------- def next(self): if self.step == 0 and not self.name.strip(): self.error = "Please enter your name." return if self.step == 1 and not self.workspace.strip(): self.error = "Your workspace needs a name." return self.error = "" self.step = min(self.step + 1, len(STEPS) - 1) def back(self): self.error, self.step = "", max(self.step - 1, 0) def add_invite(self): if "@" in self.invite and self.invite not in self.invites: self.invites.append(self.invite.strip()) self.invite = "" # -- pieces ---------------------------------------------------------------------------- def stepper(self): items = [] for i, (icon, title, subtitle) in enumerate(STEPS): done, current = i < self.step, i == self.step marker = pz.container( pz.icon("check" if done else icon, size=16, color="#ffffff" if done or current else None), width=36, height=36, center=True, ).style(background="accent" if done or current else "element", radius=18, border=(2, "accent" if current else "border_variant")) items.append(pz.row([ marker, pz.column([ui.label(title, weight="semibold" if current else None, color="text" if done or current else "text_muted"), ui.muted(subtitle, size="sm")], spacing=2), ], spacing=12, align="center")) if i < len(STEPS) - 1: items.append(pz.container(pz.rule(vertical=True, thickness=2), height=28, padding=(0, 0, 0, 17))) return pz.column([ pz.row([pz.container(pz.icon("zap", size=16, color="#ffffff"), padding=6).style( background="accent", radius=8), ui.label("Acme", size="lg", weight="semibold")], spacing=10, align="center"), pz.space(height=24), pz.column(items, spacing=4), pz.vspace(), ui.muted("Step {} of {}".format(self.step + 1, len(STEPS)), size="sm"), ui.progress(self.step + 1, max=len(STEPS), height=4), ], spacing=8, width=290, height="fill", padding=32).style(background="panel") def template_card(self, icon, title, text): selected = title == self.template return pz.container( pz.column([ pz.row([pz.container(pz.icon(icon, size=20), padding=8).style( background="accent" if selected else "element", radius=10, color="#ffffff" if selected else "text"), pz.hspace(), pz.icon("circle-check", size=18) if selected else None], align="start"), ui.label(title, weight="semibold"), ui.muted(text, size="sm"), ], spacing=10), padding=16, width="fill", height=150, ).style(background="surface", radius=12, border=(2 if selected else 1, "accent" if selected else "border"), ).on(click=lambda: setattr(self, "template", title), cursor="pointer") def profile_step(self): return pz.column([ ui.field("Full name", pz.container(ui.input(self.name, lambda v: setattr(self, "name", v)), width=340)), ui.field("What do you do?", ui.select(ROLES, self.role, lambda v: setattr(self, "role", v), width=340)), ], spacing=16) def workspace_step(self): cards = [self.template_card(*t) for t in TEMPLATES] return pz.column([ pz.column([ui.label("Workspace name", size="sm", weight="medium"), ui.input(self.workspace, lambda v: setattr(self, "workspace", v), icon="at-sign", size="lg")], spacing=6), pz.column([ui.label("Start from a template", size="sm", weight="medium"), pz.row(cards[:2], spacing=14), pz.row(cards[2:], spacing=14)], spacing=10), ], spacing=22) def team_step(self): people = [ui.list_item(email, None, icon="mail", end=ui.icon_button( "x", lambda email=email: self.invites.remove(email), size="sm")) for email in self.invites] return pz.column([ pz.row([ui.input(self.invite, lambda v: setattr(self, "invite", v), placeholder="teammate@company.com", icon="mail", on_submit=self.add_invite, size="lg"), ui.button("Add", self.add_invite, size="lg")], spacing=8), ui.card(pz.column(people, spacing=2) if people else ui.empty_state("No invites yet", icon="users"), padding=6), ], spacing=14) def summary_step(self): rows = [("Name", self.name), ("Role", self.role), ("Workspace", f"@{self.workspace}"), ("Template", self.template), ("Invites", str(len(self.invites)))] return ui.card(pz.column([pz.row([ui.muted(k), pz.hspace(), ui.label(v, weight="medium")]) for k, v in rows], spacing=12)) def view(self): icon, title, subtitle = STEPS[self.step] body = [self.profile_step, self.workspace_step, self.team_step, self.summary_step][self.step]() last = self.step == len(STEPS) - 1 content = pz.column([ pz.column([ui.badge(f"Step {self.step + 1}", tint="accent"), ui.heading(["Nice to meet you", "Create your workspace", "Bring your team", "You're ready to go"][self.step]), ui.muted(["We'll tailor Acme to how you work.", "A workspace holds your projects, people and settings. You can change it later.", "Invite people now or skip this step; they'll get an email.", "Here's what we'll set up for you."][self.step])], spacing=8), body, ui.callout(self.error, tint="error", icon="circle-alert") if self.error else None, pz.vspace(), pz.row([ ui.button("Back", self.back, icon="arrow-left", variant="ghost", disabled=self.step == 0), pz.hspace(), ui.button("Skip", self.next, variant="ghost") if self.step == 2 else None, ui.button("Launch workspace" if last else "Continue", self.next, variant="solid", size="lg", icon="rocket" if last else "arrow-right", icon_position="start" if last else "end"), ], spacing=8, align="center"), ], spacing=26, max_width=640, height="fill", padding=(48, 56)) return pz.row([self.stepper(), ui.divider(vertical=True), pz.container(content, width="fill", height="fill", align_x="center")], height="fill").style(background="background") if __name__ == "__main__": pz.run(Onboarding) ``` #### Make it yours Change `theme` (or `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), replace the sample data with your own and split pieces into [components](https://albakore.github.io/pyzed/guides/components/) when they grow. An AI agent with the [MCP server](https://albakore.github.io/pyzed/ai/mcp/) can read this block with `read_example("blocks/onboarding")` and adapt it for you. ### Music player > A music player UI in Python: library sidebar, album header with gradient art, a track list with an animated equalizer and a player bar with live progress and volume. Source: https://albakore.github.io/pyzed/blocks/music-player/ A media player: a library sidebar with playlists, an album header with gradient cover art and a soft shadow, a track list where the playing song shows an animated equalizer, and a player bar with shuffle, previous, play / pause, next, repeat, a seek bar and volume. The timer is a real subscription, so the progress moves while it plays. The screenshot follows the site's theme: switch between light and dark at the top of the page to see the block in One Light and One Dark. #### Run it From the repository root: ```bash python examples/blocks/music_player.py ``` #### What it shows - `pz.every` drives the playback clock only while playing (`subscriptions()`). - Cover art from `pz.Gradient`, a header that fades out with `pz.Paint(..., mask=pz.Mask([1, 0]))`. - An equalizer made of four small containers whose heights follow the clock. - Rows are `pz.button`s with hover and selected backgrounds from the theme. #### Components used | Component | Used for | | --- | --- | | [Slider](https://albakore.github.io/pyzed/library/slider/) | Seek bar and volume | | [Icon button](https://albakore.github.io/pyzed/library/icon-button/) | Player controls | | [List item](https://albakore.github.io/pyzed/library/list-item/) | Library and playlists | | [List header](https://albakore.github.io/pyzed/library/list-header/) | Playlists title | | [Button](https://albakore.github.io/pyzed/library/button/) | Play / pause | | [Scrollable](https://albakore.github.io/pyzed/library/scrollable/) | Album page | | [Effects](https://albakore.github.io/pyzed/library/effects/) | Gradients and shadows | #### Code The whole block, ready to copy. It only needs `pip install pyzed-ui`. ```python title="examples/blocks/music_player.py" """Music player block: a library sidebar, an album page with a track list and a player bar with live progress.""" import math import pyzed as pz from pyzed import ui COVER = pz.Gradient([(0.0, "#f97316"), (0.5, "#db2777"), (1.0, "#4f46e5")], angle=135) PLAYLISTS = [("Synthwave nights", "#8b5cf6"), ("Deep focus", "#14b8a6"), ("Morning run", "#f59e0b"), ("Lo-fi beats", "#ec4899"), ("Jazz classics", "#3b82f6")] TRACKS = [ ("Neon Skyline", "The Midnight Arcade", 214), ("Afterglow", "The Midnight Arcade", 187), ("Chrome Hearts", "The Midnight Arcade feat. Vela", 242), ("Pixel Rain", "The Midnight Arcade", 201), ("Overdrive", "The Midnight Arcade", 229), ("Last Arcade", "The Midnight Arcade", 263), ("Slow Fade", "The Midnight Arcade", 198), ] def clock(seconds): return f"{int(seconds) // 60}:{int(seconds) % 60:02d}" class MusicPlayer(pz.App): title = "Music" size = (1200, 760) def __init__(self): self.current = 2 self.elapsed = 96.0 self.playing = True self.volume = 0.7 self.shuffle, self.repeat = False, True self.liked = {0, 2, 4} def subscriptions(self): return [pz.every(0.25, self.tick, key="clock")] if self.playing else [] # -- actions --------------------------------------------------------------------------- def tick(self): self.elapsed += 0.25 if self.elapsed >= TRACKS[self.current][2]: self.skip(1) def play(self, index): self.current, self.elapsed, self.playing = index, 0.0, True def skip(self, step): self.play((self.current + step) % len(TRACKS)) def toggle_like(self, index): self.liked ^= {index} # -- pieces ---------------------------------------------------------------------------- def sidebar(self): lists = [ui.list_item(name, None, height=34, start=pz.container(width=22, height=22).style( background=pz.Gradient([color, "#111827"], angle=135), radius=5)) for name, color in PLAYLISTS] return pz.column([ pz.column([ui.list_item("Home", None, icon="house", height=32), ui.list_item("Search", None, icon="search", height=32), ui.list_item("Library", None, icon="library", selected=True, height=32)], spacing=2), ui.list_header("Playlists", end=ui.icon_button("plus", None, size="sm")), pz.column(lists, spacing=2), ], spacing=12, width=230, height="fill", padding=(16, 10)).style(background="panel") def equalizer(self): """Animated bars next to the playing track (they move with the position).""" bars = [] for i in range(4): level = 0.35 + 0.65 * abs(math.sin(self.elapsed * 3.1 + i * 1.7)) if self.playing else 0.3 bars.append(pz.container(width=3, height=4 + 10 * level).style(background="accent", radius=1)) return pz.container(pz.row(bars, spacing=2, align="end"), width=24, height=16, align_y="end") def header(self): return pz.row([ pz.container(pz.icon("disc-3", size=72, color="#ffffffcc"), width=190, height=190, center=True).style( background=COVER, radius=14, shadow=("#00000066", (0, 12), 30)), pz.column([ ui.label("ALBUM", size="xs", weight="semibold", color="text_muted"), pz.text("Midnight Arcade", size=44, weight="bold"), ui.muted("The Midnight Arcade · 2026 · 7 songs, 25 min"), pz.space(height=8), pz.row([ ui.button("Play" if not self.playing else "Pause", lambda: setattr(self, "playing", not self.playing), icon="pause" if self.playing else "play", variant="solid", size="lg"), ui.icon_button("shuffle", lambda: setattr(self, "shuffle", not self.shuffle), selected=self.shuffle, tooltip="Shuffle"), ui.icon_button("heart", None, tooltip="Save to library"), ui.icon_button("ellipsis", None), ], spacing=8, align="center"), ], spacing=6), ], spacing=28, align="end") def tracks(self): rows = [] for i, (title, artist, length) in enumerate(TRACKS): playing = i == self.current number = self.equalizer() if playing else ui.muted(str(i + 1)) rows.append(pz.button( pz.row([ pz.container(number, width=28), pz.column([ui.label(title, weight="medium", color="accent" if playing else "text"), ui.muted(artist, size="sm")], spacing=1, width="fill"), ui.icon_button("heart", lambda i=i: self.toggle_like(i), size="sm", color=self.resolved_theme().accent if i in self.liked else None), pz.container(ui.muted(clock(length)), width=50, align_x="end"), ], spacing=12, align="center"), lambda i=i: self.play(i), width="fill", padding=(6, 12), radius=8, bg=self.resolved_theme().ghost_selected if playing else "#00000000", hover_bg=self.resolved_theme().ghost_hover)) return pz.column(rows, spacing=2) def player(self): title, artist, length = TRACKS[self.current] now = pz.row([ pz.container(pz.icon("disc-3", size=22, color="#ffffffcc"), width=48, height=48, center=True).style( background=COVER, radius=8), pz.column([ui.label(title, weight="semibold"), ui.muted(artist, size="sm")], spacing=2), ui.icon_button("heart", lambda: self.toggle_like(self.current), size="sm", color=self.resolved_theme().accent if self.current in self.liked else None), ], spacing=12, align="center", width=300) controls = pz.column([ pz.row([ ui.icon_button("shuffle", lambda: setattr(self, "shuffle", not self.shuffle), selected=self.shuffle), ui.icon_button("skip-back", lambda: self.skip(-1)), pz.button(pz.icon("pause" if self.playing else "play", size=18, color="#111111"), lambda: setattr(self, "playing", not self.playing), width=40, height=40, padding=11, radius=20, bg="#ffffff", hover_bg="#e5e5e5"), ui.icon_button("skip-forward", lambda: self.skip(1)), ui.icon_button("repeat", lambda: setattr(self, "repeat", not self.repeat), selected=self.repeat), ], spacing=14, align="center"), pz.row([ ui.muted(clock(self.elapsed), size="xs"), ui.slider(self.elapsed, lambda v: setattr(self, "position", v), min=0, max=length, step=1, width=340), ui.muted(clock(length), size="xs"), ], spacing=10, align="center"), ], spacing=6, align="center", width="fill") volume = pz.row([ ui.icon_button("list-music", None, size="sm"), ui.icon_button("volume-2" if self.volume else "volume-x", lambda: setattr(self, "volume", 0 if self.volume else 0.7), size="sm"), ui.slider(self.volume, lambda v: setattr(self, "volume", v), width=110), ], spacing=6, align="center", width=300) return pz.container(pz.row([now, controls, pz.row([pz.hspace(), volume])], align="center"), padding=(12, 20), width="fill").style(background="title_bar") def view(self): main = pz.scrollable(pz.column([ pz.container(self.header(), padding=32, width="fill").style( background=pz.Paint(COVER, opacity=0.28, mask=pz.Mask([1, 0]))), pz.container(self.tracks(), padding=(0, 24, 24, 24)), ], spacing=8), height="fill") return pz.column([ pz.row([self.sidebar(), ui.divider(vertical=True), pz.container(main, width="fill", height="fill").style( background="editor")], height="fill"), ui.divider(), self.player(), ], height="fill") if __name__ == "__main__": pz.run(MusicPlayer) ``` #### Make it yours Change `theme` (or `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), replace the sample data with your own and split pieces into [components](https://albakore.github.io/pyzed/guides/components/) when they grow. An AI agent with the [MCP server](https://albakore.github.io/pyzed/ai/mcp/) can read this block with `read_example("blocks/music_player")` and adapt it for you. ### Kanban board > A kanban board in Python: columns with counters, cards with tags, priorities, due dates, checklists and assignees, filters and moving cards between columns. Source: https://albakore.github.io/pyzed/blocks/kanban/ A sprint board: four columns with counters, cards with colored tags, priority arrows, due dates (today in red), comment counts, checklist progress and assignees, a text filter, an *only mine* toggle, and buttons to add cards and move the selected one between columns. The screenshot follows the site's theme: switch between light and dark at the top of the page to see the block in One Light and One Dark. #### Run it From the repository root: ```bash python examples/blocks/kanban.py ``` #### What it shows - Cards are dataclasses; moving one is changing its `column` field. - The selected card gets an accent border and its move buttons. - Each column scrolls on its own inside a `pz.scrollable`. - A soft shadow on the cards with `.style(shadow=...)`. #### Components used | Component | Used for | | --- | --- | | [Badge](https://albakore.github.io/pyzed/library/badge/) | Tags | | [Count badge](https://albakore.github.io/pyzed/library/count-badge/) | Column counters | | [Progress](https://albakore.github.io/pyzed/library/progress/) | Checklists | | [Facepile](https://albakore.github.io/pyzed/library/facepile/) | Assignees | | [Breadcrumbs](https://albakore.github.io/pyzed/library/breadcrumbs/) | Location | | [Search input](https://albakore.github.io/pyzed/library/search-input/) | Filter | | [Icon button](https://albakore.github.io/pyzed/library/icon-button/) | Add and move | | [Scrollable](https://albakore.github.io/pyzed/library/scrollable/) | Columns | | [Button](https://albakore.github.io/pyzed/library/button/) | Only mine, new task | #### Code The whole block, ready to copy. It only needs `pip install pyzed-ui`. ```python title="examples/blocks/kanban.py" """Kanban block: a project board with columns, labelled cards, assignees, progress and moving cards along.""" from dataclasses import dataclass, field from typing import List import pyzed as pz from pyzed import ui COLUMNS = [("backlog", "Backlog", "circle-dashed", "text_muted"), ("todo", "To do", "circle", "info"), ("doing", "In progress", "circle-dot", "warning"), ("done", "Done", "circle-check", "success")] TAGS = {"design": "accent", "backend": "info", "bug": "error", "docs": "success", "ai": "warning"} PRIORITY = {"high": ("chevrons-up", "error"), "medium": ("chevron-up", "warning"), "low": ("chevron-down", "info")} @dataclass class Card: id: int title: str column: str tags: List[str] = field(default_factory=list) people: List[str] = field(default_factory=list) priority: str = "medium" due: str = "" comments: int = 0 checklist: tuple = (0, 0) CARDS = [ Card(1, "Research onboarding drop-off", "backlog", ["design"], ["ML"], "low", "Nov 12", 2), Card(2, "Dark mode for the billing page", "backlog", ["design"], [], "low"), Card(3, "Rate limit the public API", "backlog", ["backend"], ["JL"], "medium", "Nov 20"), Card(4, "Streaming answers in the support bot", "todo", ["ai", "backend"], ["KV", "IN"], "high", "Nov 3", 5, (1, 4)), Card(5, "Write the MCP setup guide", "todo", ["docs", "ai"], ["SD"], "medium", "Nov 6", 1), Card(6, "Crash when exporting large CSVs", "doing", ["bug"], ["JL"], "high", "Today", 8, (2, 3)), Card(7, "New pricing table", "doing", ["design"], ["ML", "WK"], "medium", "Oct 30", 3, (3, 5)), Card(8, "Migrate jobs to SQLModel", "doing", ["backend"], ["KV"], "medium", "Nov 1", 0, (4, 6)), Card(9, "Landing page hero", "done", ["design"], ["ML"], "medium", "", 6, (4, 4)), Card(10, "Fix login redirect loop", "done", ["bug"], ["IN"], "high", "", 2, (2, 2)), ] class Kanban(pz.App): title = "Board" size = (1200, 760) def __init__(self): self.cards = [Card(**vars(c)) for c in CARDS] self.query = "" self.mine = False self.selected = 6 def move(self, card, step): keys = [c[0] for c in COLUMNS] card.column = keys[max(0, min(len(keys) - 1, keys.index(card.column) + step))] self.selected = card.id def add(self, column): new_id = max(c.id for c in self.cards) + 1 self.cards.append(Card(new_id, "Untitled task", column, [], ["KV"], "low")) self.selected = new_id # -- view ------------------------------------------------------------------------------ def card(self, card): t = self.resolved_theme() icon, tint = PRIORITY[card.priority] done, total = card.checklist meta = [] if card.due: meta.append(pz.row([pz.icon("calendar", size=13, color=t.error if card.due == "Today" else t.icon_muted), ui.label(card.due, size="xs", color="error" if card.due == "Today" else "text_muted")], spacing=4, align="center")) if card.comments: meta.append(pz.row([pz.icon("message-square", size=13, color=t.icon_muted), ui.muted(str(card.comments), size="xs")], spacing=4, align="center")) if total: meta.append(pz.row([pz.icon("list-checks", size=13, color=t.success if done == total else t.icon_muted), ui.muted(f"{done}/{total}", size="xs")], spacing=4, align="center")) selected = card.id == self.selected body = pz.column([ pz.row([*[ui.badge(tag, tint=TAGS[tag]) for tag in card.tags], pz.hspace(), pz.icon(icon, size=14, color=t.color(tint))], spacing=6, align="center"), ui.label(card.title, weight="medium"), ui.progress(done, max=total, height=3, tint="success" if done == total else "accent") if total else None, pz.row([*meta, pz.hspace(), ui.facepile(card.people, size=22) if card.people else None], spacing=12, align="center"), pz.row([ui.icon_button("arrow-left", lambda: self.move(card, -1), size="xs", tooltip="Move left"), pz.hspace(), ui.icon_button("arrow-right", lambda: self.move(card, 1), size="xs", tooltip="Move right")]) if selected else None, ], spacing=10) return pz.container(body, padding=12, width="fill").style( background="elevated", radius=10, border=(1.5 if selected else 1, "accent" if selected else "border_variant"), shadow=("#0000001f", (0, 2), 6), ).on(click=lambda: setattr(self, "selected", card.id), cursor="pointer") def column(self, key, title, icon, tint): q = self.query.lower() cards = [c for c in self.cards if c.column == key and q in c.title.lower() and (not self.mine or "KV" in c.people)] header = pz.row([ pz.icon(icon, size=15, color=self.resolved_theme().color(tint)), ui.label(title, weight="semibold"), ui.count_badge(len(cards), tint="accent" if key == "doing" else "info"), pz.hspace(), ui.icon_button("plus", lambda: self.add(key), size="sm", tooltip="Add a card"), ui.icon_button("ellipsis", None, size="sm"), ], spacing=8, align="center") return pz.container( pz.column([header, pz.scrollable(pz.column([self.card(c) for c in cards], spacing=10), height="fill")], spacing=12, height="fill"), padding=12, width="fill", height="fill", ).style(background="panel", radius=12) def view(self): top = pz.row([ pz.column([ ui.breadcrumbs([("folder", "Acme"), "Product", "Sprint 42"]), pz.row([ui.heading("Sprint 42", level=2), ui.badge("Oct 21 – Nov 3", tint="info", icon="calendar")], spacing=10, align="center"), ], spacing=6), pz.hspace(), pz.container(ui.search_input(self.query, lambda v: setattr(self, "query", v), placeholder="Filter cards…"), width=220), ui.button("Only mine", lambda: setattr(self, "mine", not self.mine), icon="user", selected=self.mine, variant="ghost"), ui.facepile(["ML", "JL", "IN", "SD", "KV", "WK"], size=28, max_shown=5), ui.button("New task", lambda: self.add("todo"), icon="plus", variant="solid"), ], spacing=10, align="center") board = pz.row([self.column(*c) for c in COLUMNS], spacing=14, height="fill") return pz.column([top, board], spacing=20, padding=24, height="fill").style(background="background") if __name__ == "__main__": pz.run(Kanban) ``` #### Make it yours Change `theme` (or `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), replace the sample data with your own and split pieces into [components](https://albakore.github.io/pyzed/guides/components/) when they grow. An AI agent with the [MCP server](https://albakore.github.io/pyzed/ai/mcp/) can read this block with `read_example("blocks/kanban")` and adapt it for you. ## Pyzed Design download ### Download Pyzed Design > Download Pyzed Design, the free drag-and-drop visual designer for Python desktop GUIs, as a standalone app for Windows, macOS and Linux. Source: https://albakore.github.io/pyzed/download/ **Pyzed Design** is the visual designer of Pyzed as a standalone app: draw windows and components, write their code with completion and diagnostics, manage your project's dependencies and run everything with **F5**. Pick your platform; we select the one you are using. (Image: Pyzed Design with a sign-in form open) #### How it installs Pyzed Design installs with the usual tool of each system and then keeps itself up to date: | System | Installer | Where it goes | | --- | --- | --- | | Windows | `pyzed-design-windows-x86_64-setup.exe` | `%LOCALAPPDATA%\Programs\Pyzed Design`, for your user only (no administrator rights), with a Start menu entry, an optional desktop shortcut and `.pzform` files opening in it. Remove it from **Settings → Apps**. | | macOS | `pyzed-design-macos-arm64.dmg` (Intel: `-x86_64.dmg`) | Drag the app into **Applications**. | | Linux | `curl -fsSL https://albakore.github.io/pyzed/install.sh \| sh` | `~/.local/share/pyzed-design`, with the `pyzed-design` command in `~/.local/bin` and an entry in your apps menu. | The Linux script checks the download against the release's `SHA256SUMS` and needs no root. Run it again to reinstall, or with `--uninstall` to remove the app (`curl -fsSL https://albakore.github.io/pyzed/install.sh | sh -s -- --uninstall`). `PYZED_VERSION=1.3.0` installs a given version. Each release also has a portable **zip** per platform (`pyzed-design--.zip`): unpack it anywhere and run it. That zip is what the app's updater downloads. Releases up to 1.2.0 only have the zips. #### What's included Every installer and zip has everything Pyzed Design needs, so nothing else has to be installed: - **Python** and **pyzed-ui**, the same version as the release you download. - The **uv** binary, used to create projects (`uv init`), add dependencies (`uv add`) and run your forms in each project's own `.venv`. See [Projects](https://albakore.github.io/pyzed/guides/projects/). - The IBM Plex fonts and the Lucide icons. The code editor's language servers (**ty** and **ruff**) are installed per project with `uv add --dev ty ruff`, from the Project tab; see the [code editor](https://albakore.github.io/pyzed/guides/designer/#code-editor). #### Updates Pyzed Design updates itself, so you never download it again by hand. It 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**). A new version downloads in the background, verified against the release's SHA-256, and a softly blinking **Restart to update** button appears at the top right of the title bar: it swaps the app for the new version. With automatic checks off, the button reads **Update to X.Y.Z** and downloads it on click. **Help → What's new** shows the release notes at any time. Your projects and settings live outside the app, so they are kept. There are two channels: **stable** (the default) takes final releases, and **preview** also takes pre-releases (`X.Y.Z-rc.N`) to try what is coming. Switch with **Help → Get preview releases**. The installers put the app in a folder of your own (on Windows, Settings → Apps shows the new version after an update). If the app cannot write to its own folder, download the new installer or zip from this page instead. The [changelog](https://albakore.github.io/pyzed/changelog/) lists what changed, and [all releases](https://github.com/albakore/pyzed/releases) are on GitHub. More in [Projects → Updates](https://albakore.github.io/pyzed/guides/projects/#updates). #### Install with pip instead If you already use Python (3.10 or newer), or your platform has no prebuilt download (for example a Mac with an Intel chip or Linux on ARM), install the library and start the designer from it: The `designer` extra adds the ty and ruff language servers; plain `pip install pyzed-ui` works too. Inside a project, `uv run pyzed design` opens the designer with that project's environment. See [Installation](https://albakore.github.io/pyzed/getting-started/installation/) for details. #### Build it yourself The downloads are made by `scripts/build_designer.py` with PyInstaller, on each operating system: -.zip uv run --with pyinstaller --with uv python scripts/build_designer.py --installer # also the setup .exe (needs Inno Setup 6) or the .dmg `} pip={` {venv} {activate} pip install pyzed-ui pyinstaller uv python scripts/build_designer.py # dist/Pyzed Design/ and dist/pyzed-design--.zip python scripts/build_designer.py --installer # also the setup .exe (needs Inno Setup 6) or the .dmg `} python={` {py} -m pip install pyzed-ui pyinstaller uv {py} scripts/build_designer.py # dist/Pyzed Design/ and dist/pyzed-design--.zip {py} scripts/build_designer.py --installer # also the setup .exe (needs Inno Setup 6) or the .dmg `} /> **Next steps:** New to the designer? Read the [visual designer guide](https://albakore.github.io/pyzed/guides/designer/), then build a reusable widget with the [component designer](https://albakore.github.io/pyzed/guides/component-designer/). ## Pyzed Design manual ### Pyzed Design > The manual of Pyzed Design, the visual GUI designer for Python: draw windows and components, design shaders, wire events to Python, run them and ship them. Source: https://albakore.github.io/pyzed/editor/ **Pyzed Design** is the visual editor of Pyzed, built with Pyzed itself. You draw a window with the mouse, set its properties in a grid, double-click a control to write the Python code of its event, and press F5 to run it with all its logic. The same editor builds reusable **components**, GPU **shaders** made of nodes, and edits the Python of your whole project. It stands on the shoulders of tools many people already know: the form designer of **Visual Basic** and **Qt Designer** (draw, set properties, wire events), the scenes and visual shaders of **Godot** and the details panel and material editor of **Unreal Engine**, and a clean, keyboard-friendly interface inspired by the **Zed** code editor. The goal is a frontend framework for desktop apps that you can use without writing code, and extend with code when you want. #### Open it Download the [standalone app](https://albakore.github.io/pyzed/download/) (it brings its own Python and uv), or start it from a Python environment: **uv:** ```bash uv add "pyzed-ui[designer]" # in your project: Pyzed plus the ty and ruff language servers uv run pyzed design ``` **pip:** ```bash pip install "pyzed-ui[designer]" pyzed design ``` **python:** ```bash title="macOS / Linux" python3 -m pip install "pyzed-ui[designer]" python3 -m pyzed design ``` ```powershell title="Windows (PowerShell)" py -m pip install "pyzed-ui[designer]" py -m pyzed design ``` | Command | What it opens | | --- | --- | | `pyzed design` | Inside a project: its main form (`[tool.pyzed] main`). Elsewhere: the welcome window, or an empty form. | | `pyzed design login.pzform` | That form, or a new one with that name that is created on the first save. | | `pyzed design ui/rating.pzcomp` | A [component](https://albakore.github.io/pyzed/editor/component-designer/) (created on the first save if missing). | | `pyzed design fx/fire.pzshad` | A [shader made of nodes](https://albakore.github.io/pyzed/editor/shader-view/) (created on the first save if missing). | | `python -m pyzed.designer [file]` | The same, without the `pyzed` command on `PATH`. | #### The workflow in one picture ```mermaid flowchart LR TB[Toolbox] -->|drag or click| CV[Canvas] CV -->|select| PR[Properties] CV -->|double-click| EV[Events] EV --> CE[Code editor] CV -- Ctrl+S --> F[(login.pzform)] CE -- Ctrl+S --> P[(login.py)] F --> RUN["F5 · pyzed run"] P --> RUN RUN --> W[Native window] RUN -. prints, events, errors .-> CO[Console] ``` 1. **Place** controls from the toolbox: drag them onto the form, or click one and then click (or drag a rectangle) on the form. 2. **Arrange** them with the mouse, the keyboard, the smart guides, the alignment tools or a [layout container](https://albakore.github.io/pyzed/editor/layouts/). 3. **Configure** them in [Properties](https://albakore.github.io/pyzed/editor/properties/): text, data, style, anchors. 4. **Write the logic**: double-click a control to open the handler of its main event in the [code editor](https://albakore.github.io/pyzed/editor/code-editor/). 5. **Run** with F5. The [console](https://albakore.github.io/pyzed/editor/console-and-running/) shows prints, events and errors; saving updates the running window. 6. **Reuse and ship**: turn repeated pieces into [components](https://albakore.github.io/pyzed/editor/component-designer/), and package the app with `pyzed build` (see [Distribute](https://albakore.github.io/pyzed/packaging/)). #### Start here #### The views #### Designing forms #### Run, files, AI and reference **About the captures:** Every panel and view has its page, written against the current version. The captures and short recordings were made on Linux with the One Dark theme (and One Light where the site is light), so your title bar buttons and fonts may look a little different on Windows or macOS. ### The interface > A tour of every part of the Pyzed Design window: the title bar and its menus, document tabs and windows, the four docks and their panels, the toolbar, the canvas, the status bar and notifications. Source: https://albakore.github.io/pyzed/editor/interface/ Pyzed Design is one window split in a few areas, and every area has a single job. Once you know their names, the rest of this manual reads easily. The layout borrows from editors many people already know: the panels and the title bar are inspired by the [Zed](https://zed.dev) code editor, the form canvas and the property grid by Visual Basic and Qt Designer, and the node and scene ideas by Godot and Unreal Engine. ```mermaid flowchart TB T["① Title bar: ☰ menu · project / file · Run ▶ Stop · 🔍 Search · View · Help · ⚙"] subgraph W[" "] direction LR L["② Left dock
Toolbox · Project · Dependencies"] subgraph C["③ Center"] direction TB D["Document tabs"] TB["Toolbar"] F["Canvas (or the graph of a shader)"] end R["④ Right dock
Properties · Events · Agent"] end B["⑤ Bottom dock: Code · Console"] S["⑥ Status bar"] T --> W --> B --> S ``` #### ① The title bar The strip at the very top. From left to right (Run, Stop and the search field sit in the middle): | Item | What it does | | --- | --- | | **☰** | The main menu: New, Open, Go to file, Save, Settings, recent files, and the Run commands. | | **Environment** | Shown under WSL: the Linux distribution the project runs in (*Ubuntu*...), like Zed's remote indicator. | | **Project name ⌄** | The active project (or *No project*). Click it for the **project picker**: the projects open in this window and the recent ones; see [Several projects in one window](https://albakore.github.io/pyzed/editor/projects/#several-projects-in-one-window). | | **Branch** / **worktree** | The project's git branch (click it to switch branch) and, in a linked worktree, its name. | | **/ file name ⌄** | The document in front. Click it to open another form, component or shader. | | **●** | Shown when the document has unsaved changes. | | **Component** / **Shader** badge | Says which [view](https://albakore.github.io/pyzed/editor/views/) you are in when it is not a form. | | **▶ Run** (F5) | Runs the form, previews the component, or opens the shader in its own window. | | **■ Stop** (Shift+F5) | Closes what Run opened. Disabled when nothing is running. | | **🔍 Search** (Ctrl+Shift+P) | Opens the [command palette](https://albakore.github.io/pyzed/editor/menus-and-commands/#command-palette): commands, panels, editor settings, key bindings and recent files. | | **View** (panels icon) | Docks, panels, grid and guides, previews of other themes, languages and window frames. | | **Help** (? icon) | Shortcuts, the built-in reference, the welcome window, updates and About. | | **⚙** (Ctrl+,) | [Settings](https://albakore.github.io/pyzed/editor/settings/). | Every menu entry is listed with its keys in [Menus and commands](https://albakore.github.io/pyzed/editor/menus-and-commands/). Anything you can click in a menu is also in the **command palette** (the search field, or Ctrl+Shift+P): type a few letters of its name and press Enter. ##### The window frame By default Pyzed Design draws its **own window frame**: the title bar above also holds the minimize, maximize and close buttons, in the style of your system, and the borders of the window resize it. Drag the title bar to move the window; double-click it to maximize. This gives a compact, uniform window on every platform. **Prefer your system's frame?:** Turn off **Settings → Appearance → Custom window frame** to use the frame of your operating system (it applies the next time Pyzed Design opens). Your own apps can use the same frames: see `ui.window_frame` and `Window.frame` in the [API reference](https://albakore.github.io/pyzed/reference/ui/). Custom window frame off, the bar keeps the menus and Run, and your system draws the frame and its buttons around it." /> #### ② ④ ⑤ Docks and panels Panels live in **docks** around the center: left, right, bottom and a top dock that is empty until you move a panel there. Each dock shows one tab at a time. Every place takes any tab: a panel can sit next to the form tabs in the center (the code editor beside your forms, for example), a form can go into a side dock, and any tab can float in a **window of its own**, alone or stacked with others. | Panel | Dock | What it is for | Manual | | --- | --- | --- | --- | | **Toolbox** | Left | Everything you can place, above the **Outline** (the tree of what is placed); icons are [colored by role](https://albakore.github.io/pyzed/editor/toolbox-and-canvas/#icon-colors). In a shader it becomes the **Shading** sidebar and the shader's variables. | [Toolbox and canvas](https://albakore.github.io/pyzed/editor/toolbox-and-canvas/) | | **Project** | Left | The files of the project as a tree, with search, git status and a right-click menu. | [Projects](https://albakore.github.io/pyzed/editor/projects/#files) | | **Dependencies** | Left | Packages, ruff and ty of the project (only with a project open). | [Projects](https://albakore.github.io/pyzed/editor/projects/#the-dependencies-panel) | | **Properties** | Right | Everything about the selection: text, size, colors, data, behavior. | [Properties](https://albakore.github.io/pyzed/editor/properties/) | | **Events** | Right | The events of the selection and the Python handler wired to each. | [Events](https://albakore.github.io/pyzed/editor/events/) | | **Agent** | Right | An AI agent that builds forms and components and writes their code. | [AI agents](https://albakore.github.io/pyzed/editor/ai-agents/) | | **Code** | Bottom | The Python editor, while a handler or a file is open. | [Code editor](https://albakore.github.io/pyzed/editor/code-editor/) | | **Console** | Bottom | What a running form prints, the events it handles, and its errors. | [Running and the console](https://albakore.github.io/pyzed/editor/console-and-running/) | What you can do with them: - **Switch** with the tabs at the top of a dock. - **Drag** any tab, a panel's or a form's: along its row to reorder it, onto another row to stack it there, over a dock or the center to move it there, or out of the window to open it in a **window of its own**. While you drag, a copy of the tab follows the pointer, the place it would go to lights up, and over a row a bar marks where it would land. Drop more tabs on a window to stack them there, and drag them back over the main window to put them back. - **Move** a panel from the menu instead: the dock button at the end of the tab bar opens **Move *panel* to** left, right, bottom, top or **Center**, or **New window**. A panel's tab outside the docks has a **×** that sends it back to its dock. In a window of its own, the same button offers **Dock everything here back**, and closing the window does the same: forms go back to the main window and panels to their docks. - **Resize** a dock by dragging its edge that faces the canvas. - **Hide** a dock with its **×**, or toggle it: Ctrl+B (left), Ctrl+Alt+B (right), Ctrl+J (bottom). - **Save the layout** with **View → Save layout as…** and switch between layouts from **View → Layouts**. Pyzed comes with **Default**, **Coding** (code next to the properties), **Focus** (only the canvas and the right dock), **Events and code** and **Code with the forms** (the code editor as a tab beside the forms, the events on the left); **Delete this layout** removes one of yours. - **Reset** everything with **View → Reset panel layout**. Where each panel is, the tab order, the size of each dock and the panel windows (with their size and position) are saved with your settings and reopen next time. **A layout for writing code:** On a laptop, pick **View → Layouts → Coding**, or hide the left dock (Ctrl+B) and add controls with Ctrl+Shift+A. With two screens, drag **Code** and **Console** to the second one and save the result as a layout of your own. #### ③ The center: tabs, toolbar and canvas ##### Document tabs and windows Every open file is a tab: `Form · login.pzform`, `Component · rating.pzcomp`, a shader, a Python file, the reference or the release notes. The end of the tab row says which view is in front (**Form view**, **Component view**...). - **Click** a tab to switch; each document keeps its own selection and undo history. - **Drag** a tab along the row to reorder it, or over a dock to work on that form beside the one in the center. - **Drag a tab out** of the window to give it a window of its own, with its own tabs, toolbar and canvas. Drag it onto another Pyzed Design window to stack it there. **View → Move tab to new window** and **Move tab to main window** do the same with the keyboard (handy on Wayland, where apps can't know where their windows are). - Panels can share this row too: drop the **Code** or **Console** tab here and it sits among the forms. - The panels follow the form you used last, wherever it is. Closing a document window brings its tabs back to the main window. ##### Toolbar Under the tabs of a form or component: **New form**, **Open**, **Save**, **New component**; **Undo** and **Redo**; the **align**, **distribute**, **same size** and **center** tools; **Bring to front**, **Send to back** and **Delete**; and at the end **Grid**, **Snap**, **Smart guides**, **Resize contents with their anchors**, **Fit selection to grid** and **Edit code file** (F7). Tools that don't apply to the selection are grayed out, and every tooltip shows its shortcut. A shader has its own toolbar; see [the Shader view](https://albakore.github.io/pyzed/shading/shader-designer/). ##### Canvas The form exactly as it will look when it runs, inside a window frame, over a dot grid. Click to select, drag to move, pull the handles to resize. Everything about it is in [Toolbox and canvas](https://albakore.github.io/pyzed/editor/toolbox-and-canvas/). **See your form on other systems:** **View → Preview window frame…** draws the form inside the frame of macOS, Windows 11, GNOME, Ubuntu or KDE Plasma, so you can check how it will look there without leaving your computer. It changes nothing in the form. #### ⑥ The status bar The bar at the bottom tells you where you are and toggles a few things with a click: | Item | Meaning | | --- | --- | | `120, 48` | Where the pointer is on the form, in pixels. | | `sign_in 344×32 at 48, 304` | The selected control, its size and position (or the form's, or how many are selected). | | **Placing Button…** | Shown while you place a control from the toolbox. | | Theme / language | Shown while you preview another theme or language; click to change it. | | **macOS**, **Windows 11**... | The window frame the canvas shows; click to pick another. | | **12 controls** | How many controls the document has. | | **Grid 8px** · **Snap** | Click to toggle the grid and snapping. | | `Ln 12, Col 8` · **ty** · **ruff** | While coding: the cursor and the state of the language servers. | | **Update to X.Y.Z** | A new version is ready; see [Updates](https://albakore.github.io/pyzed/download/#updates). | Hide it with **View → Status bar**. sign_in button selected." plain /> #### The welcome window When Pyzed Design starts without a file, the **welcome window** greets you: start or open a project, reopen a recent one, or make a project from one of the examples (apps, games, showcase demos, blocks and integrations). Open it again from **Help → Welcome**. Everything about it is in [Welcome and examples](https://albakore.github.io/pyzed/editor/examples/). #### Notifications Short messages appear in a corner for a few seconds: saved, copied, a wire that can't connect, a command that failed... Click the **×** to dismiss one; at most four show at once. #### Opening pages and signing in Links in the editor (Help → Documentation, the reference, sign-in pages of the AI agents) open in your default browser. When Pyzed Design runs inside **WSL** on Windows, they open in the Windows browser, with no console window flashing, unless you set the `BROWSER` variable to choose another. #### Next ### Projects > Create and open uv projects in Pyzed Design, browse and search files, manage dependencies, run ruff and ty, and create the git repository. Source: https://albakore.github.io/pyzed/editor/projects/ A Pyzed app is a normal [uv](https://docs.astral.sh/uv/) project: the interface lives in `ui/`, the backend in `src/` and uv manages the dependencies. The **Project** panel (left dock, next to **Toolbox**) shows the project's files, like the explorer of a code editor, and the **Dependencies** panel next to it manages packages, tools and checks, all without leaving the editor. The background, the layout of a project and the matching `pyzed` commands are in [Projects with uv](https://albakore.github.io/pyzed/guides/projects/). (Image: The Project panel: the project's files and folders) #### How the editor finds your project When the editor starts, it looks for a Pyzed project (a `pyproject.toml` with a `[tool.pyzed]` table) at the file you opened or above it, or above the current folder. If it finds one it: - **opens it** in the Project panel, makes its folder the working folder and remembers it among the recent projects; - **activates** it, so forms and handlers can import `ui/` helpers, the `src/` package and every dependency of its `.venv`; - **discovers its components**: every `.pzcomp` of the project joins the toolbox; - **checks its tools** in the background (uv, git, ruff and ty, with their versions); - **gets its environment ready**, like Zed does when a project opens: if uv is missing it installs it with its official installer, and if the project has no `.venv` yet it runs `uv sync`, which also installs ruff and ty. A notification says when the project is ready. The title bar then shows the project's name, its git branch and, in a linked [git worktree](https://git-scm.com/docs/git-worktree), the worktree's name, each one apart, as Zed does. #### Several projects in one window Click the project's name in the title bar (or press Ctrl+Alt+O) to open the **project picker**: (Image: The project picker open under the project's name, with the projects of this window and the recent ones) - **This window** lists the projects you opened in this window; the active one has a check mark. Click another to make it active: the Project panel, Run, the checks and the AI agent all follow it. The **×** of a row takes that project out of the window. - **Recent projects** lists the ones you opened before. Click one to open it here. - Type to filter both lists, move with the arrow keys and press Enter to open the highlighted one. - **Open local folder…** and **New project…** at the bottom open those dialogs. Forms always belong to the active project: a new form is saved in its `ui/` folder by default. ##### The startup file Every project has one file the app starts from: its main form (`main` in `[tool.pyzed]`) or, when it has one, its `script`. The Project panel shows it under the project's name (**▶ Starts from** `ui/main.pzform`) and marks it with a ▶ in the tree. To change it: - click that name: a picker lists the project's forms first and then its Python scripts (the code files of forms and components are left out). Type to filter, click one or press Enter; - or right-click a form or `.py` file in the tree and choose **Set as Startup File**; - or, with a form open, run **Use this form as startup file** from the Run menu or the command palette (the palette also has **Choose startup file…**). `pyproject.toml` is updated for you: a form becomes `main` (and the `script` is dropped), a script becomes `script`. **Run** (F5) with nothing open starts the app from that file (a form opens as the main window, a script runs with `uv run`), and so do `pyzed run` and `pyzed build`. ##### Clone a repository **Clone repository…** (in the empty Project panel, the project picker and the welcome window) asks for a git URL and a folder, clones it with git and opens it, getting its environment ready like any project you open. :::tip[Branches] Click the branch next to the project's name to switch to another local branch (`git switch`). ::: #### Environments and WSL The editor knows where each project lives. Under WSL the title bar starts with the Linux distribution (*Ubuntu*, for example), and the recent projects that live in a WSL distribution show its name next to them. uv, ruff, ty and git then run in Linux, web pages open in Windows' browser, and the editor and the windows of your forms open as native Windows windows (see [WSL: native Windows windows](https://albakore.github.io/pyzed/getting-started/installation/#wsl-native-windows-windows)). Pyzed Design running **on Windows** can also open a project that lives in WSL, like Zed's WSL projects: open its folder through `\\wsl.localhost\\...` (or `\\wsl$\...`). The title bar shows the distribution, and uv, git, ruff, ty and Run go through `wsl.exe` into Linux, with Linux paths. If the distribution has no uv, opening the project installs it there, and the first `uv sync` creates a Linux `.venv`. The AI agent is told the same with every message: the active project, its environment and branch, and the other projects open in the window, so it works on the right one. #### No project open With no project, the panel offers **New project** and **Open…**, and lists your recent projects (click one to open it). (Image: The New project dialog) ##### New project | Field | What it does | | --- | --- | | **Name** | The project's name and folder name. | | **Location** | The folder it is created in; the dialog shows the full path it will create. **Browse…** opens the folder browser: walk to a folder and press **Choose this folder**. | | **Backend in src/** | `uv init --package`: a `src//` package importable from the UI and from tests. Off gives a flat layout (`uv init --app`). | | **Git repository** | Runs `git init` and writes a `.gitignore`. | **Create** runs uv in the background and then opens the new project and its main form. The target folder must be empty, or already be a project. ##### Open project **Open…** shows a folder browser instead of a text field: (Image: The Open project dialog: places, recent projects and the folders of the current one) - The bar at the top goes to the **parent folder** (up arrow) or **home**, and every part of the path is clickable. The eye shows or hides hidden folders. - The path field below it takes a folder typed or pasted; Enter goes there. - On the left, the usual **places** (home, desktop, documents, downloads, the disks) and your **recent** projects (click one to open it). - On the right, the folders of the current one; click a folder to go into it. Folders that are uv projects (they have a `pyproject.toml`) carry a **project** badge and their own **Open** button. **Open** at the bottom opens the folder you are in when it is a project. If the project has a main form, it opens in a tab. #### The project panel ##### Header The project's name, version and folder. On the right, the current **git branch**, or **Create repository** when the folder is not a repository yet (it runs `git init` and writes a `.gitignore`), and a folder button that opens another project. While a command runs, a bar with a spinner says what is going on (`Adding httpx`, `Syncing the environment`...). Commands run on a worker thread, so you can keep designing while uv downloads packages. ##### Files Under the header, the project's files and folders as a tree. Folders open and close with a click (the `ui/` and `src/` folders start open); files are colored by their git status (modified, new, deleted, conflict) and folders take the color of what changed inside them. Virtual environments, caches and build output (`.venv`, `__pycache__`, `build`, `dist`...) are left out; hidden files such as `.gitignore` stay. Clicking a file opens it where it belongs: | File | Opens | | --- | --- | | `.pzform`, `.pzcomp` | In a designer tab. | | The `.py` of a form or component | Its form, with the whole code file in the [code panel](https://albakore.github.io/pyzed/editor/code-editor/). | | Any other text file (`.py`, `.toml`, `.md`, `.json`...) | In a tab of the code panel, to edit it right there. | | Images and other binary files | In the system's app for that file type. | (Image: A project file open in the code panel, next to the README) Each open file gets a tab in the code panel, after the form's code when it is open. The editor has syntax highlighting for the file's language, search and replace (Ctrl+F), go to line (Ctrl+G) and every editing shortcut; completion and diagnostics stay with the form's code. A dot on a tab means unsaved changes: **Save** or Ctrl+S (while the file has the keyboard) writes it, and closing it asks first. Renaming or deleting a file in the tree updates its tab. The toolbar above the tree has a **search field** for files and folders (it matches letters in order, like **Go to file**, and file names count more than folders: `crt` finds `ui/cart.pzform`), **New file**, **New folder** (in the selected folder), **Collapse all** and **Refresh**. The tree reloads by itself when you save and after uv commands. **Right-click** a file or folder (or the empty space below the tree, for the project folder) for its menu, the the usual file operations of a code editor: (Image: The right-click menu of a file in the Project tab) | Item | Keys | What it does | | --- | --- | --- | | **New File** / **New Folder** | Ctrl+N / Alt+N | Creates one in that folder (a new file opens right away). | | **Reveal in File Manager** | Ctrl+Alt+R | Shows it in Files, Finder or Explorer. | | **Open in Default App** | Ctrl+Shift+Enter | Opens the file with the system's app for its type. | | **Open in the code editor** | | Opens any text file as text (a form's `.py` on its own too). | | **Open in Terminal** | | Opens the system's terminal in that folder (`$TERMINAL` first on Linux). | | **Cut** / **Copy** / **Paste** | Ctrl+X / C / V | Moves or copies it into the folder you paste on; a taken name gets ` copy`. | | **Duplicate** | Ctrl+D | Copies it next to itself and asks for the copy's name. | | **Undo** / **Redo** | Ctrl+Z / Ctrl+Shift+Z | Reverts or repeats the last new file or folder, rename, move or copy made here. | | **Copy Path** / **Copy Relative Path** | Alt+Shift+C / Ctrl+Alt+Shift+C | Copies the full path or the path from the project folder. | | **Add to .gitignore** / **Add to .git/info/exclude** | | Ignores it in git, for everyone or only in your copy. | | **Open File Permalink** / **Copy File Permalink** | | The link to it at the current commit on GitHub, GitLab or another remote (only with a remote). | | **Rename** | F2 | Renames it. | | **Trash** | Delete | Moves it to the system's trash, where you can restore it. | | **Delete** | Ctrl+Delete | Deletes it for good after asking (a folder with everything in it). | The keys work once you click in the tree, and also ↑ ↓ to move the selection, ← → to close and open folders and Enter to open the selected file. A file open in the code panel follows its renames and moves, and closes when it is deleted (unless it has unsaved changes). ##### Go to file Ctrl+P (or **File → Go to file…**) opens a search over every file and folder of the project. Type part of a name and press Enter for the best match, or click a result; a folder is revealed in the Project panel. #### The Dependencies panel With a project open, the left dock gets a **Dependencies** panel next to **Project** (when the three titles do not fit, the panels not shown keep only their icon). (Image: The Dependencies panel: packages, tools and the output of ruff) ##### Dependencies | Control | Runs | | --- | --- | | **Add package** field + **+** (or Enter) | `uv add `. Type several separated by spaces, or a constraint such as `httpx>=0.28`. Tick **dev** for the dev group. | | **Pencil** on a row | Opens **Change**: type a new requirement (`httpx==0.27.2`, `httpx>=0.28,<1`) and apply it. | | **Up arrow** on a row | Upgrades that package within its constraint. | | **Trash** on a row | `uv remove` from the group it is in. | | **Refresh** in the section header | `uv sync`: makes `.venv` match `uv.lock`. | | **Search** in the section header | **Check for updates**: looks up newer versions; rows with one show `↑version` in green (hover it for the change). | | **Up arrow** in the section header | Upgrades every package. | Dependencies of other groups show their group as a badge. Each row shows the version installed in `.venv`. It turns yellow when that is not what `uv.lock` says (hover it to see both): press **Refresh** (`uv sync`). The panel reads `pyproject.toml`, `uv.lock` and `.venv` again on its own when something else changes them, such as `uv add` in a terminal or the agent. ##### Warnings When a project cannot run as the editor expects, a yellow box at the top of the **Project** and **Dependencies** panels says why, and opening the project shows it as a notification too: - **The project uses pyzed-ui 1.2.0** while the editor is newer: forms that use controls or properties added since do not run. **Update pyzed-ui** raises its version to the editor's (`uv add "pyzed-ui>="`, which locks and syncs); when that release is not published yet, it upgrades it as far as PyPI goes. - **pyzed-ui is not a dependency** of a Pyzed project: the same button adds it. - **The environment is not created** or **packages are not installed as uv.lock says**: **Sync** runs `uv sync`. The **×** hides the box until the project is opened again. A project that takes `pyzed-ui` from a local checkout (`[tool.uv.sources]`) is not compared with the editor's version. ##### Tools uv, git, ruff and ty with the version found, or a red **missing** badge. Under them: - **Lint** runs `ruff check` on the project. - **Format** runs `ruff format`. - **Types** runs `ty check`. The **Output** section at the bottom shows the command that ran (`$ ruff check`) and the last lines of its output, in red when it failed. A failed command also shows a notification with its last line. :::tip Ruff reads its rules from `[tool.ruff]` in `pyproject.toml`, so **Lint** and the code editor's inline warnings follow the same configuration. ::: #### Running forms of a project When the editor runs inside the project's own environment (you started it with `uv run pyzed design`), **Run** (F5) runs the form in the editor's process. When it does not (the standalone app, or `pyzed design` installed with `uv tool`), **Run** saves the form and starts it with `uv run pyzed run --reload` in the project, so it has every dependency of `.venv` and updates on every save. Its output is piped into the [console](https://albakore.github.io/pyzed/editor/console-and-running/) either way. #### Building the app The Project panel does not package apps; use the command line in the project folder: ```bash uv run pyzed build # a folder with the executable for this platform uv run pyzed build --onefile # a single file uv run pyzed build --workflow # a GitHub Actions workflow for Linux, Windows and macOS ``` See [Build and distribute](https://albakore.github.io/pyzed/packaging/build/). #### The standalone app **Pyzed Design** is also published as a standalone app for Linux, Windows and macOS: the editor compiled with its own Python and the `uv` binary, so nothing else needs to be installed. Projects created from it are normal uv projects with their own `.venv`, and their forms always run through `uv run`. :::tip If completion does not know a package you just added, press **Restart servers** in the [editor settings](https://albakore.github.io/pyzed/editor/code-editor/#editor-settings) after the sync finishes: the language servers read the project's `.venv` when they start. ::: ### Welcome and examples > The welcome window of Pyzed Design: start or open a project, reopen a recent one, or create a project from one of the built-in examples, each with a thumbnail, a title and a description. Source: https://albakore.github.io/pyzed/editor/examples/ When Pyzed Design starts without a file, the **welcome window** opens. On the left you can start a new project, open one, create a form or read the documentation, and reopen a recent project. On the right is the **gallery of example projects**: each example has a thumbnail, a title and a description. (Image: The welcome window with the gallery of example projects) Open it again at any time from **Help → Welcome**, or **Help → Example projects…** to see every example. Uncheck **Show at startup** (or turn off **Welcome window** in [Settings](https://albakore.github.io/pyzed/editor/settings/)) to start straight on an empty form. #### The gallery The buttons above the gallery filter the examples by category (each shows how many it has), and the search box filters them by title and description. **All** shows every category under its own heading: | Category | Examples | | --- | --- | | **Apps** | Counter, To-do list, Todos, Pomodoro, Markdown viewer, Game of Life, Analog clock, Stopwatch, QR code, Color picker, Image viewer, Several windows, Custom window frame, Component gallery, Mini editor, Zed-like editor. | | **Tutorial** | The five steps of the tutorial (Hello, State, Layout, Async work, Notes app), Layout basics, Lists and tables, Navigation, Talking to the user, Theme switcher, Contacts with SQLite. | | **Audio** | Synthesizer, Drum machine, Modular rack, Music visualizer, MIDI controller, Webcam. | | **Games** | Snake, Breakout, Neon Invaders, Rock Storm, Pixel Run, 2048, Coin Chase, Jump Lab, Walker, Orb Rush, Target Rush, Dodge. | | **Animation** | Animations, Effects, Timeline. | | **Styling** | Styles, Web-style layout, Theming playground, and the themes Brand, Glassy dark, Pastel, Retro terminal, High contrast and Two themes. | | **Shading** | Shader gallery, Aurora shader. | | **Showcase** | Glass dashboard, Game HUD, Game main menu, Inventory. | | **Blocks** | Dashboard, Kanban board, Music player, Chat, Settings, Data table, IDE layout, Sign-in screen, Onboarding wizard. | | **Patterns** | Services (timer, worker, process, clipboard, settings), Custom components, Docks and grids, Resizable panes, Context menu, Dialogs and languages, Background work, Download progress, Live data. | | **AI** | Pyzed agent (an agent that calls tools, with a demo model that needs no key), AI chat. | | **Designer** | Forms made in Pyzed Design: Login form, Task runner (the non-visual components), Project explorer (tree, breadcrumbs, tabs, sortable table and other toolbox controls), Mini IDE (a Dock with panels on every side), Component cards, Bound sign-up form, Form over a shader. | | **Integrations** | SQLModel heroes, SQLModel inventory, Pydantic sign-up, PyPI with httpx, Downloads with httpx, SQLAlchemy contacts, SQLAlchemy quick start, Async SQLite journal, FastAPI heroes. | (Image: The Audio category of the gallery: the synthesizer, the drum machine, the modular rack, the visualizer, the MIDI controller and the webcam) (Image: The Games category of the gallery) (Image: The Showcase category of the gallery: the glass dashboard, the game HUD, the main menu and the inventory) #### Creating a project from an example Click an example to name the new project and choose where it goes (by default `~/Pyzed Projects/`; **Browse…** picks another folder). **Create project** then: 1. copies the example into the folder as a normal [uv project](https://albakore.github.io/pyzed/editor/projects/), with `pyzed-ui` and the packages the example needs in `pyproject.toml`; 2. opens it in the Project panel, with its script (or, for the Designer example, its main form) in front; 3. installs its packages with `uv sync` in the background. (Image: Naming the project made from an example) The copy is yours: change it freely, the original example is not touched. #### Running it Projects made from a code example have a `script` in `[tool.pyzed]`: ```toml [tool.pyzed] ui = "ui" script = "snake.py" ``` When no form is open, **Run** (F5) starts that script with `uv run`, and its output goes to the [console](https://albakore.github.io/pyzed/editor/console-and-running/). **Run → Run project script** does the same at any time. Open project files with unsaved changes are saved first. #### Where the examples come from The standalone **Pyzed Design** app carries every example and its thumbnail inside it (about 5 MB), so the gallery works without a connection. A designer installed with pip does not include them: the gallery offers **Download examples**, which fetches them once from the release of the installed version. Creating the project's environment with `uv sync` needs a connection the first time, to download Python and the packages. ### Views and documents > The four kinds of document in Pyzed Design and the view each one opens in: the Form view for windows, the Component view for reusable pieces, the Shader view for GPU effects and the code editor for Python. Source: https://albakore.github.io/pyzed/editor/views/ Pyzed Design edits four kinds of document. Each opens in its own **view**: the same window, but the left dock, the toolbar, the center and the Properties panel change to fit what you are making. The tab row tells you which view is in front, and the title bar shows a **Component** or **Shader** badge when it is not a form. | Document | File | Opens in | Think of it as | | --- | --- | --- | --- | | **Form** | `login.pzform` + `login.py` | Form view | A window of your app, like a Visual Basic form or a Qt Designer `.ui`. | | **Component** | `rating.pzcomp` + `rating.py` | Component view | A reusable piece placed in forms, like a Godot scene or an Unreal widget blueprint. | | **Shader** | `fire.pzshad` | Shader view | A GPU effect made of nodes, like Godot's visual shaders or Unreal's material editor. | | **Code and text** | `*.py`, `*.toml`, `*.md`... | Code editor | The Python behind everything, with completion and diagnostics. | All files are plain text (JSON or Python), so they diff well in git and you can open them in any other editor. ```mermaid flowchart LR N["New (Ctrl+N)"] --> F[Form] & C[Component] & S[Shader] F -->|"place it"| C F -->|"Shader control"| S C -->|"Shader control"| S F & C -->|"double-click a control"| K[Code editor] ``` #### Form view **For:** the windows of your app: a login, a settings page, a dashboard, a dialog. - **Left dock:** the **Toolbox** with every control, and the **Outline** of the form. - **Center:** the form drawn in a window frame, with a dot grid, handles and smart guides. - **Right dock:** **Properties** and **Events** of the selection (or of the form when nothing is selected). - **Run** (F5) opens the form as a real window with its Python handlers. Read [The Form view](https://albakore.github.io/pyzed/editor/form-view/) for a guided first form. #### Component view **For:** pieces you use many times: a star rating, a profile card, a product tile. A component has no window; it lives inside forms and other components. - The same toolbox and canvas as a form, but the canvas shows an **accent frame with the class name** instead of a window, and the title bar a **Component** badge. - With nothing selected, **Properties** shows the **Component panel**: the class name, how it shows in the toolbox, its typed **properties** and its **methods**. Its **custom events** are in the **Events** tab, below the built-in ones. - **Run** previews the component alone in a window. - Double-click a component placed on a form to open it in its own tab. Everything is in [The Component view](https://albakore.github.io/pyzed/editor/component-designer/). #### Shader view **For:** animated backgrounds, glows, transitions and game visuals drawn by the graphics card. - **Left dock:** the **Shading** sidebar (every node, by category) and the **Outline** of variables, named values and nodes. - **Center:** three tabs: **Graph** (the nodes), **Preview** (the result large, with a control per variable) and **WGSL** (the generated code, read only). - **Properties:** the settings and inputs of the selected node, of a variable, or of the shader itself. - **Run** opens the shader in a window that follows your edits. Start with [The Shader view](https://albakore.github.io/pyzed/editor/shader-view/) in this manual, or go straight to the [Shading section](https://albakore.github.io/pyzed/shading/). #### Code editor **For:** the Python of every form and component (their event handlers and methods) and any text file of the project. - Opens in the **bottom dock** when you double-click a control or an event, press F7 for the whole file, or click a file in the Project panel. Each file is a tab of the Code panel. - Completion, diagnostics, code lens, folding, a minimap, multiple cursors, find and replace, and key bindings you can change. See [Code editor](https://albakore.github.io/pyzed/editor/code-editor/). #### Switching between views - Every open document is a tab; click one to switch. Each keeps its own selection and undo history. - Opening a file that is already open switches to its tab. - Drag a tab out of the window to work on two documents side by side (see [The interface](https://albakore.github.io/pyzed/editor/interface/#document-tabs-and-windows)). **Code with a syntax error:** When you leave a form or component, its handler code is applied. If the code doesn't parse, the switch waits until you fix it, and a notification says why: nothing you wrote is lost. #### From the command line Pass a file to open it in the right view (it is created on the first save if it doesn't exist): **uv:** ```bash uv run pyzed design ui/login.pzform uv run pyzed design ui/rating.pzcomp uv run pyzed design shaders/fire.pzshad ``` **pip:** ```bash pyzed design ui/login.pzform pyzed design ui/rating.pzcomp pyzed design shaders/fire.pzshad ``` **python:** ```bash title="macOS / Linux" python3 -m pyzed design ui/login.pzform ``` ```powershell title="Windows (PowerShell)" py -m pyzed design ui\login.pzform ``` ### The Form view > Design a window of your app in Pyzed Design's Form view: place controls, set their properties, wire an event to Python and run it, with every part of the view explained. Source: https://albakore.github.io/pyzed/editor/form-view/ A **form** is one window of your app. The **Form view** is where you draw it: you pick controls from a box, drop them on the window, change their look in a list of properties, and write a few lines of Python for what happens when the user clicks or types. If you have used Visual Basic, the WinForms designer or Qt Designer, you already know the idea; if you haven't, the walkthrough below takes ten minutes. #### What you see | Area | In the Form view | | --- | --- | | **Toolbox** (left, top) | Every control you can place, grouped (Common, Display, Containers, Layout, Navigation...), with a search box. | | **Outline** (left, bottom) | The controls of the form as a tree: select, rename, nest, hide and lock them. | | **Toolbar** (center, top) | Save, undo, align, distribute, same size, center, order, grid, snap and guides. | | **Canvas** (center) | The form inside a window frame, as it will run. | | **Properties** (right) | The selected control, or the form when nothing is selected. | | **Events** (right) | What the selected control can react to, and the Python function wired to each event. | | **Code** (bottom) | The Python of the form, opened when you wire an event. | Every area is described in detail in its own page: [Toolbox and canvas](https://albakore.github.io/pyzed/editor/toolbox-and-canvas/), [Properties](https://albakore.github.io/pyzed/editor/properties/), [Events](https://albakore.github.io/pyzed/editor/events/) and [Code editor](https://albakore.github.io/pyzed/editor/code-editor/). #### Your first form: a greeter You will build a window with a text box, a button and a label. Typing a name and clicking the button shows "Hello, *name*!". 1. **Create the form.** Press Ctrl+N. In the **New** dialog keep **Form**, type `hello` as the **Name** and `Greeter` as the **Title**, pick the **Blank** template and press **Create**. Two files appear: `hello.pzform` (the layout) and `hello.py` (the code). 2. **Place a text box.** In the toolbox, drag **Text box** onto the form. It is selected and the Properties panel shows it. Change its **Name** to `your_name` and its **Placeholder** to `Your name`. 3. **Place a button.** Click **Button** in the toolbox, then drag a rectangle on the form under the text box: the button takes that size. Name it `greet` and set its **Text** to `Say hello`. 4. **Place a label.** Add a **Label** under the button, name it `message` and clear its text. 5. **Line them up.** Select the three controls (drag a rubber band around them) and press **Align left** in the toolbar. Smart guides show pink lines when edges line up while you drag. 6. **Wire the click.** Double-click the button. The Events panel wires its **click** event to a new function, `greet_click`, and opens it in the code editor. Write one line: ```python title="hello.py" def greet_click(form, event): """Runs when greet is clicked.""" form.message.text = f"Hello, {form.your_name.text}!" ``` `form` is the running window: each control is an attribute with its name (`form.message`), and its properties are attributes of the control (`form.message.text`). Changing them updates the window. 7. **Run it.** Press F5 (it saves first). Your form opens as a real window; type a name and click the button. The [console](https://albakore.github.io/pyzed/editor/console-and-running/) at the bottom shows what the form prints and any error. Keep it open, change the label's color in Properties and save: the running window updates. **Names matter:** A control's **Name** is how your code reaches it (`form.your_name`), and handlers are named after it (`greet_click`). Give controls short, meaningful names as soon as you place them; renaming one later also renames the handlers that follow that naming (`greet_click`), but code elsewhere that uses the old name must be updated by hand. #### Run the form outside the designer The form and its code are ordinary files of your project. Run them from a terminal: **uv:** ```bash uv run pyzed run hello.pzform uv run pyzed run hello.pzform --reload # updates on every save ``` **pip:** ```bash pyzed run hello.pzform pyzed run hello.pzform --reload ``` **python:** ```bash title="macOS / Linux" python3 -m pyzed run hello.pzform ``` ```powershell title="Windows (PowerShell)" py -m pyzed run hello.pzform ``` Or from your own Python code: ```python title="main.py" from pyzed import designer designer.run("hello.pzform") ``` #### What else the Form view does | Task | How | More | | --- | --- | --- | | Arrange controls in rows, columns and grids that adapt to the window | Select them, right-click → **Group in a row / column / grid**, or use a container from the toolbox | [Layouts](https://albakore.github.io/pyzed/editor/layouts/) | | Keep a control glued to an edge when the window grows | **Position → Anchors** in Properties | [Properties](https://albakore.github.io/pyzed/editor/properties/#position) | | Change colors, fonts, borders and shadows | **Style** in Properties; the whole window in the form's **Style** | [Styles and themes](https://albakore.github.io/pyzed/editor/styles-and-themes/) | | Animate on hover or click, add glow, blur or glass | **Effects** and **Motion** in Properties | [Animations and effects](https://albakore.github.io/pyzed/editor/animations-and-effects/) | | Fill fields from data and validate them | **Data** section: binding and rules | [Data binding](https://albakore.github.io/pyzed/editor/data-binding/) | | See the form in another theme, language or OS frame | **View → Preview theme / language / window frame** | [Interface](https://albakore.github.io/pyzed/editor/interface/#canvas) | | Reuse a group of controls in many forms | Turn it into a component | [Component view](https://albakore.github.io/pyzed/editor/component-designer/) | | Paint a background with a GPU effect | The **Shader** control in Display, with a `.pzshad` | [Shading](https://albakore.github.io/pyzed/shading/) | #### The `.pzform` file A form is JSON: its size and look, and every control with its type, position and properties. Its handlers live next to it in a Python module with the same name. Both are yours to version, review and edit by hand. ```json title="hello.pzform (abridged)" { "format": "pyzed.form/1", "form": {"name": "hello", "title": "Greeter", "width": 360, "height": 220, "theme": "One Dark"}, "controls": [ {"name": "your_name", "type": "input", "x": 24, "y": 24, "width": 312, "height": 32, "props": {"placeholder": "Your name"}, "events": {}}, {"name": "greet", "type": "button", "x": 24, "y": 72, "width": 120, "height": 32, "props": {"text": "Say hello"}, "events": {"click": "greet_click"}} ] } ``` **Go deeper:** This page covers the essentials of the Form view. The other pages of this section describe every control of the view in depth, starting with [Toolbox and canvas](https://albakore.github.io/pyzed/editor/toolbox-and-canvas/). ### Toolbox and canvas > Place, select, move, resize and arrange controls in Pyzed Design: snapping, smart guides, alignment, z-order and the outline of your form. Source: https://albakore.github.io/pyzed/editor/toolbox-and-canvas/ The **Toolbox** panel (left dock) holds the components on top and the **Outline** below; drag the bar between them to give one more room. The **canvas** in the center shows the document exactly as it will run, but inert: buttons do not click and text boxes do not type, so the mouse is free to edit. #### The toolbox Every component you can place, by category: **Common**, **Forms** (everything the user types or picks in), **Date & time**, **Display**, **Containers**, **Layout**, **Navigation**, **System** (what talks to the operating system), **Components** (the other [non-visual ones](https://albakore.github.io/pyzed/editor/non-visual-components/), which go to the tray under the form) and one category per group of [designed components](https://albakore.github.io/pyzed/editor/component-designer/) of your project. Hover a row for the component's role and description. | Category | Components | | --- | --- | | **Common** | Label, Heading, Button, Link, Split button | | **Forms** | Text box, Text area, Check box, Switch, Radio group, Drop-down, Slider, Number box, List box, Search box, Segmented, Toggle group, Color picker, Icon picker | | **Date & time** | [Date picker](https://albakore.github.io/pyzed/library/date-picker/), [Date range](https://albakore.github.io/pyzed/library/date-range-picker/), [Time picker](https://albakore.github.io/pyzed/library/time-picker/), [Calendar](https://albakore.github.io/pyzed/library/calendar/) | | **Display** | Progress bar, Badge, Image, SVG image, Image viewer, Shader, Icon, Markdown, Spinner, Chip, Callout, Avatar, Facepile, Counter badge, Status dot, Key binding, Stat, Chart, Table, Diff stat, Empty state, QR code, Terminal output | | **Navigation** | Tab bar, Tree view, Breadcrumbs | | **Containers** and **Layout** | Panel, Group box, Card, Divider, Flex row, Flex column, Grid, Dock, Dock panel, Spacer | | **System** | [File picker](https://albakore.github.io/pyzed/library/file-input/), [Drop zone](https://albakore.github.io/pyzed/library/drop-zone/), and in the tray: Shortcut, File watcher, Process, Clipboard, File dialog, Notification, System info, Launcher, Network monitor, Battery monitor | | **Components** | Timer, Background worker, Web request, Settings | | **Media** and **Audio** | Webcam, and the [audio controls](https://albakore.github.io/pyzed/audio/): knobs, faders, pads, piano, meters, waveforms, spectrum... | The [Project explorer](https://albakore.github.io/pyzed/editor/examples/) example puts most of them on one form, with a handler for each of their events. - **Search components…** filters the list by name (in English or in the editor's language) or by type. - **Pointer** (first row) is the normal selection tool. It is highlighted when no component is picked. ##### Icon colors Every component has a **role**, and its icon takes the role's color wherever it appears: the toolbox, the outline, the tray of non-visual components, the header of the **Properties** and **Events** panels and the chip that follows the pointer while you drag. A form's structure reads at a glance: blue boxes hold the rest, green fields take what the user types, yellow buttons run your code. The palette icon next to the search box shows the same legend. | Color | Role | What it is | | --- | --- | --- | | Blue | **Layout** | Holds and arranges other controls: panels, cards, group boxes, rows, columns, grids, docks, dividers, spacers. | | Green | **Input** | Takes a value from the user: text boxes, check boxes, switches, drop-downs, sliders, number boxes, pickers. | | Yellow | **Action** | Runs something when pressed: buttons and split buttons. | | Cyan | **Navigation** | Moves between views: tab bars, tree views, breadcrumbs, links. | | Neutral | **Display** | Shows content: labels, headings, images, tables, charts, badges. | | Red | **Media** | Sound and video: the audio controls and the webcam. | | Orange | **System** | Non-visual components: timers, processes, file watchers, web requests, the clipboard. | | Purple | **Custom** | Your own components, made with the [component designer](https://albakore.github.io/pyzed/editor/component-designer/) or `pz.Component`. | The colors come from the theme's terminal palette, so they change with the theme and stay readable in light and dark themes. The role is worked out from the component (whether it holds controls, its category and its events); a component registered from Python can name its own with `Component(..., role="input")`. ##### Placing a control There are four ways, pick the one that fits: | Gesture | Result | | --- | --- | | **Drag** a toolbox row onto the form | The control lands where you drop it, at its default size, inside the container under the pointer (it lights up while you drag). | | **Click** a row, then **click** on the form | Same, at the point you clicked. | | **Click** a row, then **drag a rectangle** on the form | The control takes the rectangle's size (at least 6×6 px); its corners snap to the grid. | | Ctrl+Shift+A (**Add control…**) | A searchable list; Enter adds the highlighted component centered on the form. | (Image: Placing a button: drag a rectangle with the tool picked) While a component is picked, the pointer is a crosshair and the status bar says `Placing Button: click or drag on the form`. Esc (or clicking **Pointer**, or the same row again) drops the tool. New controls get a free name from their type (`button1`, `button2`...) and are selected, with the Properties panel in front. (Image: Add control: a fuzzy-searchable list of components) :::tip The **+** on a container's row in the outline, and **Add inside…** in its context menu, open the same list but add the new control *inside* that container. ::: #### Selecting | Gesture | Selects | | --- | --- | | Click a control | That control (the others are deselected). | | Click an already selected control | Keeps the selection and makes that control the **primary** one: the reference for alignment and same size, drawn with a thicker outline. | | Drag on an empty spot | A rubber band: every control it touches. A container and the controls inside it count once (the container). | | Ctrl+A | Every control. | | Click an empty spot | Nothing: the Properties panel then shows the form itself. | | A row in the outline | That control, even if it is hidden behind others. | With several controls selected, the Properties panel shows the alignment and spacing tools instead of properties, and the Events panel asks you to pick one control. #### Moving and resizing - **Drag** a selected control to move the whole selection. Positions snap to the grid (when snapping is on) and the group stays inside the form. - **Drop** it on a container to put it inside, or on the bare form to take it out of its container. The target lights up with an accent frame while you drag. - The **eight handles** of a single selected control resize it from that edge or corner; the opposite edge stays put. Controls are never smaller than the minimum size and stay inside their container. - The **grip** under the form's bottom-right corner resizes the form (at least 120×80, snapped to the grid). - Esc during any drag cancels it and puts everything back. - **Arrow keys** nudge the selection by 1 px; Shift+arrows move it by one grid step (1 px when snapping is off). Consecutive nudges are a single undo step. Locked controls do not move or resize (they show a small lock above their corner), and alignment tools skip them. ##### Containers and their grip A container's children cover most of it, so a press there picks a child. To pick or drag the container itself, use the **grip tab** with its name above its top-left corner: it shows while the pointer is over the container and while the container, or a control inside it, is selected. Right-click the grip for the container's context menu. See [Layouts and containers](https://albakore.github.io/pyzed/editor/layouts/). #### Snapping, grid and smart guides | Tool | Where | What it does | | --- | --- | --- | | **Show grid** | Toolbar, status bar, **View → Grid**, G | Shows or hides the dots of the grid. | | **Snap to grid** | Toolbar (magnet), status bar, **View** | Rounds positions and sizes to the grid step while you drag, place and resize. | | **Smart guides** | Toolbar (ruler), **View** | Snaps a moving control to the edges and centers of the other controls and of the form, and draws a red guide line while they line up. | | **Resize contents with their anchors** | Toolbar (anchor) | Resizing the form, a component or a free container moves and stretches what is inside by its anchors, as the running window does. On by default. | | **Fit selection to grid** | Toolbar | Moves and resizes the selected controls (all of them when nothing is selected) onto the grid. | | **Grid size** | Properties of the form (**Designer** section) | The grid step of this form (2 to 64 px, 8 by default). New forms take the default from [Settings](https://albakore.github.io/pyzed/editor/settings/). | (Image: A smart guide: the check box lines up with the centers of the other controls) Smart guides snap within 4 px of a target, on both axes at once, so a control can line up with one control's left edge and another's middle in the same drag. #### Align, distribute and size The toolbar acts on the selection: | Button | Effect | | --- | --- | | **Align left / centers / right** | Lines up the left edges, the horizontal centers or the right edges. | | **Align top / middles / bottom** | Lines up the top edges, the vertical centers or the bottom edges. | | **Distribute horizontally / vertically** | Spaces three or more controls evenly between the first and the last. | | **Same width / height** | Gives every selected control the primary control's width or height. | | **Center in form horizontally / vertically** | Centers the selection in the form. | | **Bring to front / Send to back** | Changes the z-order (what is drawn on top). | | **Delete** | Deletes the selection (Delete or Backspace). | With several controls selected, alignment uses the bounds of the selection; with a single control it aligns to the form. The Properties panel shows the same tools while several controls are selected. (Image: Three controls selected, ready to align) #### Clipboard and history - Ctrl+C, Ctrl+X, Ctrl+V: copy, cut and paste. Pasted controls are shifted a little so you can see them; a container is copied with everything inside it. Pasted controls are **not** wired: their events start empty, so they do not share handlers with the originals. - Copies go to the **system clipboard**, so you can paste controls into **another form**: another tab, another Pyzed Design window, or the same form after restarting. In another form a pasted control keeps its name when it is free there (fresh names otherwise), along with its properties, style, [binding and rules](https://albakore.github.io/pyzed/editor/data-binding/). Pasting text that is not Pyzed controls pastes the last copy made in this window. - Ctrl+D duplicates the selection without touching the clipboard. - Ctrl+Z undoes, Ctrl+Shift+Z (or Ctrl+Y) redoes. Each document keeps its own history; repeated edits of one field (typing a title, nudging) merge into one step. #### The context menu Right-click a control on the canvas, its row in the outline or a container's grip: (Image: The context menu of a button) | Entry | Does | | --- | --- | | **View code** | Opens the handler of the control's main event (or opens a designed component in its own tab). | | **Help** | Opens the control's page in the [reference](https://albakore.github.io/pyzed/editor/reference/) (Shift+F1). | | **Add inside…** | Containers only: the Add control list, adding inside this container. | | **Group in a row / column / grid / panel** | Wraps the selection in a new flex row, flex column, grid or panel. | | **Move out of *container*** | Takes the control out of its container, into the container's parent. | | **Cut, Copy, Paste, Duplicate** | As the shortcuts. | | **Bring to front, Send to back** | Z-order. | | **Lock / Unlock** | Locks the control in place. | | **Delete** | Deletes the selection. | The menu opens at the pointer and stays inside the window. Esc or a click elsewhere closes it. #### The outline The **Outline** lists every control of the document as a tree. The first row is the form (or the component) itself: click it to select it, **+** adds a control to it. (Image: The outline: a locked control and a hidden one) - **Order**: top-most first, like layers in a drawing app. Children of a flex row, a flex column or a grid are listed in their layout order instead. - **Chevron**: folds or unfolds a container's children. On a placed designed component, it lists the component's public properties and methods; click one to insert `form.ada.rate()` (or `self.stars.pick()` in a component) at the code editor's cursor, or to copy it when no code is open. - **Eye**: hides the control. Hidden controls are invisible when the form runs and their names are grayed in the outline. See [Dialogs, drawers and other hidden controls](#dialogs-drawers-and-other-hidden-controls). - **Lock**: locks the control so it cannot be moved or resized by accident. - **+** (containers): adds a control inside. - **Drag a row** to reorder it: a line shows where it goes (before or after a row), and a frame when it would go *inside* a container. Drop it on the canvas to move it into the container under the pointer, or onto the form. - **Right-click** a row for the context menu. :::tip The outline is the easiest way to pick a control buried under others, or a [non-visual component](https://albakore.github.io/pyzed/editor/non-visual-components/) such as a timer, and to move controls between containers precisely. ::: #### Dialogs, drawers and other hidden controls A dialog, a drawer or a page that only appears when a button is pressed is a container with **Visible** off, shown from code with `form.status_dialog.show()` (and `.hide()` to close it). The canvas leaves hidden controls out, so a form with many of them does not turn into a pile of overlapping panels: 1. **Select it in the outline** (or anything inside it). It comes up in front of the form, which is dimmed behind it, as it will look when the form runs. A chip at the top names it. 2. **Edit it as usual**: move, resize, drop new controls into it from the toolbox, change its properties. 3. **Go back** with Esc, the chip's **×**, or a click on the dimmed form. To see every hidden control at once, dimmed in its place, turn on **View → Show all hidden controls**. ##### Hide a control in the editor only If you would rather keep every control on the canvas and hide some just while you work, like layers in a drawing app, set **Settings → Form canvas → Outline eye** to **Hidden in the editor only**. The eye then hides the control (and what is inside it) on the canvas only: it stays hidden even when selected, the running form still shows it, and **Visible** is set in the Properties panel. Controls whose Visible is off are drawn normally in this mode. What the eye hides is saved with the form. #### The status bar From left to right: the pointer's position on the form, what is selected (`sign_in 344×32 at 48, 304`, or `3 selected`, or the form and its size) and, while a component is picked, the placing hint. On the right: the window frame shown around the form (**GNOME**, **macOS**...: click to preview another platform's), the number of controls, **Grid 8px** (click to show or hide the grid) and **Snap** (click to toggle snapping). While the code editor is open it also shows the cursor's line and column and the state of the language servers. Hide it with **View → Status bar**. ### Properties > The property grid of Pyzed Design section by section: the form, a control, several controls at once, and the list and table editors. Source: https://albakore.github.io/pyzed/editor/properties/ The **Properties** panel (right dock) edits whatever is selected. Its content changes with the selection: | Selection | The panel shows | | --- | --- | | Nothing | The **form** itself: window, look, global style and designer settings. In a component, the [Component panel](https://albakore.github.io/pyzed/editor/component-designer/#the-component-panel). | | One control | Its name, **Position** first, then its own properties by group, **Behavior**, its style (**Text**, **Fill**, **Border**, **Spacing**), **Effects** and **Animation**. | | Several controls | Alignment, distribution and same-size tools. | Every section is collapsible: click its header to fold it. The folded sections are remembered while the editor is open, so you can keep only the ones you use. Every change is one undo step, and typing in a field merges into a single step. ##### Number fields Every number in the panel works like in Figma: - **Drag it sideways**: press on the number and move the mouse left or right to lower or raise it. Small steps (opacity, line height) move slowly, so you can aim. - **Double-click it** to type a value; Enter keeps it. A single click puts the cursor in the field, and ↑ / ↓ add or take one step. - The letter or unit inside the field (**X**, **W**, **px**, **°**) says what the number is. In your own apps the same field is [`ui.number_field`](https://albakore.github.io/pyzed/library/number-field/): `drag=False` turns dragging off, `steppers=False` hides the `−`/`+` buttons. #### A control (Image: The properties of a table: the rows editor) ##### Header and name The top of the panel shows the component's icon, its kind and a one-line description. Under it, **Name** is the control's identifier: the attribute you use in code (`form.email`) and the prefix of its handlers. Type a new name and press Enter to rename it; handlers that follow the naming convention (`email_change`) are renamed in the code file too. Names are made valid and unique for you. The **book** button at the right of the header (Shift+F1) opens the control's page in the [built-in reference](https://albakore.github.io/pyzed/editor/reference/): every property with its type and default, its events and its methods. Hover a property's label for a one-line description. ##### The component's own sections Each component groups its properties in **Appearance**, **Data**, **Layout** or **Behavior** sections (a section with the same name as one of the panel's own, like **Behavior**, is merged into it). The kind of a property decides its editor: | Kind | Editor | Example | | --- | --- | --- | | Text | A text field. | A button's `text`, a text box's `placeholder`. | | Multiline | A small code editor with wrapping. | A text area's `text`, a markdown's `text`. | | Number | A number field you drag or type in, with limits. | A slider's `min`, `max`, `step`. | | File, folder | A field and a **folder** button that opens the system's file dialog (see below). | An image's `source`, a shader's `file`, a file watcher's `path`. | | Switch | An on/off switch. | `checked`, `secure`, `wrap`. | | Choice | Up to four short choices side by side as a segmented control; longer lists as a drop-down. | A button's `variant`, a label's `size`. | | Color | A swatch and a field for a theme token (`accent`, `text_muted`) or `#hex`. | A label's `color`. | | Icon | A preview, a field for a [Lucide](https://lucide.dev) icon name and a **…** button: click the preview or **…** to pick from the icon dialog (see below). | A button's `icon`. | | List | One field per item (see below). | A drop-down's `options`, a list box's `items`, a tab bar's `tabs`. | | Rows | A small spreadsheet (see below). | A table's `data`. | Hover a property's name to read what it does, when it has a tip. The [component library](https://albakore.github.io/pyzed/library/) documents every property of every component. Controls that hold a value also show **Bind to** and their validation rules (**Required**, **Minimum length**, **Pattern**...) in the **Data** section: see [Data binding and validation](https://albakore.github.io/pyzed/editor/data-binding/). ##### Files and folders Every property that holds a file or a folder has a **folder** button at its end. It opens the system's dialog (filtered by kind: images, sounds, fonts, shaders, data files...) and writes the path for you. A file inside your project is written **relative to the project** (`assets/logo.png`), so the project still works when you move it. Typing the path by hand still works. This covers the components' own properties (`source`, `file`, `path`, `directory`...), any text property whose name says it is a path (`icon_path`, `output_dir`, `cwd`...), the [properties of your own components](https://albakore.github.io/pyzed/editor/component-designer/) typed `pz.FilePath` or `pz.FolderPath`, and the folder fields of the editor's dialogs. In your own apps the same field is `ui.path_input`. ##### The icon dialog Clicking an icon property's preview or its **…** button opens **Choose an icon**: a search box, a category drop-down and a grid of every bundled icon. The search matches names and Lucide's tags ("person" finds `user`, "trash" finds `trash-2`), Enter picks the first match, and a click on an icon sets the property and closes the dialog. The icon in use is highlighted and shown at the bottom, next to the number of matches; **×** clears the property. (Image: The icon dialog opened from a button's Icon property) The same dialog picks the **Icon** of a [designed component](https://albakore.github.io/pyzed/editor/component-designer/) and the default of its icon properties. In your own apps it is [`ui.IconPicker`](https://albakore.github.io/pyzed/library/icon-picker/). ##### The list editor (Image: A list box's items, one field per item) Lists get one numbered field per item, with buttons to **move it up**, **move it down** and **remove** it, and **Add item** at the end. The page icon at the right of the property's title switches to **Edit as text**: one item per line in a small editor, handy for pasting a long list. The list icon switches back (**Edit item by item**). ##### The rows editor A table's data is a small spreadsheet. The first row (marked **H**) is the header; the others are numbered. **Add row** and **Add column** grow it, the **×** at the end of a row removes the row and the **×** above a column removes the column (a table keeps at least one). It scrolls sideways when the table is wider than the panel. As with lists, the page icon switches to plain text: one row per line, cells separated by commas. ##### Style: Text, Fill, Border, Spacing The control's style is split in four short sections, the same properties as [`pz.Style`](https://albakore.github.io/pyzed/theming/styles/). Empty fields keep the component's own look, and **Reset style** at the end clears them all. See [Styles and themes](https://albakore.github.io/pyzed/editor/styles-and-themes/). | Section | Fields | | --- | --- | | **Text** | **Font** (family and weight), **Size** (font size and line height), **Color**, **Italic**. | | **Fill** | The background: a color, a gradient or a mask. | | **Border** | **Color**, **Width** and **Corners** (one radius, or one per corner with the **link** button). | | **Spacing** | The **box model**, **Align content**, **Max size** and **Clip content**. | The box model is drawn like the browser's: an outer amber ring for the **margin** (space outside the control) and an inner green ring for the **padding** (space between its border and its content), with one number per side. Drag or double-click any of them. **Align content** is a 3×3 box: click a dot to put the content in that corner, side or the center. The shadow lives in **Effects**, next to opacity, glow, bloom and glass; **Effects** and **Animation** are covered in [Animations and effects](https://albakore.github.io/pyzed/editor/animations-and-effects/). ##### Position **Position** is always the first section. For a control placed freely (on the form, or in a panel, group box or card): | Field | Meaning | | --- | --- | | **Inside** | Only inside a container: the container's name (click it to select the container) and a button to **move out** of it. | | **Align** | Six buttons that put the control against the left edge, the center, the right edge, the top, the middle or the bottom of its container (or the form). | | **Position** | **X** and **Y**, relative to the form or the container. | | **Size** | **W** and **H** in pixels. | | **L · T**, **R · B** | Shown in Position and Size instead of X and W (or Y and H) when the control stretches on that axis: its distances to the sides of the container. Changing one keeps the opposite side where it is. | | **Rotation** | Degrees clockwise, around the pivot (the same value as the style's `rotate`). | | **Anchor** | The anchor preset, drawn as a box in a frame, and its name (`Top · Left`). Click it to open the 4×4 grid of presets (see below). | **More options** at the end shows the fields you need less often: | Field | Meaning | | --- | --- | | **Min size**, **Max size** | Limits for the width and height when the control stretches (`0`: no limit), so a stretched panel stops at a readable width. | | **Pivot** | The point the control turns and scales around, and that stays put when a size limit stops it from stretching: `0 0` is the top-left corner, `0.5 0.5` (the default) the center, `1 1` the bottom-right corner. | (Image: The Position section with the anchor presets open) For a child of an **auto layout** (a flex row or column, or a grid) the section is different, because its container places it: **Width** and **Height** each say **Fixed** (keep this size) or **Fill** (take the room the container leaves), **Grow** shares the free space between children that fill, and in a grid **Column span** says how many cells it takes. These children have no X and Y: drag them to reorder them. See [Layouts and containers](https://albakore.github.io/pyzed/editor/layouts/). ##### Auto layout A flex row, column or grid shows an **Auto layout** section, as in Figma: | Field | Meaning | | --- | --- | | **Direction** | Side by side (row) or one under the other (column). A grid shows **Columns** instead. | | **Align** | A 3×3 box: click where the children sit inside the container (left, center or right; top, middle or bottom). | | **Gap** | Space between the children, or **Space between**, **Space around** or **Space evenly** to spread them over the free space. A grid has a gap and a row height. | | **Padding** | Space between the container's border and its children: horizontal and vertical, or one per side with the **square** button. | | **Fill across** | Children stretch to the container's height (in a row) or width (in a column). | Select some controls and press Shift+A (**Add auto layout**) to wrap them in a row or a column, guessed from how they are placed. Ctrl+G groups them in an invisible panel without moving them, and Ctrl+Shift+G takes the children out of a group or layout and removes it, keeping their place and size. The presets are the anchors of Unity, Unreal and Godot. The columns say how the control follows the width (**Left**, **Center**, **Right** or **Stretch**) and the rows how it follows the height (**Top**, **Middle**, **Bottom** or **Stretch**); the last cell, **Stretch · Stretch**, fills the container. Picking a preset only changes how the control follows a resize; turn on **Also move it there** to also put it against the edges it is anchored to (centered, or filling the container on stretched axes). Under the hood a preset is a set of edges, as in WinForms: | Anchors | When the window grows | | --- | --- | | Left + Top (default) | The control stays where it is. | | Left + Right | It stretches horizontally. | | Right only | It moves with the right edge. | | Neither left nor right | It stays centered horizontally. | | Top + Bottom, Bottom only, neither | The same, vertically. | ##### Behavior | Field | Meaning | | --- | --- | | **Visible** | Hidden controls are not shown when the form runs (`form.status.show()` brings them back). | | **Enabled** | Disabled controls are drawn grayed and ignore the user. | | **Locked** | Design-time only: the control cannot be moved or resized on the canvas. | #### Several controls With two or more controls selected the panel shows **Align** (left edges, centers, right edges, top edges, middles, bottom edges) and **Spacing and size** (distribute horizontally or vertically, same width, same height). They work as the [toolbar buttons](https://albakore.github.io/pyzed/editor/toolbox-and-canvas/#align-distribute-and-size). #### The form Click an empty spot of the canvas, or the first row of the outline, to edit the form itself. | Section | Field | Meaning | | --- | --- | --- | | **Window** | **Name** | The form's name: the file name and the prefix of the form's handlers. | | | **Title** | The window title. | | | **Width**, **Height** | The window's size (also the grip under the canvas). | | | **Resizable** | Whether the user can resize the running window. | | **Look** | **Theme** | Any registered theme: One Dark, One Light, Ayu, Gruvbox... or your own. | | | **Accent** | Seven preset accent colors and **×** for the theme's own. | | | **Custom accent** | Any `#rrggbb` accent. | | | **Background** | A color for the window background (`theme default` when empty). | | | **Corner radius** | The radius of every control (0 to 24). | | | **Font size** | The base text size of the window. | | **Style** | | The window's **global style**: font, text color, weight, size and line height for every text that does not set its own; background, border color, radius and padding of the window. | | **Designer** | **Grid size** | The grid step of this form. | The form's look is independent of the editor's own theme: a form can be light while you design in a dark editor. ### Layouts and containers > Panels, group boxes and cards, flex rows and columns, grids, spacers and anchors: arrange controls of Python desktop forms in Pyzed Design. Source: https://albakore.github.io/pyzed/editor/layouts/ Controls can live directly on the form, at fixed positions, or inside **containers** that place them for you. The same layout code runs in the editor and in the running app, so what you see while designing is what you get. (Image: A sidebar form: a flex column with its children) #### The three kinds of container | Kind | Components | How children are placed | | --- | --- | --- | | **Free** | Panel, Group box, Card | Where you drop them, in coordinates relative to the container. Their anchors follow the container's edges. | | **Flex** | Flex row, Flex column | One after another, like CSS flexbox. | | **Grid** | Grid | In cells, left to right and top to bottom. | | **Dock** | Dock | Dock panels only, on their side under a row of tabs (see [Dock](#dock)). | ```mermaid flowchart TB F[Form] --> P["Panel (free)"] F --> R["Flex row"] F --> G["Grid"] P --> A["button at x=16, y=8"] R --> B1["button · grow 1"] R --> S["spacer"] R --> B2["button"] G --> L1["label"] & I1["text box · span 2"] ``` #### Putting controls in a container - **Drop** a control on a container (it lights up with an accent frame while you drag): from the toolbox, from the canvas or from the outline. - **Draw** a new control inside it: pick a component and click or drag inside the container. - **Add inside…**: the **+** on the container's outline row, or its context menu, opens the Add control list. - **Group**: select some controls and press Ctrl+G to put them in an invisible panel that wraps them exactly, without moving them. Shift+A (**Add auto layout**) wraps them in a flex row or column instead, guessed from how they are placed. **Edit → Group in a row / column / grid** (palette) and the context menu offer every kind, including **Group in a panel**. - **Ungroup**: Ctrl+Shift+G on a container takes its children out and removes it. They keep the place and size they had on the canvas, even the ones a flex row stretched. - **Take out**: drag the control onto the bare form, use **Move out of *container*** in its context menu, or the arrow next to **Inside** in its Position section. Moving or resizing a container moves its children with it, and copy, cut, paste, duplicate and delete take them along. A container cannot be dropped inside itself or its own children. #### Flex row and flex column | Property | Meaning | | --- | --- | | **Direction** | `row` (left to right) or `column` (top to bottom). A flex row and a flex column are the same container with a different default. | | **Gap** | Space between children, in pixels. | | **Padding** | Space between the container's edge and its children: one number for every side, two (vertical and horizontal) or four (top, right, bottom, left). | | **Justify** | Where children go along the main axis when there is room left: `start`, `center`, `end`, `space-between`, `space-around`, `space-evenly`. | | **Align** | Across the main axis: `stretch` (fill), `start`, `center`, `end`. | | **Background** | A color behind the children. | The **Auto layout** section of the Properties panel edits all of these the way Figma does: a direction, a 3×3 **Align** box (click where the children sit), the gap (or a way to spread them), the padding and **Fill across**. See [Properties](https://albakore.github.io/pyzed/editor/properties/#auto-layout). Each child says in its Position section whether its **Width** and **Height** are **Fixed** (its own size) or **Fill** (the room the container leaves); a child that fills across overrides the container's **Align** for itself (`align_self` in the `.pzform`). Each child also gets a **Grow** field: the share of the free space it takes (0 keeps its own size). Two children with grow 1 split the free space in halves; 2 and 1 in thirds. When the children do not fit, they all shrink in proportion to their size, like CSS `flex-shrink`. A **Spacer** is an empty control with grow 1: put one between two buttons and the second one is pushed to the end of the row. #### Grid | Property | Meaning | | --- | --- | | **Columns** | Number of columns of equal width. | | **Gap** | Space between cells, both ways. | | **Padding** | Space inside the container's edge. | | **Row height** | Height of every row; **0** shares the container's height between the rows. | | **Align** | How a child sits in its cell: `stretch` fills it, otherwise `start`, `center` or `end` at its own size. | | **Background** | A color behind the cells. | Each child gets a **Column span**: how many columns it takes (1 to 24). A child that does not fit in the rest of a row starts a new one. #### Dock A **Dock** (Layout category) is the layout of Zed and of Pyzed Design itself: a center area with panels on its left, right, top and bottom. A new Dock comes with three **Dock panels**: Explorer on the left, Terminal at the bottom and Editor in the center. Drop controls on a panel as on any free container (anchors work inside it). | Where | Property | Meaning | | --- | --- | --- | | Dock panel | **Title**, **Icon** | What its tab shows. | | Dock panel | **Side** | `left`, `right`, `top`, `bottom` or `center` (no tab, between the others). | | Dock | **Left width**, **Right width**, **Top height**, **Bottom height** | The size of each side, tabs included. | | Dock | **Saved layout** | Where the user left the panels (filled in at run time; save it with a Settings component). | Panels on the same side share its place as tabs; the top-most one (last in the outline order) is the tab shown, so use **Bring to front** or the outline to edit the others. When the form runs, the tabs switch panels, dragging a tab onto another side moves it there, right-clicking a tab offers the same moves, and dragging a side's inner edge resizes it. Each change raises the dock's `layout` event with the new layout. ```python def dock1_layout(form, event): """Runs when a panel of dock1 is moved, resized or switched.""" form.status.text = f"Left side: {event.value['sizes'].get('left', 220):.0f} px" ``` #### Order of children In a flex or grid container, order matters, position does not: children have no X and Y. To reorder them, drag a child within the container on the canvas (it drops at the place under the pointer) or drag its row in the outline, which lists them in layout order. #### Anchors: responsive free layouts On the form and in free containers, **anchors** decide what happens when the running window (or the container) is resized, like in WinForms. Anchored to left and right, a control stretches; anchored only to the right, it follows that edge; anchored to neither, it stays centered. The same applies vertically with top and bottom. Pick them in the **Position** section of [Properties](https://albakore.github.io/pyzed/editor/properties/#position) from a grid of 16 presets, as in Unity, Unreal or Godot, and give a stretched control a **minimum** and **maximum size** so it stops growing at a readable width (its **pivot** says which side it keeps then). The running form applies all of them; in code without the designer, [`pz.anchor_layout`](https://albakore.github.io/pyzed/guides/layout/#anchored-layouts) does the same. Anchors work while you design too: resizing the form (or a component) with its corner grip or its **Width** and **Height**, or resizing a free container, moves and stretches what is inside exactly as the running window would, size limits and pivot included. Turn it off with the anchor button of the toolbar (**Resize contents with their anchors**) to change the size without touching the controls. A common recipe for a resizable window: 1. A **flex column** anchored on all four sides fills the window. 2. Rows inside it with **grow** on the part that should take the extra space (a text area, a list). 3. A **flex row** at the bottom with a **spacer** and the buttons, so they stay on the right. :::tip Templates in **New form** use these recipes: the *dialog* puts its buttons in a row anchored left, right and bottom; the *sidebar* uses a column anchored left, top and bottom next to another anchored on all sides; the *settings* template is a two-column grid. Start from one and look at its properties. ::: ### Styles and themes > Style controls in Pyzed Design: the Style section, a color picker with gradients and masks, the form's global style and built-in themes. Source: https://albakore.github.io/pyzed/editor/styles-and-themes/ A form's look comes from three layers, from broad to specific: ```mermaid flowchart LR T["Theme
(One Dark, Gruvbox...)"] --> L["Form look
accent · background · radius · font size"] L --> G["Global style
the form's Style section"] G --> C["Control style
each control's Style section"] ``` Each layer only overrides what it sets; empty fields fall through to the layer above. #### Themes Pick the form's theme in **Properties → Look → Theme** (select an empty spot of the canvas first). Every registered theme is listed: the built-in One, Ayu and Gruvbox families and any theme your project registers. Next to it: - **Accent**: seven preset colors, or **×** for the theme's own accent. **Custom accent** takes any `#rrggbb`. - **Background**: a window background color (empty keeps the theme's). - **Corner radius**: the radius every control uses (0 for square corners). - **Font size**: the base size of every text. (Image: A form with the Gruvbox Light theme and a custom accent) The form's theme is independent from the **editor's** theme (Settings → Appearance → Theme) and from the **code editor's** theme (Editor settings → Theme): you can design a light form in a dark editor. #### The Style section Every control has a **Style** section in Properties, like CSS for that one control (see [Styles](https://albakore.github.io/pyzed/theming/styles/)): | Field | Takes | | --- | --- | | **Background** | A color, a linear gradient or a masked fill: click the swatch or the summary to open the picker; **×** removes it. | | **Text color** | A theme token (`accent`, `text_muted`, `success`...) or `#rrggbb` / `#rrggbbaa`. | | **Font** | Default, sans, serif or mono. | | **Weight** | Default, thin, light, normal, medium, semibold, bold, black. | | **Italic** | Default, on or off. | | **Font size** | Pixels; 0 keeps the component's own size. | | **Line height** | A multiple of the font size; 0 keeps it. | | **Border color**, **Border width** | The control's border. | | **Corner radius** | One number, or four: top-left, top-right, bottom-right, bottom-left (two numbers repeat). | | **Shadow** | A color, then **Offset** (x and y, −64 to 64) and **Blur** (0 to 96) appear. | | **Padding** | Space inside: one number, two (vertical horizontal) or four (top right bottom left). | | **Margin** | Space outside, same format. | | **Max width**, **Max height** | 0 means no limit. | | **Align content**, **Align vertically** | Where the content sits in the control's box: start, center or end. | | **Clip** | Cut what overflows the box. | Empty fields keep the component's own look. **Reset style** (at the end of the section, once something is set) clears every override of that control. Half-typed values (`#3b`) are skipped while drawing instead of failing, so you can type freely. (Image: A button with a masked gradient, a serif global font and the color picker open) #### The color picker Click a color's swatch to open the picker, floating next to the panel; click outside it to close it. - **Saturation/value square**, **hue bar** and **alpha bar**, the current color with a hex field (type `#rrggbb` or `#rrggbbaa`) and its opacity in percent. - **Swatches** of the form's theme: accent, success, warning, error, info, text, muted text and background. - For a **background**, **Solid / Gradient** switches to a linear gradient: click the stops bar to add a stop with the color the gradient has there, drag a stop to move it, select it to recolor it, set the **Angle** (in 15° steps with the buttons) and remove the selected stop (a gradient keeps at least two). - For a background, **Mask** fades the fill from opaque to transparent: its own stops bar with an **Opacity** per stop (up to 8 stops) and an angle. :::tip The picker works in concrete colors: picking turns a theme token into its hex value. To keep a color that follows the theme (so the form looks right in a light and a dark theme), type the token (`accent`, `text_muted`, `surface`...) in the field next to the swatch instead. ::: #### The form's global style With nothing selected, the form's **Style** section is the window's global style: - **Text color, font, weight, italic, font size, line height** apply to every text that does not set its own. - **Background, border color, corner radius and padding** become the window's. It runs the same way (`Window.style`), and so do components made in the editor. A serif global font, for example, changes every label, button and input of the form at once. #### Colors as properties Some components also have color properties of their own (a label's **Color**, a panel's **Background**, an icon's **Color**). They take the same values: a theme token or a hex color, with a swatch that previews it in the form's theme. ### Animations and effects > Give controls shadow, blur, glow, bloom, glass and opacity in Pyzed Design, and animate them when they appear, on hover, click, a new value, a failed rule or another control's event. Source: https://albakore.github.io/pyzed/editor/animations-and-effects/ Every visual control has two sections at the end of the **Properties** panel, after its style: **Effects** and **Animation**. What you set there is saved in the `.pzform` and plays when the form runs. It is the same system as [`widget.animate()` and the style effects](https://albakore.github.io/pyzed/animation/) of the library. (Image: The Effects and Animation sections of a button) #### Effects | Field | What it does | | --- | --- | | **Opacity** | How visible the control and everything inside it is (1 is opaque). | | **Shadow** | A drop shadow: pick a color, then **Offset** (X and Y) and **Blur**. | | **Blur** | Blurs the control and everything inside it, in pixels. | | **Backdrop blur** | Blurs what is behind the box, in pixels. | | **Glow** | A soft halo around the box. Pick a color (or a theme token such as `accent`); **Glow size** sets how far it spreads. | | **Bloom** | Light that spills from the bright parts of the control. **Intensity** goes from 0 to 1. | | **Glass** | A translucent panel that blurs what is behind it, with a light edge. **Tint** colors it and **Strength** sets how much of the tint shows. | Rotation is in the **Position** section. Sub-fields only show while the effect is on. Clear the color (or turn **Glass** off) to remove it. Effects are style properties (`opacity`, `shadow`, `blur`, `backdrop_blur`, `glow`, `bloom`, `glass`), so they also show in code as `control.style` and work with **Reset style**. Glass looks best over a colorful background, such as a panel with a gradient. #### Animation An animation is two choices: **when** it plays and **what** plays (a preset such as *Fade in up*, *Pulse* or *Shake*). A control without animations shows a few ready-made ones to add with one click (**Fade in when it appears**, **Lift on hover**, **Pulse on click**, **Shake when the value is invalid**...), only those that suit the control, and **Add animation** for any other. Each row is one animation: an icon for what plays it, a summary (**Fade in up · 280 ms**, with **+delay** and **×repeat** when set) and the trigger under it. The **▶** button previews it and the trash can removes it. Click a row to open its editor: | Field | Meaning | | --- | --- | | **When** | What plays it (see the table below). Under the field, a line says how the chosen one starts. | | **Control**, **Event** | Only for **On another control's event**: which control (or the form) and which of its events. | | **Animation** | The preset. The ones that suit the trigger come first (entrances for *appears*, interactions for hover and press, attention seekers for clicks and events); the tooltip describes the selected one. | | **Duration**, **Delay** | Milliseconds. Empty keeps the preset's own timing. | | **Easing** | How the speed changes along the way (ease, linear, ease out cubic, spring...). | | **Repeat** | 1, 2, 3, 5 or infinite. | | When | Plays | | --- | --- | | **When it appears** | Once, when the form opens or the control is shown. | | **While hovered**, **While pressed**, **While focused** | Forward while the condition holds, and back when it ends. | | **On click**, **On double click**, **On right click** | Once on each click. | | **When the pointer enters**, **When the pointer leaves** | Once each time the mouse comes in or goes out. | | **When its value changes** | Once on each new value (typing, a new choice, a toggled switch...). | | **When Enter is pressed** | Once when the user submits a text box. | | **When its value is invalid** | Once each time a [validation rule](https://albakore.github.io/pyzed/editor/data-binding/) fails: a classic *shake* on a wrong password. | | **On another control's event** | Once on each event of another control or the form: a label that pulses when **Save** is clicked, a card that fades in when the form loads. | | **When played from code** | Only when a handler calls `play_animation()` (see below). | The triggers that play on an event work even if the control has no handler for it, and they play before the handler runs. **Add animation** adds a row (a *Fade in up* when it appears) and opens it. Picking another preset resets the row's timing to the new preset's. Every change can be undone. ##### Playing it from your code Choose **When played from code** and the animation waits for your handlers: it doesn't play when the form opens. The open row shows the line that plays it, with a copy button. Paste it in any handler (a button's click, a timer's tick, a failed validation): ```python def sign_in_click(form, event): if not form.password.validate(): form.password.play_animation() # plays the password box's "When played from code" animations ``` `play_animation()` also replays the control's **When it appears** animations. Give it a preset name, a `pz.Animation` or property values to play something the control has no row for, and use `stop_animation()` to stop what it started (a loop, for example): | Call | What plays | | --- | --- | | `form.title.play_animation()` | Its own rows set to **When played from code** (and replays **When it appears**). | | `form.title.play_animation("tada")` | That preset, once. | | `form.badge.play_animation("pulse", repeat=3, duration=400)` | A preset with other timing (any [`animate()` option](https://albakore.github.io/pyzed/animation/animate/#timing-options)). | | `form.card.play_animation(scale=1.05, duration=200)` | To these values and back. | | `form.spinner.stop_animation()` | Stops what `play_animation` started. | [Start here: apply and play](https://albakore.github.io/pyzed/animation/start/) compares this with `pz.Trigger`, the way a code-only app does the same. ##### Previewing The canvas stays still while you edit, so controls don't move under the mouse. **Preview** (at the bottom of the section, or **▶** on a row) plays the control's animations once on the canvas, as if it had just appeared; **Stop** ends the preview. Run the form (**F5**) to see every animation with its real trigger. #### In code Animations are saved as plain data on the control: ```json "animations": [ {"preset": "fade_in_up"}, {"preset": "lift", "on": "hover", "duration": 180} ] ``` The `"on"` values are `mount` (the default), `hover`, `press`, `focus`, `click`, `double_click`, `right_click`, `enter`, `leave`, `change`, `submit`, `invalid`, `event` and `manual` (**When played from code**). An `event` entry also names its source and event: `{"preset": "tada", "on": "event", "source": "save", "event": "click"}` (an empty `source` is the form). `Document.set_animations(name, animations)` changes them from a script and `Control.animations` reads them. To play one from a handler, call `form..play_animation(...)` (see above); to make your own presets, use the library's `pz.Animation` and `define_animation` (see the [preset list](https://albakore.github.io/pyzed/animation/presets/)). #### Timeline animations For animations that move several controls together (an intro, a transition between states), open the **Animation** panel from **View → Animation** (`Ctrl+Shift+M`). It edits **clips**: timelines saved in the `.pzform` that play the same way as [`pz.AnimationPlayer`](https://albakore.github.io/pyzed/animation/timeline/) does in code. (Image: The Animation panel: dragging the playhead through an intro clip makes the title drop in, the subtitle fade in and the Sign in button grow on the form above) An empty panel lists the three steps (add a track, add keys, play it) and has a **New animation** button. | Action | Result | | --- | --- | | **Add track for …** | Adds a track for a property of the selected control (opacity, position, scale, colors, size…) or any of its properties, such as a label's text. | | **Click** anywhere on the ruler or the lanes, or **drag** | Moves the playhead; the canvas shows the form at that time. It snaps to keys nearby. | | **Double-click** a lane | Adds a key at that time with the value the track has there. | | **Drag** a key | Moves it in time. You can grab it from a few pixels away (the pointer becomes a hand) and it snaps to the keys of the other tracks, the start and the end. A label shows its time and value while you drag or hover it. | | **Click** a key | Selects it: edit its time, value and easing below the tracks. | | **Right-click** a key | Deletes it. | | **Calls** lane | Type a handler name and press Enter (or double-click the lane) to call that method of the form's code file at the playhead. Drag a call to move it; right-click it to remove it. | | **Length**, **loop**, **autoplay** | Set how long the clip lasts, whether it repeats, and whether it plays when the form opens. | With the pointer over the panel, the keyboard edits the timeline: | Key | Does | | --- | --- | | `Delete` / `Backspace` | Deletes the selected key. | | `Space` | Plays or pauses the preview. | | `←` / `→` | Moves the selected key (or the playhead) by 0.01 s; with `Shift`, by 0.1 s. | | `Home` / `End` | Jumps to the start or the end. | (Image: Adding a scale track for the title from the panel, double-clicking its lane to add a key, dragging the key earlier and choosing Ease out back for it) Controls inside an animated container follow its opacity, position and blur. Every change can be undone. ##### How a clip starts A clip plays in one of two ways, and the bar under the tracks says which one applies to the clip you are editing: - **⚡ Play when the form opens** (in the toolbar) makes it the form's autoplay clip: "Plays when the form opens." - Otherwise it waits for a handler. The bar shows the line to call, with a copy button: ```python def replay_click(form, event): form.animations.play("intro", restart=True) # from the start, even if it is playing # also: form.animations.pause(), .resume(), .seek(0.5), .play_backwards("intro"), .queue("outro") ``` `form.animations` is the form's [`pz.AnimationPlayer`](https://albakore.github.io/pyzed/animation/player/): it can also pause, seek, queue clips and say when one ends. The [Animation section](https://albakore.github.io/pyzed/animation/) explains clips, tracks, keys, easing and the player in depth. ### Events and handlers > The Events panel of Pyzed Design: every event of every component and form, and how to write Python event handlers with a double click. Source: https://albakore.github.io/pyzed/editor/events/ Controls raise **events** (a click, a change of text, a timer tick) and each event can be **wired** to a Python function, its **handler**, in the form's code file (`login.py` next to `login.pzform`). #### The Events panel The **Events** panel (right dock, next to Properties) lists the events of the selected control, or of the form when nothing is selected, with the handler wired to each one (`—` when there is none). (Image: The Events panel of a text box: submit is wired to password_submit) | Action | Result | | --- | --- | | **Double-click** an event, or press its **`<>`** button | Wires it if needed (creating the handler stub) and opens the handler in the code editor. | | **Unlink** button (wired events only) | Disconnects the event **and deletes its handler's code**. | | Hover an event | A tooltip says when it runs and what `event.value` holds. | | The **book** button in the header | Opens the control's page in the [reference](https://albakore.github.io/pyzed/editor/reference/), with every event and its handler name (Shift+F1). | The event being edited is highlighted. With several controls selected, the panel asks you to pick one: events belong to a single control. ##### Faster ways in - **Double-click a control** on the canvas: opens the handler of its **main event** (the first one: `click` for a button, `change` for a text box, `tick` for a timer). - Enter with a control selected does the same. - **View code** in the control's context menu. - F7 (**Edit code file**) opens the whole file. Handlers are named `_` (`sign_in_click`), or `form_` for the form, and the stub carries a docstring saying when it runs. Renaming a control renames the handlers that follow this convention. #### Every event Every visible control has three common events on top of its own: | Event | Runs when | `event.value` | | --- | --- | --- | | `mouse_enter` | The pointer enters the control. | `None` | | `mouse_leave` | The pointer leaves the control. | `None` | | `right_click` | The control is right-clicked. | `None` | | Component | Own events | | --- | --- | | Label, Image, Icon, Avatar, Counter badge, Status dot, Facepile, QR code, SVG image | `click`, `double_click` | | Button, Link | `click` | | Chip | `click`, `remove` (its x, when **Removable** is on) | | Split button | `click` (the main part), `choose` (an item of its menu: its text), `menu` (the menu opens) | | Text box, Search box | `change` (the new text), `submit` (Enter pressed) | | Text area | `change` (the new text) | | Check box, Switch | `change` (`True` or `False`) | | Radio group, Drop-down, Segmented | `change` (the selected option) | | Tab bar | `change` (the selected tab), `close` (a tab closed with its x when **Closable** is on: its name) | | Toggle group | `change` (the pressed value, or the list of pressed values when **Multiple** is on) | | Slider | `change` (the new number while dragging), `release` (the drag ended: save or apply here) | | Number box | `change` (the new number) | | List box | `change` (the selected item), `open` (an item double-clicked) | | Table | `change` (the selected row's index), `open` (a row double-clicked: its index), `sort` (a header clicked when **Sortable** is on: its title) | | Tree view | `change` (the selected item's path, such as `"Docs/README.md"`), `toggle` (an item folded or unfolded), `open` (an item without children double-clicked) | | Breadcrumbs | `click` (the index of the part, 0 for the first) | | Markdown | `link` (the address of the clicked link; without a handler the link opens in the browser) | | Color picker, Icon picker | `change` (the new color or icon name) | | Dock | `layout` (a panel was moved, resized or switched: the layout dict, also kept in **Saved layout**) | | Timer, Shortcut, File watcher, Process, Background worker, Web request, Clipboard, Settings, File dialog, Notification | Their own (`tick`, `activate`, `change`, `output`, `done`, `response`...): see [Non-visual components](https://albakore.github.io/pyzed/editor/non-visual-components/). They have no mouse events. | | Heading, Progress bar, Badge, Spinner, Callout, Key binding, Stat, Chart, Empty state, Diff stat, Image viewer, Terminal output, containers, Spacer, Divider | Only the common ones | The form itself: | Event | Runs when | `event.value` | | --- | --- | --- | | `load` | Once, when the window opens. | `None` | | `close` | The window is closing. Return `False` to keep it open. | `None` | | `key` | On every key press. | The combo, such as `"ctrl-s"` | | `resize` | The window is resized. | `(width, height)` | [Designed components](https://albakore.github.io/pyzed/editor/component-designer/) and `pz.Component` classes add their own **custom events** when you place them on a form. The Events tab lists them in a **Custom events** section, apart from the **Built-in events** of the control. #### Writing handlers A handler receives the running `form` and an `event`. Controls are attributes of the form and their properties are attributes of the control; changing them updates the window. ```python def sign_in_click(form, event): if "@" not in form.email.text: form.status.text = "Enter a valid email address." return form.email.focus() # handlers may return commands form.sign_in.enabled = False form.progress.show() form.ticker.running = True # a Timer control starts ticking ``` | Name | What it is | | --- | --- | | `event.sender` | The control (or form) that raised the event. | | `event.name` | The event (`"click"`, `"change"`...). | | `event.value` | Its value: new text, checked state, key combo... | | `form.` | A control; read and write its properties (`form.email.text`). | | `form..visible`, `.enabled`, `.x`, `.y`, `.width`, `.height`, `.anchor` | Its layout and behavior fields. | | `form..show()`, `.hide()` | Shows or hides it. | | `form..focus()` | A command that focuses it (text boxes and text areas). | | `form.title`, `form.name`, `form.size` | The window's title (assigning it renames the window), the form's name and the current size. | | `form.close()` | A command that closes the window. | | `form.open("details", data=...)` | A command that opens another form in a new window; it gets `data` as `form.data`. | | `form.window` | The Pyzed window running this form. | Handlers can return anything a Pyzed callback can: a `Command`, a list of commands, a coroutine (for async work) or a generator. Helpers at the top of the file, imports and other modules of the project all work as usual. ```mermaid sequenceDiagram actor User participant Iced as Iced (Rust) participant RT as FormRuntime participant H as login.py User->>Iced: clicks "Sign in" Iced->>RT: callback for sign_in.click RT->>H: sign_in_click(form, event) H->>RT: form.status.text = "..." RT->>Iced: view() with the new properties Iced->>User: redraws the window ``` ##### When a handler fails If a handler raises, the running window does not close: an **Event handler error** callout appears at the bottom of it with the last line of the error (dismiss it with its ×), and the full traceback goes to the [console](https://albakore.github.io/pyzed/editor/console-and-running/). If an event is wired to a function that is not in the code file, the callout says so. :::tip Type `form.` in a handler and the completion list offers the controls of the form; `form.email.` lists that text box's properties, and `event.` the event's fields. See [Code editor](https://albakore.github.io/pyzed/editor/code-editor/#completion). ::: #### Opening other windows ```python def details_button_click(form, event): return form.open("details", data=form.order_id.text) # details.py def form_load(form, event): form.title = f"Order {form.data}" ``` The name is looked up next to the current form, then in the project's `ui/` folder. Create the second form with **New form** (Ctrl+N); the *dialog* template comes with **OK** and **Cancel** already wired to `form.close()`. ### Non-visual components > Timers, keyboard shortcuts, file watchers, processes, background workers, web requests, the clipboard, settings, file dialogs, notifications, tray icons, system information, the launcher and network and battery monitors in Pyzed Design: components with properties and events that draw nothing. Source: https://albakore.github.io/pyzed/editor/non-visual-components/ Some parts of an app have **properties and events but nothing to show**: a timer that ticks, a folder you watch, a command running in the background. Visual Basic and Qt Designer kept them in a **tray under the form**, and Pyzed Design does the same. They are not drawn on the form, neither while you design nor when it runs: they only appear in the tray and in the **Outline**, where they belong to the form. (Image: Adding a Timer, a File watcher and a Web request from the toolbox: each one goes to the tray under the form, and a double click opens its tick handler) #### Adding one 1. Find it in the toolbox, under **Components** or **System** (or type its name in **Search components…**). 2. **Click it**: it goes straight to the tray, selected, with its properties on the right. Dragging it onto the form does the same; it never lands inside a container. 3. Set its properties in the **Properties** panel, and **double-click** its chip to write the handler of its main event (`tick` for a timer, `output` for a process...). The **Events** panel lists all of them. | In the tray | Result | | --- | --- | | Click a chip | Selects the component. | | Double-click a chip | Opens the handler of its main event. | | Right-click a chip | View code, Help, Cut, Copy, Duplicate, Enable or Disable, Delete. | In the **Outline**, non-visual components are listed after the controls, under **Non-visual**. They have no position, size, style or animations: only their own properties and **Enabled** (a disabled component raises no events). Copy, paste, undo and rename work as with any control, and renaming one renames its handlers. #### The components | Component | What it does | Events | | --- | --- | --- | | **Timer** | Runs `tick` every `interval` milliseconds while `running`. Turn off `repeat` to tick once (a delay). | `tick` (the count) | | **Shortcut** | A keyboard shortcut for the whole window: `keys` such as `ctrl-s`, several separated by commas. `mod-s` is Cmd+S on macOS and Ctrl+S elsewhere. | `activate` (the combo) | | **File watcher** | Watches a folder (`path`), the files that match `pattern` (`*.csv`, several separated by `;`), with `recursive` for subfolders. | `change` (the changed paths), `created`, `modified`, `deleted` (one path) | | **Process** | Runs `command` in `directory` and streams what it prints, line by line. `autostart` starts it when the form opens. | `output`, `error_output` (a line), `finished` (exit code), `started` (process id), `failed` | | **Background worker** | Runs slow work in a thread so the window stays responsive. | `work` (in the thread), `progress`, `done` (the result), `failed` (the exception) | | **Web request** | Calls `url` with `method`, `headers` (one `Name: value` per line) and `body`, in the background. | `response` (the answer), `failed` (no answer) | | **Clipboard** | Copies and pastes text; with `watch` it notices what is copied, in this app or another one. | `change`, `paste` (the text) | | **Settings** | Values kept between runs (a JSON file per user), and the value of the controls listed in `remember`. | `change` (`(key, value)`) | | **File dialog** | The system's dialog: `mode` is `open`, `open_many`, `save` or `folder`; `filters` has one `Images: png, jpg` per line. | `chosen` (the path, or a list), `cancelled` | | **Notification** | A desktop notification with `title` and `text`. | `shown`, `failed` | | **Tray icon** | An icon in the system tray with an `icon`, a `tooltip` and a `menu`: one item per line, `-` for a separator, `[x]` or `[ ]` for a check item and indented lines for a submenu. It shows when the form opens unless **Show when the form opens** is off. On GNOME it needs the AppIndicator extension. | `click`, `double_click` (Windows), `menu` (the item's text), `failed` | | **System info** | The computer: `os`, `os_version`, `machine`, `hostname`, `user`, `home`, `cpu_count`, `language`, memory and free disk (of `disk_path`). With `interval` it checks again periodically. | `update` (a dict with all of it) | | **Launcher** | Opens `target` with the program the system uses for it: a file, a folder, a web page or a `mailto:` link. | `opened`, `failed` (the target) | | **Network monitor** | Whether the internet can be reached, checking `host`:`port` every `interval` ms while `running`. | `change` (`True`/`False`), `online`, `offline` | | **Battery monitor** | The battery's charge and whether it is plugged in, every `interval` ms. | `change` (`(percent, plugged)`), `low` (under `low_level`), `plugged`, `unplugged` | Each component's page in the built-in reference (Shift+F1 with it selected) lists every property and event with its handler name. #### Using them from the handlers They are attributes of the form like any control. Their properties read and write as attributes, and they have **methods**: ```python def run_click(form, event): if form.proc.running: form.proc.kill() else: form.proc.start(form.command.text) # or the command set in the designer def proc_output(form, event): form.output.text += event.value + "\n" # one line of what the program printed def proc_finished(form, event): form.status.text = "Done." if event.value == 0 else f"It ended with code {event.value}." ``` | Component | Methods and state | | --- | --- | | Timer | `start()`, `stop()`, `running`, `count` | | Shortcut | `matches(combo)` | | File watcher | `start()`, `stop()`, `running`, `check()` | | Process | `start(command=None)`, `write(text)`, `kill()`, `running`, `exit_code`, `pid` | | Background worker | `run(value)`, `cancel()`, `report(progress)`, `busy`, `cancelled`, `result` | | Web request | `send(url=None, json=None, body=None)`, `busy`, `response` | | Clipboard | `copy(text)` and `paste()` (commands: return them), `text` | | Settings | `settings1["key"]`, `settings1["key"] = value`, `get(key, default)`, `set()`, `delete()`, `keys()` | | File dialog | `show()` (a command: return it), `path`, `paths` | | Notification | `show(text=None)` | | Tray icon | `show()` (returns whether it could), `hide()`, `visible`, `checked(item)`, `set_checked(item, value)` | | System info | `check()`, `env(name)`, `folder("documents")`, `memory_available`, `disk_free`, `is_windows`, `is_mac`, `is_linux` | | Launcher | `open(target=None)`, `reveal(path)` (shows it in the file manager), `email(to, subject, body)` | | Network monitor | `check()`, `online`, `start()`, `stop()` | | Battery monitor | `check()`, `percent`, `plugged`, `has_battery` | Events that happen in the background (a line of output, a file that changed, a web answer) reach your handlers on the window's thread, so handlers can change controls freely. The one exception is the worker's `work` event: ```python def count_click(form, event): form.primes.run(200_000) # starts work in a thread def primes_work(form, event): # runs in the thread: do not touch controls here found = 0 for n in range(2, event.value): if event.sender.cancelled: # form.primes.cancel() was called return None if all(n % d for d in range(2, int(n ** 0.5) + 1)): found += 1 if n % 2000 == 0: event.sender.report(n * 100 // event.value) return found # arrives in done def primes_progress(form, event): form.progress.value = event.value def primes_done(form, event): form.status.text = f"{event.value:,} primes" ``` Commands are returned, as in every handler: ```python def open_click(form, event): return form.file_dialog1.show() # opens the system's dialog def file_dialog1_chosen(form, event): form.editor.text = Path(event.value).read_text() def copy_click(form, event): return form.clipboard1.copy(form.editor.text) def http1_response(form, event): if event.value.ok: form.stars.text = str(event.value.json()["stargazers_count"]) ``` (Image: The Task runner example running: the process streams its output, the timer moves the clock and the worker fills the progress bar) The **Task runner** example (`examples/designer/tasks.pzform`) uses seven of them together: a Timer for the clock, a Process for the command, a Background worker that counts primes, a File watcher, Settings that remember the command, a Shortcut (Ctrl+R) and a Notification. Open it with `pyzed design examples/designer/tasks.pzform`. #### Remembering what the user left Put a **Settings** component on the form and list control names in **Remember**, one per line: when the form closes their values (the text of a text box, the state of a check box, a slider's value...) are saved, and the next time it opens they are back. The file is `
..json` in Pyzed's per-user settings folder, or the name in **File**. Your own values go in the same file: ```python def form_load(form, event): form.volume.value = form.prefs.get("volume", 50) def volume_change(form, event): form.prefs["volume"] = event.value ``` #### In code, without the designer Each component is a class of [`pyzed.services`](https://albakore.github.io/pyzed/guides/services/) (`pz.Timer`, `pz.Process`, `pz.FileWatcher`...), so the same objects work in a `pz.App` written by hand, or in a script with no window. :::tip A busy **Process** or **File watcher** can flood the console with events: `output`, `error_output`, `progress` and `tick` are never logged, and **Settings → Running** turns event logging off for the rest. ::: ### Data binding and validation > Bind controls to form data in Pyzed Design, validate user input with rules, translate texts with @keys and preview forms in other themes or languages. Source: https://albakore.github.io/pyzed/editor/data-binding/ A form can show and edit **data** without a line of glue code: bind each control to a field of `form.data`, give the input controls **rules**, and call `form.validate()` before you use the data. #### Binding a control Select a control and fill **Bind to** in the **Data** section of [Properties](https://albakore.github.io/pyzed/editor/properties/). The field is a name (`email`) or a path into nested data (`customer.address.city`, `items.0.title`). (Image: The Data section of a text box: bound to email, required, with a pattern and a message) - **Input controls** bind in both directions: they show the field and write back what the user types or picks. Text boxes, text areas and search boxes bind `text`; check boxes and switches `checked`; radio groups, drop-downs, segmented controls, tab bars and list boxes `selected`; sliders and number boxes `value`. - **Display controls** only show it: labels, headings, badges, links, chips, markdown and callouts bind `text`, stats and progress bars `value`, images `source`, charts `values` and tables `data`. - A display control's **Bind to** can also be a **Python expression** over the data's fields, re-evaluated on every render: ```python f"Signing in as {email}" if email else "" f"{len(items)} items, {sum(i.price for i in items):.2f} €" ``` Nested dicts read with dots (`customer.city`); `data` is the whole object and `form` the running form. A path to a field that is still missing leaves the control's designed value; an expression that raises leaves it too and shows the error in the window. #### The form's data `form.data` is whatever the form that opened this one passed, or a dict the form makes itself: ```python def edit_click(form): return form.open("customer", data=selected_customer) # a dict, a dataclass, any object ``` | Opened with | `form.data` is | | --- | --- | | `data=` a dict or an object | That same object: the user's edits change it in place. | | Nothing, with bound controls | A new dict, filled with the controls' designed values (`{"email": "", "remember": False}`). | Assigning `form.data = {...}` replaces it and refreshes every bound control, which is the way to load a record: ```python def form_load(form): form.data = database.load_customer(42) ``` #### Validation rules The **Data** section also holds the rules of a two-way control. The rules depend on the kind of control: | Rule | Controls | Fails when | | --- | --- | --- | | **Required** | Every input control | The value is empty (an unchecked box, for check boxes and switches). | | **Minimum length**, **Maximum length** | Text boxes, text areas, search | The text is shorter or longer. | | **Pattern** | Text boxes, text areas, search | The whole text does not match the regular expression. | | **Minimum**, **Maximum** | Sliders, number boxes | The number is smaller or larger. | | **Message** | Every input control | Replaces the built-in message of any rule (`@key` translates it). | An empty value only fails **Required**, so an optional email field with a pattern accepts a blank. The built-in messages ("This field is required", "At least 8 characters") follow the app's language. The running form checks a control's rules **as the user types**, and shows the message in a bubble under the control, which gets a red border: (Image: The running login form: the email fails its pattern and the empty password is required) #### Validating in code ```python def sign_in_click(form): if not form.validate(): # checks every visible, enabled control and shows the errors return auth.sign_in(form.data["email"], form.data["password"]) ``` | Code | Does | | --- | --- | | `form.validate()` | Checks every bound control's rules (hidden and disabled ones are skipped) and returns `True` when all pass. | | `form.valid` | `True` while no control shows an error. | | `form.errors` | `{"email": "Enter a valid email", ...}`: the controls that show an error. | | `form.email.validate()` | Checks one control; `True` when it passes. | | `form.email.error` | The control's message, or `None`. Assign a text to show your own error (`form.email.error = "Already registered"`), `None` to clear it. | A server-side check therefore looks like any other: ```python async def sign_in_click(form): if form.validate() and not await api.email_free(form.data["email"]): form.email.error = "That email is already registered" ``` #### Translatable texts Any text property that starts with `@` is a **translation key**: a label whose text is `@login.title` shows `pz.t("login.title")` in the app's language. `@@` writes a literal `@`. List items (a drop-down's options, a tab bar's tabs) translate one by one, and so do the form's title and a rule's message. The catalogs live in a `locales/` folder next to the form, or in any folder above it up to the project root (`pyproject.toml` or `.git`), one file per language: ```json title="locales/es.json" {"login": {"title": "Iniciar sesión", "email": "Correo", "sign_in": "Entrar"}} ``` The designer and the running form load them on their own. See [Translations](https://albakore.github.io/pyzed/guides/i18n/) for plurals, fallbacks and switching the language at run time. #### Preview in another theme or language **View → Preview theme…** and **View → Preview language…** pick a theme and a language for the canvas, without touching the form: the form keeps its own theme and the app its language, and the first entry of each picker goes back to them. (Image: The login form previewed in One Light and in Spanish, with the language picker open) While a preview is on, the status bar shows it (click it to pick another), and **Run** (F5) starts the form with the same theme and language, so you can try the whole flow in Spanish or in a light theme. The running form reads them from the `PYZED_PREVIEW_THEME` and `PYZED_PREVIEW_LANGUAGE` environment variables, which also work from a terminal: ```bash PYZED_PREVIEW_LANGUAGE=es PYZED_PREVIEW_THEME="One Light" uv run python main.py ``` #### Copying controls between forms Copy (Ctrl+C) puts the selected controls on the **system clipboard**, with their bindings and rules, so you can paste them into another form, in another tab or another Pyzed Design window. See [Toolbox and canvas](https://albakore.github.io/pyzed/editor/toolbox-and-canvas/#clipboard-and-history). ### Code editor > The Python code editor inside Pyzed Design: completion, hover, go to definition, rename, quick fixes, diagnostics, code lens, sticky scroll, folding, minimap, ty and ruff language servers, keys and settings. Source: https://albakore.github.io/pyzed/editor/code-editor/ The **Code** panel (bottom dock) is a Python editor backed by two language servers from Astral: **ty** (types, completion, references) and **ruff** (linting and formatting). It shows up when you open a handler or a project file and goes away when you close it. (Image: Writing the code of a button's click event) #### What it edits The editor shows either **one handler** or the **whole code file**: | Mode | How to get there | Title | | --- | --- | --- | | One handler | Double-click a control or an event, Enter on a selected control, **View code**, a method in the [Component panel](https://albakore.github.io/pyzed/editor/component-designer/#the-component-panel). | `login.py › sign_in_click()` | | Whole file | F7, **Edit code file** (toolbar, ☰ menu, palette), or **Whole file** in the header. | `login.py` | | A project file | A click on a text file in the [Project tab](https://albakore.github.io/pyzed/editor/projects/#files), or Ctrl+P. | `src/shop/cart.py` | **Whole file** and **This handler** in the header switch between the two. In handler mode you only see the function, but the language servers always see the whole module with that handler swapped in, so the imports and helpers at the top of the file are known to completion and diagnostics. #### The header | Item | Does | | --- | --- | | Title | The code file and, in handler mode, the function. | | **Problems** (`2 · 1`) | Errors and warnings of the code; click to show or hide the problems list. A check mark means there are none. | | **Gear** | Opens the [editor settings](#editor-settings). | | **Whole file / This handler** | Switches the mode. | | **Apply** | Checks the syntax, writes the code to the document and, with *Format with ruff on Apply* on, formats it. | | **×** | Closes the editor (the code is applied first). | Your code is applied to the document on its own whenever you switch handlers, switch mode, change tab, save or run; **Apply** is for checking it (and formatting it) right away. If the code does not parse, a red line under the editor says where (`Line 3: invalid syntax`), and saving, running and switching wait until you fix it. #### Writing code - **Syntax highlighting** for Python, with the theme of the editor (or its own, see settings). - **Indentation**: Tab inserts spaces (the *Tab size*), or a tab with *Indent with tabs*; Enter keeps the indentation of the line (its spaces and tabs) and adds a level after a `:`; Backspace in the indentation removes a whole level (one space with *Backspace removes a whole level* off). See [Tabs or spaces](#tabs-or-spaces). - **Brackets and quotes**: typing `(`, `[`, `{`, `"` or `'` inserts its pair, wraps the selection if there is one, and typing the closing character steps over it instead of doubling it. - **Gutter**: line numbers (they follow wrapped lines) and the current line highlighted. - **Folding**: chevrons in the gutter fold and unfold blocks (functions, classes, `if`s...); Ctrl+Shift+[ folds the innermost block around the cursor and Ctrl+Shift+] unfolds it. - **Minimap**: the code zoomed out on the right; click or drag it to scroll. - **Input methods**: Chinese, Japanese and Korean input methods (IME) work: the text being composed is drawn at the cursor and the committed text goes in at every cursor. - **Whitespace**: optionally draws spaces as `·` and tabs as `→`. - **Zoom**: Ctrl+=, Ctrl+- or Ctrl+wheel change the font size; Ctrl+0 resets it. The size is saved. - **Scrolling**: the mouse wheel scrolls the code (Shift+wheel sideways) and the scrollbars sit right of the minimap. The view also follows the cursor. - **Sticky scroll**: while you scroll, the lines that open the blocks you are in (`class`, `def`, `for`, `if`, `with`...) stay pinned at the top of the editor, the innermost one last, so you always know where you are. Click one to jump to it. Up to five lines are pinned; turn it off with *Sticky scroll* in the settings. - **Long lines** scroll sideways by default; Alt+Z (or *Wrap long lines* in the settings) wraps them at the editor's width instead. (Image: The whole file with two handlers folded and the minimap) ##### Keys in the editor | Keys | Action | | --- | --- | | Arrows, Home, End | Move the cursor. | | Ctrl+← / → | Move by word. | | Ctrl+Home / End | Start or end of the code. | | Page Up / Page Down | One page up or down. | | Shift + any of the above | Extend the selection. | | Ctrl+A | Select everything. | | Ctrl+C / X / V | Copy, cut, paste. | | Tab | Indent (insert spaces). | | Ctrl+Space | Open the completion list. | | F12, Ctrl+click | Go to the definition. | | Alt+← / → | Go back / forward after a jump. | | Shift+F12 | Find every reference. | | F2 | Rename the symbol. | | Ctrl+. | Quick fixes and refactorings. | | Ctrl+Shift+[ / ] | Fold / unfold at the cursor. | | Ctrl+= / - / 0 | Font bigger, smaller, default. | | Ctrl+Z / Ctrl+Shift+Z | Undo, redo (typing is grouped into one step). | | Ctrl+F / H / G | Find, replace, go to line (see below). | | Esc | Back to one cursor, close the completion list, or leave the editor. | On macOS, Cmd works where these say Ctrl. The editor's global commands (F5, Ctrl+S, Ctrl+Shift+P...) work while you type; the canvas shortcuts (Delete, arrows, Ctrl+D) do not, so typing never moves a control. #### Tabs or spaces Python refuses a block that mixes tabs and spaces, so the editor keeps to one of them: - **Indent with tabs** (Editor settings → Editing) makes Tab, Enter and indent (Ctrl+]) write tab characters. Tabs are drawn *Tab size* columns wide, and Enter copies the spaces and tabs of the line above as they are, in either mode. - **Convert indentation to tabs** and **Convert indentation to spaces**, in the command palette (Ctrl+Shift+P), rewrite the indentation of the file the code editor shows. The text inside multi-line strings is left as it is. - **Convert indentation on save** does it every time a Python file (or the form's code) is saved, so pasted code never breaks the file. - The code the designer writes for you (a new handler or method) follows the file: in a file indented with tabs it is written with tabs. Holding Backspace or Delete deletes at the pace set under **Deleting**: the first press always deletes; the next ones wait *Delay before repeating* and then come at most *Deletions per second* times a second. They can be slower than your system's key repeat, not faster. #### Find, replace and multiple cursors (Image: Find (Ctrl+F) with every form.status selected as a cursor) - Ctrl+F opens the find bar with the word under the cursor; matches are highlighted in the code and the bar counts them. Enter or F3 go to the next one, Shift+F3 to the previous one, and Alt+Enter puts a cursor on every match. The buttons match case, whole words or a regular expression. - Ctrl+H also shows the replace field: **Replace** changes the current match, **Replace all** every match in one undo step. With a regular expression, `\1` in the replacement inserts a group. - Ctrl+G goes to a line (`42`) or a line and column (`42:8`). - The editor has multiple cursors and the usual editing keys of modern code editors: Alt+click, Ctrl+Alt+↑/↓, Ctrl+D, Alt+Shift+↑/↓ to duplicate lines on every cursor, Ctrl+Shift+L, Alt+↑/↓ to move lines, Ctrl+/ to comment, Ctrl+Z / Ctrl+Shift+Z and more. See the full list in [the code editor component](https://albakore.github.io/pyzed/components/code/#multiple-cursors-and-zeds-keys) or press F1 in the designer. #### Completion (Image: Completing a function of the re module, with its signature and docstring) - The list opens while you type a name or after a `.` (not in comments or strings), and Ctrl+Space opens it anywhere. Turn off *Autocomplete while typing* to open it only with the keys. - ↑/↓ move (wrapping around), Page Up/Page Down jump nine rows, Enter or Tab insert, Esc closes; a click inserts too. The footer shows the position (`3/42`). - The mouse wheel scrolls the list without moving the highlighted entry, and a scrollbar on its right shows where you are. Long names and signatures are cut with `…` to fit; the pane beside the list shows them whole. - Typing keeps filtering the list: names that start with what you typed come first, then names that contain the letters in order (`gtx` finds `get_text`). - Each entry has an icon for its kind and, beside the list, a pane with its **signature** and **documentation** (the docstring, as Markdown). | Icon color | Kind | | --- | --- | | Purple | Functions and methods | | Yellow | Classes, enums, type parameters | | Blue | Variables, attributes and properties | | Cyan | Constants | | Red | Modules | | Accent | Controls of the form | - **Private** names (`_cache`) are dimmed, carry a small lock and are listed after the public ones; dunders (`__init__`) come last. - **Auto-import**: picking a name that is not imported yet also adds its `import` at the top of the file, even in handler mode. - The list opens below the word, or above it when there is more room there. ##### The form's own names A handler's `form` and `event` have no type the servers could read, so the editor completes them itself: | You type | You get | | --- | --- | | `form.` | Every control of the form, plus `title`, `name`, `size`, `window` and `close()`. | | `form.email.` | The properties of that control, with their types, plus `name`, `type`, `visible`, `enabled`, `show()`, `hide()` and `focus()`. | | `event.` | `sender`, `name` and `value`. | (Image: form. completes the form's own controls) #### Hover, navigation and refactoring (Image: The hover card of a function: its signature and docstring) These work like in VS Code or Zed, with ty answering where it can and the editor filling in what only the designer knows (the controls of the form). - **Hover**: rest the mouse on a name for half a second and a card shows its type or signature and its docstring (as Markdown), plus the problems at that spot. `form.email` and `form.email.text` describe the control and its property. The card stays while the mouse is over it, so you can scroll it and select its text. Turn it off with *Information on hover* in the settings. - **Go to definition**: F12 or Ctrl+click on a name opens where it is defined: further down the same code, another handler of the form (the editor switches to it), a file of your project or a library in the `.venv` (opened read-only in a tab). Ctrl+F12 goes to the definition of its **type**. On `form.email`, it selects the control on the canvas. Without ty it still finds the `def` and `class` of the file. - **Back and forward**: Alt+← returns to where you were before the jump, and Alt+→ goes forward again. - **Find references**: Shift+F12 lists every use of the name under the editor, with the file, line and text; click one to go there. - **Rename**: F2 opens a field on the name; type the new one and press Enter. Every use changes, in this file and in the other files of the project that use it (they open as unsaved tabs for you to check and save). Renaming a handler keeps its events wired to it (`on sign_in.click`), and one Ctrl+Z takes the rename back. - **Quick fixes** (Ctrl+., or **Quick fix…** in the hover card): the fixes ruff and ty offer for the line, such as removing an unused import, plus: - **Import** a name that is not defined, from the module that has it. - **Install a missing package**: on `import httpx` with httpx not installed, *Install httpx in the project (uv add)* adds it to `pyproject.toml` and the `.venv`, and the error goes away once it is installed. Known names are translated to their package (`cv2` → `opencv-python`, `PIL` → `pillow`, `yaml` → `pyyaml`). - **Organize imports** (Alt+Shift+O) sorts and groups the imports with ruff, and **Fix all auto-fixable problems** (command palette) applies every safe ruff fix. #### Contextual help Shift+F1 opens the [built-in reference](https://albakore.github.io/pyzed/editor/reference/) on the name under the cursor: `form.email` or `form.email.text` opens the page of the control's kind, `form` and `event` open the form scripting pages, and library names (`pz.text`, `ui.button`, `Theme`) open their API page. With no match it searches the reference for the name. #### Diagnostics and code lens (Image: Inline errors, the problems list and code lens) - **Inline diagnostics** (error lens): the most severe problem of each line is written at its end, in red for errors, yellow for warnings and blue for information. - **Problems list**: under the editor, every problem with its line and column, message and source (`ruff (F401)`, `ty`). Click one to jump to it. The header button shows the counts and hides or shows the list. - **Code lens** at the end of each `def` and `class` line: how many **references** it has (from ty, or counted by name when ty is off), how many classes **implement** a class, and which events **call** a handler (`on sign_in.click`). #### Language servers The servers start in the folder of the nearest project marker above the code file (`pyproject.toml`, `uv.lock`, `setup.py`, `.git`) and use its `.venv` (where `uv sync` installs your dependencies), or `$VIRTUAL_ENV`. The code's own folder and `src/` are added as import roots, so a UI kept in its own folder still resolves both: ``` myapp/ ├── pyproject.toml ← uv add httpx ├── .venv/ ← uv sync ├── src/myapp/ └── ui/ ├── login.pzform ├── login.py ← import httpx / from helpers import greet / from myapp import api └── helpers.py ``` When the project has no `.venv` yet (a form you just opened, or a project before its first `uv sync`), ty uses the Python the designer runs on, so `import pyzed` and the standard library resolve instead of being reported as missing. As soon as the `.venv` appears, or `pyproject.toml` or `uv.lock` change (adding a dependency in the [Project tab](https://albakore.github.io/pyzed/editor/projects/)), the servers restart with it, so new packages resolve without reopening the project. The editor looks for `ty` and `ruff` in the project's `.venv` first, then next to the editor, then on `PATH`. Install them with `uv add --dev ty ruff` in the project, or with `pip install "pyzed-ui[designer]"` next to the editor. Ruff reads its rules from `[tool.ruff]` in `pyproject.toml`. While the code editor is open, the status bar shows a dot per server: green when **ready**, yellow while **starting**, red when **missing** or failed. Hover it for details; click it to open the editor settings. ```mermaid sequenceDiagram participant E as Code editor participant A as CodeAssist participant I as CodeIntel participant T as ty server participant R as ruff server E->>A: on_change(text), on_cursor(line, col) A->>I: update(module, handler, text) I->>T: didChange (whole module, handler swapped in) I->>R: didChange T-->>I: publishDiagnostics R-->>I: publishDiagnostics A->>I: complete(line, col) I->>T: textDocument/completion T-->>A: items (via pyzed.post, on the UI thread) A->>E: popup, inline errors, code lens ``` #### Editor settings The **gear** in the code panel header and the server dots in the status bar open them: they are the *Code editor* pages of the [Settings dialog](https://albakore.github.io/pyzed/editor/settings/), with its search, its profiles, **↺** to put a setting back to its default, and the *Project* scope to give a project its own (its tab size, say). (Image: Editor settings with the index of sections) | Section | Setting | What it does | | --- | --- | --- | | Appearance | **Theme** | *Same as the designer*, or any theme for the editor alone. | | | **Font** | *Default* (IBM Plex Mono) or any installed font; also in Settings → Fonts and size. | | | **Font size** | 8 to 32 (also Ctrl+=/-/0 and Ctrl+wheel). | | | **Line height** | Compact, Standard or Comfortable. | | | **Line numbers**, **Highlight current line** | The gutter and the current-line band. | | | **Show whitespace** | Spaces as `·`, tabs as `→`. | | Cursor | **Shape** | Bar, Block, Underline or Hollow. | | | **Blink** | Whether the cursor blinks. | | | **Width** | 1 to 4 pixels, for the bar cursor. | | Editing | **Tab size** | 1 to 8 columns per level, and the width tabs are drawn with. | | | **Indent with tabs** | Tab, Enter and Ctrl+] indent with tab characters instead of spaces. | | | **Convert indentation on save** | Saving a Python file rewrites its indentation with tabs or spaces, as set above. | | | **Backspace removes a whole level** | Off: Backspace in the indentation removes one space at a time. | | | **Autocomplete while typing** | Off: the list only opens with Ctrl+Space. | | | **Auto-close brackets and quotes** | Pairs, wrapping the selection and stepping over the closer. | | | **Code folding** | The chevrons and the fold keys. | | | **Match brackets** | Outlines the bracket that pairs with the one at the cursor (Ctrl+M jumps to it). | | | **Highlight selection matches** | Highlights the other places with the selected text. | | | **Format with ruff on Apply** | Runs `ruff format` on the code when you press Apply. | | Deleting | **Delay before repeating** | Milliseconds Backspace and Delete wait, held down, before they repeat (500 by default; 0 keeps the system's). | | | **Deletions per second** | How fast they delete while held down (20 by default; 0 keeps the system's pace). | | Scrolling and wrapping | **Wrap long lines** | Off by default: long lines scroll sideways. Alt+Z toggles it. | | | **Scrollbars** | Shows or hides the scrollbars. | | | **Minimap** | The zoomed-out code on the right. | | | **Sticky scroll** | Keeps the lines of the enclosing blocks pinned at the top while you scroll. | | Code lens and diagnostics | **Code lens** | References, implementations and wired events at the end of definitions. | | | **Inline diagnostics** | Problems at the end of the line. | | | **Information on hover** | The card with the type, documentation and problems of the name under the mouse. | | Language servers | **ty**, **ruff** | Turn each server on or off. Each row shows its state and version. Below: the project root and the Python in use. | | | **Restart servers** | Restarts both; use it after `uv sync` or after installing them. | (Image: The Language servers section of the editor settings: ty and ruff with their state and version, the project root and the Python in use) ##### Key bindings **Key bindings** in the index opens the [keymap editor](https://albakore.github.io/pyzed/editor/keymap/), where you can change the keys of every action of the editor, the code panel and the rest of the designer. The changes are saved in `keymap.json`, in Zed's format. In your own apps, `ui.editor(keymap=...)` takes keys → action (the same format as Zed's keymaps, `None` to take a default key away), and `pyzed.ui.editor.keymap_for` turns per-action changes into it. ### Running and the console > Run forms and components with F5 in Pyzed Design, read output, events and errors in the console, and see changes live with hot reload on save. Source: https://albakore.github.io/pyzed/editor/console-and-running/ #### Run and Stop **Run** (F5, the green button in the title bar, the ☰ menu or the console) opens the document in a new window with all its logic; **Stop** (Shift+F5, the square next to Run) closes it. (Image: The login form running) What Run does depends on the document and on where the editor runs: ```mermaid flowchart TD F5([F5]) --> P{Code parses?} P -- no --> E[Notification: fix the code first] P -- yes --> K{Component?} K -- yes --> CP["Component preview window
with an event log"] K -- no --> U{Editor inside the
project's environment?} U -- yes --> IN["Runs in the editor's process
the current document, saved or not"] U -- no --> UV["Saves, then
uv run pyzed run --reload"] IN --> C[Console] UV --> C ``` - **In the editor's process** (you started the editor with `uv run pyzed design`, or there is no project): the form runs as it is in the editor, **including unsaved changes**, with its code file compiled fresh. Running again closes the previous window first. - **With `uv run`** (the standalone Pyzed Design, or an editor installed with `uv tool`, which does not have the project's dependencies): the form is saved and started in its own process with `uv run pyzed run --reload`, so every package of `.venv` is available. - **A component** opens in a [preview window](https://albakore.github.io/pyzed/editor/component-designer/#running-and-using-it) that lists the custom events it raises. With **Save before running** on (Settings → Running), Run always saves first. :::tip If the code file fails when it is loaded (an import error, a typo at the top level), Run stops and the console shows the traceback, with a notification naming the error. ::: #### The console The **Console** panel (bottom dock, Ctrl+Shift+Y or **View → Console**) shows what the running form does, without leaving the editor: (Image: The console after a few clicks and an error) | Line | Comes from | | --- | --- | | ▶ `login started` / ■ `login stopped` | A run starting and ending (with `(uv run)` when it runs in its own process). | | Event (accent, lightning icon) | Each event the form handles, with its value and the handler it calls: `sign_in.click → sign_in_click()`. `tick`, `mouse_enter` and `mouse_leave` are left out so they do not flood it. | | Output | Whatever the handlers `print`. | | Error output (yellow) | What they write to `stderr`. | | Error (red) | The full traceback of a handler that raised, starting at the handler. | The toolbar of the console: - **Filter** box: shows only the lines containing the text. - **Level toggles**, one per kind of line present, with its count: click one to hide or show those lines. - **Copy** puts the whole log on the clipboard; **Clear** empties it. - A **Running** badge while a form runs, **Stop** and **Run**. The list keeps scrolling to the newest line while you are at the bottom; scroll up to read older lines in peace. Output still reaches the terminal the editor was started from. When Run opens the console is decided by **Settings → Running → Open the console on Run**, and whether events are logged by **Log events in the console**. The console stays in its dock until you hide it, once it has something to show or you opened it from the View menu. ##### Selecting and copying lines Click a line to select it, Shift+click to select a range and Ctrl+click (Cmd on macOS) to add or remove one; Ctrl+A selects every line shown, Ctrl+C copies the selection and Escape clears it. A selected error shows its whole traceback under it. A **right click** on a line opens its menu: | Item | What it does | | --- | --- | | **Copy line** / **Copy N lines** | The selected lines, with their time and level. | | **Copy text only** | Just the text of the lines (and their traceback). | | **Copy all** / **Select all** | Every line the filters show. | | **Go to file.py:12** | Opens the file of a traceback line in the Code panel, on that line (also with a double click). | | **Show the error…** | The error in the [error dialog](#when-the-editor-itself-fails), with the traceback selectable. | | **Hide error lines** | Turns off that level's toggle. | | **Show in Logs** / **Clear** | Opens the Logs panel, or empties the console. | #### The Logs panel **View → Logs** (Ctrl+Shift+U) opens the **Logs** panel, the editor's own log: everything Pyzed Design does and every error it meets, while the console is about your running form. Like any panel, it can go to another dock or a window of its own. Each line has its time, a level and a **type**: | Type | What it logs | | --- | --- | | Designer | The editor starting and closing, settings changed, and its unexpected errors with their traceback. | | Commands | Every command run from a menu, the command palette or a shortcut. | | Documents | Forms and components opened and saved. | | Project | The project and its `uv` commands. | | Run | Runs starting and stopping, the events and the errors of the running form. | | Code, Agent, Layout, Updates | The code editor, the AI agents, panels moved or shown, and updates. | | System | Python warnings. | The toolbar filters by text, by **type** (the *All types* dropdown) and by **level** (debug, info, event, warning, error: debug lines are hidden until you turn their toggle on). Lines are selected, copied and opened from their menu exactly as in the console, and the menu adds **Only Designer** (or the line's type). The folder button opens the folder with the log files. The same lines (events and up) are written to `logs/pyzed-design.log` in the Pyzed config folder (the file is renewed past 1 MB, keeping the last three): attach it to a bug report. **Settings → Logs and errors → Write a log file** turns it off. When errors or warnings have been logged, the status bar shows their count at the left; click it to open the panel. #### When the editor itself fails A bug in Pyzed Design never closes it: - An action that fails (a click, a key, a menu command, a background task) opens an **error dialog** with the error, where it happened and the traceback, which you can select and copy. Its buttons **Copy error** (everything, ready to paste in an issue), **Go to file.py:12** (opens the line it comes from, when it is a file you can open), **Show in Logs** and **Close**. More errors while it is open are counted in it. - A panel that fails to draw shows an **error card** in its place, with **Copy error** and **Details**, and the rest of the window keeps working; the canvas, the dialogs and the whole window are protected the same way. Every one of them is in the Logs panel with its traceback. **Settings → Logs and errors → Show a dialog on errors** turns the dialog off (the errors are still logged). For your own apps, the same pieces are in the library: [`ui.error_boundary`, `ui.error_dialog` and `ui.ErrorInfo`](https://albakore.github.io/pyzed/reference/ui/#console-errors-and-help). #### Errors in the running window A handler that raises does not close the window: an **Event handler error** callout appears at its bottom with the last line of the error, and the traceback goes to the console. Fix the code, save, and try again (see below). #### Live updates on save While a form runs, **Save** (Ctrl+S) updates the open window without restarting it: the layout, the properties and the code are replaced, while what the user typed or picked in each control (texts, check boxes, selections, slider values) stays. If the new version fails to load, the window keeps running the old one and shows the error. A form started with `uv run` (or with `pyzed run app.pzform --reload` from a terminal) watches its files instead, so saving from any editor updates it too. From code, use `designer.run(path, reload=True)` or set `PYZED_RELOAD=1`. ```bash pyzed run login.pzform # run a form without the editor pyzed run login.pzform --reload # ...and update it on every save pyzed run ui/rating.pzcomp # preview a designed component ``` ### The Component view > Draw reusable components in Pyzed Design with properties, state, methods and custom events, and nest them like Godot scenes. Source: https://albakore.github.io/pyzed/editor/component-designer/ The designer 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 then place it in forms or inside other components. The idea is the same as scenes in Godot: every component has its own properties, methods and events, and can contain other components. (Image: A ProfileCard component that contains a Rating component) A designed component **is** a [`pz.Component`](https://albakore.github.io/pyzed/guides/components/). The designer writes an ordinary class, so drawing it and writing it by hand are the same thing, and you can switch between them at any time. #### Creating one Press **New component…** at the end of the toolbox or in the ☰ menu (Ctrl+Shift+N), or **New…** (Ctrl+N, the first toolbar button) and pick the **Component** card. Give it a name and a folder; **Inherits from** can start it from another component (see [Inheritance](#inheritance)). **Create component** saves both files and opens it. You can also open a `.pzcomp` file: (Image: The New component dialog) ```bash pyzed design ui/rating.pzcomp # created on first save if it does not exist ``` A component is not a form: it has no window, title or menu, and it only exists inside a form or another component. The designer shows it that way: a **Component** badge in the title bar, and an accent frame with the class name on the canvas instead of window buttons. #### Form view and Component view Every open file is a tab above the canvas. A `.pzform` opens in the **Form view** and a `.pzcomp` in the **Component view**, where you work on the class: its properties, events, methods and child components. Double-click a component placed on a form, or on another component, to open it in its own Component view tab: (Image: A form and the component it uses, each in its tab) Switching tabs keeps each document as it was, undo history included. Closing a tab with unsaved changes asks whether to save them first. A component is two files next to each other: | File | What it holds | | --- | --- | | `rating.pzcomp` | The layout: the controls placed on it, its size, how it shows in the toolbox and its bindings (JSON). | | `rating.py` | The class: its properties, custom events and methods. | ```python from typing import Annotated import pyzed as pz from pyzed.designer import SceneComponent class Rating(SceneComponent, scene="rating.pzcomp"): """Five stars the user can click.""" value: Annotated[int, pz.Range(0, 5)] = 3 # How many stars are lit readonly: bool = False clicks: int = pz.state(0, doc="Times the user picked a rating") events = {"change": "A star was picked (event.value is the rating)"} def star1_click(self, event): return self.pick(1) def pick(self, stars): """Lights ``stars`` stars and tells the parent.""" if self.readonly: return None self.value = stars self.clicks += 1 return self.emit("change", stars) ``` #### The Component panel With nothing selected, the Properties tab shows the component itself, in collapsible sections: | Section | Fields | | --- | --- | | **Class** | **Name** (the Python class name, CamelCase), **Description** (the class docstring, shown as the toolbox tooltip) and **Inherits** (a designed component to extend, see [Inheritance](#inheritance)). | | **Toolbox** | **Label**, **Icon** (a Lucide name, or pick it from the [icon dialog](https://albakore.github.io/pyzed/editor/properties/#the-icon-dialog)) and **Category**: how the component shows in the toolbox of forms and other components. | | **Layout** | **Width**, **Height**, **Background** (transparent when empty) and **Grid size** of the design surface. | | **Inherited** | Only with a base: what comes from each base class, read only. | | **Properties** | A row per property (type icon, name and value), and **Public** / **Private** buttons to add one. | | **Methods** | The class's code, grouped, a field to add a method and **Edit the class**. | The **custom events** live in the **Events** tab, in a **Custom events** section below the **Built-in events** (`setup`, `mount`, `unmount`): a card per event (name and description), and **Event** to add one. They are the events the component raises with `self.emit("change", value)`. The parent handles them with `on_change=...` in code, or in the Events tab of a form, where they show apart from the built-in events of the control. - **Properties**: like Unreal's details panel, each property is one compact row: its type icon, its name (a lock marks a private one) and its default value. Lists, dicts and instances list their items below it. The arrow before the name opens its settings: **Name**, **Type**, **Annotation**, **Access**, **Description**, move and **Remove**. A **public** property is a prop the parent passes (`Rating(value=4)`); a **private** one is state, declared with `pz.state`. Both are attributes in the code: `self.value`, `self.clicks`. The type decides the editor the property gets, here and in the Properties tab of the forms that use the component (see [Property types](#property-types)). - **Methods**: every method of the class, grouped in **Lifecycle** (`setup()`, `on_mount()`, `on_unmount()`, added when you click them), **Event handlers** (the code of the controls' events) and your own **Methods** (a name starting with `_` is marked private). Click one to open it in the code editor. Type a name and press **+ Method** (or Enter) to add a reusable method; **Edit the class** opens the whole file. Each change is written to the class right away, and what you write in the class shows up in the panel. If the code has a syntax error, the panel shows the last version that parsed, with a warning, until you fix it. #### Property types As in the Godot inspector and Unreal's details panel, the type of a property picks its editor, and each type has its own colored icon. Pick a common type in the **Type** list of the property's settings, or write any annotation in **Annotation**: | Type | Annotation in the class | Editor | | --- | --- | --- | | str, int, float, bool | `str`, `int`... | Text, number box or switch. | | Color | `pz.Color` | Color swatch with a theme token or `#rrggbb`. | | Icon | `pz.Icon` | Icon picker. | | Multiline | `pz.Multiline` | Text area. | | File, folder | `pz.FilePath`, `pz.FolderPath` (or `pathlib.Path`) | A path field with a button that opens the file or folder dialog. | | Choice | `Literal["fast", "slow"]` | Drop-down; the card has an **Options** field. | | Range | `Annotated[int, pz.Range(0, 5)]` | Number box with limits; the card has **Min**, **Max** and **Step**. | | Optional | `float \| None`, `Optional[float]` | **None** with **+** to set a value, or the value's editor with **×** to empty it. | | Union | `int \| str` | A list of the types, then the editor of the picked one. | | list, set, tuple | `list[str]`, `set[int]`, `tuple[int, int]` | The item count with **+**, and a row per item (`[0]`, `[1]`...) with its own editor. | | dict | `dict[str, int]` | The entry count with **+**, and a row per entry: its key and its value. | | Class | `Point`, `Point \| None` | **New Point** creates an instance with its defaults; then a row per field. **×** empties it. | | Any | `Any`, anything else | A Python expression. | Types nest: `list[Point]`, `dict[str, list[int]]` or `Optional[tuple[int, int]]` show their items inside one another. A class can be a dataclass or any class of the module with annotated fields, or another component. (Image: Properties of several types) ```python @dataclass class Point: x: int = 0 y: int = 0 class Gadget(SceneComponent, scene="gadget.pzcomp"): mixed: int | str | list[str] = 0 maybe: Optional[float] = None scores: dict[str, int] = {"ada": 5} origin: Point = Point() ``` The Properties panel and the code are the same thing: what you change in the panel is written to the class at once, even with the class open in the code editor (what you typed there and did not apply yet is kept), and what you write in the class shows up in the panel. The designer adds the imports these types need (`import pyzed as pz`, `from typing import Annotated, Optional`). ```python class Rating(SceneComponent, scene="rating.pzcomp"): value: Annotated[int, pz.Range(0, 5)] = 3 # How many stars are lit class ProfileCard(SceneComponent, scene="profile_card.pzcomp"): tint: pz.Color = "text_muted" # Color of the role line ``` #### Reusable code A component's class is ordinary Python, so it can hold any code and not just event handlers: helper methods, private state, computed values, imports. A public method is part of the component's API, and forms and parent components can call it: ```python class ProfileCard(SceneComponent, scene="profile_card.pzcomp"): def rate(self, stars): """Rates the person from code, as if the user clicked the stars.""" return self.stars.pick(stars) ``` (Image: The pick() method of Rating open in the code editor) #### Events of the controls Double-click a control, or use the Events tab, to write the code of its events, as in a form. In a component they are methods: `def star1_click(self, event)`. Controls are attributes of the component and their properties read and write like attributes: ```python def clear_click(self, event): self.query.text = "" self.results.items = [] ``` Every instance has its own copy of the controls, so two ratings on one form keep their own state. #### Bindings Select a control and the **Bindings** section links its properties to the component's. Pick a property in the list, or choose **Expression…** and write any Python expression over them: (Image: The headline follows the person property) | Control property | Binding | | --- | --- | | `headline.text` | `person` | | `caption.text` | `f"{value} / 5"` | | `star3.variant` | `"solid" if value >= 3 else "ghost"` | Bindings are evaluated on every render, so the controls always follow the properties. #### Nesting components Saved components appear in the toolbox under their category, in forms and in other components. The designer finds every `.pzcomp` of the project when it starts, and reloads a component when you save it. Place a `Rating` inside a `ProfileCard`, bind its `value` to the card's `score`, and forward its event: ```python class ProfileCard(SceneComponent, scene="profile_card.pzcomp"): """A person with a rating.""" person: str = "Ada Lovelace" job: str = "Mathematician" score: int = 4 events = {"rated": "The rating changed (event.value is the new score)"} def stars_change(self, event): """The inner Rating raised change.""" self.score = event.value return self.emit("rated", event.value) ``` (Image: Two cards on a form) ##### Child components in the outline A child component is an attribute of its parent, and its properties and methods are reachable from it: `self.stars.value`, `self.stars.pick(5)`. From a form it works the same way: `form.ada.rate(5)`. In the **Outline**, the arrow next to a placed component lists its public properties and methods. Click one to insert it in the code editor at the cursor (`self.stars.pick()` in a component, `form.ada.rate()` in a form): (Image: The outline lists the properties and methods of a placed component) A property that a designed component changes itself, such as `self.score` above, keeps its value until the parent passes a different one, like an exported variable in Godot. A form handler can set it too: ```python def ada_rated(form, event): event.sender.score = event.value form.status.text = f"{event.sender.person} now has {event.value} stars" ``` #### Responsive components A component placed on a form, or inside another component, takes the size of its rectangle there, and its controls follow their **anchors** like on a form: anchored left and right, a control stretches with the component; anchored right, it moves with the right edge. Flex rows, columns and grids inside the component lay out at the new size too. In the example, the frame of `ProfileCard` is anchored on all four sides and the headline, the role and the rating on left, top and right. `Reviews` puts its two cards in a **flex row** with `grow` 1 each, so they share the width of the window and shrink with it: (Image: The two cards shrink with a narrow window) Used from code, a component is as wide as there is room for, up to its design width, and as tall as its design height. #### Inheritance A component can inherit from another one to reuse its layout and its logic. Pick the base in **Inherits from** when you create it, or later in the **Inherits** field of the Class section. The class then derives from the base class: ```python from pyzed.designer import import_component ProfileCard = import_component("profile_card.pzcomp") class TeamCard(ProfileCard, scene="team_card.pzcomp"): """A profile card that also shows the team.""" team: str = "Analytical Engine" def stars_change(self, event): """Does what a ProfileCard does, and the badge turns green with five stars.""" result = super().stars_change(event) self.team_label.tint = "success" if event.value == 5 else "accent" return result ``` `import_component` imports the class of a `.pzcomp` relative to the module, so the base and the new component can live in any folder. What the new component gets: - **Controls**: the base's controls show on the canvas, with yours on top. You can move or change an inherited control (the change is saved in your `.pzcomp` only), but not rename or delete it, because the base's code uses it. Saving writes only your controls and the inherited ones you changed. - **Properties, custom events and methods**: everything of the base class, as in any Python subclass. Bindings can use inherited properties. Define a method again to override it, and call `super()` to run the base's. - **Changes to the base**: saving the base reloads the components that inherit from it, so they pick up its new controls and code. The **Inherited** section of the panel lists what comes from each base: (Image: TeamCard inherits from ProfileCard) #### Running and using it Press **F5** to open the component in a preview window. The window lists the custom events it raises, so you can try it before using it anywhere. `pyzed run rating.pzcomp` does the same from a terminal. (Image: The preview window) In code, import the class and use it like any component: ```python from ui.rating import Rating class Review(pz.App): stars = 3 def view(self): return Rating(value=self.stars, on_change=lambda e: setattr(self, "stars", e.value)) ``` Forms find the components of their project on their own when they run (`pyzed run`, `designer.run`). #### Scripting components Everything the panel does is available from Python: ```python from pyzed import designer doc = designer.new_component("Rating", "ui/rating.pzcomp", width=232, height=32, icon="star") doc.add("button", 0, 0, 28, 28) doc.add_member("value", "int", 3) doc.add_member("clicks", "int", public=False) doc.add_event("change", "A star was picked") doc.set_binding("button1", "variant", '"solid" if value >= 1 else "ghost"') doc.wire("button1", "click") doc.save() big = designer.new_component("BigRating", "ui/big_rating.pzcomp", extends="ui/rating.pzcomp") big.save() designer.discover("ui") # import every designed component of a folder ``` See [`examples/designer/components`](https://github.com/albakore/pyzed/tree/main/examples/designer/components) for the Rating, ProfileCard, TeamCard and Reviews example. :::tip Designed components are discovered when the editor starts and reloaded when you save them, so keep the component and a form that uses it open in two tabs: edit the component, press Ctrl+S, switch to the form tab and press F5. ::: ### The Shader view > How the Shader view of Pyzed Design fits in the editor: the Shading sidebar, the Graph, Preview and WGSL tabs, its toolbar and status bar, and where to learn each node. Source: https://albakore.github.io/pyzed/editor/shader-view/ The third kind of document of Pyzed Design is a **shader**: a GPU effect built by wiring nodes, saved as a `.pzshad` file. It opens in the **Shader view**, a node editor in the spirit of Godot's visual shaders and Unreal Engine's material editor. This page shows how the view fits in the editor; the [Shading section](https://albakore.github.io/pyzed/shading/) teaches shaders themselves and lists every node. #### How the editor changes in a shader | Area | In the Shader view | | --- | --- | | **Title bar** | A **Shader** badge after the file name. **Run** opens the shader in a window that follows your edits. | | **Left dock: Design** | The **Shading** sidebar (every node by category, with a search box) and the **Outline**: the shader's **variables**, its **named values** and its **nodes**. | | **Center** | Three tabs: **Graph**, **Preview** and **WGSL**, with the shader's own toolbar. | | **Right dock: Properties** | The selected node (its settings, inputs and outputs), the selected variable, or the shader itself when nothing is selected. | | **Events** | Says that shaders have no events, and how to use this one: `pz.shader("fire.pzshad")` or a Shader control on a form. Its variables are what your code changes. | | **Status bar** | The pointer's position on the graph, how many nodes and wires there are (or how many are selected), and the zoom (click it for 100%). | The three tabs of the center: **Graph:** Graph: the nodes, each with a live preview, and the small Output preview in the corner." /> **Preview:** Preview: the finished shader, large, with a control per variable." /> **WGSL:** WGSL: the code the graph compiles to, read only." /> #### The toolbar | Button | Keys | Does | | --- | --- | --- | | **New shader**, **Open**, **Save** | Ctrl+O, Ctrl+S | File commands. | | **Undo**, **Redo** | Ctrl+Z, Ctrl+Shift+Z | Every change to the graph. | | **Graph**, **Preview**, **WGSL** | | The three tabs of the view. | | **Add a node** | Shift+A | Opens the node search where the pointer is. | | **Delete** | Delete | Removes the selected nodes. | | **Arrange nodes in columns** | Ctrl+L | Tidies the graph from inputs (left) to Output (right). | | **Fit everything** | F | Zooms to show the whole graph. | | **Stop time** / **Start time** | | Freezes every preview at one moment, to study a frame, and starts it again. | | **Small preview** | | Shows or hides the Output preview in the corner of the graph. | | **Compiles** / *N problems* | | Hover to read the problems; the node at fault turns red. | | **Run** | F5 | The shader in a window of its own. | #### Three quick gestures - **Add a node:** double-click the canvas and type its name, or drag it from the Shading sidebar. - **Wire:** drag from an output (right side of a node) to an input (left side). Drop a wire on empty space to add a node already connected to it. - **Change a value:** an input with no wire shows its value on the node; type a number or pick a color there. Every gesture and shortcut is in the full [Shader view reference](https://albakore.github.io/pyzed/shading/shader-designer/). **Shaders in forms:** The toolbox's **Shader** control (in **Display**) draws a `.pzshad` on a form or a component; set its **Shader file** and its **variables** in Properties. A running form reloads the shader when you save it here. ### Files, templates and scripting > Document tabs, new forms and templates, opening and saving, the .pzform format, custom components and the Python scripting API of Pyzed Design. Source: https://albakore.github.io/pyzed/editor/files-and-scripting/ #### Document tabs Every open file is a tab above the toolbar: `Form · login.pzform` or `Component · rating.pzcomp` (with an accent icon). A dot marks unsaved changes, and the end of the tab bar says which view you are in: **Form view** or **Component view**. - Click a tab to switch. Each document keeps its own selection and undo history; the code editor is applied and closed when you leave a tab (if the code does not parse, the switch waits until you fix it). - The **×** on a tab closes it. With unsaved changes, a dialog offers **Save**, **Don't save** and **Cancel**. - Opening a file that is already open switches to its tab, keeping its unsaved changes. An untouched empty untitled form is replaced instead of piling up tabs. - Double-clicking a placed [designed component](https://albakore.github.io/pyzed/editor/component-designer/) opens its file in a tab. - Drag a tab along the row to reorder it. Drag it out of the window to open it in a **window of its own**, with its own tabs, toolbar and canvas; drag a tab onto another Pyzed Design window to stack it among that window's tabs. **Move tab to new window** and **Move tab to main window** (View menu, command palette) do the same without the mouse, also on Wayland, where apps cannot tell where their windows are. - The panels (Properties, Outline, Code, Events...) stay in the main window and follow the document you used last. A window that is not the active one shows its form as it is; its first click only makes it active, so a click never changes a form you were not looking at. Closing a document window moves its tabs back to the main window. The window title shows the active document, after a `●` when it has unsaved changes. #### New form **New form** (Ctrl+N, the **New…** toolbar button or the ☰ menu) opens the New dialog. At the top, **Form** or **Component** picks what to create (**New component**, Ctrl+Shift+N, opens it on Component). (Image: The New form dialog with its templates) | Field | Meaning | | --- | --- | | **Name** | The file name and the prefix of the form's handlers. A free default (`form1`, `form2`...) is proposed. | | **Title** | The window title (the name when empty). | | **Folder** | Where the files go: the current document's folder, or the project's `ui/` folder. The dialog shows the path it will create, and warns if it exists. | | **Template** | The starting point (below). | | Template | Gives you | | --- | --- | | **Blank** | An empty window. | | **Dialog** | A heading, a message and a row of **Cancel** and **OK** buttons anchored to the bottom, both already wired to close the window. | | **Sign in** | Email, password, "remember me", a sign-in button and a status label in a centered column. | | **Sidebar** | Navigation buttons in a column on the left and a content column on the right. | | **Settings** | Labelled options (text box, drop-down, switch, slider) in a two-column grid, with a Save button. | **Create** writes the `.pzform` and its `.py` and opens them. **Unsaved form** opens the template without writing anything: it is saved (asking for a path) the first time you save. New forms take the grid size from [Settings](https://albakore.github.io/pyzed/editor/settings/). #### Open, save and save as - **Open…** (Ctrl+O, or click the file name in the title bar) lists every Pyzed file of the project (or the working folder), skipping `.git`, `.venv`, `node_modules`, `build`...: forms (`.pzform`), components (`.pzcomp`), shaders (`.pzshad`) and animation libraries (`.pzanim`). Type to filter the list, type a path and press Enter, or press **Browse…** for the system's file dialog. Forms, components and shaders open in their view; an animation library opens as JSON in the code editor, with a note listing its clips. - The installers make the system open these four kinds of file in Pyzed Design when you double-click them (and `pyzed design path/to/file` opens any of them, or a script in the code editor). - **Recent** files are listed at the end of the ☰ menu (the last five). - **Save** (Ctrl+S) writes the document and its code file. An untitled document asks for a path first. Saving also refreshes the form running from F5. - **Save as…** (Ctrl+Shift+S) asks for a new path, relative to the working folder; the suffix is added when you leave it out. #### The files A form is two files side by side, both meant to be committed: ``` login.pzform ← layout: JSON, diffs nicely login.py ← event handlers: plain Python ``` The `.pzform` file stores the form settings and, for each control, its type, position, size, properties, anchors and which handler each event calls: ```json { "format": "pyzed.form/1", "form": { "name": "login", "title": "Sign in", "width": 440, "height": 400, "theme": "One Dark", "events": { "load": "form_load" } }, "controls": [ { "name": "sign_in", "type": "button", "x": 48, "y": 304, "width": 344, "height": 32, "props": { "text": "Sign in", "variant": "solid", "icon": "log-in" }, "events": { "click": "sign_in_click" }, "anchor": ["left", "top"] } ] } ``` The editor edits the `.py` file with `ast`: it finds, adds, renames and replaces handler functions without touching the rest of the file, so your imports, helpers and comments stay as you wrote them. You can edit the code file in any other editor too; the form only needs the handler functions to exist. A designed component is also two files, `rating.pzcomp` (layout) and `rating.py` (the class); see [Component view](https://albakore.github.io/pyzed/editor/component-designer/). #### Adding your own components Register a component and it shows up in the toolbox, with its properties in the grid and its events in the Events panel: ```python from pyzed import ui from pyzed.designer import Component, Prop, register register(Component( type="rating", label="Rating", icon="star", category="Custom", size=(120, 24), props=(Prop("value", "number", 3, min=0, max=5),), events=("change",), render=lambda control, ctx: ui.label("★" * int(control.props["value"])), )) ``` `render` is used both in the editor (inert) and at run time; use `ctx.on(control, "change", "value")` for callbacks so the same code works in both. Every `pz.Component` subclass of your project appears in the toolbox too (see [Components](https://albakore.github.io/pyzed/guides/components/)), and so do the components you draw in the [component designer](https://albakore.github.io/pyzed/editor/component-designer/). They are loaded whenever a project opens (at start, from the project picker or from a recent folder) and when you open a form of another folder. Pyzed imports a module for its components only if its top level just defines things: a module that calls `pz.run(...)` outside `if __name__ == "__main__":` is skipped. A control whose component cannot be found (its module fails to import, or the form comes from a newer Pyzed) shows as a yellow **Component not found** box with its type: the rest of the form still opens, and its properties and events are kept when you save. #### Your own templates The New form templates are data, not code. Each one is a folder: ```text toolbar/ ├── template.json {"label": "Toolbar", "icon": "panel-top", "description": "A row of buttons on top.", "order": 10} ├── form.pzform the form to start from: draw it in the designer and save it here └── handlers.py.tmpl optional: the code of the events the form wires (Python) ``` `{title}` in the form's texts becomes the new form's title, and a size asked for in the dialog resizes the form following the anchors. The built-in ones live in `pyzed/designer/templates/`. Load a folder of yours with: ```python from pyzed.designer import new new.load_templates("my_templates") # every subfolder with a template.json ``` A template can also be a Python function that fills an empty form: ```python def toolbar(form): ... # add controls to the empty form new.register_template(new.Template("toolbar", "Toolbar", "panel-top", "A row of buttons on top.", toolbar)) ``` Registered templates appear in the New form dialog after the built-in ones. #### Scripting forms Everything the editor does is available from Python, which is handy for generating forms or for tests: ```python from pyzed import designer from pyzed.designer import Document doc = Document(designer.Form(name="hello", title="Hello", width=320, height=140)) doc.add("input", 16, 16, 200, 28) doc.add("button", 224, 16) doc.select_all() doc.align("middle") handler = doc.wire("button1", "click") doc.set_handler_source(handler, "def button1_click(form, event):\n form.button1.text = form.input1.text\n") doc.save("hello.pzform") designer.run("hello.pzform") ``` `designer.new_form(path, template="dialog")` creates a form and its handlers module from a template (in a project, `project.find().new_form("settings")` puts it in `ui/`). `designer.open_window(form_or_path, data=...)` returns the command that opens a form from any app, and also accepts a `designer.Form` built in code (with `handlers={"name": function}`). The full API is in the [designer reference](https://albakore.github.io/pyzed/reference/designer/). #### How it is built ```mermaid classDiagram class Form { controls; events; theme; grid } class Control { name; type; x; y; width; height; props; events; anchor } class Component { type; props; events; render() } class Document { form; code; selection; history; add(); align(); wire() } class History { push(); undo(); redo() } class Designer { pyzed App: docks and gestures } class FormRuntime { dispatch(); view() } Form "1" *-- "*" Control Control ..> Component : type Document --> Form Document --> History Designer --> Document FormRuntime --> Form ``` - `model.py`: the form and its controls, saved as JSON. - `components.py`: the component library and the `render` shared by the editor and the runtime. - `geometry.py`: snapping, alignment, distribution, resizing, anchors and smart guides (pure functions). - `layout.py`: where every control goes, flex and grid containers included. - `history.py`: undo/redo as snapshots; repeated edits of one field merge into one step. - `code.py`: finds, adds, renames and replaces handler functions with `ast`. - `document.py`: every editing command, with no UI, so it is fully testable. - `commands.py`: every command in one table, read by the menus, the palette, the keyboard and the shortcut list. - `docks.py`, `panels.py`, `surface.py`, `dialogs.py`, `console.py`, `project_panel.py`: the views. - `app.py`: the `Designer` app wiring clicks and keys to the document. - `scene.py`, `scene_code.py`, `scene_runtime.py`, `scene_panel.py`: the component designer. - `runtime.py`: `FormApp` and `FormWindow`, which draw a form and call its handlers. - `lsp.py`, `intellisense.py`, `code_panel.py`, `settings.py`: the code editor and its language servers. - `preferences.py`, `locale.py`: the editor's settings and its translations, read from `locales/.json`. - `new.py`: the New form templates, read from the folders in `templates/`. ### Exporting your app > Export a Pyzed project from Pyzed Design as an app for Windows, macOS and Linux on x64 and ARM: PyInstaller or Nuitka, app icon, splash screen, file details and a GitHub Actions workflow for every platform. Source: https://albakore.github.io/pyzed/editor/exporting/ **File → Export app…** (Ctrl+Shift+E) turns the open [project](https://albakore.github.io/pyzed/editor/projects/) into an app that people run without installing Python: a `.exe` on Windows, a `.app` on macOS, an executable on Linux. Like Godot's export window, one dialog holds everything: the app's name and details, the platforms, the packager, the icon and the splash screen. #### Export in three steps 1. Open a project (**Project → Open**) and press Ctrl+Shift+E. 2. Fill in **General** (at least the name) and, if you like, the **Icon** and the **Splash screen**. 3. Press **Build for …** (the name of your system). The output of the build streams into the [Console](https://albakore.github.io/pyzed/editor/console-and-running/); when it is done a notification says where the app is and how big it is, and **Show in folder** opens it. Later builds do not need the dialog: **Run → Build app for this computer** (Ctrl+Shift+B) builds with the same settings. The result goes to `dist/` in the project folder (`dist/Shop/`, `dist/Shop.exe` or `dist/Shop.app`). **The settings live in pyproject.toml:** The dialog saves to the `[tool.pyzed.build]` table of `pyproject.toml` when you close it or build. `uv run pyzed build` in a terminal and the CI workflow use exactly the same settings; see [How building works](https://albakore.github.io/pyzed/packaging/build/) for every key. #### General | Field | Used for | | --- | --- | | **Name** | The executable, the `.app` and the folder in `dist/`. Empty: the project's name. | | **Version** | The Windows file version and the macOS bundle version. Empty: `[project] version`. | | **Publisher** | The company in the Windows file properties. | | **Identifier** | A reverse domain such as `com.acme.shop`: the macOS bundle id and the Linux app id. macOS keeps the app's settings and permissions under it, so do not change it between releases. | | **Description**, **Copyright** | The Windows file properties and the macOS `Info.plist`. | #### Platforms and architectures PyInstaller and Nuitka build for the system they run on: a Windows computer makes the Windows app, a Mac the macOS app. The list shows the six targets Pyzed supports, with **This computer** on yours: | Target | System | GitHub machine that builds it | | --- | --- | --- | | `windows-x64` | Windows 10 and 11 on Intel and AMD | `windows-latest` | | `windows-arm64` | Windows 11 on ARM (Snapdragon) | `windows-11-arm` | | `macos-arm64` | Macs with Apple silicon (M1 and later) | `macos-latest` | | `macos-x64` | Intel Macs | `macos-15-intel` | | `linux-x64` | Linux on Intel and AMD | `ubuntu-22.04` | | `linux-arm64` | Linux on ARM (Raspberry Pi 4/5, ARM servers) | `ubuntu-22.04-arm` | Check the platforms you want and press **Write CI workflow**: it writes `.github/workflows/build.yml`, which builds each checked target on GitHub's own machines every time you push a version tag (`git tag v1.0.0 && git push --tags`). See [Build in CI](https://albakore.github.io/pyzed/packaging/ci/). The Linux machines are the oldest Ubuntu GitHub offers on purpose: an app built against an older system runs on more distributions. **Archive** also packs the app in a `zip` (or `tar.gz`) named like `shop-windows-x86_64.zip`, the name the [self-updater](https://albakore.github.io/pyzed/packaging/updates/) looks for, ready to attach to a GitHub Release. **A WSL project builds the Linux app:** On Windows, a project that lives in WSL builds inside WSL: **This computer** is then `linux-x64`, and you get the Linux app without leaving Windows. #### Backend: PyInstaller or Nuitka | | PyInstaller | Nuitka | | --- | --- | --- | | How | Bundles your `.py` files as they are, next to Python | Compiles your code and its dependencies to C, then to machine code | | Build time | Seconds | Minutes (faster with [ccache](https://ccache.dev) installed) | | Speed | Normal Python | Compiled: faster pure-Python code | | Size of a single file | Larger | About 15% smaller (better compression) | | Size of a folder | Smaller | Larger (compiled code takes more room than `.pyc`) | | Needs | Nothing (uv installs it) | A C compiler | Both run inside the project's environment with `uv run --with …`, so you install neither yourself. Nuitka needs a C compiler: `gcc` on Linux (`sudo apt install gcc` and friends), the Xcode command line tools on macOS (`xcode-select --install`), and on Windows Visual Studio's Build Tools, or Nuitka downloads one the first time. | Option | What it does | | --- | --- | | **Single file** | One executable that unpacks itself to a temporary folder when it starts. A folder starts faster and can [update itself](https://albakore.github.io/pyzed/packaging/updates/). | | **Window only** | No console next to the app. Turn it off while you look for an error: the console shows prints and tracebacks. | | **Optimization** | Python's `-O` (drops `assert`) or `-OO` (also docstrings). Leave it at *None* if a library uses docstrings at runtime. | | **Slim** | Leaves out standard modules apps do not use (`tkinter`, `test`, `idlelib`, `lib2to3`, `ensurepip`...). | | **Strip symbols** | Removes debug symbols from the libraries: smaller on Linux and macOS. | | **Compress with UPX** | Compresses the binaries with [UPX](https://upx.github.io/) if it is installed. Some antivirus programs flag UPX-packed files, so it is off by default. | | **More options** | Anything else, passed to the backend as is (for example `--lto=yes` for Nuitka). | #### Icon Choose one square picture, ideally a 1024 × 1024 PNG with a transparent background. The build makes every file the systems want from it, with no extra tool: - `app-icon.ico` with 16 to 256 pixel images, for the Windows `.exe`, Explorer and the taskbar; - `app-icon.icns` up to 1024 pixels (Retina), for the macOS bundle, Finder and the Dock; - `app-icon.png` (256 pixels), shipped with the app as the icon of its windows: the title bar and the taskbar on Windows and Linux. The preview shows how it looks small: if the 16 pixel version is a blur, simplify the drawing. An `.ico` or `.icns` file is used as it is, for its own system. In code, any window can have its own icon with `Window.icon` (see [Icons and metadata](https://albakore.github.io/pyzed/packaging/icons-metadata/)). #### Splash screen A splash screen is a picture in a window without a frame, centered on the screen, while the app starts. It stays up for **Duration** seconds at least; then the startup form opens and the splash closes. If the form takes longer to load (a big database, a slow `load` event), the splash stays until it is ready, so people never stare at an empty desktop. **Text** is written over the bottom of the image (for example *Loading…*). **Early splash** also asks the backend for its own splash, which the bootloader shows even before Python starts, while a single-file app unpacks itself. PyInstaller has one on Windows and Linux (it draws with Tk, so the project's Python needs `tkinter`; uv's Pythons have it), Nuitka for single files on Windows. When both are on, the early splash gives way to the Pyzed one as soon as the app runs. Make the image the size you want the window (it is scaled down to fit 900 × 600 if larger); a PNG or JPEG of about 600 × 360 works well. #### Files and modules The build already finds the UI folder, the backend in `src/` and every module the forms' code imports. Add here what it cannot see: | List | For | | --- | --- | | **Files and folders** | Data the app opens: images, a database, a config file. Same path from the project root, inside the app. | | **Hidden imports** | Modules loaded by name (`importlib.import_module("plugins.csv")`). | | **Whole packages** | Packages with data files or plugins that need everything shipped (`babel`, `certifi`...). | | **Leave out** | Modules not to ship, to make the app smaller (`numpy.tests`). | #### Command line The last section shows the same build as a terminal command, the table it writes to `pyproject.toml` and a button that copies the full PyInstaller or Nuitka command line, to adapt it or report a problem. #### Next - [How building works](https://albakore.github.io/pyzed/packaging/build/): every setting, and what ends up in the app. - [Build in CI](https://albakore.github.io/pyzed/packaging/ci/) for the platforms you do not have. - Signing and installers: [Windows](https://albakore.github.io/pyzed/packaging/windows/), [macOS](https://albakore.github.io/pyzed/packaging/macos/), [Linux](https://albakore.github.io/pyzed/packaging/linux/). - Something wrong? [Troubleshooting](https://albakore.github.io/pyzed/packaging/troubleshooting/). ### AI agents > The Agent panel of Pyzed Design: an AI agent that builds forms and components and writes their Python code, with inline assist and several model providers. Source: https://albakore.github.io/pyzed/editor/ai-agents/ The **Agent** panel (right dock, next to Properties and Events) is an AI agent. Describe what you want and it reads the open form, adds and arranges controls, wires events, writes the handlers, creates new forms and components, and checks its own code. Every change it makes is an ordinary undo step, and you can reject the whole run at once. (Image: The agent added a switch and wired its event) #### Getting started 1. Open the panel: the **Agent** tab in the right dock, **View → Agent panel**, or Ctrl+Alt+A. 2. The first time, the panel asks you to **connect a model** and lists the main providers. Click one to [sign in with the browser](#signing-in), or **All providers…** for the rest. 3. Type what you want and press Enter. Claude needs the optional dependencies (`anthropic` and `keyring`); the compiled Pyzed Design already has them: ```bash pip install "pyzed-ui[agents]" # or: uv add "pyzed-ui[agents]" ``` (Image: An empty thread: the whole panel is the message editor) ##### Signing in Signing in happens inside Pyzed Design; no other program is needed. - **Anthropic (Claude), OpenAI, Gemini, xAI, Mistral, DeepSeek, Groq**: these providers don't let other apps sign in with OAuth (Anthropic's subscription sign-in is only for its own apps), so Pyzed signs in through a page of its own. Your browser opens a **Connect … to Pyzed Design** page served by Pyzed on this computer (`127.0.0.1`, a one-time address); it links to the provider's API keys page. Create a key there and paste it on Pyzed's page, or just click the key's **Copy** button: Pyzed notices the copied key too. Pyzed checks the key with the provider, saves it in the system keychain, loads the provider's models and comes back to the front, so you can close the tab and keep working. While it waits, a **Signing in** card offers **Paste key** (read the clipboard now), **Open the key page** and **Cancel**. The clipboard is read only while that card is showing (at most five minutes), and only a text that looks like that provider's key is used. The local page only accepts a key sent from itself: other sites and other host names can't reach it. To use your Claude Pro or Max plan instead of a key, see [Your Claude subscription](#your-claude-subscription). - **OpenRouter**: real OAuth with PKCE. You approve Pyzed on OpenRouter's page and the key it issues comes back to Pyzed directly; nothing to copy. - **GitHub Copilot**: **Sign in with GitHub** uses GitHub's device flow. Pyzed opens `github.com/login/device` and shows a short code; type it there, approve Pyzed, and the token comes back on its own (the card has a button to copy the code, and **Cancel**). It reaches the models of your GitHub account and Copilot plan through GitHub Models. Signing in needs a GitHub App client ID in the provider's settings; without one, **Sign in with a token** takes a fine-grained personal access token with the **Models** permission. - **Google Vertex AI**: **Sign in with Google** opens Google's consent page (OAuth with PKCE) and the token comes back to Pyzed; it is refreshed on its own. If you already ran `gcloud auth application-default login`, those credentials are used and nothing else is needed. Pick the **Google Cloud project** (with Vertex AI enabled) in the provider's settings, or click **List my projects**. - **Anthropic without a key**: [workload identity federation](#claude-without-an-api-key) exchanges a token from your identity provider for a short-lived Claude token. - **Ollama, LM Studio**: nothing to sign in to. Clicking them asks the local server for its models. You can always paste a key by hand in the [settings](#settings) or set the provider's environment variable. (Image: Signing in to Anthropic: the key page is open and Pyzed waits for the copied key) #### The panel The panel looks and works like Zed's Agent Panel. The same thread view is used for Pyzed Agent and for [external agents](#external-agents) such as Claude Code, Codex or Gemini CLI. | Part | What it does | | --- | --- | | **Toolbar** | The agent's icon and the thread's title (**New Pyzed Agent Thread** while it is empty). Click the title to rename it. On the right: **+** (a new thread with Pyzed Agent or any [external agent](#external-agents), and **Add more agents**), **full screen** (the panel fills the window; click again to put it back in its dock) and **⋯**. | | **⋯ menu** | **Current Thread** (**Rename Thread**, **Copy Thread as Markdown**, **Open Thread as Markdown**), **MCP Servers** (**Add Server…**), **Context** (**Skills** and **Open Project Rules** / **Create Project Rules**, the project's `AGENTS.md`), **Settings**, **Thread History** and **Show Thinking**. With an external agent, also **Reauthenticate** and **Reload Agent** (its program starts again). | | **Your messages** | Cards in the editor's color. Click one to edit it: Enter (or the ↵ button) sends it again from that point and replaces what followed; Esc or ✕ cancels. See [Editing a message and checkpoints](#editing-a-message-and-checkpoints). External agents keep their own history, so their messages can't be edited yet, as in Zed. | | **The agent's answer** | Markdown. Its reasoning is folded under **Thinking** (click it to read it). | | **Tool calls** | One line per step, as it happens: the icon of the kind of step, what the agent does (**Adding controls**, **Read file**…) and what it works on. Click a line to see its input and output. Edits and commands are cards: the diff, or the command's output while it runs. A call that waits for your OK asks on its card: **Always allow**, **Allow** or **Reject**. A failed call shows a red ✕. | | **End of a turn** | How long the agent took and how many tokens it wrote (`42s • 3.8k tokens`), **Copy This Agent Response** and, on the last turn, **Scroll to Top**. While it works, a spinner with the time and the tokens so far. | | **Activity bar** | Attached to the top of the message editor, what needs your attention: a call **Awaiting Confirmation**, the **Plan** (`Current: … 2 left`; click it to see every step), the **Edits** (`2 files • +12 -3`, with **Review Changes**, **Reject All** and **Keep All**; unfold it for **Keep** or **Reject** on each file) and the [queued messages](#queued-messages). | | **Message editor** | Grows with the text. Enter sends and Shift+Enter starts a new line. Type @ to [add context](#adding-context-with-) and / for [commands](#commands-with-); in their list ↑ ↓ move, Enter picks and Esc closes. The arrows at its top right make it taller. An empty thread is all message editor, like in Zed. | | **Left of the bottom row** | **+** (**Add context**: files, the form, its code, controls, **Attach images…**, **Paste image** and **Screenshot of the form**; see [Images](#images)), **Follow the Agent** (the crosshair: see [Watching the agent work](#watching-the-agent-work)), **Fast mode** (on Claude Opus 5.5 and Opus 5 through the Claude API: faster answers at a higher price), **Thinking** (the brain: lets Claude think before answering) and its **effort** (**Low**, **Medium**, **High**, **Extra high**, **Max**). The last three show for the models that have them. | | **Right of the bottom row** | A small ring with how full the model's context window is (`48k / 1M`; yellow past 80% and red past 95%), the **profile**, the **model** (every provider with its models; one that is not set up offers **Connect…**) and **Send**. While the agent works, **Send** becomes **Stop**, or **Queue and Send** when there is text. | | **Agent options** | With an external agent, the options it offers take the place of the profile and the model, as in Zed: its modes (Claude Code's **Default**, **Accept edits**, **Plan mode**…), its models (**Fable 5.1**, **Opus 5.5**, **Sonnet 5.5**, **Haiku 5.5**), its effort or reasoning level and anything else it lists. Claude Code and Codex run the latest version of their adapter, so new models show up on their own. Pyzed remembers what you pick for the next threads with that agent. | The profile says what Pyzed Agent may do, like Zed's profiles: | Profile | What the agent may do | | --- | --- | | **Write** | Edits forms, code and files and runs the app. Edits to the open form never ask (every change can be undone); with **Ask before writing files** on (the default, in the same menu) it asks before creating documents or writing files. | | **Ask** | Reads only: answers questions and suggests code without changing anything. | | **Minimal** | No tools: a plain conversation with the model. | ##### Editing a message and checkpoints Click one of your messages to change it and send it again: the thread goes back to that point (what came after it is replaced by the new answer), and so do the forms and code the agent changed after it. Everything after the message you are editing is dimmed until you send it or cancel. When the agent changed documents after a message, **Restore Checkpoint** shows above it. It puts those forms and their code back as they were when you sent that message, without touching the thread. The checkpoints last while Pyzed Design is open. ##### Queued messages You can keep writing while the agent works: Enter puts the message in a queue, shown in the activity bar (**1 Queued Message**), and it is sent when the agent finishes. Unfold it to edit (it goes back to the message editor) or remove a message. **Send Now** stops the agent so the next message goes right away; **Clear All** empties the queue. A run that stops with an error or waits for a sign-in leaves the queue waiting. (Image: Every provider in the model picker) Each message carries a short note of where you are (the project and its environment, the open document with its controls and code, the selected controls, the handler in the code editor), so "align these" or "validate this field" refer to what you are looking at. Pyzed Agent also starts out knowing the toolbox (every type with its props and events), the themes, the `pz.ui` components and Pyzed's best practices, so it builds right away instead of looking them up first. ##### Adding context with @ Type @ (or click **+**) and pick what to attach: the **selection**, the open **form**, its **code**, one of its **controls**, a **file** of the project (an image file goes as an image), or a **screenshot** of the open form. Keep typing to filter the list; Enter picks the highlighted row. What you pick shows as a chip above the message and its content goes with the message, so the agent doesn't have to look for it. Files that may hold secrets (`.env`, keys…) are never offered. (Image: Adding context with @) ##### Commands with / Type / at the start of the message: | Command | Does | | --- | --- | | `/new` | Starts a new thread. | | `/app` | Starts a request to [build a whole application](#building-an-application). | | `/history` | Shows the past threads. | | `/settings` | Opens the AI agents settings. | | `/form`, `/component` | Starts a request to generate a form or a component. | | `/explain` | Explains the open form and its handlers. | | `/fix` | Checks the form's code with `validate_code` and fixes what it finds. | | `/tidy` | Aligns the controls and evens out the margins. | | `/look` | Takes a screenshot of the form, looks at it and improves its layout and design. | ##### What the agent can do | Tool | Does | | --- | --- | | `get_document` | Reads the open form or component: settings, every control with its rect, props, events and style, the selection and the code. | | `add_controls`, `add_control`, `update_control`, `remove_controls` | Place a whole layout in one step, or one control; change, move into a container, rename and delete controls. Types and props are checked against the toolbox. | | `set_form`, `select_controls` | Change the form's title, size, theme or style; select controls so you see them. | | `wire_event`, `set_handler`, `set_code` | Connect events and write one handler or the whole module (it must parse). | | `new_document`, `save_document` | Create a new form or component next to the open one and open it in a tab; save. | | `list_components`, `describe_component`, `search_icons`, `list_themes`, `read_docs`… | The same knowledge as the [MCP server](https://albakore.github.io/pyzed/guides/mcp/): components, icons, themes and this documentation. | | `validate_code`, `validate_form` | Check code and forms before handing them back. | | `list_files`, `read_file`, `search_text`, `write_file` | Work with files of the project, never outside its folder. | | `screenshot_form` | Renders the open form to a picture and looks at it, to check the layout as you see it. | | `run_app` | Runs the app (the project's main form, or another form) in the project's own environment without showing a window, and returns what it printed (errors and tracebacks included) and a picture of its window. | | `update_plan` | Shows the agent's plan as a checklist above the message box and keeps it current. | | `get_project`, `create_project`, `add_dependency`, `check_project`, `open_document` | Read the open project (forms, components, dependencies, entry point), create a new one and open it, add a package with uv, run the project's checks and open any of its files in a tab. | | `list_examples`, `read_example`, `new_form` | Pyzed's examples to learn from, and a starting form written for a description. | | `mcp__` | The tools of the [MCP servers](#mcp-servers) you added. | Tools that **create or write files** ask first while **Ask before writing files** is on: their card shows **Always allow**, **Allow** and **Reject**. **Always allow** is remembered: **Settings → Permissions** lists those tools, and **Ask again** undoes it. Turning **Ask before writing files** off skips the question. Changes to the open document never ask, since they can be undone. #### Images The agent can look at pictures: a mock-up to rebuild, a screenshot of a bug, the form as it looks now. Add them with **+**: - **Attach images…** opens the system's file dialog (PNG, JPEG, GIF or WebP, up to 5 MB each). - **Paste image** takes the picture you copied (a screenshot, an image from the browser). - **Screenshot of the form** renders the open form and attaches it. They show as thumbnails above the message and in the thread. Tools can answer with images too: `screenshot_form` and the tools of MCP servers that return pictures. The model must take images (Claude, GPT-4.1 and later, Gemini, most vision models of Ollama…). #### Building an application `/app` or a plain request ("Create an app to track my expenses") start a whole application, not only a form. The agent knows the editor (projects, forms, components, styles, data binding, the [MCP server](https://albakore.github.io/pyzed/guides/mcp/) and these docs) and, in each message, what is open: the project and its files, the form, the selection and the other tabs. It then: 1. Makes a plan with `update_plan`: a **Plan** card above the message box shows each step, ticks it off as it goes and folds away when everything is done (click its header to fold it yourself). 2. Creates a [project](https://albakore.github.io/pyzed/editor/projects/) with `create_project` (or uses the open one) and adds the packages it needs with `add_dependency`. 3. Writes the forms and components, places their controls, wires the events and writes the code, reading Pyzed's examples and docs when it needs them, and saves them. 4. Looks at each form with `screenshot_form`, and runs `check_project` and `validate_code` until they pass. 5. Runs the app with `run_app`, reads its errors and the picture of its window, fixes what is wrong and runs it again until it starts cleanly. Then it tells you what it built and how to run it (F5). It works on its own from start to end, for up to 150 model answers per message. For no interruptions, turn off **Ask before writing files** in the profile menu; while it is on, the agent asks before writing files and before running the app. #### Generating forms and components **View → Generate a form with AI…** and **Generate a component with AI…** (also in the command palette) open the panel with the start of the request written for you: finish the sentence and press Enter. For example: > Create a new form for a customer: name, email, a country drop-down and Save / Cancel buttons; Save validates > the email and shows a toast. The agent creates `customer.pzform` and `customer.py`, opens them, places and aligns the controls, wires the buttons and writes the handlers. With a component it creates the `.pzcomp` and its class, which then shows up in the toolbox like any [component](https://albakore.github.io/pyzed/editor/component-designer/). #### Inline assist In the code editor, Ctrl+Alt+I (or the ✦ button in its header) opens a bar over the code. Describe the change and press Enter: the model rewrites the handler (or the whole file, in whole-file mode) and the editor shows the result. **Undo** puts the previous code back, **Keep** closes the bar. Nothing is applied to the form until you click **Apply**, as with your own edits. (Image: Inline assist over a handler) #### Settings The settings open inside the panel, as in Zed: **⋯ → Agent settings**, **Settings → AI agents → AI agents settings…** or **AI agents settings…** in the command palette. The arrow in the header goes back to the thread. First come the everyday ones: the default model (with Claude's **Effort**, **Thinking** and **Fast mode** when Pyzed Agent answers with Claude; an external agent's own options are in the message box), the model providers and the permissions. **Advanced** unfolds the external agents, the MCP servers, the skills, the rules, Pyzed's MCP in your agents and more options. (Image: Providers, keys and the agent's options) ##### Providers | Provider | How to connect | Notes | | --- | --- | --- | | **Anthropic** | **Sign in with the browser**, API key, `ANTHROPIC_API_KEY` or [identity federation](#claude-without-an-api-key) | Claude models stream their reasoning summaries, use adaptive thinking and take effort and fast mode. An existing Anthropic CLI profile (`ant auth login`) is also used, but the CLI is not needed. | | **OpenAI** | **Sign in with the browser**, API key or `OPENAI_API_KEY` | | | **Google Gemini** | **Sign in with the browser**, AI Studio key or `GEMINI_API_KEY` | Through Gemini's OpenAI-compatible endpoint. | | **OpenRouter** | **Sign in with the browser** (OAuth), API key or `OPENROUTER_API_KEY` | OAuth with PKCE: the key OpenRouter issues for Pyzed goes to the keychain. | | **xAI**, **Mistral**, **DeepSeek**, **Groq** | **Sign in with the browser**, API key or `XAI_API_KEY`, `MISTRAL_API_KEY`, `DEEPSEEK_API_KEY`, `GROQ_API_KEY` | Their models are listed after signing in. | | **GitHub Copilot** | **Sign in with GitHub** (device flow), a fine-grained token with the **Models** permission or `GITHUB_TOKEN` | The models of GitHub Models (`openai/gpt-4.1`, …) with your account's limits. Signing in needs a **GitHub App client ID** (or `PYZED_GITHUB_CLIENT_ID`). | | **Google Vertex AI** | **Sign in with Google**, or the Google Cloud credentials of this computer (`gcloud auth application-default login`) | Gemini through Vertex AI's OpenAI-compatible endpoint, with no key. Needs a **Google Cloud project** and a **Location** (`global` by default); signing in with Google needs an **OAuth client** (Desktop app) whose secret goes to the keychain. | | **Ollama**, **LM Studio** | Nothing: they run on your computer | Change the **Server URL** if they run elsewhere. | | **OpenAI-compatible** | Server URL, and a key if the server needs one | vLLM, llama.cpp, a company gateway… | Click a provider to open it: **Sign in with the browser** (see [Signing in](#signing-in)), paste a key and **Save**, **Get a key** (opens the provider's key page), **Remove key**, and **Refresh models**, which asks the provider for its list of models so they show up in the model picker. (Image: The Anthropic provider open in the AI agents settings: an API key field, Sign in with the browser, Get a key and the workload identity federation fields) **Test the connection** checks the provider with what is set up and shows where the credentials come from. ##### Claude without an API key Open **Anthropic** and fill in **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 anywhere. First create the issuer and the federation rule in the Claude Console, then fill in: | Field | What goes there | | --- | --- | | **Federation rule** | The rule's ID, `fdrl_…`. | | **Organization ID** | Your organization's UUID. | | **Service account** | `svac_…`, when the rule doesn't name one. Optional. | | **Workspace** | `wrkspc_…` or `default`. Optional. | | **Identity token** | Where the token comes from: a **File** (the path of the JWT, or `ANTHROPIC_IDENTITY_TOKEN_FILE`), a **Command** that prints a fresh one (for example `gcloud auth print-identity-token`) or a **Variable** (`ANTHROPIC_IDENTITY_TOKEN`). | A saved API key wins over federation; remove it to use federation. The `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID` and `ANTHROPIC_IDENTITY_TOKEN_FILE` variables work too. ##### MCP servers **MCP servers** adds the tools of any [Model Context Protocol](https://modelcontextprotocol.io) server to the agent's. **Add an MCP server** asks for: - a **Name**; - the **Command**, as you would type it in a terminal (`npx -y @modelcontextprotocol/server-github`, `uvx mcp-server-fetch`…), **or the URL** of a remote server (Streamable HTTP); - **Secrets**: `NAME=value` pairs separated by `;`, passed as environment variables (or as headers for a URL). They go to the system keychain, never to `agents.json`. **Test** starts the server and lists its tools; the switch turns it off without removing it. Their tools show up as `mcp__`. Tools the server doesn't mark as read-only ask first while **Ask before writing files** is on, like file writes; with the **Ask** profile they are left out. ##### External agents **External agents** run Claude Code, Gemini CLI, Codex or any agent that speaks the [Agent Client Protocol](https://agentclientprotocol.com) inside the panel, as in Zed: - **Install from registry** lists the agents of the [ACP registry](https://agentclientprotocol.com/registry) with a search box. **Install** downloads the agent for your system (a program, or a package run with `npx` or `uvx`) into Pyzed's settings folder and adds it. When the registry has a newer version, its row offers **Update**; **Uninstall** removes it and its files. - **Add custom agent** takes any command, its arguments and its secrets. Start a thread with an agent from the **+** menu, or pick it in the model picker under **External agents**. The switch on each row hides it without removing it. The agent runs as its own program in the project folder and signs in its own way. When it needs a sign-in, the thread shows **Sign in to …** with one button per way the agent offers, as in Zed: - **In the browser** (a globe): the agent opens its sign-in page and the panel waits; once you approve, Pyzed comes back to the front and sends your message. - **In a terminal**: the agent's login (for example `claude /login` or `gemini` with its login option) opens in a terminal; the panel checks every few seconds and goes on by itself when you are signed in. - **With a key**: the agent reads a key from a variable; Pyzed opens the page to get one and the agent's **Secrets**, where it is kept in the keychain. Under WSL the browser and the terminal are the ones of Windows. Pyzed answers its requests: it reads and writes files only inside the project and never the ones that may hold secrets, and asks before acting according to the profile (**Ask** allows only reads, **Minimal** refuses every tool). With **Let external agents control the editor** on (the default), the agent gets the built-in agent's tools over MCP: it reads and changes the open form and its code, creates forms and components, runs the app with `run_app`, keeps a plan and knows Pyzed's components and docs. Its changes show up live in the editor, and **Keep** and **Reject** work as with the built-in agent. Pyzed serves these tools on `127.0.0.1` at a one-time address with a token only that agent receives (agents that only start local servers get a small bridge program). Its own file and shell tools stay its own. The plan the agent follows shows in the **Plan** card. ##### Pyzed's MCP in your agents When Pyzed Design starts, it looks for the AI agents installed on this computer (Claude Code, Claude Desktop, Cursor, Windsurf, Gemini CLI, Codex, VS Code and Zed) and adds [Pyzed's MCP server](https://albakore.github.io/pyzed/guides/mcp/) to the ones that do not have it yet, so they know Pyzed's components, docs and checks wherever you use them. A message then asks you to restart them: an agent reads its MCP servers when it starts. **Restart agents** restarts the external agents running in Pyzed; close and open the others. **Settings → Pyzed's MCP in your agents** lists each agent with **Add** or **Remove**. Pyzed adds the server once to each agent: if you take it out, Pyzed leaves it out. Only the `pyzed` entry of each agent's settings changes, a copy of the file is kept next to it (`.pyzed-backup`), and comments in settings files (Zed's and VS Code's have them) are kept. Turn **Add it to new agents on start** off to stop it. The agents start the server with [uv](https://docs.astral.sh/uv/) (`uvx --from "pyzed-ui[mcp]" pyzed mcp`; Pyzed Design brings its own `uv`). ##### Watching the agent work With **Follow the agent** on (the crosshair at the bottom of the message editor, on by default), the files the agent writes open as it works: forms and components in their tab, code and other text files in the code editor, and for an external agent, the file it is editing. A file that is already open shows the new text right away, unless it has changes of yours that are not saved. The message box keeps the keyboard, so you can keep typing. When the agent finishes, stops with an error, asks for your OK or waits for a sign-in while Pyzed Design is in the background, a desktop notification tells you. Turn it off with **Notify when the agent finishes** in the settings. ##### Your Claude subscription **Use my Claude subscription** (first in an empty panel, and in the model picker under **External agents**) uses your Claude Pro or Max plan through Claude Code, Anthropic's own agent, since Anthropic only lets its own apps sign in with a subscription: 1. It needs [Node.js](https://nodejs.org/en/download) 22 or later; without it, Pyzed opens the download page. Pyzed finds Node also when it was installed with nvm, volta, fnm or in `~/.local/bin`, even if Pyzed Design was opened from the desktop's menu. If an answer fails because Node.js is missing, **Install Node.js** opens the download page and **Use Pyzed Agent** sends the message to Pyzed's own agent instead. 2. It adds Claude Code as an external agent, picks it and lets it control the editor. 3. If Claude Code is not signed in yet, it opens in a terminal: choose to sign in with your Claude account and finish in the browser. Pyzed notices the sign-in, comes back to the front and is ready. Claude Code keeps its own sign-in; Pyzed never sees it. Usage counts toward your plan. When an answer fails because Claude Code is signed out, **Sign in to Claude** opens the terminal again. ##### Skills A skill is a folder with a `SKILL.md` file (a `name` and a `description` in its header, then instructions) and any files it needs, the same format Claude Code and other agents use. The agent sees the name and the description of each skill and reads the rest when a task needs it. Pyzed finds skills in the project (`.claude/skills/` and `.agents/skills/`), in your home folder (`~/.claude/skills/` and `~/.agents/skills/`) and in its own settings folder. **Skills** lists them with a switch to turn each one off. Paste the address of a Git repository and click **Install**, or **From a folder…**, to add skills; skills installed from Git offer **Update**. **Show in folder** opens the skill's folder. Claude Code reads its own skills; the switches apply to Pyzed Agent. ##### Where keys are stored Keys and tokens go to the **operating system's keychain**: Keychain on macOS, Credential Manager on Windows, Secret Service (GNOME Keyring, KWallet) on Linux, under the service `pyzed`. They are never written to Pyzed's settings files, to threads, or to the project. If no keychain is available, a key you paste is kept in memory until the designer closes, and the settings say so; use an environment variable instead in that case. Environment variables, apps opened from the Finder or the Start menu, MCP secrets and good practices are covered in [API keys and credentials](https://albakore.github.io/pyzed/ai/api-keys/). The rest stays on your computer too: the settings in `agents.json` and the threads in `agents/threads/`, both in [Pyzed's settings folder](https://albakore.github.io/pyzed/editor/settings/#where-they-are-saved). Nothing is sent anywhere except the requests to the provider you picked. ##### Agent options | Setting | What it does | | --- | --- | | **Default model** | The model new threads start with. | | **Effort** | How much the model thinks before answering (`low` … `max`). Claude only; `high` by default. | | **Thinking** | Lets Claude think before answering. Off, it answers sooner. Also in the message box. | | **Fast mode** | Faster answers at a higher price (Claude Opus 5.5 and Opus 5). Also in the message box. | | **Ask before writing files** | Creating forms and components and saving files wait for your OK. On by default; also in the profile menu. | | **Show thinking** | Shows the model's reasoning summaries in the thread. | | **Follow the agent** | Opens the files the agent writes as it works. | | **Notify when the agent finishes** | A desktop notification while the window is in the background. | | **Rules** | Instructions added to every thread: "Answer in Spanish", "Name buttons with a `_button` suffix"… | | **Project rules** | The project's own rules file, as in Zed: the first of `.rules`, `.cursorrules`, `.windsurfrules`, `.clinerules`, `.github/copilot-instructions.md`, `AGENT.md`, `AGENTS.md`, `CLAUDE.md` or `GEMINI.md` at its root. Pyzed Agent reads it in every thread of that project; **Open** edits it, **Create AGENTS.md** starts one. External agents read their own (Claude Code reads `CLAUDE.md`). | #### Getting good results - **Start from the screen, not the widgets.** *"A customer form: name, email, a country drop-down and Save / Cancel; Save validates the email and shows a toast"* gives the agent the goal and the rules at once. - **Select, then ask.** Select a group of controls and say *"put these in a row with 8 px spacing"*. - **Use @ for precision.** `@email_input` plus *"only accept company addresses"* leaves no doubt about which control. - **Show it a picture.** Attach a mock-up or a screenshot of the bug, or ask for `/look` to have it check the form. - **Let it check itself.** Finish requests with *"then run /fix"*, or ask it to validate before it answers. - **Keep rules in the settings.** *Rules* are added to every thread: *"Answer in Spanish"*, *"Use the Gruvbox Dark theme"*. - **Review, then keep.** After a run, **Agent changes** offers **Keep** or **Reject** for the whole run. More prompts that work well are in the [prompting guide](https://albakore.github.io/pyzed/ai/prompting/). #### History **Thread History** in the **⋯** menu (or `/history`) lists your threads grouped by date (today, yesterday, this week…), with a search box and the agent of each thread. Click one to continue it (the agent remembers the whole conversation), or delete it. The last 200 threads are kept. #### Working on the active project The agents work on the project you have open: its folder is where they read and write files, where external agents and MCP servers start, and where skills and project rules are looked up. Each message also tells them where the project lives (this computer, or a WSL distribution such as Ubuntu, where commands run in Linux), its Git branch and the other projects of the window, so a project in WSL is treated as Linux from the start. Each project has its own threads, like in Zed: switching to another project (the project picker in the title bar) opens a new thread there and the history shows that project's threads. The thread you were in stays in its own project's history. A run that is still going finishes on the project where it started. #### Using agents in your own app The agent is not tied to the designer: `pyzed.agents` gives any app the same providers, keychain storage and agent loop. See [AI agents in your app](https://albakore.github.io/pyzed/guides/agents/). ### Menus and commands > The title bar, every menu entry, the toolbar, the command palette, Add control and the dockable panels of Pyzed Design, the Python GUI builder. Source: https://albakore.github.io/pyzed/editor/menus-and-commands/ Every command of the editor lives in one table, so the menus, the command palette, the keyboard and the shortcut list always agree. This page lists them all. #### The title bar From left to right: the **☰** menu, the environment under WSL, the project's name (click it for the project picker: the projects of this window and the recent ones), the git branch (click it to switch), the file name (click it to open another file), a dot when there are unsaved changes and a **Component** or **Shader** badge in those views. In the middle: **Run** (F5), **Stop** and the search field, which opens the [command palette](#command-palette). At the right: the **View** menu, the **Help** menu and the **Settings** gear, followed by the window buttons when Pyzed Design draws its own frame (see [The interface](https://albakore.github.io/pyzed/editor/interface/#the-window-frame)). #### ☰ File menu (Image: The ☰ menu with recent files) | Entry | Keys | Does | | --- | --- | --- | | New form… | Ctrl+N | The New dialog, on Form. | | New component… | Ctrl+Shift+N | The New dialog, on Component. | | Open… | Ctrl+O | Lists the project's forms, components, shaders and animation libraries; **Browse…** for any file. | | Go to file… | Ctrl+P | Finds a project file by name and opens it; see [Go to file](https://albakore.github.io/pyzed/editor/projects/#go-to-file). | | Open recent project… | Ctrl+Alt+O | The project picker of the title bar; see [Several projects in one window](https://albakore.github.io/pyzed/editor/projects/#several-projects-in-one-window). | | Save | Ctrl+S | Saves the document and its code. | | Save as… | Ctrl+Shift+S | Saves under a new path. | | Settings… | Ctrl+, | The [settings](https://albakore.github.io/pyzed/editor/settings/). | | *Recent* | | The last five files opened. | | Run | F5 | [Runs](https://albakore.github.io/pyzed/editor/console-and-running/) the form or previews the component. | | Stop | Shift+F5 | Closes the running form. | | Run project script | | Starts the project's `[tool.pyzed] script` with `uv run` (see [Welcome and examples](https://albakore.github.io/pyzed/editor/examples/#running-it)). | | Choose startup file… | | Picks the form or script the app starts from (see [The startup file](https://albakore.github.io/pyzed/editor/projects/#the-startup-file)). | | Use this form as startup file | | Makes the open form the startup file. | | Edit code file | F7 | Opens the whole code file in the code editor. | #### Edit commands The Edit commands have no menu of their own: they are on the toolbar, in the control's context menu, on the keyboard and in the command palette. | Command | Keys | | --- | --- | | Undo / Redo | Ctrl+Z / Ctrl+Shift+Z | | Cut / Copy / Paste | Ctrl+X / Ctrl+C / Ctrl+V | | Duplicate | Ctrl+D | | Delete | Delete | | Select all | Ctrl+A | | Add control… | Ctrl+Shift+A | | Group in a row / in a column / in a grid | | | Convert indentation to tabs / to spaces | (palette) the file in the code editor; see [Tabs or spaces](https://albakore.github.io/pyzed/editor/code-editor/#tabs-or-spaces) | #### View menu (Image: The View menu) | Entry | Keys | Does | | --- | --- | --- | | Command palette… | Ctrl+Shift+P | Runs any command by name. | | Left dock / Right dock / Bottom dock | Ctrl+B / Ctrl+Alt+B / Ctrl+J | Shows or hides each dock (checked when shown). | | Console | Ctrl+Shift+Y | Shows the console and keeps it open. | | Logs | Ctrl+Shift+U | Shows the Logs panel: the editor's events and errors. | | Agent panel | Ctrl+Alt+A | Shows or hides the [AI agent](https://albakore.github.io/pyzed/editor/ai-agents/) (checked when shown). | | Generate a form with AI… / Generate a component with AI… | | Opens the agent with the start of a request ("Create a new form for…") for you to finish. | | Move tab to new window / Move tab to main window | | Gives the document in front a window of its own, or brings it back among the main window's tabs. See [Document tabs](https://albakore.github.io/pyzed/editor/files-and-scripting/#document-tabs). | | Toolbar, Status bar | | Shows or hides them. | | Grid | G | Shows or hides the dot grid. | | Snap to grid | | Toggles snapping. | | Smart guides | | Toggles the alignment guides. | | Show all hidden controls | | Draws every hidden control dimmed on the canvas. Off, a hidden one appears only while it is selected. | | Preview window frame… | | Shows the form on the canvas in the window frame of another platform: macOS, Windows 11, GNOME, Ubuntu or KDE Plasma (checked while it is not this system's). | | Preview theme…, Preview language… | | Shows the canvas (and the next run) in another theme or language without changing the form (checked while on). See [Preview in another theme or language](https://albakore.github.io/pyzed/editor/data-binding/#preview-in-another-theme-or-language). | | Reset panel layout | | Puts every panel back in its default dock and size, and closes the panel windows. | | Save layout as… | | Saves where every panel is, the dock sizes and the panel windows under a name. | | Delete this layout | | Removes the saved layout in use. | | *Panels* | | One entry per panel, with the dock it is in (`Properties · Right`, or `Window` when it floats): click to bring it to the front. | | *Layouts* | | The work layouts (Default, Coding, Focus, Events and code, Code with the forms, and yours); the one in use is checked. | The toggles are remembered between sessions. #### Help menu (Image: The Help menu open from the question mark of the title bar) | Entry | Keys | Does | | --- | --- | --- | | Keyboard shortcuts | F1 | A searchable list of every shortcut. | | Reference | Shift+F1 | The [built-in reference](https://albakore.github.io/pyzed/editor/reference/), on the page of the selected control or of the name under the cursor. | | Welcome | | The [welcome window](https://albakore.github.io/pyzed/editor/examples/): start or open a project, recent projects and the example gallery. | | Example projects… | | The welcome window with every example. | | Documentation | | Opens this documentation in the browser. | | Report a problem | | Opens the issue tracker. | | What's new | | A tab with the release notes of this version (or of the update waiting to install). | | Check for updates | | Looks for a newer release on GitHub now; see [Updates](https://albakore.github.io/pyzed/download/#updates). | | Check for updates automatically | | Toggles the daily check (on by default). | | Get preview releases | | Also offers pre-releases (`X.Y.Z-rc.N`): the preview channel. See [Updates](https://albakore.github.io/pyzed/download/#updates). | | About Pyzed Design | | The versions of Pyzed, Python and Iced, the platform, and links. | (Image: The About dialog) #### The toolbar Under the document tabs, in groups from left to right: 1. **New…** (a form, a component or a shader, picked in the New dialog), **Undo** and **Redo**. 2. While something is selected: **Align left, centers, right, top, middles, bottom**; **Center in form horizontally, vertically**, then **Distribute** (three or more controls) and **Same width, height** (two or more); **Bring to front**, **Send to back** and **Delete**. With nothing selected a hint takes their place. 3. At the end, how the canvas behaves: **Show grid**, **Snap to grid**, **Smart guides**, **Resize contents with their anchors**, **Fit selection to grid**, and **Edit code file**. **Open** and **Save** are in the ☰ menu and on Ctrl+O and Ctrl+S. Every button has a tooltip with its shortcut, and the editing tools scroll sideways when the window is narrow. Hide it with **View → Toolbar**. See [Toolbox and canvas](https://albakore.github.io/pyzed/editor/toolbox-and-canvas/#align-distribute-and-size). #### Command palette The search field in the middle of the title bar, or Ctrl+Shift+P, opens a search box over every command that can run right now, as `Group: Command` with its shortcut, and over the places of the editor: `Panel: Properties` shows a panel, `Editor settings: Cursor` opens that page of the [editor settings](https://albakore.github.io/pyzed/editor/code-editor/#editor-settings) (the keys are in `File: Keymap editor`), and `Recent: login.pzform` opens a recent file. Type a few letters (the search is fuzzy: `vgr` finds *View: Grid*), ↑/↓ to move, Enter to run, Esc to close. It also reaches commands that have no menu, such as the Edit ones. (Image: The command palette) #### Add control Ctrl+Shift+A is the palette for components: a fuzzy-searchable list of every component with its category. Enter adds the highlighted one, centered on the form, and selects it. From a container (its **+** in the outline, or **Add inside…**), it adds inside that container instead. #### Keyboard shortcuts dialog F1 lists every shortcut by group (File, Edit, View, Run, Help, Canvas, Code) with a search box. See the [complete table](https://albakore.github.io/pyzed/editor/keyboard-shortcuts/). (Image: The keyboard shortcuts dialog) #### Panels and docks Eight panels live in four docks around the canvas: | Panel | Default dock | Shows | | --- | --- | --- | | **Toolbox** | Left | The toolbox and the outline. | | **Project** | Left | The project's files. | | **Dependencies** | Left | Dependencies, ruff and ty (only with a project open). | | **Properties** | Right | The property grid. | | **Events** | Right | The events of the selection. | | **Agent** | Right | The [AI agent](https://albakore.github.io/pyzed/editor/ai-agents/). | | **Code** | Bottom | The code editor (only while a handler or the file is open). | | **Console** | Bottom | The console (once it has something to show). | Each dock shows one tab at a time. At the end of the tab bar: - the **dock button** opens a menu to **Move *panel* to** the left, right, bottom or top dock, the **Center** (next to the form tabs), a **New window**, or **Hide dock**; - the **×** hides the dock (bring it back with its shortcut or the View menu). Any tab goes anywhere: drag a panel's or a form's tab to reorder it, onto another row or over a dock or the center to move it there, or out of the window to float it in a window of its own; drop more tabs on a window to stack them. The place a tab would land in lights up while you drag. In a window of its own the dock button offers **Dock everything here back** and the **×** does the same. (Image: Moving the Properties panel to another dock) Drag the edge of a dock that faces the canvas to resize it (each dock has a minimum size). Where every panel is, which docks are hidden, their sizes and the panel windows are saved with your settings, and **View → Save layout as…** keeps them as a work layout to switch to later; **View → Reset panel layout** (or **Settings → Panels → Reset**) restores the default. ```mermaid flowchart LR subgraph Left Toolbox Project Dependencies end subgraph Center Top["Top dock (empty by default)"] Canvas subgraph Bottom Code Console end end subgraph Right Properties Events Agent end ``` :::tip On a wide screen, move **Events** to the left dock (or the top one) to see Properties and Events at the same time; on a small one, hide the left dock with Ctrl+B while you write code, and use Ctrl+Shift+A to add controls without the toolbox. ::: #### Notifications Short messages appear in a corner for a few seconds: saved, copied, opened, a command that failed... Click the × to dismiss one; at most four are shown at once. ### Built-in reference > The searchable class reference inside Pyzed Design, contextual help with Shift+F1, and the same reference browser in your own apps. Source: https://albakore.github.io/pyzed/editor/reference/ Like Godot's, Pyzed Design carries its own documentation: every form control, every function and class of the library and every component of your project, with signatures, parameters, properties, events and examples. It works offline and in the editor's language. (Image: The reference open on the Button page, with the class list on the left) #### Opening it | How | Opens | | --- | --- | | Shift+F1 with a control selected | The page of that control's kind (Button, Table…). | | Shift+F1 in the code editor | The page of the name under the cursor: `form.email.text` opens Text box, `ui.button` and `pz.text` open their library pages. With no page for it, the name is searched. | | The **book** button in the Properties and Events headers | The page of the selected control. | | **Help** in a control's context menu | The page of that control. | | **Help → Reference** | The reference, where you left it. | The reference opens as a **Reference** tab next to your documents. Close it like any tab; it remembers the page, the search and the history. #### Finding a page The left column is the **class list**, in sections: | Section | Pages | | --- | --- | | **Form controls** | Every toolbox control, in toolbox order: description, an example handler, its properties (with type and default), its events (with the handler name) and the methods every control has. | | **Form code** | How a form's code works: the `form` object, a control from code (`ControlProxy`) and the `event` a handler receives. | | **App and running**, **Widgets**, **Components (pyzed.ui)**… | The library, one section per page of the [API reference](https://albakore.github.io/pyzed/reference/app/), with live signatures. | | **Your components** | Every `pz.Component` subclass of your project, with its props, state, events and methods taken from your code and docstrings. | Click a section to fold it. Type in **Search the reference** to filter everything: exact names come first, every word must match (`tab bar` finds `ui.tab_bar`), and each result shows its first sentence. (Image: Searching "tab") #### Reading a page - The header shows the page's icon, its name, its kind (control, function, class…) and where it is in the list. - Links to other names open their page inside the reference; the ← and → buttons go back and forward. - **Open online** opens the same page on this website. - Library descriptions come from the same source as the website, and signatures and members are read from the installed code, so the reference always matches the version you run. #### Documenting your own components Write docstrings: the class docstring becomes the page's description, `pz.state(..., doc="…")` describes a state variable, and the docstrings of public methods describe them. The page updates as soon as the code is loaded again (hot reload included). ```python class Counter(pz.Component): """A number with + and − buttons.""" step: int = 1 value = pz.state(0, doc="The current count.") def reset(self): """Puts the count back to zero.""" self.value = 0 ``` #### In your own apps The reference is part of the library, not only the editor, so you can ship it inside your app, test it, or add your own sections: ```python import pyzed as pz from pyzed import reference, ui class Docs(pz.App): def __init__(self): self.help = reference.Browser("pyzed.ui.button") def view(self): return ui.reference_browser(self.help, on_open_url=print) reference.search("tab bar")[0].id # "pyzed.ui.tab_bar" reference.lookup("ui.button") # "pyzed.ui.button" reference.page("pyzed.text").body # Markdown ``` `reference.register(reference.Provider(...))` adds a section of your own (your app's help, a plugin's API). See [Built-in reference](https://albakore.github.io/pyzed/reference/app/#built-in-reference) and [`ui.reference_browser`](https://albakore.github.io/pyzed/reference/ui/#reference_browser) in the API reference. ### Keyboard shortcuts > Every keyboard shortcut of Pyzed Design, the Python GUI designer: global commands, the canvas, dialogs and the built-in code editor. Source: https://albakore.github.io/pyzed/editor/keyboard-shortcuts/ Press F1 in the editor for a searchable version of this list. On macOS, Cmd works wherever a global shortcut says Ctrl. :::tip These are the default keys. Change any of them in the [keymap editor](https://albakore.github.io/pyzed/editor/keymap/) (Ctrl+K Ctrl+S); F1 always shows the keys in force. ::: #### Global These work anywhere, even while you type in the code editor. | Keys | Command | | --- | --- | | Ctrl+N | New form | | Ctrl+Shift+N | New component | | Ctrl+O | Open | | Ctrl+P | Go to file | | Ctrl+Alt+O | Open recent project (the project picker) | | Ctrl+S | Save | | Ctrl+Shift+S | Save as | | Ctrl+, | Settings | | Ctrl+K Ctrl+S | [Keymap editor](https://albakore.github.io/pyzed/editor/keymap/) | | Ctrl+Shift+P | Command palette | | Ctrl+Shift+A | Add control | | Ctrl+B | Show or hide the left dock | | Ctrl+Alt+B | Show or hide the right dock | | Ctrl+J | Show or hide the bottom dock | | Ctrl+Shift+Y | Console | | Ctrl+Shift+U | Logs | | Ctrl+Alt+A | Show the [Agent panel](https://albakore.github.io/pyzed/editor/ai-agents/) | | Ctrl+Alt+I | [Inline assist](https://albakore.github.io/pyzed/editor/ai-agents/#inline-assist) in the code editor | | F5 | Run | | Shift+F5 | Stop | | F7 | Edit code file | | F1 | Keyboard shortcuts | | Shift+F1 | [Reference](https://albakore.github.io/pyzed/editor/reference/) of the selected control or of the name under the cursor | While a dialog is open, only the palette, Settings, Keyboard shortcuts and Keymap editor keys act; the others wait until it is closed. #### Canvas These act when the canvas has the keyboard: after you click the form, a control, a toolbox row or an outline row. Moving the pointer into a dock gives the keyboard to that panel (so typing in a field never moves a control), and the code editor keeps it while you type; click the canvas again to get the canvas shortcuts back. | Keys | Action | | --- | --- | | ← → ↑ ↓ | Move the selection by 1 px | | Shift+arrows | Move it by one grid step (1 px with snapping off) | | Delete, Backspace | Delete the selection | | Ctrl+C | Copy | | Ctrl+X | Cut | | Ctrl+V | Paste | | Ctrl+D | Duplicate | | Ctrl+A | Select every control | | Ctrl+G | Group the selection in an invisible panel | | Ctrl+Shift+G | Ungroup: take the children out of the selected container | | Shift+A | Add auto layout: wrap the selection in a flex row or column | | Ctrl+Z | Undo | | Ctrl+Shift+Z, Ctrl+Y | Redo | | Enter | Open the code of the selected control's main event | | G | Show or hide the grid | | Esc | Cancel a drag, close a menu, or drop the placing tool and go back to the pointer | #### Mouse | Gesture | Action | | --- | --- | | Click | Select a control (on a selected one: make it the primary one) | | Drag a control | Move the selection; drop on a container to put it inside | | Drag a handle | Resize | | Drag on an empty spot | Rubber-band selection | | Double-click a control | Open its main event's code, or a designed component in its tab | | Right-click | Context menu | | Drag a number sideways (Properties) | Lower or raise it; double-click it to type | | Drag from the toolbox | Place a control where you drop it | | Drag an outline row | Reorder, or move into another container | | Drag the grip under the form | Resize the form | | Ctrl+wheel in the code editor | Font size | | Ctrl+click in the code editor | Go to the definition | | Rest the mouse on a name in the code editor | Its type, documentation and problems | #### Lists, palettes and dialogs | Keys | Action | | --- | --- | | ↑ / ↓ | Move in the command palette and Add control | | Enter | Run the highlighted command, add the highlighted control, submit a dialog's field | | Esc | Close the dialog, palette or menu | #### Project tree After a click in the [Project tab](https://albakore.github.io/pyzed/editor/projects/#files)'s tree: | Keys | Action | | --- | --- | | ↑ ↓ | Move the selection | | ← → | Close / open the folder | | Enter | Open the file | | Ctrl+N / Alt+N | New file / new folder | | F2 | Rename | | Ctrl+X / Ctrl+C / Ctrl+V | Cut / copy / paste | | Ctrl+D | Duplicate | | Ctrl+Z / Ctrl+Shift+Z | Undo / redo a file change | | Delete / Ctrl+Delete | Move to the trash / delete for good | | Ctrl+Alt+R | Reveal in the file manager | | Ctrl+Shift+Enter | Open in the default app | | Alt+Shift+C / Ctrl+Alt+Shift+C | Copy the path / the relative path | #### Code editor | Keys | Action | | --- | --- | | Ctrl+Space | Show completions | | ↑ / ↓ | Move in the completion list (wraps around) | | Page Up / Page Down | Move nine rows in the list, or a page in the code | | Enter, Tab | Accept the completion | | Esc | Close the completion list | | F12 / Ctrl+F12 | Go to the definition / the type's definition | | Alt+← / → | Go back / forward | | Shift+F12 | Find all references | | F2 | Rename the symbol | | Ctrl+. | Quick fixes and refactorings | | Alt+Shift+O | Organize imports | | Tab | Indent with spaces | | Enter | New line, keeping the indentation (one level more after `:`) | | Backspace | In the indentation, remove one level | | Ctrl+Shift+[ | Fold the block around the cursor | | Ctrl+Shift+] | Unfold it | | Ctrl+= / Ctrl+- / Ctrl+0 | Bigger, smaller, default font | | Ctrl+← / → | Move by word | | Ctrl+Home / End | Start or end of the code | | Shift+movement | Select | | Ctrl+A, Ctrl+C, Ctrl+X, Ctrl+V | Select all, copy, cut, paste | | Ctrl+F / Ctrl+H | Find / find and replace | | F3 / Shift+F3 | Next / previous match | | Ctrl+G | Go to line | | Alt+Z | Wrap long lines on or off | | Alt+click | Add a cursor | | Ctrl+Alt+↑ / ↓ | Add a cursor above / below | | Ctrl+D | Select the next match as another cursor | | Ctrl+Shift+L | Select every match | | Alt+↑ / ↓ | Move the line up / down | | Alt+Shift+↑ / ↓ | Duplicate the line up / down, on every cursor (also Ctrl+Alt+Shift+↑ / ↓) | | Ctrl+Shift+D | Duplicate the selection of every cursor (the line without one) | | Ctrl+Shift+K | Delete the line | | Ctrl+/ | Toggle comment | | Ctrl+Z / Ctrl+Shift+Z | Undo / redo | | Ctrl+K Ctrl+0 / J | Fold / unfold everything | The full list is in [the code editor component](https://albakore.github.io/pyzed/components/code/#multiple-cursors-and-zeds-keys). Every one of these keys can be changed in the [keymap editor](https://albakore.github.io/pyzed/editor/keymap/). ### Keymap > Change any keyboard shortcut of Pyzed Design, like Zed's keymap: a keymap editor with search and conflicts, and a keymap.json file with contexts. Source: https://albakore.github.io/pyzed/editor/keymap/ Every key in Pyzed Design comes from one keymap, like Zed's: the menus, the command palette, the canvas, the project tree, the code panel and the code editor itself. You can change any of them, add more keys to an action, take keys away and put the defaults back, from a window or by editing a file. #### The keymap editor Open it with **File → Keymap editor**, Ctrl+K Ctrl+S (press both in turn), **Settings → Key bindings** or **Editor settings → Key bindings**. (Image: The keymap editor searching for "save": Save with its default keys and Save as with keys the user changed) Each row is an action with one of its keys: - **Action**: its name and the name it has in `keymap.json` (`workspace::SaveAs`). - **Keys**: the keys that run it, or a dash when it has none. - **Context**: where the keys work (see [Contexts](#contexts)). - **Source**: **Default**, or **User** for the keys you set. Type in the search field to filter by name, keys or context. **Changed** shows only the actions you changed and **Conflicts** the keys that run two actions in the same place. ##### Change keys 1. Click the pencil of a row (or **+** to give the action one more key). 2. Press the new keys. For a chord, press two in turn, such as Ctrl+K then Ctrl+G. 3. Choose the context if it should work somewhere else. 4. Press Enter (or the check mark) to save, or Escape to cancel. (Image: Recording Ctrl+K Ctrl+G for Go to line in the code panel) While you record, the line above the list warns you when those keys already run another action in that context. :::tip The trash icon takes the keys away, the arrow puts the action's default keys back, and **Reset all** puts back every default key. ::: ##### Conflicts When the same keys run two actions in the same context, both rows say so. The last binding wins: yours over the defaults. The row of the action that runs says **Overrides**, and the other says **Shadowed by**. (Image: Ctrl+D bound to Grid on the canvas, shadowing Duplicate) #### Contexts A context is the part of the designer that has the keyboard. Keys are looked up from the innermost context to **Workspace**, so the same keys can do different things in different places: Ctrl+N is **New form** anywhere, but **New file** in the project tree. | Context | Where | Inside | | --- | --- | --- | | `Workspace` | Anywhere in the designer | (outermost) | | `Canvas` | The form canvas, after you click the form or a control | Workspace | | `ProjectPanel` | The project tree | Workspace | | `CodePanel` | The code panel: completions, find, go to line, wrapping | Workspace | | `Editor` | The text of the code editor | CodePanel | :::note In the `Editor` context a chord must start with Ctrl+K, like the editor's own chords. Inside the editor, Ctrl+K waits for the editor's second key, so use the menu to open the keymap editor from there. ::: #### The keymap.json file Your changes are saved in `keymap.json`, next to the designer's settings: | System | Folder | | --- | --- | | Windows | `%APPDATA%\pyzed` | | macOS | `~/Library/Application Support/pyzed` | | Linux | `~/.config/pyzed` (or `$XDG_CONFIG_HOME/pyzed`) | `PYZED_CONFIG_DIR` changes the folder. **Edit keymap.json** in the keymap editor opens it in the code panel, and it applies as soon as you save it. It uses Zed's format: a list of blocks, each with a `context` and its `bindings` from keys to an action. `null` takes a default key away. Comments (`//`) and trailing commas are allowed. ```json // Pyzed Design key bindings [ { "context": "Workspace", "bindings": { "ctrl-alt-s": "workspace::SaveAs", "ctrl-shift-s": null } }, { "context": "Editor", "bindings": { "ctrl-alt-d": "editor::DuplicateLineDown", "ctrl-d": null } }, { "context": "Canvas", "bindings": { "ctrl-k ctrl-g": "workspace::Grid" } } ] ``` - **Keys** are written like Zed's: modifiers `ctrl`, `alt`, `shift` (`cmd` and `super` count as `ctrl`) joined with `-` to a key (`a`, `f2`, `enter`, `space`, `up`, `pageup`...). A chord is two keys separated by a space. - **Actions** are written `namespace::Action`: `workspace::` for the commands of the menus, `canvas::`, `project_panel::`, `code_panel::` and `editor::`. The keymap editor shows each action's name. :::caution If the file has a mistake (an unknown action, keys or context), the rest still works and the keymap editor lists the problems at the top. ::: In earlier versions, the code editor's keys were saved under `"keymap"` in `designer.json`. The designer moves them to `keymap.json` the first time it starts. #### In your own apps The code editor component takes the same kind of keymap: `ui.editor(keymap={"ctrl-alt-d": "duplicate_line_down", "ctrl-d": None})`. See [the code editor component](https://albakore.github.io/pyzed/components/code/#multiple-cursors-and-zeds-keys). ### Settings > The preferences of Pyzed Design and its code editor: search, profiles, per-project settings, Reset to default, the project's renderer (GPU or CPU), where settings are saved and how to translate the visual designer. Source: https://albakore.github.io/pyzed/editor/settings/ **Settings** (Ctrl+,, the gear at the right of the title bar, or the ☰ menu) holds every preference of the designer and of its code editor in one dialog. Changes apply at once and are saved right away; **Done** closes the dialog. (Image: The settings dialog) - **The index** on the left groups the pages: *Designer* (General, Fonts and size, Form canvas, Running, Logs and errors, Telemetry), *Code editor* (Appearance, Cursor, Editing, Deleting, Scrolling and wrapping, Code lens and diagnostics, Language servers) and, with a project open, *Project* (Rendering). A number next to a page counts its changed settings. - **Search settings…** (focused when the dialog opens) finds a setting by its name or its help, in English or in your language, across every page. - **A dot** after a setting's name means it is not at its default; the **↺** button at its right puts it back. - **Reset all** puts every setting of the profile back to its default. It asks for a second press, so one click can not lose your settings. - **Open settings file** opens the JSON file the page writes to. (Image: Searching the settings) #### Profiles A profile is a whole set of preferences you can switch between, like VS Code's profiles: one for everyday work, one with big fonts for presentations, one for a small laptop screen... The **Profile** list at the top right switches; switching applies the profile at once (theme, fonts, sizes, editor options). | Button | What it does | | --- | --- | | **+** | A new profile, a copy of the one in use, with the name you type. | | **✎** | Renames the profile in use. | | **🗑** | Deletes the profile in use and goes back to *Default*. *Default* can not be renamed or deleted. | Profiles only hold preferences: the panel layout, the recent files and the [keymap](https://albakore.github.io/pyzed/editor/keymap/) stay the same whatever the profile. (Image: A profile with two changed settings) #### Project settings With a project open, **User | Project** at the top says where your changes go: - **User**: your own settings, in the active profile. They apply to every project. - **Project**: this project's own settings, saved in `.pyzed/settings.json` inside the project, like Zed's `.zed/settings.json`. They win over the user's while the project is open, and travel with it (commit the file so everyone who opens the project gets them). In the Project scope, the dot marks the settings the project sets, and **↺** removes them so the user's value applies again; **Remove project settings** removes all of them. In the User scope, a setting the project sets shows *This project sets its own*. A few settings belong to the user only (the welcome window, the window frame, updates, the log file). (Image: The project scope: the project uses tabs of 2) ```json { "editor": { "tab_size": 2, "hard_tabs": false, "format_on_apply": true }, "designer": { "grid": 4, "snap": true } } ``` #### Rendering **Project → Rendering** chooses how the project's app draws its windows. It is saved in the project's `pyproject.toml`, so it applies when the designer runs the project, when you run it with `pz.run` or `pyzed run`, and in the [exported app](https://albakore.github.io/pyzed/editor/exporting/). | Setting | Options | What it does | | --- | --- | --- | | **Renderer** | Auto, GPU, CPU | *Auto* (the default) uses the GPU when there is a real one and the CPU renderer elsewhere (virtual machines, remote desktops). *GPU* always uses it: [shaders](https://albakore.github.io/pyzed/shading/shaders/) need it. *CPU* (tiny-skia) starts faster and uses less memory, and works everywhere. | | **Graphics API** | Auto, Vulkan, Metal, DirectX 12, OpenGL | The GPU's API. *Auto* tries Vulkan, Metal or DirectX 12, then OpenGL. | | **GPU** | Auto, Fast, Saves battery | On laptops with two GPUs: the dedicated one or the integrated one. | | **Vsync** | Auto, On, Off | *On* waits for the screen (no tearing); *Off* draws as fast as it can (to measure performance). | (Image: The project's renderer) ```toml [tool.pyzed.rendering] renderer = "gpu" # "auto", "gpu" or "cpu" gpu_api = "vulkan" # "auto", "vulkan", "metal", "dx12" or "gl" power = "low" # "auto", "high" or "low" vsync = "off" # "auto", "on" or "off" ``` In code, `App.rendering` does the same for one app, and wins over `pyproject.toml`: ```python class Game(pz.App): rendering = pz.Rendering(renderer="gpu", vsync="off") ``` The settings become the environment variables Iced and wgpu read (`ICED_BACKEND`, `WGPU_BACKEND`, `WGPU_POWER_PREF`, `ICED_PRESENT_MODE`); one you set yourself still wins. While a project sets its own renderer, Run always starts the form in its own process, since the designer's window already has one. #### Appearance | Setting | What it does | | --- | --- | | **Theme** | The editor's theme (One Dark, One Light, Ayu, Gruvbox...). Forms keep their own theme. | | **Language** | *System default*, English or Español: the language of the menus and panels. | | **Toolbar** | Shows or hides the toolbar. | | **Status bar** | Shows or hides the status bar. | | **Welcome window** | Opens the [welcome window](https://albakore.github.io/pyzed/editor/examples/) at startup (on by default). | | **Custom window frame** | On by default: Pyzed Design draws its own title bar and window buttons, in the style of your system. Turn it off to use the system's frame (applies the next time it opens). | | **Form preview frame** | The window frame the canvas shows around the form: this system's, macOS, Windows 11, GNOME, Ubuntu or KDE Plasma. | #### Fonts and size | Setting | What it does | | --- | --- | | **Interface font** | The font of the menus, panels, dialogs and properties. *Default* is IBM Plex Sans; the list has the bundled fonts, the system's and any loaded with `pz.load_font`. | | **Interface size** | Makes the whole editor bigger or smaller, from 75% to 150%, panel windows included. **View → Zoom in the interface**, **Zoom out the interface** and **Reset the interface zoom** do the same in 5% steps; give them keys in the [keymap editor](https://albakore.github.io/pyzed/editor/keymap/). | | **Code font** | The font of the code editor and other code. *Default* is IBM Plex Mono. | | **Code font size** | 8 to 32 points, the same setting as in the [code editor's settings](https://albakore.github.io/pyzed/editor/code-editor/#editor-settings). | | **Code line height** | Compact, Standard or Comfortable. | Every change shows at once, without restarting, and is saved for the next time. :::tip A monospaced font keeps code columns aligned: pick one for **Code font** (its name usually says *Mono* or *Code*). For the interface, any font with regular, medium and semibold weights looks best. ::: #### Form canvas | Setting | What it does | | --- | --- | | **Show grid** | The dot grid on the canvas (also G). | | **Snap to grid** | Rounds positions and sizes to the grid. | | **Smart guides** | Aligns to other controls while dragging. | | **Outline eye** | **Hidden when running** (the default): the eye turns Visible off; the control is also left out of the canvas and shown while selected. **Hidden in the editor only**: the eye hides it on the canvas only and the running form still shows it. | | **Show all hidden controls** | Draws every control whose Visible is off, dimmed. Off (the default), a hidden control appears only while it or something inside it is selected. | | **Grid size for new forms** | The grid step (2 to 64) given to forms and components you create. Each form then keeps its own in Properties. | #### Running | Setting | What it does | | --- | --- | | **Save before running** | F5 saves the document first. | | **Open the console on Run** | Brings the console to the front when a form starts. | | **Log events in the console** | Writes every event the running form handles, with its value and handler. | #### Logs and errors | Setting | What it does | | --- | --- | | **Show a dialog on errors** | Opens the [error dialog](https://albakore.github.io/pyzed/editor/console-and-running/#when-the-editor-itself-fails) when something fails. Off, errors only go to the Logs panel. | | **Write a log file** | Also writes the [logs](https://albakore.github.io/pyzed/editor/console-and-running/#the-logs-panel) to `logs/pyzed-design.log` in the Pyzed config folder (applies on restart). | #### More The end of the index has links to the rest: | Item | What it does | | --- | --- | | **Key bindings** | The [keymap editor](https://albakore.github.io/pyzed/editor/keymap/): change the keys of any action. | | **AI agents** | The [agent's settings](https://albakore.github.io/pyzed/editor/ai-agents/#settings): model providers, keys and browser sign-in, default model, effort and permissions. | | **Reset panel layout** | Puts every panel back in its default dock and size. | #### Telemetry The designer can keep usage and performance numbers to help improve it, but only if you turn it on: it is **off by default**. In **Settings → Telemetry**, *Collect usage and performance data* records: - how many times each command and editor action is used (`command.save`, `editor.select_next`…); - how long the designer takes to start, to draw a frame and to run each command (count, total, maximum and a histogram); - the number of sessions and their length. It never records your code, texts, form or file names, or paths. The data is kept in `telemetry.json` in the Pyzed config folder; **View** opens it and **Delete** empties it (and changes the random installation id). Nothing leaves your computer unless you also set an **Endpoint**: then the designer posts the data there as JSON when it closes, and starts counting again. A send that fails is retried the next time. `PYZED_TELEMETRY=0` turns telemetry off whatever the settings say. ```json {"schema": 1, "installation": "4f0c…", "app": "pyzed-designer", "version": "0.4.0", "os": "linux", "arch": "x86_64", "python": "3.13", "since": "…", "until": "…", "sessions": 3, "counters": {"command.save": 12, "editor.select_next": 7}, "timings": {"render": {"count": 840, "total_ms": 2210.4, "max_ms": 41.2, "buckets": [690, 120, 25, 5, 0, 0, 0, 0]}}} ``` The `buckets` count durations up to 4, 8, 16, 33, 50, 100 and 250 ms, and longer. #### Where they are saved Everything the editor remembers goes to one file, `designer.json`, in Pyzed's settings folder: | Platform | Folder | | --- | --- | | Windows | `%APPDATA%\pyzed` | | macOS | `~/Library/Application Support/pyzed` | | Linux | `$XDG_CONFIG_HOME/pyzed` or `~/.config/pyzed` | Set `PYZED_CONFIG_DIR` to use another folder (handy for a portable setup or for tests). The file has two sections: `"designer"` (theme, language, canvas defaults, toolbar and status bar, run options, recent files, and where each panel is docked, which docks are hidden and their sizes) and `"editor"` (the code editor). They hold the active profile; `"profile"` names it and `"profiles"` keeps the others (only what differs from the defaults). A project's own settings are in its `.pyzed/settings.json`. The [AI agent](https://albakore.github.io/pyzed/editor/ai-agents/) keeps its settings in `agents.json` and its threads in `agents/threads/` in the same folder; API keys are never written there, they go to the system keychain. Invalid values are ignored and the defaults used, so a hand-edited file can not break the editor. ```json { "designer": { "theme": "One Light", "language": "es", "ui_font": "Inter", "ui_scale": 1.1, "grid": 8, "snap": true, "console_on_run": true, "docks": { "events": "left" }, "sizes": { "right": 360 }, "recent": ["/home/ada/shop/ui/main.pzform"] }, "editor": { "font_family": "JetBrains Mono", "font_size": 15, "tab_size": 4, "minimap": false, "cursor_shape": "block", "format_on_apply": true } } ``` #### Translating the editor The editor's interface is translated with [`pz.t`](https://albakore.github.io/pyzed/guides/i18n/). English and Spanish are built in; add a catalog under the `designer` key to translate it to another language. The **Language** setting offers the built-in ones; for another language, let the editor follow the system or set `"language"` in the `designer` section of `designer.json`. Missing strings fall back to English, so a partial translation already works. ### Tips and tricks > Small habits that make you faster in Pyzed Design: shortcuts, multi-selection, smart guides, templates and quick ways to build Python forms. Source: https://albakore.github.io/pyzed/editor/tips-and-tricks/ #### Placing and arranging - **Draw instead of drop**: click a component in the toolbox, then drag a rectangle on the form. The control gets exactly that size, snapped to the grid, and you skip a resize. - **Add without the toolbox**: Ctrl+Shift+A, type two or three letters, Enter. With the left dock hidden (Ctrl+B) the canvas gets the whole width. - **Pick the alignment reference**: after selecting several controls, click the one that is already right. It becomes the primary control (thicker outline), and **Same width** / **Same height** copy *its* size. - **Align to the form**: with a single control selected, the align buttons line it up with the form's edges and center; **Center in form** centers the whole selection as a group. - **Nudge in grid steps**: Shift+arrows moves by one grid step, plain arrows by one pixel. A run of nudges is a single undo step. - **Cancel a bad drag**: press Esc before letting go of the mouse and everything goes back. - **Fix a messy layout at once**: with nothing selected, **Fit selection to grid** (toolbar) snaps every control of the form onto the grid. - **Guides fight you?** Turn off **Smart guides** (ruler) or **Snap** (magnet, or click *Snap* in the status bar) for pixel-precise placement, and turn them back on after. #### Containers - **Wrap, do not rebuild**: select controls that already look right and use **Group in a row / column / grid / panel** from the context menu. The container is created around them. - **Grab the container, not its child**: use the grip tab above its top-left corner, or its row in the outline. - **Push to the end**: a **Spacer** in a flex row pushes everything after it to the far end; two spacers center what is between them. - **Equal columns**: in a flex row, give each child **Grow** 1; in a grid, use **Row height** 0 so the rows share the height. - **Make it resizable**: anchor the main container on all four sides and let flex containers inside share the space with **Grow**. Run with F5 and resize the window to check. #### Outline - **Hide while you work**: the eye in the outline hides a control (a dialog, an overlay) so you can work on what is under it. Remember to show it again, or set it visible from code (`form.panel.show()`). - **Lock the background**: lock big panels and images so a stray drag does not move them while you arrange the controls on top. Locked controls are also skipped by the align and size tools. - **Reorder flex children** by dragging their rows in the outline: the line shows exactly where they land. #### Code - **Enter is "edit code"**: select a control and press Enter to jump to its main event handler. - **Name first, code second**: rename controls (**Name**, then Enter) before writing handlers. Renames later still update handlers that follow the `control_event` convention, but code that mentions `form.button1` elsewhere is not rewritten. - **Let completion teach you**: `form.` lists the controls, `form.status.` the properties of that control, `event.` the event's fields, and the documentation pane shows each property's description. - **Insert member calls**: on a placed designed component, open its chevron in the outline and click a method to type `form.card.rate()` at the cursor. - **Share logic between controls**: handlers are plain functions, so put the common part in a helper at the top of the file and call it from each handler; `event.sender` tells which control raised the event. - **Format on Apply**: turn on *Format with ruff on Apply* in the editor settings and press **Apply** to tidy the handler you just wrote. - **Bigger font for demos**: Ctrl+= in the code editor, Ctrl+0 to go back. - **Restart servers** after `uv add` / `uv sync` if completion does not know a new package yet. #### Running - **F5 runs what you see**: inside the project's environment, Run uses the current document, unsaved changes included. Keep the window open and press Ctrl+S to update it in place, keeping what you typed into it. - **Read the console, not the terminal**: events show their values (`email.change ('ada@') → email_change()`), and errors show the full traceback starting at your handler. Filter it by a control's name to follow just that one. - **Turn off event logging** (Settings → Running) when a timer or a slider floods the console. - **Preview components alone**: open a `.pzcomp` and press F5; the preview lists every custom event the component raises, with its value. #### Layout of the editor - **Two panels at once**: move **Events** to the left (or top) dock with the dock button, so Properties and Events are both visible. - **Fold the sections you do not use** in Properties: they stay folded while you move between controls. - **Lost a panel?** **View → Reset panel layout** puts everything back. - **Portable settings**: set `PYZED_CONFIG_DIR` to keep the editor's settings with a project or on a USB stick. #### Files - **Commit both files**: the `.pzform` is readable JSON that diffs well, and the `.py` is plain Python. Reviewers can read both. - **Edit the code anywhere**: the editor only rewrites the handler functions it manages, so you can keep the code file open in another editor as well; run with `pyzed run app.pzform --reload` to see every save. - **Templates are a head start**: the *Dialog* template already wires OK and Cancel; *Settings* shows a grid of labels and fields. Register your own with `designer.register_template`. ## Shading ### Shading > GPU shaders in Pyzed: build them by wiring nodes in Pyzed Design, load .pzshad shader graphs from Python, or write WGSL by hand, and paint any widget with them. Source: https://albakore.github.io/pyzed/shading/ A **shader** is a tiny program that decides the color of every pixel of a widget, many times per second, on the graphics card. It is how Pyzed draws animated backgrounds, glowing effects, radar screens and game visuals without slowing your app down. If you have used Godot's visual shaders or Unreal Engine's material editor, Pyzed's shader graphs will feel familiar. examples/showcase/shaders." size="wide" /> ```mermaid flowchart LR nodes["Nodes in the Shader view"] --> file[".pzshad file"] code["pyzed.shading in Python"] --> file file --> wgsl["WGSL"] hand["WGSL written by hand"] --> widget wgsl --> widget["pz.shader widget"] --> gpu["GPU"] ``` #### Start here #### Every node The [node catalog](https://albakore.github.io/pyzed/shading/nodes/) describes all the nodes of a graph, by category, with their ports, settings and an example each. #### Three ways to make one | Way | For | Page | | --- | --- | --- | | **Nodes in the Shader view** | Everyone: wire boxes with the mouse and see the result on every node. | [The Shader view](https://albakore.github.io/pyzed/shading/shader-designer/) | | **`pyzed.shading` in Python** | Generating or editing graphs from code, tests, tools. | [Shader graphs](https://albakore.github.io/pyzed/shading/shader-graphs/) | | **WGSL by hand** | Full control, porting shaders from elsewhere. | [WGSL shaders](https://albakore.github.io/pyzed/shading/shaders/) | All three end up as the same WGSL code on the GPU, so they are equally fast, and a `.pzshad` made with the mouse can be changed from Python (and the other way round). **Shaders need a GPU:** On a machine without a usable graphics card, a shader widget draws its `fallback` color or gradient instead. Give every shader a sensible fallback so your interface still reads well there. #### See them in action pz.shader in the component library.', href: '/pyzed/library/shader/', icon: 'puzzle' }, { title: 'pyzed.shading API', text: 'Every class and function to load, build and edit graphs in code.', href: '/pyzed/reference/shading/', icon: 'list-format' }, ]} /> **Status of this section:** Shader graphs are new in Pyzed. Every page here describes what works today, and the node catalog is generated from the code, so it always matches your version, with a capture of every node. Most recipes show their graph in the Shader view; the rest can be built in a minute from their steps. ### Node catalog > Every node of Pyzed's shader graphs, by category: what it does, its inputs, outputs and settings, and a small example you can paste. Source: https://albakore.github.io/pyzed/shading/nodes/ A shader graph is made of **92 node types** (and 137 entries in the Shader view's sidebar, since many nodes come in several flavors: *Add*, *Multiply* and *Step* are all the **Math** node). This catalog describes each one: what it is for in plain words, its ports and settings, and an example in Python. #### How to read a node ```mermaid flowchart LR in1["inputs (left)"] --> node["Node
settings inside"] --> out1["outputs (right)"] ``` - **Inputs** are on the left of a node and **outputs** on the right. A wire goes from an output to an input. - An input that is not wired uses its **default**: a number, a vector, a color, or a *context* value such as *UV* (the pixel's position) or *Time*. You can type another value right on the node. - **Settings** are not ports: an operation, a noise kind, a blend mode, the stops of a gradient. Change them in the Properties panel (or as keyword arguments in Python). ##### Types | Type | What it holds | Example | | --- | --- | --- | | `float` | One number | `0.5`, a brightness, a time | | `int` | A whole number | `5` octaves | | `bool` | True or false | is the mouse over the shader? | | `vec2` | Two numbers | a position `(x, y)`, a size | | `vec3` | Three numbers | a color `(r, g, b)` from 0 to 1 | | `vec4` | Four numbers | a color with alpha `(r, g, b, a)` | | `texture` | An image | a `texture` variable | | `T` | Any of the number types | Math nodes: the widest type wired to them | Values convert on the way: a `float` fills every component of a vector (`0.5` becomes `(0.5, 0.5, 0.5)`), and a `vec3` becomes a color with alpha 1. A wire that can never work, such as a texture into a number, is refused. **Numbers from 0 to 1:** In shaders, colors and most masks go from **0 to 1**, not 0 to 255: `(1, 0.5, 0)` is orange, `0` is black and `1` is white. Many nodes return 0..1 so you can multiply them together, like stacking stencils. #### Every node | | | | --- | --- | | **Input** | [UV](https://albakore.github.io/pyzed/shading/nodes/inputs/#uv), [Pixel](https://albakore.github.io/pyzed/shading/nodes/inputs/#pixel), [Size](https://albakore.github.io/pyzed/shading/nodes/inputs/#size), [Time](https://albakore.github.io/pyzed/shading/nodes/inputs/#time), [Mouse](https://albakore.github.io/pyzed/shading/nodes/inputs/#mouse), [Centered UV](https://albakore.github.io/pyzed/shading/nodes/inputs/#centered-uv) | | **Parameters** | [Parameter](https://albakore.github.io/pyzed/shading/nodes/inputs/#parameter) | | **Constants** | [Float](https://albakore.github.io/pyzed/shading/nodes/inputs/#float), [Int](https://albakore.github.io/pyzed/shading/nodes/inputs/#int), [Bool](https://albakore.github.io/pyzed/shading/nodes/inputs/#bool), [Vector2](https://albakore.github.io/pyzed/shading/nodes/inputs/#vector2), [Vector3](https://albakore.github.io/pyzed/shading/nodes/inputs/#vector3), [Vector4](https://albakore.github.io/pyzed/shading/nodes/inputs/#vector4), [Color](https://albakore.github.io/pyzed/shading/nodes/inputs/#color), [Math constants](https://albakore.github.io/pyzed/shading/nodes/inputs/#math-constants) | | **Math** | [Math](https://albakore.github.io/pyzed/shading/nodes/math/#math-1), [Function](https://albakore.github.io/pyzed/shading/nodes/math/#function), [Mix](https://albakore.github.io/pyzed/shading/nodes/math/#mix), [Clamp](https://albakore.github.io/pyzed/shading/nodes/math/#clamp), [Smoothstep](https://albakore.github.io/pyzed/shading/nodes/math/#smoothstep), [Remap](https://albakore.github.io/pyzed/shading/nodes/math/#remap), [Multiply add](https://albakore.github.io/pyzed/shading/nodes/math/#multiply-add) | | **Vector** | [Compose](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#compose), [Decompose](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#decompose), [Swizzle](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#swizzle), [Dot](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#dot), [Cross](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#cross), [Length](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#length), [Distance](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#distance), [Normalize](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#normalize), [Reflect](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#reflect), [Polar](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#polar) | | **UV** | [Tiling and offset](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#tiling-and-offset), [Scroll](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#scroll), [Rotate](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#rotate), [Twirl](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#twirl), [Pixelate](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#pixelate), [Barrel](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#barrel), [Kaleidoscope](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#kaleidoscope), [Flip](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#flip), [Repeat](https://albakore.github.io/pyzed/shading/nodes/vectors-uv/#repeat) | | **Noise** | [Noise](https://albakore.github.io/pyzed/shading/nodes/noise-patterns/#noise-1), [Fractal noise](https://albakore.github.io/pyzed/shading/nodes/noise-patterns/#fractal-noise), [Voronoi](https://albakore.github.io/pyzed/shading/nodes/noise-patterns/#voronoi), [White noise](https://albakore.github.io/pyzed/shading/nodes/noise-patterns/#white-noise), [Random](https://albakore.github.io/pyzed/shading/nodes/noise-patterns/#random) | | **Patterns** | [Checker](https://albakore.github.io/pyzed/shading/nodes/noise-patterns/#checker), [Stripes](https://albakore.github.io/pyzed/shading/nodes/noise-patterns/#stripes), [Grid](https://albakore.github.io/pyzed/shading/nodes/noise-patterns/#grid), [Dots](https://albakore.github.io/pyzed/shading/nodes/noise-patterns/#dots), [Linear gradient](https://albakore.github.io/pyzed/shading/nodes/noise-patterns/#linear-gradient), [Radial gradient](https://albakore.github.io/pyzed/shading/nodes/noise-patterns/#radial-gradient), [Wave](https://albakore.github.io/pyzed/shading/nodes/noise-patterns/#wave) | | **Shapes** | [Circle](https://albakore.github.io/pyzed/shading/nodes/shapes/#circle), [Box](https://albakore.github.io/pyzed/shading/nodes/shapes/#box), [Ring](https://albakore.github.io/pyzed/shading/nodes/shapes/#ring), [Segment](https://albakore.github.io/pyzed/shading/nodes/shapes/#segment), [Polygon](https://albakore.github.io/pyzed/shading/nodes/shapes/#polygon), [Star](https://albakore.github.io/pyzed/shading/nodes/shapes/#star), [Combine shapes](https://albakore.github.io/pyzed/shading/nodes/shapes/#combine-shapes), [Fill](https://albakore.github.io/pyzed/shading/nodes/shapes/#fill), [Stroke](https://albakore.github.io/pyzed/shading/nodes/shapes/#stroke), [Glow](https://albakore.github.io/pyzed/shading/nodes/shapes/#glow) | | **Color** | [HSV to RGB](https://albakore.github.io/pyzed/shading/nodes/color-effects/#hsv-to-rgb), [RGB to HSV](https://albakore.github.io/pyzed/shading/nodes/color-effects/#rgb-to-hsv), [Hue shift](https://albakore.github.io/pyzed/shading/nodes/color-effects/#hue-shift), [Saturation](https://albakore.github.io/pyzed/shading/nodes/color-effects/#saturation), [Brightness and contrast](https://albakore.github.io/pyzed/shading/nodes/color-effects/#brightness-and-contrast), [Luminance](https://albakore.github.io/pyzed/shading/nodes/color-effects/#luminance), [Grayscale](https://albakore.github.io/pyzed/shading/nodes/color-effects/#grayscale), [Invert](https://albakore.github.io/pyzed/shading/nodes/color-effects/#invert), [Gamma](https://albakore.github.io/pyzed/shading/nodes/color-effects/#gamma), [Posterize](https://albakore.github.io/pyzed/shading/nodes/color-effects/#posterize), [Blend](https://albakore.github.io/pyzed/shading/nodes/color-effects/#blend), [Palette](https://albakore.github.io/pyzed/shading/nodes/color-effects/#palette), [Gradient](https://albakore.github.io/pyzed/shading/nodes/color-effects/#gradient), [RGBA](https://albakore.github.io/pyzed/shading/nodes/color-effects/#rgba), [Layer](https://albakore.github.io/pyzed/shading/nodes/color-effects/#layer) | | **Effects** | [Vignette](https://albakore.github.io/pyzed/shading/nodes/color-effects/#vignette), [Scanlines](https://albakore.github.io/pyzed/shading/nodes/color-effects/#scanlines), [Film grain](https://albakore.github.io/pyzed/shading/nodes/color-effects/#film-grain) | | **Texture** | [Sample texture](https://albakore.github.io/pyzed/shading/nodes/color-effects/#sample-texture) | | **Logic** | [Compare](https://albakore.github.io/pyzed/shading/nodes/logic-utility/#compare), [Select](https://albakore.github.io/pyzed/shading/nodes/logic-utility/#select), [Logic](https://albakore.github.io/pyzed/shading/nodes/logic-utility/#logic-1) | | **Variables** | [Set value](https://albakore.github.io/pyzed/shading/nodes/logic-utility/#set-value), [Get value](https://albakore.github.io/pyzed/shading/nodes/logic-utility/#get-value) | | **Utility** | [Reroute](https://albakore.github.io/pyzed/shading/nodes/logic-utility/#reroute), [Expression](https://albakore.github.io/pyzed/shading/nodes/logic-utility/#expression), [Shader function](https://albakore.github.io/pyzed/shading/nodes/logic-utility/#shader-function), [Comment](https://albakore.github.io/pyzed/shading/nodes/logic-utility/#comment) | | **Output** | [Output](https://albakore.github.io/pyzed/shading/nodes/logic-utility/#output-1) | **Try a node:** Each node is shown as it looks in the Shader view, with the preview of its default settings. To play with one, add it from the Shading sidebar and wire it to the Output, or paste its example into a Python file and run it. ### Inputs, parameters and constants > The nodes every shader starts from: where the pixel is, the time, the mouse, your own variables and fixed values. Source: https://albakore.github.io/pyzed/shading/nodes/inputs/ Every example on this page runs after these two lines, and ends by sending a color to the Output node: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Input Where the pixel being painted is, how big the area is, the time and the mouse. Almost every graph starts with one of these. ##### UV `uv` · Input · `g.uv(...)` Where the pixel is, from (0, 0) at the top left to (1, 1) at the bottom right. The most used node. `x` goes from 0 at the left to 1 at the right and `y` from 0 at the top to 1 at the bottom, whatever the size of the widget. Wire `x` into a Gradient to get a horizontal ramp. | Output | Type | | --- | --- | | `uv` | `vec2` | | `x` | `float` | | `y` | `float` | ```python title="Example" uv = g.uv() g.output(g.gradient(uv["x"])) ``` ##### Pixel `pixel` · Input · `g.pixel(...)` Where the pixel is, in pixels from the top left. The same as UV but in pixels: use it for things that must keep their size in pixels when the widget grows, such as scanlines every 3 pixels. | Output | Type | | --- | --- | | `px` · pixel | `vec2` | | `x` | `float` | | `y` | `float` | ```python title="Example" px = g.pixel() lines = g.fract(px["y"] / 4.0) g.output(g.rgba(g.step(0.5, lines), 1.0)) ``` ##### Size `size` · Input · `g.size(...)` The size of the shaded area in pixels, and its aspect ratio (width / height). The width and height of the area in pixels. `aspect` (width ÷ height) keeps circles round on wide widgets; Centered UV already uses it for you. | Output | Type | | --- | --- | | `size` | `vec2` | | `width` | `float` | | `height` | `float` | | `aspect` | `float` | ```python title="Example" s = g.size() g.output(g.rgba(g.fract(s["aspect"]), 1.0)) ``` ##### Time `time` · Input · `g.time(...)` Seconds since the app started (times scale), and their sine and cosine. Seconds since the app started: anything wired to it moves. `sin` and `cos` swing smoothly between −1 and 1, perfect for pulses. `scale` speeds it up or slows it down. | Input | Type | Default | | --- | --- | --- | | `scale` | `float` | `1` | | Output | Type | | --- | --- | | `time` | `float` | | `sin` | `float` | | `cos` | `float` | ```python title="Example" t = g.time(scale=2.0) pulse = t["sin"] * 0.5 + 0.5 g.output(g.rgba(g.hsv_to_rgb(0.6, 0.8, pulse), 1.0)) ``` ##### Mouse `mouse` · Input · `g.mouse(...)` Where the pointer is from 0 to 1 (the center while it is outside), and whether it is over the shader. Where the pointer is, in UV (0 to 1). While it is outside the shader, `position` is the center and `inside` is false. Great for spotlights and hover effects. | Output | Type | | --- | --- | | `position` | `vec2` | | `inside` | `bool` | ```python title="Example" m = g.mouse() light = g.radial_gradient(center=m["position"], radius=0.3) g.output(g.rgba(light, 1.0)) ``` ##### Centered UV `centered_uv` · Input · `g.centered_uv(...)` UV with (0, 0) in the middle and circles kept round: the place to draw shapes. Aspect UV goes from 0 to the aspect ratio across, so noise and cells stay square. The coordinates to draw shapes with: (0, 0) is the middle and one unit is the height, so a circle stays round on any widget. `aspect_uv` keeps noise and cells square. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | Output | Type | | --- | --- | | `centered` | `vec2` | | `aspect_uv` · aspect uv | `vec2` | ```python title="Example" c = g.centered_uv() d = g.length(c["centered"]) g.output(g.rgba(g.smoothstep(0.4, 0.0, d), 1.0)) ``` #### Parameters A **Parameter** node reads one of the shader's variables: the values your Python code (or a slider in the Preview tab) changes. ##### Parameter `parameter` · Parameters · `g.parameter(...)` Reads a variable of the shader (see the Variables list): a mutable one changes without recompiling, a constant is built into the code. Your shader's knobs. `g.uniform(...)` adds a mutable variable and a Parameter node at once; in the designer, drag a variable from the Outline onto the graph. A `color` variable gives `color`, `rgb` and `alpha` outputs. | Output | Type | | --- | --- | | `value` | `float` | | Setting | Values | Default | | --- | --- | --- | | `name` | `variable` | — | The output takes the variable's type; a `color` variable gives `color`, `rgb` and `alpha`. ```python title="Example" tint = g.uniform("tint", "color", "#22d3ee") glow = g.radial_gradient(radius=0.6) g.output(g.rgba(tint["rgb"] * glow, 1.0)) ``` #### Constants Fixed values written into the shader. When a value never changes, a constant is the simplest choice; when you want to change it from code, use a variable instead. ##### Float `float` · Constants · `g.float(...)` A fixed number. One fixed number. Most inputs already take a typed value directly, so you only need this node to feed the same number into several places. | Output | Type | | --- | --- | | `value` | `float` | | Setting | Values | Default | | --- | --- | --- | | `value` | `float` | `0.5` | ```python title="Example" half = g.float(0.5) g.output(g.rgba(g.compose(half, half, 1.0), 1.0)) ``` ##### Int `int` · Constants · `g.int(...)` A fixed whole number. A fixed whole number, for inputs such as the octaves of Fractal noise. | Output | Type | | --- | --- | | `value` | `int` | | Setting | Values | Default | | --- | --- | --- | | `value` | `int` | `1` | ```python title="Example" octaves = g.int(3) n = g.fbm(octaves=octaves) g.output(g.rgba(n, 1.0)) ``` ##### Bool `bool` · Constants · `g.bool(...)` A fixed true or false. A fixed true or false, for Select and Logic nodes. | Output | Type | | --- | --- | | `value` | `bool` | | Setting | Values | Default | | --- | --- | --- | | `value` | `bool` | `true` | ```python title="Example" on = g.bool(True) g.output(g.rgba(g.select(on, 1.0, 0.2), 1.0)) ``` ##### Vector2 `vec2` · Constants · `g.vec2(...)` Two fixed numbers. Two fixed numbers: a point or a size. | Output | Type | | --- | --- | | `value` | `vec2` | | Setting | Values | Default | | --- | --- | --- | | `value` | `vec2` | `(0.5, 0.5)` | ```python title="Example" center = g.vec2([0.3, 0.5]) g.output(g.rgba(g.radial_gradient(center=center, radius=0.3), 1.0)) ``` ##### Vector3 `vec3` · Constants · `g.vec3(...)` Three fixed numbers. Three fixed numbers: often an RGB color written as numbers from 0 to 1. | Output | Type | | --- | --- | | `value` | `vec3` | | Setting | Values | Default | | --- | --- | --- | | `value` | `vec3` | `(1, 1, 1)` | ```python title="Example" orange = g.vec3([1.0, 0.5, 0.0]) g.output(g.rgba(orange, 1.0)) ``` ##### Vector4 `vec4` · Constants · `g.vec4(...)` Four fixed numbers. Four fixed numbers: a color with alpha, as numbers. | Output | Type | | --- | --- | | `value` | `vec4` | | Setting | Values | Default | | --- | --- | --- | | `value` | `vec4` | `(1, 1, 1, 1)` | ```python title="Example" g.output(g.vec4([0.1, 0.2, 0.4, 1.0])) ``` ##### Color `color` · Constants · `g.color(...)` A fixed color. A fixed color picked with the color picker (or written as `#rrggbb`). | Output | Type | | --- | --- | | `color` | `vec4` | | `rgb` | `vec3` | | `alpha` | `float` | | Setting | Values | Default | | --- | --- | --- | | `value` | `color` | `#7c3aed` | ```python title="Example" g.output(g.color("#7c3aed")) ``` ##### Math constants `math_constants` · Constants · `g.math_constants(...)` π, τ (2π), e and the golden ratio. π, τ (2π), e and φ, so you don't have to type 3.14159. Multiply a 0..1 value by τ to get a full turn in radians. | Output | Type | | --- | --- | | `pi` · π | `float` | | `tau` · τ | `float` | | `e` | `float` | | `phi` · φ | `float` | ```python title="Example" k = g.math_constants() wave = g.sin(g.uv()["x"] * k["tau"] * 3.0) * 0.5 + 0.5 g.output(g.rgba(wave, 1.0)) ``` **Always up to date:** This page is generated from the node catalog of `pyzed.shading`, so the ports, types and defaults are always those of this version. Each example is compiled when the docs are built. **See also:** [Getting started](https://albakore.github.io/pyzed/shading/getting-started/) · [The Shader view](https://albakore.github.io/pyzed/shading/shader-designer/) · [Recipes](https://albakore.github.io/pyzed/shading/recipes/) · [Shader graphs in Python](https://albakore.github.io/pyzed/shading/shader-graphs/) ### Shader graphs > Shaders made of nodes (.pzshad): draw them with pz.shader, change their variables from Python, and build or edit graphs in code with pyzed.shading. Source: https://albakore.github.io/pyzed/shading/shader-graphs/ A **shader graph** is a [shader](https://albakore.github.io/pyzed/shading/shaders/) made of nodes instead of WGSL: inputs such as UV and Time, math, noise, shapes and colors, wired into an Output node. You build it in the designer's [Shader view](https://albakore.github.io/pyzed/shading/shader-designer/) and save it as a `.pzshad` file; `pyzed.shading` compiles it to the WGSL that `pz.shader` runs on the GPU, so it is exactly as fast as a hand-written shader. ```python import pyzed as pz pz.shader("shaders/fire.pzshad", uniforms={"speed": 2.0}, width=320, height=200) ``` #### Drawing a `.pzshad` `pz.shader` takes the path of a `.pzshad` (a `str` or a `Path`) where it takes WGSL code. `uniforms` sets the shader's **mutable variables** by name, and the shader's own settings (its fallback color, whether it animates) apply unless you pass them: ```python from pathlib import Path import pyzed as pz SHADERS = Path(__file__).parent / "shaders" class Menu(pz.App): speed = 1.0 def view(self): return pz.stack([ pz.shader(SHADERS / "synthwave.pzshad", width="fill", height="fill"), pz.slider(0, 3, self.speed, on_change=lambda v: setattr(self, "speed", v)), ]) ``` Changing a mutable variable costs nothing: the code is compiled once and only the value changes, so a slider or an animation can drive it at full frame rate. #### Loading and changing variables `shading.load` returns a `Shader`: the graph plus its own values for the variables. It is cached by path, and it reloads by itself when the file changes, so saving in the designer updates a running app. ```python from pyzed import shading # a shader with the variables speed and tint (mutable) and octaves (a constant) fire = shading.load("shaders/fire.pzshad") fire["speed"] = 2.0 # a mutable variable: no recompiling fire.set(speed=2.0, tint="#ff8800") # several at once fire.set(octaves=6) # a constant: the code is built again fire.variables # {"speed": 2.0, "tint": "#ff8800", "octaves": 6} pz.shader(fire, width=320, height=200) # or fire.widget(width=320, height=200) print(fire.wgsl) # the generated code ``` | Variable | In the graph | Changing it | | --- | --- | --- | | **Mutable** | A uniform, read by the GPU every frame. | Free: `fire["speed"] = 2`. | | **Constant** | A literal written into the code. | Builds the code again (fine now and then, not every frame). | Variables can be `float`, `int`, `bool`, `vec2`, `vec3`, `vec4`, `color` (a hex color or a theme token such as `"accent"`) or `texture` (an image path). A shader takes up to 32 mutable variables and 8 textures. #### Building a graph in code `shading.Graph` is the same graph the designer edits, with an API that reads like the nodes: call a node type on the graph to add it, pass inputs as arguments, and pick an output with `node["name"]`. Arithmetic on outputs adds math nodes for you. ```python from pyzed import shading g = shading.Graph("rings") speed = g.uniform("speed", "float", 1.0, min=0, max=4) # a mutable variable and a node that reads it d = g.ring(g.centered_uv()["centered"], radius=0.3, thickness=0.02) glow = g.glow(d["distance"], radius=0.1) hue = g.time()["time"] * speed["value"] * 0.1 # Multiply nodes color = g.hsv_to_rgb(hue, 0.8, 1.0)["rgb"] * glow["value"] g.output(g.rgba(color, 1.0)["color"]) g.arrange() # columns, left to right, as the designer's Arrange g.save("shaders/rings.pzshad") # open it in Pyzed Design to keep editing ``` - `g.add("noise", kind="simplex", scale=4)` is the long form; `g.noise(...)`, `g.sin(x)`, `g.multiply(a, b)` and every entry of the sidebar work as methods. Keys such as `"func:sin"` or `"math:max"` work in `add`. - Positional arguments go to the inputs in order; keywords go to inputs or settings by name. A value can be a number, a list (a vector), a color, another node's output or a context such as `"@uv"`. - `node["x"]` is an output; vectors have swizzles (`uv["uv"].x`, `color.xyz`). - `g.uniform(name, type, value)` and `g.constant(...)` add a variable and a node that reads it; `g.get(name)` adds another reader; `g.set_variable(name, value=..., mutable=False)` changes one. - `g.connect(src, out, dst, inp)` and `g.disconnect(dst, inp)` wire by node id; types are checked and cycles refused (`GraphError`). - `g.compile()` returns the WGSL (`.code`), the uniforms and textures for `pz.shader`, and the problems by node (`.errors`); `.check()` also runs the GPU compiler and puts what it finds on the node that wrote the line. #### Nodes that make graphs smaller | Node | What it does | | --- | --- | | **Set value / Get value** | Names a value once and reads it anywhere, without a long wire. | | **Expression** | Your own WGSL: an expression of `a`, `b`, `c` and `d` (and `uv`, `px`, `size`, `time`), or a function body with `return`. The types of `a`–`d` follow what is wired to them. | | **Shader function** | Another `.pzshad` used as a function: its mutable variables are inputs, its `uv` input moves or warps it, its outputs are the color. | | **Reroute** | A point to route a wire around other nodes. | #### In forms and from the command line The designer's **Shader** control (toolbox → Display) draws a `.pzshad` on a form; its `variables` property (`{"speed": 2}`) sets the variables, from the Properties panel or from a handler. `pyzed run fire.pzshad` shows a shader filling a window, with sliders for its mutable variables (press **V**). #### The file format A `.pzshad` is JSON: the shader's name and description, its variables, its nodes (type, position, inputs and settings) and its links. It diffs well in git, and an AI agent can write one: the [MCP server](https://albakore.github.io/pyzed/guides/mcp/) has `list_shader_nodes`, `new_shader` and `check_shader`. ```json { "format": "pyzed.shader/1", "name": "glow", "variables": [{"name": "speed", "type": "float", "value": 1.0, "mutable": true, "min": 0, "max": 4}], "nodes": [{"id": "1", "type": "time", "x": 0, "y": 0}, "..."], "links": [{"from": "1", "out": "time", "to": "3", "in": "a"}, "..."] } ``` ### Your first shader > A gentle start with GPU shaders in Pyzed: build a glowing, pulsing circle in the Shader view of Pyzed Design without writing code, then use it in an app. Source: https://albakore.github.io/pyzed/shading/getting-started/ In this page you build a **glowing circle that pulses**, in about ten minutes, by connecting boxes with the mouse. You don't need to know math or WGSL. At the end you will have a `.pzshad` file you can drop into any Pyzed app, and you will know the handful of ideas every shader is built on. #### Three ideas, in plain words 1. **A shader is a recipe that runs for every pixel.** The graphics card asks "what color is *this* pixel?" for every pixel of the widget, sixty times per second, all at the same time. Your graph is the answer. 2. **Each pixel knows where it is.** The **UV** node says where: `x` goes from 0 (left) to 1 (right) and `y` from 0 (top) to 1 (bottom). Shapes, gradients and patterns are just math on that position. 3. **Colors are numbers from 0 to 1.** Red is `(1, 0, 0)`, white `(1, 1, 1)`, a mid gray `(0.5, 0.5, 0.5)`. Multiplying a color by a number from 0 to 1 darkens it; that is how masks work. **Familiar territory:** If you have used Godot's visual shaders, Unreal Engine's material editor or Blender's shader nodes, the Shader view works the same way: nodes on a canvas, wires from outputs to inputs, a live preview on every node. #### Build it in the Shader view 1. **Open Pyzed Design.** Start the app you [downloaded](https://albakore.github.io/pyzed/download/), or launch it from Python: **uv:** ```bash uv run pyzed design # inside a uv project with pyzed-ui uvx --from "pyzed-ui[designer]" pyzed design # without a project ``` **pip:** ```bash pip install "pyzed-ui[designer]" pyzed design ``` **python:** ```bash title="macOS / Linux" python3 -m pyzed design ``` ```powershell title="Windows (PowerShell)" py -m pyzed design ``` 2. **Create a shader.** Press **New** (Ctrl+N), pick the **Shader** card, name it `pulse` and choose the **Blank** template. **Create shader** saves `pulse.pzshad` and opens it. Shader, a name, a folder and a template." size="narrow" /> 3. **Look around.** The Blank template is three nodes: **UV** → **Gradient** → **Output**. Each node shows a small preview of what it makes, and the **Output** preview in the corner is the finished shader. The left dock now holds the **Shading** sidebar (every node, by category) and the **Outline** (variables and nodes). 4. **Draw a circle.** Double-click an empty spot of the canvas (or press Shift+A) and type `circle`; press Enter to add the **Circle** node. Do the same for **Fill**. Drag from the Circle's `distance` output to the Fill's `distance` input: the Fill preview shows a white disc on black. Circle node. Its preview shows the distance: the disc in the middle and rings growing outward." size="small" plain /> **Why two nodes?:** A shape node gives a *distance* (how far each pixel is from the edge), not a picture. **Fill** turns that distance into a clean mask: 1 inside, 0 outside, with smooth edges. **Stroke** would draw only the outline and **Glow** a soft light. 5. **Color it.** Add an **RGBA** node. Wire Fill's `mask` into RGBA's `rgb` (one number fills red, green and blue: white) and RGBA's `color` into the Output's `color`. The old Gradient is no longer used: select it and press Delete. 6. **Make it pulse.** Add a **Time** node and a **Multiply add** node. Wire Time's `sin` (a value that swings between −1 and 1) into Multiply add's `a`, and type `0.05` in `b` and `0.3` in `c`: the result swings between 0.25 and 0.35. Wire it into the Circle's `radius`. Every preview starts to breathe. 7. **Add a glow.** Add a **Glow** node, wire the Circle's `distance` into it, and an **Add** node that sums Fill's `mask` and Glow's `value`. Wire the sum into RGBA's `rgb` instead of the mask. 8. **Give it a color you can change.** In the Outline, press the arrow of **Add** and pick **color variable**; name it `tint` and pick a pink. Drag `tint` from the Outline onto the graph: it becomes a **Parameter** node. Add a **Multiply** node: `tint`'s `rgb` times the sum, into RGBA's `rgb`. 9. **Try it.** Open the **Preview** tab: the shader large, with a slider or a color swatch for every variable. The **WGSL** tab shows the code the graph compiles to. Press F5 to open it in a window of its own that follows your edits. Save with Ctrl+S. **When a wire is refused:** Wires are typed: a color can't go into an input that wants a true/false value, for example. When the Shader view refuses a wire it says why in a notification. The badge in the toolbar says **Compiles**, or how many problems there are; hover it to read them, and the node with the problem turns red. #### Use it in an app A `.pzshad` file is used like an image: give its path to `pz.shader`. `uniforms` sets its mutable variables by name, so your code can change the color while the app runs, at no cost. ```python title="app.py" from pathlib import Path import pyzed as pz HERE = Path(__file__).parent class Pulse(pz.App): title = "My first shader" size = (420, 320) def view(self): return pz.shader(HERE / "pulse.pzshad", uniforms={"tint": "#22d3ee"}, width="fill", height="fill", fallback="#1e1b4b") if __name__ == "__main__": pz.run(Pulse) ``` Run the app, or look at the shader alone (press V in that window for a slider per variable): **uv:** ```bash uv run app.py uv run pyzed run pulse.pzshad ``` **pip:** ```bash python app.py pyzed run pulse.pzshad ``` **python:** ```bash title="macOS / Linux" python3 app.py python3 -m pyzed run pulse.pzshad ``` ```powershell title="Windows (PowerShell)" py app.py py -m pyzed run pulse.pzshad ``` **No GPU?:** On a computer without a usable graphics card (some virtual machines and remote desktops), a shader widget draws its `fallback` color or gradient instead. Always give one, so your interface still reads well there. #### The same shader in Python Everything the Shader view does is also a Python API. This builds the same graph and saves it; open the file in Pyzed Design to keep editing it with the mouse. ```python title="make_pulse.py" from pyzed import shading g = shading.Graph("pulse") tint = g.uniform("tint", "color", "#f472b6") # a variable and a Parameter node radius = g.time()["sin"] * 0.05 + 0.3 # Math nodes, added for you circle = g.circle(radius=radius) light = g.fill(circle) + g.glow(circle, radius=0.1) g.output(g.rgba(tint["rgb"] * light, 1.0)) g.arrange() # tidy columns, like the toolbar's Arrange g.save("pulse.pzshad") ``` #### Where to go next .pzshad files from code.', href: '/pyzed/shading/shader-graphs/', icon: 'seti:python' }, ]} /> ### Math > Arithmetic, one-value functions (sin, fract, abs...), mix, clamp, smoothstep and remap: the glue of every shader graph. Source: https://albakore.github.io/pyzed/shading/nodes/math/ Every example on this page runs after these two lines, and ends by sending a color to the Output node: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Math Every math node works on numbers **and** vectors: with a `vec3` it works on each component (red, green and blue at once). Their inputs and outputs have the generic type `T`. ##### Math `math` · Math · `g.math(...)` An arithmetic operation on two values (of any type: vectors work per component). Two values in, one out. In Python you rarely add it by hand: `a + b`, `a * 2` or `a ** 2` on ports add Math nodes for you. Each operation is its own entry in the sidebar (Add, Multiply, Step...). | Input | Type | Default | | --- | --- | --- | | `a` | `T` | `0` | | `b` | `T` | `1` | | Output | Type | | --- | --- | | `result` | `T` | | Setting | Values | Default | | --- | --- | --- | | `op` | `add`, `subtract`, `multiply`, `divide`, `modulo`, `power`, `min`, `max`, `step`, `atan2`, `snap` | `add` | **Sidebar entries** | Entry | What it does | In Python | | --- | --- | --- | | Add | a + b | `g.add(...)` | | Subtract | a − b | `g.subtract(...)` | | Multiply | a × b | `g.multiply(...)` | | Divide | a ÷ b | `g.divide(...)` | | Modulo | The remainder of a ÷ b (always the sign of b). | `g.modulo(...)` | | Power | a to the power of b. | `g.power(...)` | | Min | The smaller of a and b. | `g.min(...)` | | Max | The larger of a and b. | `g.max(...)` | | Step | 0 where b is below the edge a, 1 from there on. | `g.step(...)` | | Atan2 | The angle of the point (b, a) in radians: a is y, b is x. | `g.atan2(...)` | | Snap | a rounded to the nearest multiple of b. | `g.snap(...)` | ```python title="Example" uv = g.uv() stripes = g.step(0.5, g.fract(uv["x"] * 8.0)) g.output(g.rgba(stripes * 0.8 + 0.1, 1.0)) ``` ##### Function `func` · Math · `g.func(...)` A function of one value (vectors work per component). One value in, one out: rounding, roots, powers, trigonometry and more. Each function is its own entry in the sidebar (Sin, Fract, Abs...) and a method in Python (`g.sin(x)`, `g.fract(x)`). | Input | Type | Default | | --- | --- | --- | | `x` | `T` | `0` | | Output | Type | | --- | --- | | `result` | `T` | | Setting | Values | Default | | --- | --- | --- | | `op` | `abs`, `negate`, `sign`, `floor`, `ceil`, `round`, `fract`, `trunc`, `sqrt`, `inverse_sqrt`, `square`, `exp`, `exp2`, `log`, `log2`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `sinh`, `cosh`, `tanh`, `one_minus`, `reciprocal`, `saturate`, `degrees`, `radians` | `sin` | **Sidebar entries** | Entry | What it does | In Python | | --- | --- | --- | | Abs | x without its sign. | `g.abs(...)` | | Negate | −x | `g.negate(...)` | | Sign | −1, 0 or 1: the sign of x. | `g.sign(...)` | | Floor | x rounded down. | `g.floor(...)` | | Ceil | x rounded up. | `g.ceil(...)` | | Round | x rounded to the nearest whole number. | `g.round(...)` | | Fract | What is after the point: x − floor(x). Repeats every 1. | `g.fract(...)` | | Trunc | x without what is after the point. | `g.trunc(...)` | | Sqrt | The square root of x. | `g.sqrt(...)` | | Inverse sqrt | 1 / √x | `g.inverse_sqrt(...)` | | Square | x × x | `g.square(...)` | | Exp | e to the power of x. | `g.exp(...)` | | Exp2 | 2 to the power of x. | `g.exp2(...)` | | Log | The natural logarithm of x. | `g.log(...)` | | Log2 | The base 2 logarithm of x. | `g.log2(...)` | | Sin | The sine of x (radians). | `g.sin(...)` | | Cos | The cosine of x (radians). | `g.cos(...)` | | Tan | The tangent of x (radians). | `g.tan(...)` | | Asin | The arc sine of x, in radians. | `g.asin(...)` | | Acos | The arc cosine of x, in radians. | `g.acos(...)` | | Atan | The arc tangent of x, in radians. | `g.atan(...)` | | Sinh | The hyperbolic sine of x. | `g.sinh(...)` | | Cosh | The hyperbolic cosine of x. | `g.cosh(...)` | | Tanh | The hyperbolic tangent of x: an S curve from −1 to 1. | `g.tanh(...)` | | One minus | 1 − x: turns 0..1 around. | `g.one_minus(...)` | | Reciprocal | 1 / x | `g.reciprocal(...)` | | Saturate | x kept between 0 and 1. | `g.saturate(...)` | | Degrees | Radians to degrees. | `g.degrees(...)` | | Radians | Degrees to radians. | `g.radians(...)` | ```python title="Example" t = g.time()["time"] wave = g.abs(g.sin(g.uv()["x"] * 10.0 + t)) g.output(g.rgba(wave, 1.0)) ``` ##### Mix `mix` · Math · `g.mix(...)` Blends from a (t = 0) to b (t = 1): a lerp. Blends two values: `t` = 0 gives `a`, 1 gives `b`, 0.5 half of each. The node you will use most to combine colors. | Input | Type | Default | | --- | --- | --- | | `a` | `T` | `0` | | `b` | `T` | `1` | | `t` | `T` | `0.5` | | Output | Type | | --- | --- | | `result` | `T` | ```python title="Example" uv = g.uv() sky = g.mix([0.1, 0.2, 0.5], [0.9, 0.6, 0.3], uv["y"]) g.output(g.rgba(sky, 1.0)) ``` ##### Clamp `clamp` · Math · `g.clamp(...)` x kept between min and max. Keeps a value inside a range, so a bright spot never goes over 1 or a dark one under 0. | Input | Type | Default | | --- | --- | --- | | `x` | `T` | `0` | | `min` | `T` | `0` | | `max` | `T` | `1` | | Output | Type | | --- | --- | | `result` | `T` | ```python title="Example" v = g.uv()["x"] * 2.0 - 0.5 g.output(g.rgba(g.clamp(v, 0.0, 1.0), 1.0)) ``` ##### Smoothstep `smoothstep` · Math · `g.smoothstep(...)` 0 below edge0, 1 above edge1 and a smooth curve between them. A soft threshold: 0 below `edge0`, 1 above `edge1`, and a smooth curve between. Use it to turn a distance or a noise into a soft-edged mask. | Input | Type | Default | | --- | --- | --- | | `edge0` | `T` | `0` | | `edge1` | `T` | `1` | | `x` | `T` | `0.5` | | Output | Type | | --- | --- | | `result` | `T` | ```python title="Example" n = g.noise(scale=6.0) mask = g.smoothstep(0.45, 0.55, n) g.output(g.rgba(mask, 1.0)) ``` ##### Remap `remap` · Math · `g.remap(...)` x moved from the range in_min..in_max to out_min..out_max. Moves a value from one range to another: −1..1 (a sine) to 0..1 (a color), or 0..1 to 0.2..0.8. | Input | Type | Default | | --- | --- | --- | | `x` | `T` | `0.5` | | `in_min` · in min | `T` | `0` | | `in_max` · in max | `T` | `1` | | `out_min` · out min | `T` | `-1` | | `out_max` · out max | `T` | `1` | | Output | Type | | --- | --- | | `result` | `T` | ```python title="Example" s = g.time()["sin"] v = g.remap(s, -1.0, 1.0, 0.2, 0.8) g.output(g.rgba(v, 1.0)) ``` ##### Multiply add `multiply_add` · Math · `g.multiply_add(...)` a × b + c `a × b + c` in one node: scale and shift a value, such as `sin × 0.5 + 0.5` to bring −1..1 into 0..1. | Input | Type | Default | | --- | --- | --- | | `a` | `T` | `1` | | `b` | `T` | `1` | | `c` | `T` | `0` | | Output | Type | | --- | --- | | `result` | `T` | ```python title="Example" s = g.time()["sin"] g.output(g.rgba(g.multiply_add(s, 0.5, 0.5), 1.0)) ``` **Always up to date:** This page is generated from the node catalog of `pyzed.shading`, so the ports, types and defaults are always those of this version. Each example is compiled when the docs are built. **See also:** [Getting started](https://albakore.github.io/pyzed/shading/getting-started/) · [The Shader view](https://albakore.github.io/pyzed/shading/shader-designer/) · [Recipes](https://albakore.github.io/pyzed/shading/recipes/) · [Shader graphs in Python](https://albakore.github.io/pyzed/shading/shader-graphs/) ### The Shader view > Build GPU shaders in Pyzed Design by wiring nodes, like Godot's visual shaders: live previews on every node, a sidebar of shading nodes and variables your code changes. Source: https://albakore.github.io/pyzed/shading/shader-designer/ :::tip[New to shaders?] Start with [Your first shader](https://albakore.github.io/pyzed/shading/getting-started/): a ten-minute walk through this view that builds a glowing circle step by step. This page is the full reference of the view. ::: The designer has a third kind of document: **shaders**. Instead of writing WGSL, you wire nodes on a graph, as in Godot's visual shaders or Blender's shader editor. Every node shows a live preview of what it makes, the **Output** node is the finished shader, and the graph is saved as a `.pzshad` file that `pz.shader` draws on the GPU. (Image: The Shader view: the Shading sidebar, the graph with a preview on every node and the Output preview) There is no WGSL to write and no Rust: the designer compiles the graph to WGSL as you edit it, checks it with the GPU compiler, and points at the node when something is wrong. #### Creating one Press **New** (Ctrl+N) and pick the **Shader** card. Give it a name and a folder and pick a template: **Blank** (a gradient), **Plasma**, **Clouds**, **Glow ring**, **Cells** or **Retro screen**. **Create shader** saves it and opens it. You can also open a `.pzshad` file directly: (Image: The New shader dialog with its templates) ```bash pyzed design effects/fire.pzshad # created on first save if it does not exist pyzed run effects/fire.pzshad # the shader filling a window ``` #### The Shader view A shader opens in the **Shader view**, with three tabs in its toolbar: | Tab | What it shows | | --- | --- | | **Graph** | The nodes on a canvas you pan and zoom. Each node has a preview of its first output; the small **Output** window in the corner shows the finished shader. | | **Preview** | The finished shader, large, over a checkerboard (so transparency shows), with a slider for every variable. | | **WGSL** | The code the graph compiles to, read only, with the compiler's messages on their lines. **Copy** puts it on the clipboard. | The toolbar also has **Add a node** (Shift+A), **Delete**, **Arrange nodes in columns** (Ctrl+L), **Fit everything** (F), **Stop time** (to freeze every preview at one moment), **Small preview** and **Run** (F5), which opens the shader in a window of its own that follows your edits. A badge says **Compiles** or how many problems there are; hover it to read them. (Image: The WGSL tab: the code the glow ring graph compiles to, with line numbers) ##### Working on the canvas | Gesture | What it does | | --- | --- | | Drag from an output to an input | Wires them. The wire takes the color of the value's type. | | Drag a wire onto a node | Connects it to the first input (or output) that takes it. | | Drop a wire on empty space | Opens the node search; the node you pick is added there, already wired. | | Drag from a connected input | Picks the wire up again, to move it or drop it (disconnect). | | Double-click or right-click the canvas | The node search, where the pointer is. | | Drag a node | Moves it (and the rest of the selection). | | Drag on empty space | Selects the nodes in the box. | | Wheel | Zooms around the pointer. | | Middle button, or Space + drag | Pans. | | Right-click a node | Duplicate, copy, show or hide its preview, disconnect all, delete. | | Double-click a node | Shows its settings; on a **Shader function** node, opens that `.pzshad`. | The keyboard works as on forms: Ctrl+Z / Ctrl+Shift+Z, Delete, Ctrl+C / Ctrl+X / Ctrl+V (nodes travel between shaders and windows), Ctrl+D, Ctrl+A, arrows to nudge, **P** to show or hide the selected nodes' previews, Ctrl+0 for 100% and Ctrl+= / Ctrl+- to zoom. (Image: The node search opened on the graph, filtered by "noi": Noise, Random, Voronoi, Film grain and the other noise nodes) (Image: The right-click menu of a node: Duplicate, Copy, Hide preview, Disconnect all and Delete) ##### Inline values An input that is not connected shows its value right on the node: a number field, a switch or a color swatch. Inputs such as `uv`, `p` or `px` default to a **context** value (the pixel's UV, its centered position, its pixel coordinates); the Properties panel switches them between that and a fixed value. #### The Shading sidebar The left dock's **Toolbox** panel has the **Shading** sidebar instead of the form toolbox. Its entries are grouped by category; click one to add it in the middle of the graph, or drag it onto the graph. The search field matches titles, categories and keywords in English and in the designer's language. | Category | Nodes | | --- | --- | | **Input** | UV, Pixel, Size, Time, Mouse, Centered UV | | **Parameters** | Parameter (reads a variable) | | **Constants** | Float, Int, Bool, Vector2/3/4, Color, Math constants | | **Math** | Add, Subtract, Multiply, Divide, Modulo, Power, Min, Max, Step, Atan2, Snap; Abs, Floor, Fract, Sqrt, Sin, Cos, Exp, Log and the other one-value functions; Mix, Clamp, Smoothstep, Remap, Multiply add | | **Vector** | Compose, Decompose, Swizzle, Dot, Cross, Length, Distance, Normalize, Reflect, Polar | | **UV** | Tiling and offset, Scroll, Rotate, Twirl, Pixelate, Barrel, Kaleidoscope, Flip, Repeat | | **Noise** | Value, Perlin and Simplex noise, Fractal noise (fBm), Voronoi, White noise, Random | | **Patterns** | Checker, Stripes, Grid, Dots, Linear and Radial gradients, Waves (sine, triangle, square, sawtooth) | | **Shapes** | Circle, Box, Ring, Segment, Polygon, Star (signed distances), Union, Subtract and Intersect (smooth too), Fill, Stroke, Glow | | **Color** | HSV ↔ RGB, Hue shift, Saturation, Brightness and contrast, Luminance, Grayscale, Invert, Gamma, Posterize, Blend (12 layer modes), Palette, Gradient (color ramp), RGBA, Layer | | **Effects** | Vignette, Scanlines, Film grain | | **Texture** | Sample texture | | **Logic** | Compare, Select (if/else), And/Or/Xor/Not | | **Variables** | Set value, Get value (named values inside the graph) | | **Utility** | Reroute, Expression (your own WGSL), Shader function (another `.pzshad`), Comment | | **Output** | Output: the color of every pixel (and its alpha) | Every node, with its ports, settings and an example, is described in the [node catalog](https://albakore.github.io/pyzed/shading/nodes/). Ports are typed like Godot's: `float`, `int`, `bool`, `vec2`, `vec3`, `vec4` and `texture`. Most math nodes are generic: they take the widest type wired to them (a `float` times a `vec3` is a `vec3`), and values convert on the way (a `float` fills every component, a `vec3` becomes a color with alpha 1). A wire that can never work (a texture into a number) is refused with a message. #### Variables in the outline The **Outline** under the sidebar lists the shader's **variables**, its **named values** (from Set value nodes) and every node. **Add** creates a variable; its arrow picks the type (`float`, `int`, `bool`, `vec2`, `vec3`, `vec4`, `color` or `texture`). Drag a variable onto the graph to add a node that reads it. (Image: The Preview tab with a slider per variable, and a variable in the Properties panel) | Kind | What it is | From code | | --- | --- | --- | | **Mutable** | A uniform: the GPU reads it every frame, so changing it costs nothing and needs no recompiling. | `shader["speed"] = 2` or `pz.shader("x.pzshad", uniforms={"speed": 2})` | | **Constant** | Written into the generated code as a literal. Changing it builds the code again. | `shader.set(steps=8)` | A variable has a value, a range (for the sliders) and a description. A `texture` variable is an image file; a **Sample texture** node reads its color at a UV. Up to 32 mutable variables and 8 textures fit in one shader. #### Properties The Properties panel shows what is selected: - **A node**: its description, title, its settings (operation, noise kind, blend mode, gradient stops, the code of an Expression...), each input with its type (a link to the node wired into it, or its value) and its outputs. Problems found by the compiler show here and on the node, in red. - **A variable**: name, type, mutable or constant, value, range and description, and the line of Python that changes it. - **Nothing**: the shader itself: its name and description, the size of the Preview and Run windows, the **fallback** color drawn where there is no GPU, and whether it animates. (Image: The Properties panel of a Glow node: its title, its inputs with the wire from Ring and two values, and its output) (Image: The Properties panel of the color variable: type color, mutable, its value, a description and the line of Python that changes it) #### Reusing shaders A `.pzshad` is a file like a picture: use it anywhere. - **In code**: `pz.shader("shaders/fire.pzshad", uniforms={"speed": 2})`, or load it with `pyzed.shading.load` and set its variables. See [Shader graphs](https://albakore.github.io/pyzed/shading/shader-graphs/). - **In forms**: the toolbox's **Shader** control (in Display) draws a `.pzshad`. Its **Shader file** property is the path, and **variables** (`{"speed": 2}`) sets its variables, from the Properties panel or from a handler. - **In other shaders**: the **Shader function** node runs another `.pzshad` as a function. Its mutable variables become the node's inputs, and its `uv` input moves, tiles or warps it. - **In games**: the [showcase](https://albakore.github.io/pyzed/showcase/) backgrounds (nebula, synthwave, embers, color blobs, the radar) are `.pzshad` files in `examples/showcase/shaders`; open one to see how it is built. A running app reloads a `.pzshad` when the file changes, so saving in the designer updates it. ### Shader recipes > Ready-to-use shader graphs for Pyzed: gradients, a neon glow, plasma, clouds, cells, a retro CRT screen, a mouse spotlight, a loading spinner, fire and moving stripes, each with its nodes and its Python code. Source: https://albakore.github.io/pyzed/shading/recipes/ Each recipe is a small shader you can build in the [Shader view](https://albakore.github.io/pyzed/shading/shader-designer/) by following its chain of nodes, or create in Python by running its code. The first six are also **templates** of the New dialog (**New → Shader**), so you can open them ready-made and take them apart. **How to read the chains:** `UV.y → Gradient → Output` means: wire the `y` output of a **UV** node into a **Gradient**, and the Gradient into the **Output**. Numbers in parentheses are values to type on the node. Every node is described in the [node catalog](https://albakore.github.io/pyzed/shading/nodes/). Every code block below is a complete script: save it as `recipe.py`, run it, and it writes a `.pzshad` next to it. **uv:** ```bash uv run recipe.py uv run pyzed run sky.pzshad # look at the result ``` **pip:** ```bash python recipe.py pyzed run sky.pzshad ``` **python:** ```bash title="macOS / Linux" python3 recipe.py && python3 -m pyzed run sky.pzshad ``` ```powershell title="Windows (PowerShell)" py recipe.py; py -m pyzed run sky.pzshad ``` #### Gradient background *Template: **Blank*** · Level: first steps A smooth color ramp from top to bottom: the simplest useful shader, and a nicer backdrop than a flat color. **Chain:** `UV.y → Gradient → Output`. Click the Gradient node and edit its stops in Properties: drag a stop to move it, click the bar to add one. ```python from pyzed import shading g = shading.Graph("sky") sky = g.gradient(g.uv()["y"], stops=[[0.0, "#0b1026"], [0.6, "#3b2a6b"], [1.0, "#f08a5d"]]) g.output(sky) g.save("sky.pzshad") ``` Try `UV.x` for a horizontal ramp, or a **Linear gradient** node before the Gradient for any angle. #### Neon glow ring *Template: **Glow ring*** · Level: first steps A ring that breathes, with a soft light around it, in a color your code can change: loading screens, status indicators, game power-ups. Neon glow ring template open in the Shader view." size="wide" /> **Chain:** `Time × pulse → Sin × 0.02 + 0.28 → Ring.radius`, `Ring → Glow (radius 0.12) × color.rgb → RGBA → Output`. ```python from pyzed import shading g = shading.Graph("glow_ring") color = g.uniform("color", "color", "#22d3ee") pulse = g.uniform("pulse", "float", 2.0, min=0, max=6) wobble = g.sin(g.time()["time"] * pulse) * 0.02 ring = g.ring(radius=0.28 + wobble, thickness=0.01) light = g.glow(ring, radius=0.12, intensity=1.2) g.output(g.rgba(color["rgb"] * light, 1.0)) g.save("glow_ring.pzshad") ``` Change the **Ring** for a **Star** or a **Box** and you have a neon sign. #### Plasma *Template: **Plasma*** · Level: basic math Three sine waves (across, down and around the center) added together and colored with a **Palette**: the classic demo-scene effect. Plasma template open in the Shader view." size="wide" /> **Chain:** `sin(UV.x × frequency + time)` + `sin(UV.y × frequency + time × 1.3)` + `sin(length(Centered UV) × frequency × 1.5 − time)`, then `× 0.166 + 0.5 → Palette → RGBA → Output`. ```python from pyzed import shading g = shading.Graph("plasma") speed = g.uniform("speed", "float", 1.0, min=0, max=4) freq = g.uniform("frequency", "float", 10.0, min=1, max=30) t = g.time()["time"] * speed uv = g.uv() a = g.sin(uv["x"] * freq + t) b = g.sin(uv["y"] * freq + t * 1.3) c = g.sin(g.length(g.centered_uv()["centered"]) * freq * 1.5 - t) v = (a + b + c) * 0.166 + 0.5 # three waves from −1..1, brought into 0..1 g.output(g.rgba(g.palette(v), 1.0)) g.save("plasma.pzshad") ``` #### Drifting clouds *Template: **Clouds*** · Level: noise Fractal noise that scrolls with the wind, cut into clouds by a **Smoothstep** over a sky gradient. `cover` decides how cloudy it is. Drifting clouds template open in the Shader view." size="wide" /> **Chain:** `Scroll (speed: wind) → Fractal noise → Smoothstep` (edges from `cover`), then `Mix (sky, white, clouds) → RGBA → Output`. ```python from pyzed import shading g = shading.Graph("clouds") wind = g.uniform("wind", "vec2", [0.04, 0.01]) cover = g.uniform("cover", "float", 0.5, min=0, max=1) n = g.fbm(g.scroll(speed=wind)["uv"], scale=3.0, octaves=6) edge = 1.0 - cover clouds = g.smoothstep(edge - 0.2, edge + 0.25, n) sky = g.gradient(g.uv()["y"], stops=[[0.0, "#2b6cb0"], [1.0, "#90cdf4"]]) g.output(g.rgba(g.mix(sky["rgb"], [1.0, 1.0, 1.0], clouds), 1.0)) g.save("clouds.pzshad") ``` **Tip:** Swap the white for an orange and the sky stops for a dark purple and you get a sunset; scroll faster and lower `cover` for a storm. #### Living cells *Template: **Cells*** · Level: noise Voronoi cells that wander, each with its own color, separated by glowing borders: organic backgrounds, microscope views, stained glass. Living cells template open in the Shader view." size="wide" /> **Chain:** `Voronoi (scale) → cell → Palette × 0.6 → RGBA`; `Voronoi → edge → Smoothstep (0, 0.06)`; then `Mix (edge color, cells, border) → Output`. ```python from pyzed import shading g = shading.Graph("cells") scale = g.uniform("scale", "float", 6.0, min=1, max=20) edge_color = g.uniform("edge", "color", "#f472b6") v = g.voronoi(scale=scale) # its phase follows the time: the cells move inside = g.rgba(g.palette(v["cell"]) * 0.6, 1.0) border = g.smoothstep(0.0, 0.06, v["edge"]) g.output(g.mix(edge_color["color"], inside, border)) g.save("cells.pzshad") ``` #### Retro CRT screen *Template: **Retro screen*** · Level: effects A synthwave gradient seen through an old television: rolling scanlines, dark corners and film grain. The effect nodes return numbers around 1, so you multiply your color by them. Retro CRT screen template open in the Shader view." size="wide" /> **Chain:** `UV.y → Gradient → rgb × Scanlines × Vignette × Film grain → RGBA → Output`. ```python from pyzed import shading g = shading.Graph("retro_screen") lines = g.uniform("scanlines", "float", 0.3, min=0, max=1) base = g.gradient(g.uv()["y"], stops=[[0.0, "#ff6ec7"], [0.55, "#7873f5"], [1.0, "#1b1035"]]) color = base["rgb"] * g.scanlines(strength=lines, speed=20.0) * g.vignette(strength=0.9) * g.grain(strength=0.08) g.output(g.rgba(color, 1.0)) g.save("retro_screen.pzshad") ``` Add a **Barrel** node in front of the gradient's UV (and multiply by its `inside`) to curve the glass, as in the `crt.pzshad` of the [showcase](https://albakore.github.io/pyzed/showcase/shaders/). #### Mouse spotlight Level: first steps A dark room lit where the pointer is. When the pointer leaves the shader, the light rests in the center. **Chain:** `Mouse.position → Radial gradient.center`, `Mix (dark, warm, light) → RGBA → Output`. ```python from pyzed import shading g = shading.Graph("spotlight") light = g.radial_gradient(center=g.mouse()["position"], radius=0.35) room = g.mix([0.03, 0.03, 0.06], [1.0, 0.85, 0.6], light) g.output(g.rgba(room, 1.0)) g.save("spotlight.pzshad") ``` #### Loading spinner Level: basic math A ring with a bright head and a fading tail that turns forever. The transparent background lets it sit on any card. **Chain:** `Polar.angle01 − Time × 0.8 → Fract` (the tail), `Ring → Fill` (the shape), and the Output's `alpha` from `Fill × tail`. ```python from pyzed import shading g = shading.Graph("spinner") tail = g.fract(g.polar()["angle01"] - g.time()["time"] * 0.8) ring = g.fill(g.ring(radius=0.3, thickness=0.06)) g.output(g.rgba(g.compose(0.4, 0.6, 1.0) * tail, ring * tail)) # alpha: only the ring shows g.save("spinner.pzshad") ``` #### Fire Level: noise Fractal noise that scrolls upward, faded toward the top and colored by a black → red → orange → yellow ramp. examples/showcase/shaders, built on the same idea: rising noise through a color ramp." size="wide" /> **Chain:** `Scroll (speed: 0, speed × 0.6) → Fractal noise × UV.y × 1.6 → Gradient → Output`. ```python from pyzed import shading g = shading.Graph("fire") speed = g.uniform("speed", "float", 1.0, min=0, max=4) rising = g.scroll(speed=g.compose(0.0, speed * 0.6, type="vec2")) heat = g.fbm(rising["uv"], scale=4.0, octaves=5) * g.uv()["y"] * 1.6 g.output(g.gradient(heat, stops=[[0.0, "#000000"], [0.35, "#7f1d1d"], [0.6, "#f97316"], [0.85, "#fde047"], [1.0, "#ffffff"]])) g.save("fire.pzshad") ``` **Note:** UV's `y` grows downward, so `× UV.y` makes the bottom hottest. Scroll adds `speed × time` to the coordinates, so the noise drifts the *opposite* way: a positive `y` speed makes the flames rise. Flip the sign to make them fall. #### Moving stripes Level: first steps Diagonal stripes that slide sideways, like a barber pole or an "in progress" bar. **Chain:** `Scroll (0.2, 0) → Stripes (count 10, angle 0.785) → Mix (white, red) → RGBA → Output`. ```python from pyzed import shading g = shading.Graph("barber") s = g.stripes(g.scroll(speed=[0.2, 0.0])["uv"], count=10.0, angle=0.785, width=0.5) g.output(g.rgba(g.mix([0.95, 0.95, 0.95], [0.85, 0.15, 0.2], s), 1.0)) g.save("barber.pzshad") ``` #### Put a recipe to work ```python import pyzed as pz class Splash(pz.App): title = "Loading" def view(self): return pz.stack([ pz.shader("clouds.pzshad", uniforms={"cover": 0.6}, width="fill", height="fill", fallback="#2b6cb0"), pz.center(pz.shader("spinner.pzshad", width=96, height=96, fallback="#1e293b")), ]) pz.run(Splash) ``` **Performance:** Shaders are fast, but each pixel runs the whole graph. Fractal noise with many octaves on a full-screen widget is the most expensive thing on this page; lower the octaves or the widget's size if an old laptop struggles. #### More to explore ### Shaders > Paint widgets on the GPU with your own WGSL shaders - backgrounds, effects and game visuals - with uniforms from Python and the mouse position. Source: https://albakore.github.io/pyzed/shading/shaders/ `pz.shader` is a widget painted on the GPU by a function you write in [WGSL](https://www.w3.org/TR/WGSL/), the shading language of WebGPU. The GPU runs it for every pixel, every frame, so animated backgrounds, plasma, fire or a radar sweep cost Python nothing. :::tip[Prefer nodes?] You can build shaders without writing WGSL: wire nodes in the designer's [Shader view](https://albakore.github.io/pyzed/shading/shader-designer/) and draw the `.pzshad` file with `pz.shader("fire.pzshad")`. See [Shader graphs](https://albakore.github.io/pyzed/shading/shader-graphs/). ::: (Image: Six shaders: plasma, ripple, tunnel, CRT, Voronoi and fire) ```python WAVES = """ fn shade(uv: vec2, px: vec2, size: vec2, time: f32) -> vec4 { let wave = 0.5 + 0.5 * sin(uv.x * 12.0 + time * speed()); return vec4(mix(vec3(0.1, 0.1, 0.3), tint().rgb, wave), 1.0); } """ pz.shader(WAVES, uniforms={"speed": 2.0, "tint": "accent"}, height=200) ``` #### The `shade` function Your code defines `shade`, plus any helper functions it needs. It gets: | Argument | Meaning | | --- | --- | | `uv` | Where the pixel is, from `(0, 0)` at the top left to `(1, 1)` at the bottom right. | | `px` | The same position in pixels. | | `size` | The widget's size in pixels. | | `time` | Seconds since the app started. | It returns the pixel's color as `vec4(red, green, blue, alpha)`, each from 0 to 1, in the same color space as CSS colors. `mouse()` returns the pointer position as a `uv` (negative when the pointer is outside the widget); a shader that calls it redraws when the mouse moves. #### Uniforms `uniforms` passes up to 32 values from Python. Each becomes a function with its name: | Value | WGSL | | --- | --- | | A number | `speed()` returns `f32` | | 2, 3 or 4 numbers | `vec2`, `vec3` or `vec4` | | A color (hex or theme token) | `vec4` with the color's red, green, blue and alpha | Change the values in `view()` like any other property: the shader is compiled once and only the values change, so a slider can drive it at full frame rate. #### Options - `width`, `height`: the size (`"fill"` by default). - `animate=False` draws a still picture that only changes when the view does. - `time=1.5` freezes `time` at that value. - `fallback` is a color or gradient drawn where there is no GPU (and behind the shader where it is transparent). - `textures={"photo": "assets/photo.png"}` passes up to 8 images: each becomes a function, `photo(uv)`, that returns the image's color at `uv` (it repeats outside 0..1). #### Errors The code is checked when the widget is created, so a mistake raises a `ValueError` in Python with the compiler's message and the line: ```text ValueError: invalid shader: error: no definition in scope for identifier: `colr` ┌─ wgsl:24:12 ``` #### Combining with other effects A shader is a widget, so it works with the rest of Pyzed: put it in a `pz.stack` behind glass panels (they blur it), clip it with a container, rotate or fade it with `style()` and `animate()`. The [showcase](https://albakore.github.io/pyzed/showcase/) demos use shaders for a space nebula, a synthwave horizon, drifting embers and moving color blobs. #### Limits - Shaders need the GPU renderer. On the software renderer (no GPU, and some CI machines) the widget shows `fallback`; screenshots taken there show it too. Without a real GPU, Pyzed picks the software renderer; start the app with `ICED_BACKEND=wgpu` to run shaders on an emulated GPU instead (on Linux, Mesa's software Vulkan driver, `mesa-vulkan-drivers`). - The corners of a shader are square: `radius` on a container around it does not round them. ### Vectors and UV > Build and split vectors, measure distances and angles, and move, tile, rotate, twist or bend the coordinates. Source: https://albakore.github.io/pyzed/shading/nodes/vectors-uv/ Every example on this page runs after these two lines, and ends by sending a color to the Output node: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Vector Vectors are groups of 2, 3 or 4 numbers: a position (x, y), a color (r, g, b) or a color with alpha (r, g, b, a). These nodes build, split and measure them. ##### Compose `compose` · Vector · `g.compose(...)` Builds a vector from its components. Builds a vector from separate numbers: three values into an RGB color, two into a point. The `type` setting picks vec2, vec3 or vec4 (the sidebar has *Compose vec2* and *Compose vec4* too). | Input | Type | Default | | --- | --- | --- | | `x` | `float` | `0` | | `y` | `float` | `0` | | `z` | `float` | `0` | | Output | Type | | --- | --- | | `vector` | `vec3` | | Setting | Values | Default | | --- | --- | --- | | `type` | `vec2`, `vec3`, `vec4` | `vec3` | The inputs follow the `type` setting: `x` and `y` for vec2, up to `w` for vec4. **Sidebar entries** | Entry | What it does | In Python | | --- | --- | --- | | Compose | Builds a vector from its components. | `g.compose(...)` | | Compose vec2 | Builds a vec2 from x and y. | `g.compose_vec2(...)` | | Compose vec4 | Builds a vec4 from x, y, z and w. | `g.compose_vec4(...)` | ```python title="Example" uv = g.uv() rgb = g.compose(uv["x"], uv["y"], 0.5) g.output(g.rgba(rgb, 1.0)) ``` ##### Decompose `decompose` · Vector · `g.decompose(...)` The components of a vector (x, y, z and w; r, g, b and a for colors). Splits a vector into its components: `x`, `y`, `z`, `w` (the red, green, blue and alpha of a color). | Input | Type | Default | | --- | --- | --- | | `vector` | `T` | `(0, 0, 0, 1)` | | Output | Type | | --- | --- | | `x` | `float` | | `y` | `float` | | `z` | `float` | | `w` | `float` | ```python title="Example" c = g.color("#f97316") parts = g.decompose(c) g.output(g.rgba(parts["x"], 1.0)) ``` ##### Swizzle `swizzle` · Vector · `g.swizzle(...)` Picks and reorders components: “yx”, “zzz”, “rgb”, “x”… Picks and reorders components with a mask: `yx` swaps x and y, `rgb` drops alpha, `xxx` repeats one. In Python, `port.yx` or `port.rgb` adds it for you. | Input | Type | Default | | --- | --- | --- | | `vector` | `T` | `(0, 0, 0, 1)` | | Output | Type | | --- | --- | | `result` | `vec2` | | Setting | Values | Default | | --- | --- | --- | | `mask` | `text` | `yx` | ```python title="Example" uv = g.uv()["uv"] g.output(g.rgba(g.compose(uv.y, uv.x, 0.0), 1.0)) ``` ##### Dot `dot` · Vector · `g.dot(...)` The dot product of a and b: how much they point the same way. The dot product: how much two directions point the same way (1 same, 0 square, −1 opposite). Also `p · p` is the squared length. | Input | Type | Default | | --- | --- | --- | | `a` | `T` | `(1, 0)` | | `b` | `T` | `(1, 0)` | | Output | Type | | --- | --- | | `result` | `float` | ```python title="Example" p = g.uv()["uv"] - 0.5 d = g.dot(p, p) g.output(g.rgba(1.0 - d * 4.0, 1.0)) ``` ##### Cross `cross` · Vector · `g.cross(...)` The cross product of two vec3. The cross product of two 3D vectors: a direction perpendicular to both. Mostly for lighting tricks. | Input | Type | Default | | --- | --- | --- | | `a` | `vec3` | `(1, 0, 0)` | | `b` | `vec3` | `(0, 1, 0)` | | Output | Type | | --- | --- | | `result` | `vec3` | ```python title="Example" n = g.cross([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]) g.output(g.rgba(n, 1.0)) ``` ##### Length `length` · Vector · `g.length(...)` How long a vector is. How long a vector is. The length of Centered UV is the distance to the center: a circle in one node. | Input | Type | Default | | --- | --- | --- | | `vector` | `T` | `(1, 1)` | | Output | Type | | --- | --- | | `length` | `float` | ```python title="Example" d = g.length(g.centered_uv()["centered"]) g.output(g.rgba(1.0 - d, 1.0)) ``` ##### Distance `distance` · Vector · `g.distance(...)` How far apart a and b are. How far apart two points are. By default `a` is the pixel's UV, so it measures from the pixel to `b`. | Input | Type | Default | | --- | --- | --- | | `a` | `T` | *UV* (context) | | `b` | `T` | `(0.5, 0.5)` | | Output | Type | | --- | --- | | `distance` | `float` | ```python title="Example" d = g.distance(b=[0.5, 0.5]) g.output(g.rgba(g.smoothstep(0.3, 0.0, d), 1.0)) ``` ##### Normalize `normalize` · Vector · `g.normalize(...)` The vector made 1 long. The same direction with a length of 1: for directions, light vectors and reflections. | Input | Type | Default | | --- | --- | --- | | `vector` | `T` | `(1, 1)` | | Output | Type | | --- | --- | | `result` | `T` | ```python title="Example" n = g.normalize(g.uv()["uv"] - 0.5) g.output(g.rgba(g.compose(n.x, n.y, 0.5) * 0.5 + 0.5, 1.0)) ``` ##### Reflect `reflect` · Vector · `g.reflect(...)` The direction i bounced off a surface facing n. Bounces direction `i` off a surface that faces `n`, like a ball off a wall. | Input | Type | Default | | --- | --- | --- | | `i` | `T` | `(1, -1)` | | `n` | `T` | `(0, 1)` | | Output | Type | | --- | --- | | `result` | `T` | ```python title="Example" r = g.reflect(g.uv()["uv"] - 0.5, [0.0, 1.0]) g.output(g.rgba(g.compose(r.x, r.y, 0.5) + 0.5, 1.0)) ``` ##### Polar `polar` · Vector · `g.polar(...)` The distance and angle of the pixel around center (aspect kept): angle in radians (−π..π) and from 0 to 1. Distance and angle around a center: the key to radial things such as radar sweeps, pie charts, sun rays and spirals. `angle01` goes 0..1 around the circle. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | `center` | `vec2` | `(0.5, 0.5)` | | Output | Type | | --- | --- | | `radius` | `float` | | `angle` | `float` | | `angle01` · angle 0..1 | `float` | | `polar` | `vec2` | ```python title="Example" p = g.polar() rays = g.step(0.5, g.fract(p["angle01"] * 12.0)) g.output(g.rgba(rays, 1.0)) ``` #### UV UV nodes change **where** the next nodes look, not the color: feed their `uv` output into the `uv` (or `p`) input of a pattern, a noise or a shape to move, repeat, rotate or distort it. ##### Tiling and offset `tiling_offset` · UV · `g.tiling_offset(...)` uv × tiling + offset: repeats and moves what uses it. Repeats and moves what comes next: `tiling` (2, 2) shows it twice in each direction, `offset` slides it. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | `tiling` | `vec2` | `(2, 2)` | | `offset` | `vec2` | `(0, 0)` | | Output | Type | | --- | --- | | `uv` | `vec2` | ```python title="Example" uv = g.tiling_offset(tiling=[4.0, 4.0]) g.output(g.rgba(g.fract(uv["uv"]).x, 1.0)) ``` ##### Scroll `scroll` · UV · `g.scroll(...)` Moves uv over time: uv + speed × time. Moves the coordinates over time: wire it before a noise or a pattern to make it drift like clouds or a conveyor belt. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | `speed` | `vec2` | `(0.1, 0)` | | `time` | `float` | *Time* (context) | | Output | Type | | --- | --- | | `uv` | `vec2` | ```python title="Example" uv = g.scroll(speed=[0.1, 0.0]) n = g.fbm(uv["uv"], scale=3.0) g.output(g.rgba(n, 1.0)) ``` ##### Rotate `rotate` · UV · `g.rotate(...)` Turns uv around center by angle (radians). Turns the coordinates around `center` by `angle` radians (wire Time into it to spin). | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | `center` | `vec2` | `(0.5, 0.5)` | | `angle` | `float` | `0.5` | | Output | Type | | --- | --- | | `uv` | `vec2` | ```python title="Example" uv = g.rotate(angle=g.time()["time"]) s = g.stripes(uv["uv"], count=8.0) g.output(g.rgba(s, 1.0)) ``` ##### Twirl `twirl` · UV · `g.twirl(...)` Twists uv around center, more the farther away. Twists the coordinates around a center, more the farther away: whirlpools and portals. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | `center` | `vec2` | `(0.5, 0.5)` | | `strength` | `float` | `6` | | Output | Type | | --- | --- | | `uv` | `vec2` | ```python title="Example" uv = g.twirl(strength=8.0) c = g.checker(uv["uv"], cells=[8.0, 8.0]) g.output(g.rgba(c, 1.0)) ``` ##### Pixelate `pixelate` · UV · `g.pixelate(...)` Snaps uv to a grid of cells, for a pixel-art look. Snaps the coordinates to a grid of cells, so whatever comes next looks like pixel art. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | `cells` | `vec2` | `(48, 32)` | | Output | Type | | --- | --- | | `uv` | `vec2` | ```python title="Example" uv = g.pixelate(cells=[24.0, 16.0]) g.output(g.gradient(g.noise(uv["uv"], scale=4.0))) ``` ##### Barrel `barrel` · UV · `g.barrel(...)` Bends uv like a curved CRT screen; inside is 0 where the bent uv leaves the screen. Bends the coordinates like the curved glass of an old CRT. Multiply the color by `inside` to black out the corners that fall off the screen. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | `amount` | `float` | `0.25` | | Output | Type | | --- | --- | | `uv` | `vec2` | | `inside` | `float` | ```python title="Example" b = g.barrel(amount=0.3) c = g.checker(b["uv"], cells=[10.0, 8.0]) g.output(g.rgba(c * b["inside"], 1.0)) ``` ##### Kaleidoscope `kaleidoscope` · UV · `g.kaleidoscope(...)` Mirrors uv around center into segments. Mirrors the coordinates into `segments` slices around a center, like a kaleidoscope. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | `center` | `vec2` | `(0.5, 0.5)` | | `segments` | `float` | `6` | | Output | Type | | --- | --- | | `uv` | `vec2` | ```python title="Example" uv = g.kaleidoscope(segments=8.0) n = g.noise(uv["uv"], scale=6.0) g.output(g.rgba(g.palette(n), 1.0)) ``` ##### Flip `flip` · UV · `g.flip(...)` Mirrors uv horizontally, vertically or both. Mirrors the coordinates horizontally (`x`), vertically (`y`) or both. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | Output | Type | | --- | --- | | `uv` | `vec2` | | Setting | Values | Default | | --- | --- | --- | | `axis` | `x`, `y`, `both` | `x` | ```python title="Example" uv = g.flip(axis="y") g.output(g.gradient(uv["uv"].y)) ``` ##### Repeat `repeat` · UV · `g.repeat(...)` Repeats space in cells around (0, 0): draw one shape, get a grid of them. Cell is which cell the pixel is in. Repeats space in cells: draw one shape and get a whole grid of them. `cell` tells which cell a pixel is in, to give each copy its own color. | Input | Type | Default | | --- | --- | --- | | `p` | `vec2` | *Centered* (context) | | `cell` | `vec2` | `(0.25, 0.25)` | | Output | Type | | --- | --- | | `p` | `vec2` | | `cell` | `vec2` | ```python title="Example" r = g.repeat(cell=[0.25, 0.25]) d = g.circle(r["p"], radius=0.08) g.output(g.rgba(g.fill(d), 1.0)) ``` **Always up to date:** This page is generated from the node catalog of `pyzed.shading`, so the ports, types and defaults are always those of this version. Each example is compiled when the docs are built. **See also:** [Getting started](https://albakore.github.io/pyzed/shading/getting-started/) · [The Shader view](https://albakore.github.io/pyzed/shading/shader-designer/) · [Recipes](https://albakore.github.io/pyzed/shading/recipes/) · [Shader graphs in Python](https://albakore.github.io/pyzed/shading/shader-graphs/) ### Noise and patterns > Organic randomness (Perlin, simplex, fractal, Voronoi) and regular patterns (checker, stripes, grid, dots, gradients, waves). Source: https://albakore.github.io/pyzed/shading/nodes/noise-patterns/ Every example on this page runs after these two lines, and ends by sending a color to the Output node: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Noise Noise gives values between 0 and 1 that look random but change smoothly: the base of clouds, fire, water, marble and terrain. ##### Noise `noise` · Noise · `g.noise(...)` Smooth random values from 0 to 1 (value, Perlin or simplex noise). Smooth randomness from 0 to 1. **Perlin** is the classic, **simplex** looks more natural, **value** is blockier and cheaper. `scale` is how many bumps fit across. | Input | Type | Default | | --- | --- | --- | | `p` | `vec2` | *Aspect UV* (context) | | `scale` | `float` | `8` | | Output | Type | | --- | --- | | `value` | `float` | | Setting | Values | Default | | --- | --- | --- | | `kind` | `value`, `perlin`, `simplex` | `perlin` | **Sidebar entries** | Entry | What it does | In Python | | --- | --- | --- | | Noise | Smooth random values from 0 to 1 (value, Perlin or simplex noise). | `g.noise(...)` | | Value noise | Blocky smooth noise from 0 to 1. | `g.value_noise(...)` | | Simplex noise | Simplex noise from 0 to 1. | `g.simplex_noise(...)` | ```python title="Example" n = g.noise(scale=5.0, kind="simplex") g.output(g.rgba(n, 1.0)) ``` ##### Fractal noise `fbm` · Noise · `g.fbm(...)` Layers of noise, each finer and fainter (fBm): clouds, smoke, fire, terrain. Several layers of noise, each finer and fainter: the look of clouds, smoke and mountains. More `octaves` add detail (and cost). | Input | Type | Default | | --- | --- | --- | | `p` | `vec2` | *Aspect UV* (context) | | `scale` | `float` | `4` | | `octaves` | `int` | `5` | | `lacunarity` | `float` | `2` | | `gain` | `float` | `0.5` | | Output | Type | | --- | --- | | `value` | `float` | | Setting | Values | Default | | --- | --- | --- | | `kind` | `value`, `perlin`, `simplex` | `value` | ```python title="Example" n = g.fbm(scale=3.0, octaves=6) g.output(g.gradient(n)) ``` ##### Voronoi `voronoi` · Noise · `g.voronoi(...)` Cells around random points: the distance to the nearest point and the second nearest, to the nearest edge, and a random number per cell. Phase moves the points. Cells around random points, like cracked mud, crystals or giraffe skin. `edge` is small near the borders, `cell` is a random number per cell (great for a color each). `phase` moves the points. | Input | Type | Default | | --- | --- | --- | | `p` | `vec2` | *Aspect UV* (context) | | `scale` | `float` | `6` | | `jitter` | `float` | `1` | | `phase` | `float` | *Time* (context) | | Output | Type | | --- | --- | | `distance` | `float` | | `second` | `float` | | `edge` | `float` | | `cell` | `float` | ```python title="Example" v = g.voronoi(scale=6.0) color = g.palette(v["cell"]) g.output(g.rgba(color * g.smoothstep(0.0, 0.05, v["edge"]), 1.0)) ``` ##### White noise `white_noise` · Noise · `g.white_noise(...)` A different random number from 0 to 1 for every value of p. A different random number for each input value, with no smoothness: TV static, sparkles, dithering. | Input | Type | Default | | --- | --- | --- | | `p` | `vec2` | *Pixel* (context) | | Output | Type | | --- | --- | | `value` | `float` | | `random2` | `vec2` | ```python title="Example" n = g.white_noise() g.output(g.rgba(n, 1.0)) ``` ##### Random `random` · Noise · `g.random(...)` A random number from 0 to 1 for a seed (the same seed, the same number). A random number for a seed: the same seed always gives the same number. Feed it a cell number to give each cell its own value. | Input | Type | Default | | --- | --- | --- | | `seed` | `float` | `1` | | Output | Type | | --- | --- | | `value` | `float` | ```python title="Example" r = g.repeat(cell=[0.2, 0.2]) v = g.random(r["cell"].x * 7.0 + r["cell"].y) g.output(g.rgba(v, 1.0)) ``` #### Patterns Regular patterns, mostly 0 or 1 (black or white). Multiply a color by them, or use them as the `t` of a Mix or a Gradient. ##### Checker `checker` · Patterns · `g.checker(...)` A checkerboard of 0 and 1. A checkerboard: 0 and 1 in alternating squares. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *Aspect UV* (context) | | `cells` | `vec2` | `(8, 8)` | | Output | Type | | --- | --- | | `value` | `float` | ```python title="Example" c = g.checker(cells=[8.0, 8.0]) g.output(g.rgba(g.mix(0.2, 0.9, c), 1.0)) ``` ##### Stripes `stripes` · Patterns · `g.stripes(...)` Parallel stripes of 0 and 1 (angle in radians, width from 0 to 1). Parallel stripes at any angle; `width` is how much of each stripe is white. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | `count` | `float` | `10` | | `angle` | `float` | `0` | | `width` | `float` | `0.5` | | Output | Type | | --- | --- | | `value` | `float` | ```python title="Example" s = g.stripes(count=12.0, angle=0.785) g.output(g.rgba(s, 1.0)) ``` ##### Grid `grid` · Patterns · `g.grid(...)` Grid lines: 1 on the lines, 0 between them (thickness from 0 to 1 of a cell). Thin grid lines: graph paper, blueprints and synthwave floors. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *Aspect UV* (context) | | `cells` | `vec2` | `(8, 8)` | | `thickness` | `float` | `0.06` | | Output | Type | | --- | --- | | `value` | `float` | ```python title="Example" l = g.grid(cells=[10.0, 10.0], thickness=0.05) g.output(g.rgba(g.mix([0.05, 0.05, 0.15], [0.2, 0.8, 1.0], l), 1.0)) ``` ##### Dots `dots` · Patterns · `g.dots(...)` A grid of round dots: 1 inside, 0 outside (radius from 0 to 0.5 of a cell). A grid of round dots: polka dots and halftone looks. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *Aspect UV* (context) | | `cells` | `vec2` | `(10, 10)` | | `radius` | `float` | `0.3` | | Output | Type | | --- | --- | | `value` | `float` | ```python title="Example" d = g.dots(cells=[12.0, 12.0], radius=0.25) g.output(g.rgba(d, 1.0)) ``` ##### Linear gradient `linear_gradient` · Patterns · `g.linear_gradient(...)` 0 to 1 across the area in a direction (angle in radians). 0 to 1 across the area in a direction (`angle` in radians, 0 is left to right). | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | `angle` | `float` | `0` | | Output | Type | | --- | --- | | `value` | `float` | ```python title="Example" t = g.linear_gradient(angle=1.57) g.output(g.gradient(t)) ``` ##### Radial gradient `radial_gradient` · Patterns · `g.radial_gradient(...)` 1 at center, fading to 0 at radius (aspect kept). 1 in the center fading to 0 at `radius`: spotlights, soft glows and vignettes. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | `center` | `vec2` | `(0.5, 0.5)` | | `radius` | `float` | `0.5` | | Output | Type | | --- | --- | | `value` | `float` | ```python title="Example" r = g.radial_gradient(radius=0.5) g.output(g.rgba(g.compose(r, r * 0.6, 0.2), 1.0)) ``` ##### Wave `wave` · Patterns · `g.wave(...)` A repeating wave from 0 to 1 (sine, triangle, square or sawtooth): frequency waves per unit of x. A repeating wave from 0 to 1 in four shapes: **sine** (smooth), **triangle**, **square** (on/off, blinking) and **sawtooth** (ramp). With the default `x` (time) it animates; wire a UV coordinate to get bands. | Input | Type | Default | | --- | --- | --- | | `x` | `float` | *Time* (context) | | `frequency` | `float` | `1` | | `phase` | `float` | `0` | | Output | Type | | --- | --- | | `value` | `float` | | Setting | Values | Default | | --- | --- | --- | | `shape` | `sine`, `triangle`, `square`, `sawtooth` | `sine` | **Sidebar entries** | Entry | What it does | In Python | | --- | --- | --- | | Wave | A repeating wave from 0 to 1 (sine, triangle, square or sawtooth): frequency waves per unit of x. | `g.wave(...)` | | Square wave | On and off: 0 or 1, repeating. | `g.square_wave(...)` | | Sawtooth wave | Ramps from 0 to 1 and starts again. | `g.sawtooth_wave(...)` | ```python title="Example" w = g.wave(g.uv()["x"], frequency=6.0, shape="triangle") g.output(g.rgba(w, 1.0)) ``` **Always up to date:** This page is generated from the node catalog of `pyzed.shading`, so the ports, types and defaults are always those of this version. Each example is compiled when the docs are built. **See also:** [Getting started](https://albakore.github.io/pyzed/shading/getting-started/) · [The Shader view](https://albakore.github.io/pyzed/shading/shader-designer/) · [Recipes](https://albakore.github.io/pyzed/shading/recipes/) · [Shader graphs in Python](https://albakore.github.io/pyzed/shading/shader-graphs/) ### Shapes > Circles, boxes, rings, lines, polygons and stars as signed distances, combined and drawn with fill, stroke and glow. Source: https://albakore.github.io/pyzed/shading/nodes/shapes/ Every example on this page runs after these two lines, and ends by sending a color to the Output node: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Shapes Shapes are **signed distances** (SDF): each pixel gets its distance to the edge of the shape, negative inside and positive outside. A distance is not a picture yet: **Fill**, **Stroke** and **Glow** turn it into a mask from 0 to 1. Distances can be combined (union, subtract, intersect) before drawing, even with a smooth seam. ##### Circle `circle` · Shapes · `g.circle(...)` The signed distance to a circle (negative inside). Use Fill or Stroke to draw it. A circle around `center` with a `radius` (in units of the height). Draw it with Fill, Stroke or Glow. | Input | Type | Default | | --- | --- | --- | | `p` | `vec2` | *Centered* (context) | | `center` | `vec2` | `(0, 0)` | | `radius` | `float` | `0.3` | | Output | Type | | --- | --- | | `distance` | `float` | ```python title="Example" d = g.circle(radius=0.3) g.output(g.rgba(g.fill(d), 1.0)) ``` ##### Box `box` · Shapes · `g.box(...)` The signed distance to a rectangle of size (rounded by radius). A rectangle of `size`, with corners rounded by `radius`: cards, buttons, panels. | Input | Type | Default | | --- | --- | --- | | `p` | `vec2` | *Centered* (context) | | `center` | `vec2` | `(0, 0)` | | `size` | `vec2` | `(0.6, 0.4)` | | `radius` | `float` | `0.05` | | Output | Type | | --- | --- | | `distance` | `float` | ```python title="Example" d = g.box(size=[0.8, 0.5], radius=0.08) g.output(g.rgba(g.fill(d), 1.0)) ``` ##### Ring `ring` · Shapes · `g.ring(...)` The signed distance to a ring of radius and thickness. A ring (a donut outline) of `radius` and `thickness`: loaders, targets, halos. | Input | Type | Default | | --- | --- | --- | | `p` | `vec2` | *Centered* (context) | | `center` | `vec2` | `(0, 0)` | | `radius` | `float` | `0.3` | | `thickness` | `float` | `0.05` | | Output | Type | | --- | --- | | `distance` | `float` | ```python title="Example" d = g.ring(radius=0.3, thickness=0.04) g.output(g.rgba(g.fill(d), 1.0)) ``` ##### Segment `segment` · Shapes · `g.segment(...)` The signed distance to a line from a to b, thickness wide. A line from `a` to `b` with round ends and a `thickness`. | Input | Type | Default | | --- | --- | --- | | `p` | `vec2` | *Centered* (context) | | `a` | `vec2` | `(-0.3, 0)` | | `b` | `vec2` | `(0.3, 0)` | | `thickness` | `float` | `0.04` | | Output | Type | | --- | --- | | `distance` | `float` | ```python title="Example" d = g.segment(a=[-0.4, -0.2], b=[0.4, 0.2], thickness=0.05) g.output(g.rgba(g.fill(d), 1.0)) ``` ##### Polygon `polygon` · Shapes · `g.polygon(...)` The signed distance to a regular polygon (sides corners, radius to them). A regular polygon with `sides` corners: triangles, hexagons, octagons. | Input | Type | Default | | --- | --- | --- | | `p` | `vec2` | *Centered* (context) | | `center` | `vec2` | `(0, 0)` | | `radius` | `float` | `0.3` | | `sides` | `float` | `6` | | `rotation` | `float` | `0` | | Output | Type | | --- | --- | | `distance` | `float` | ```python title="Example" d = g.polygon(radius=0.35, sides=6.0) g.output(g.rgba(g.fill(d), 1.0)) ``` ##### Star `star` · Shapes · `g.star(...)` The signed distance to a star (inner is how deep its valleys are, 0 to 1). A star with `points` tips; `inner` is how deep the valleys between tips are. | Input | Type | Default | | --- | --- | --- | | `p` | `vec2` | *Centered* (context) | | `center` | `vec2` | `(0, 0)` | | `radius` | `float` | `0.35` | | `points` | `float` | `5` | | `inner` | `float` | `0.45` | | `rotation` | `float` | `0` | | Output | Type | | --- | --- | | `distance` | `float` | ```python title="Example" d = g.star(radius=0.4, points=5.0, inner=0.45) g.output(g.rgba(g.fill(d), 1.0)) ``` ##### Combine shapes `sdf_op` · Shapes · `g.sdf_op(...)` Joins two shapes (union), cuts b out of a (subtract) or keeps where they overlap (intersect); smooth melts the seam. Combines two shapes before drawing them: **Union** joins them, **Subtract shape** cuts `b` out of `a`, **Intersect** keeps the overlap. `smooth` above 0 melts the seam, like two drops of water merging. | Input | Type | Default | | --- | --- | --- | | `a` | `float` | `1` | | `b` | `float` | `1` | | `smooth` | `float` | `0` | | Output | Type | | --- | --- | | `distance` | `float` | | Setting | Values | Default | | --- | --- | --- | | `op` | `union`, `subtract`, `intersect` | `union` | **Sidebar entries** | Entry | What it does | In Python | | --- | --- | --- | | Union | Joins two shapes; smooth melts them together. | `g.union(...)` | | Subtract shape | Cuts shape b out of shape a. | `g.subtract_shape(...)` | | Intersect | Keeps where two shapes overlap. | `g.intersect(...)` | ```python title="Example" a = g.circle(center=[-0.15, 0.0], radius=0.25) b = g.circle(center=[0.15, 0.0], radius=0.25) d = g.union(a, b, smooth=0.1) g.output(g.rgba(g.fill(d), 1.0)) ``` ##### Fill `fill` · Shapes · `g.fill(...)` Draws a shape: 1 inside, 0 outside, with an antialiased edge softness pixels wide. Draws a shape solid: 1 inside, 0 outside, with an antialiased edge `softness` pixels wide. Raise `softness` for a blurry edge. | Input | Type | Default | | --- | --- | --- | | `distance` | `float` | `0` | | `softness` | `float` | `1` | | Output | Type | | --- | --- | | `mask` | `float` | ```python title="Example" d = g.box(size=[0.6, 0.6], radius=0.1) g.output(g.rgba(g.fill(d, softness=8.0), 1.0)) ``` ##### Stroke `stroke` · Shapes · `g.stroke(...)` Draws the outline of a shape, width wide (in the shape's units). Draws only the outline of a shape, `width` wide. | Input | Type | Default | | --- | --- | --- | | `distance` | `float` | `0` | | `width` | `float` | `0.02` | | `softness` | `float` | `1` | | Output | Type | | --- | --- | | `mask` | `float` | ```python title="Example" d = g.star(radius=0.4) g.output(g.rgba(g.stroke(d, width=0.02), 1.0)) ``` ##### Glow `glow` · Shapes · `g.glow(...)` A soft light around a shape that fades over radius. A soft light around a shape that fades over `radius`: neon signs and halos. Add it to a Fill for a solid shape with a glow. | Input | Type | Default | | --- | --- | --- | | `distance` | `float` | `0` | | `radius` | `float` | `0.08` | | `intensity` | `float` | `1` | | Output | Type | | --- | --- | | `value` | `float` | ```python title="Example" d = g.ring(radius=0.3, thickness=0.01) light = g.glow(d, radius=0.1) g.output(g.rgba(g.compose(0.1, 0.8, 1.0) * light, 1.0)) ``` **Always up to date:** This page is generated from the node catalog of `pyzed.shading`, so the ports, types and defaults are always those of this version. Each example is compiled when the docs are built. **See also:** [Getting started](https://albakore.github.io/pyzed/shading/getting-started/) · [The Shader view](https://albakore.github.io/pyzed/shading/shader-designer/) · [Recipes](https://albakore.github.io/pyzed/shading/recipes/) · [Shader graphs in Python](https://albakore.github.io/pyzed/shading/shader-graphs/) ### Color, effects and textures > Gradients, palettes, hue, saturation, blend modes and layers, screen effects (vignette, scanlines, grain) and images. Source: https://albakore.github.io/pyzed/shading/nodes/color-effects/ Every example on this page runs after these two lines, and ends by sending a color to the Output node: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Color Colors are `vec3` (r, g, b) or `vec4` (r, g, b, a) with values from 0 to 1, not from 0 to 255: `(1, 0.5, 0)` is orange. ##### HSV to RGB `hsv_to_rgb` · Color · `g.hsv_to_rgb(...)` A color from hue (0 to 1 around the wheel), saturation and value. A color from hue (0..1 around the color wheel: red, yellow, green, cyan, blue, magenta, red), saturation and value. Wire Time into the hue for a rainbow that cycles. | Input | Type | Default | | --- | --- | --- | | `h` · hue | `float` | `0` | | `s` · saturation | `float` | `1` | | `v` · value | `float` | `1` | | Output | Type | | --- | --- | | `rgb` | `vec3` | ```python title="Example" uv = g.uv() g.output(g.rgba(g.hsv_to_rgb(uv["x"], 0.8, 1.0), 1.0)) ``` ##### RGB to HSV `rgb_to_hsv` · Color · `g.rgb_to_hsv(...)` The hue, saturation and value of a color. Splits a color into hue, saturation and value, to change one of them. | Input | Type | Default | | --- | --- | --- | | `rgb` | `vec3` | `(1, 0, 0)` | | Output | Type | | --- | --- | | `hsv` | `vec3` | | `h` · hue | `float` | | `s` · saturation | `float` | | `v` · value | `float` | ```python title="Example" hsv = g.rgb_to_hsv([1.0, 0.5, 0.0]) g.output(g.rgba(g.hsv_to_rgb(hsv["h"], 0.3, hsv["v"]), 1.0)) ``` ##### Hue shift `hue_shift` · Color · `g.hue_shift(...)` Turns the hue of a color (shift from 0 to 1 is a full turn). Turns the hue of a color around the wheel (`shift` 1 is a full turn). | Input | Type | Default | | --- | --- | --- | | `rgb` | `vec3` | `(1, 0, 0)` | | `shift` | `float` | `0.25` | | Output | Type | | --- | --- | | `rgb` | `vec3` | ```python title="Example" g.output(g.rgba(g.hue_shift([1.0, 0.2, 0.2], g.time(scale=0.2)["time"]), 1.0)) ``` ##### Saturation `saturation` · Color · `g.saturation(...)` More (above 1) or less (below 1) colorful; 0 is gray. More (above 1) or less (below 1) colorful; 0 is gray. | Input | Type | Default | | --- | --- | --- | | `rgb` | `vec3` | `(1, 0.5, 0)` | | `amount` | `float` | `1.5` | | Output | Type | | --- | --- | | `rgb` | `vec3` | ```python title="Example" c = g.palette(g.uv()["x"]) g.output(g.rgba(g.saturation(c, 0.4), 1.0)) ``` ##### Brightness and contrast `brightness_contrast` · Color · `g.brightness_contrast(...)` Lighter or darker, and more or less contrast. Lighter or darker, and more or less contrast, like a photo editor. | Input | Type | Default | | --- | --- | --- | | `rgb` | `vec3` | `(0.5, 0.5, 0.5)` | | `brightness` | `float` | `0` | | `contrast` | `float` | `1` | | Output | Type | | --- | --- | | `rgb` | `vec3` | ```python title="Example" c = g.palette(g.uv()["x"]) g.output(g.rgba(g.brightness_contrast(c, 0.1, 1.4), 1.0)) ``` ##### Luminance `luminance` · Color · `g.luminance(...)` How bright a color looks, from 0 to 1. How bright a color looks to the eye, as one number from 0 to 1. | Input | Type | Default | | --- | --- | --- | | `rgb` | `vec3` | `(1, 1, 1)` | | Output | Type | | --- | --- | | `value` | `float` | ```python title="Example" l = g.luminance([0.2, 0.7, 0.3]) g.output(g.rgba(l, 1.0)) ``` ##### Grayscale `grayscale` · Color · `g.grayscale(...)` The color in shades of gray. The color in shades of gray (black and white). | Input | Type | Default | | --- | --- | --- | | `rgb` | `vec3` | `(1, 0.5, 0)` | | Output | Type | | --- | --- | | `rgb` | `vec3` | ```python title="Example" c = g.palette(g.uv()["x"]) g.output(g.rgba(g.grayscale(c), 1.0)) ``` ##### Invert `invert` · Color · `g.invert(...)` The opposite color: 1 − color (alpha is kept). The negative of a color (1 − color); alpha is kept. | Input | Type | Default | | --- | --- | --- | | `color` | `T` | `(1, 0.5, 0)` | | Output | Type | | --- | --- | | `color` | `T` | ```python title="Example" c = g.gradient(g.uv()["x"]) g.output(g.invert(c["color"])) ``` ##### Gamma `gamma` · Color · `g.gamma(...)` Brightens (above 1) or darkens (below 1) the midtones. Brightens (above 1) or darkens (below 1) the midtones without touching black and white. | Input | Type | Default | | --- | --- | --- | | `rgb` | `vec3` | `(0.5, 0.5, 0.5)` | | `gamma` | `float` | `2.2` | | Output | Type | | --- | --- | | `rgb` | `vec3` | ```python title="Example" v = g.uv()["x"] g.output(g.rgba(g.gamma(g.compose(v, v, v), 2.2), 1.0)) ``` ##### Posterize `posterize` · Color · `g.posterize(...)` Keeps only a few steps of each component. Keeps only a few steps of each color: cel shading and retro looks. | Input | Type | Default | | --- | --- | --- | | `color` | `T` | `(0.3, 0.6, 0.9)` | | `steps` | `float` | `4` | | Output | Type | | --- | --- | | `color` | `T` | ```python title="Example" c = g.palette(g.uv()["x"]) g.output(g.rgba(g.posterize(c, 4.0), 1.0)) ``` ##### Blend `blend` · Color · `g.blend(...)` Blends layer over base like an image editor's layer modes. Blends `layer` over `base` like the layer modes of an image editor: normal, multiply, screen, overlay, add, subtract, difference, darken, lighten, soft light, color dodge and color burn. | Input | Type | Default | | --- | --- | --- | | `base` | `vec3` | `(0.2, 0.4, 0.8)` | | `layer` | `vec3` | `(1, 0.6, 0.2)` | | `opacity` | `float` | `1` | | Output | Type | | --- | --- | | `rgb` | `vec3` | | Setting | Values | Default | | --- | --- | --- | | `mode` | `normal`, `multiply`, `screen`, `overlay`, `add`, `subtract`, `difference`, `darken`, `lighten`, `soft_light`, `color_dodge`, `color_burn` | `multiply` | ```python title="Example" base = g.gradient(g.uv()["y"]) clouds = g.fbm(scale=3.0) layer = g.compose(clouds, clouds, clouds) g.output(g.rgba(g.blend(base["rgb"], layer, 0.8, mode="screen"), 1.0)) ``` ##### Palette `palette` · Color · `g.palette(...)` A smooth rainbow-like color for t, from Inigo Quilez's cosine palettes: a + b × cos(2π(c × t + d)). Turns a number from 0 to 1 into a smooth, rainbow-like color with a cosine formula. Change `d` to shift the colors; it is the quickest way to color noise. | Input | Type | Default | | --- | --- | --- | | `t` | `float` | `0` | | `a` | `vec3` | `(0.5, 0.5, 0.5)` | | `b` | `vec3` | `(0.5, 0.5, 0.5)` | | `c` | `vec3` | `(1, 1, 1)` | | `d` | `vec3` | `(0, 0.33, 0.67)` | | Output | Type | | --- | --- | | `rgb` | `vec3` | ```python title="Example" n = g.noise(scale=4.0) g.output(g.rgba(g.palette(n), 1.0)) ``` ##### Gradient `gradient` · Color · `g.gradient(...)` A color ramp: the color at t (0 to 1) between the stops you set. A color ramp you edit with stops: the color at `t` between the stops. In the designer you drag the stops; in Python pass `stops=[[position, color], ...]`. | Input | Type | Default | | --- | --- | --- | | `t` | `float` | `0.5` | | Output | Type | | --- | --- | | `color` | `vec4` | | `rgb` | `vec3` | | `alpha` | `float` | | Setting | Values | Default | | --- | --- | --- | | `stops` | `stops` | `0` #1e1b4b, `0.5` #db2777, `1` #fde047 | ```python title="Example" ramp = g.gradient(g.uv()["x"], stops=[[0.0, "#0f172a"], [0.6, "#7c3aed"], [1.0, "#f472b6"]]) g.output(ramp) ``` ##### RGBA `rgba` · Color · `g.rgba(...)` A color with alpha from a vec3 color and an opacity. Joins an RGB color and an opacity into the `vec4` the Output wants. | Input | Type | Default | | --- | --- | --- | | `rgb` | `vec3` | `(1, 1, 1)` | | `alpha` | `float` | `1` | | Output | Type | | --- | --- | | `color` | `vec4` | ```python title="Example" g.output(g.rgba([0.2, 0.6, 1.0], 0.5)) ``` ##### Layer `layer` · Color · `g.layer(...)` Puts front over back using front's alpha. Puts `front` over `back` using front's alpha, like stacking two layers. | Input | Type | Default | | --- | --- | --- | | `back` | `vec4` | `(0, 0, 0, 1)` | | `front` | `vec4` | `(1, 1, 1, 0.5)` | | Output | Type | | --- | --- | | `color` | `vec4` | ```python title="Example" back = g.gradient(g.uv()["y"]) spot = g.rgba([1.0, 1.0, 1.0], g.fill(g.circle(radius=0.2))) g.output(g.layer(back["color"], spot)) ``` #### Effects Screen effects that return a number around 1: multiply your final color by them. ##### Vignette `vignette` · Effects · `g.vignette(...)` Darkens toward the corners: multiply a color by it. Darkens toward the corners, drawing the eye to the center. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | `strength` | `float` | `0.8` | | Output | Type | | --- | --- | | `value` | `float` | ```python title="Example" c = g.gradient(g.uv()["x"]) g.output(g.rgba(c["rgb"] * g.vignette(strength=0.9), 1.0)) ``` ##### Scanlines `scanlines` · Effects · `g.scanlines(...)` The horizontal lines of a CRT screen: multiply a color by it. The horizontal lines of an old TV; `speed` makes them roll. | Input | Type | Default | | --- | --- | --- | | `px` · pixel | `vec2` | *Pixel* (context) | | `spacing` | `float` | `3` | | `strength` | `float` | `0.25` | | `speed` | `float` | `0` | | Output | Type | | --- | --- | | `value` | `float` | ```python title="Example" c = g.gradient(g.uv()["y"]) g.output(g.rgba(c["rgb"] * g.scanlines(spacing=3.0, strength=0.3), 1.0)) ``` ##### Film grain `grain` · Effects · `g.grain(...)` Random speckles that change every frame: multiply a color by it. Random speckles that change every frame, like film. | Input | Type | Default | | --- | --- | --- | | `px` · pixel | `vec2` | *Pixel* (context) | | `strength` | `float` | `0.12` | | Output | Type | | --- | --- | | `value` | `float` | ```python title="Example" c = g.gradient(g.uv()["x"]) g.output(g.rgba(c["rgb"] * g.grain(strength=0.15), 1.0)) ``` #### Texture Images in a shader: add a `texture` variable (an image file) and read its colors. ##### Sample texture `sample_texture` · Texture · `g.sample_texture(...)` The color of a texture (an image variable) at uv. Reads the color of an image at a UV. Add a `texture` variable whose value is an image path (relative to the `.pzshad`), wire it into `texture`, and warp the UV first for ripples or pixelation. | Input | Type | Default | | --- | --- | --- | | `texture` | `texture` | — | | `uv` | `vec2` | *UV* (context) | | Output | Type | | --- | --- | | `color` | `vec4` | | `rgb` | `vec3` | | `alpha` | `float` | ```python title="Example" photo = g.uniform("photo", "texture", "photo.png") uv = g.pixelate(cells=[40.0, 30.0]) g.output(g.sample_texture(photo, uv["uv"])) ``` **Always up to date:** This page is generated from the node catalog of `pyzed.shading`, so the ports, types and defaults are always those of this version. Each example is compiled when the docs are built. **See also:** [Getting started](https://albakore.github.io/pyzed/shading/getting-started/) · [The Shader view](https://albakore.github.io/pyzed/shading/shader-designer/) · [Recipes](https://albakore.github.io/pyzed/shading/recipes/) · [Shader graphs in Python](https://albakore.github.io/pyzed/shading/shader-graphs/) ### Logic, variables and utility > Comparisons and if/else, named values, reroutes, your own WGSL, other shaders as functions, comments and the Output node. Source: https://albakore.github.io/pyzed/shading/nodes/logic-utility/ Every example on this page runs after these two lines, and ends by sending a color to the Output node: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Logic True/false values and choosing between two values. Shaders run on every pixel at once, so prefer Mix and Smoothstep for soft transitions and keep Select for hard either/or choices. ##### Compare `compare` · Logic · `g.compare(...)` Whether a is less, greater or equal to b (for vectors: every component). Compares `a` and `b` (`<`, `<=`, `>`, `>=`, `==`, `!=`) and gives true or false. | Input | Type | Default | | --- | --- | --- | | `a` | `T` | `0` | | `b` | `T` | `0.5` | | Output | Type | | --- | --- | | `result` | `bool` | | Setting | Values | Default | | --- | --- | --- | | `op` | `<`, `<=`, `>`, `>=`, `==`, `!=` | `<` | ```python title="Example" left = g.compare(g.uv()["x"], 0.5, op="<") g.output(g.rgba(g.select(left, 1.0, 0.2), 1.0)) ``` ##### Select `select` · Logic · `g.select(...)` if condition then a else b. If `condition` is true, `a` (then); otherwise `b` (else). | Input | Type | Default | | --- | --- | --- | | `condition` | `bool` | `true` | | `a` · then | `T` | `1` | | `b` · else | `T` | `0` | | Output | Type | | --- | --- | | `result` | `T` | ```python title="Example" inside = g.mouse()["inside"] g.output(g.rgba(g.select(inside, [0.2, 0.8, 0.4], [0.3, 0.3, 0.3]), 1.0)) ``` ##### Logic `logic` · Logic · `g.logic(...)` and, or, xor and not of true/false values. `and`, `or`, `xor` and `not` of true/false values. | Input | Type | Default | | --- | --- | --- | | `a` | `bool` | `true` | | `b` | `bool` | `false` | | Output | Type | | --- | --- | | `result` | `bool` | | Setting | Values | Default | | --- | --- | --- | | `op` | `and`, `or`, `xor`, `not` | `and` | ```python title="Example" uv = g.uv() both = g.logic(g.compare(uv["x"], 0.5, op=">"), g.compare(uv["y"], 0.5, op=">"), op="and") g.output(g.rgba(g.select(both, 1.0, 0.0), 1.0)) ``` #### Variables Named values inside the graph: compute something once, name it, and read it anywhere without a long wire across the canvas. ##### Set value `set_var` · Variables · `g.set_var(...)` Gives a value a name, so Get value nodes can read it anywhere without a long wire. Gives a value a name. Any **Get value** with the same name reads it, so you can reuse it far away without a wire crossing the whole canvas. | Input | Type | Default | | --- | --- | --- | | `value` | `T` | `0` | | Output | Type | | --- | --- | | `value` | `T` | | Setting | Values | Default | | --- | --- | --- | | `name` | `text` | `value1` | ```python title="Example" g.set_var(g.noise(scale=4.0), name="n") n = g.get_var(name="n") g.output(g.rgba(g.palette(n), 1.0)) ``` ##### Get value `get_var` · Variables · `g.get_var(...)` Reads a value named by a Set value node. Reads a value named by a **Set value** node. | Output | Type | | --- | --- | | `value` | `T` | | Setting | Values | Default | | --- | --- | --- | | `name` | `local` | `value1` | ```python title="Example" g.set_var(g.uv()["x"], name="x") g.output(g.gradient(g.get_var(name="x"))) ``` #### Utility Nodes that keep big graphs tidy and let you go beyond the catalog. ##### Reroute `reroute` · Utility · `g.reroute(...)` A point to bend a wire around other nodes. A dot that only bends a wire around other nodes: it changes nothing. | Input | Type | Default | | --- | --- | --- | | `in` | `T` | `0` | | Output | Type | | --- | --- | | `out` | `T` | ```python title="Example" x = g.reroute(g.uv()["x"]) g.output(g.gradient(x)) ``` ##### Expression `expression` · Utility · `g.expression(...)` Your own WGSL: an expression of a, b, c and d (and uv, px, size, time), or a function body with return. The types of a to d follow what is connected. Your own line of WGSL when no node does what you need: an expression of `a`, `b`, `c` and `d` (plus `uv`, `px`, `size` and `time`), or a function body with `return`. Pick the result's `type`. | Input | Type | Default | | --- | --- | --- | | `a` | `any` | `0` | | `b` | `any` | `0` | | `c` | `any` | `0` | | `d` | `any` | `0` | | Output | Type | | --- | --- | | `result` | `float` | | Setting | Values | Default | | --- | --- | --- | | `code` | `code` | `a + b` | | `type` | `float`, `vec2`, `vec3`, `vec4`, `int`, `bool` | `float` | The types of `a` to `d` follow what is wired to them (`any`); `type` sets the result's. ```python title="Example" e = g.expression(g.uv()["x"], 8.0, code="abs(sin(a * b + time))", type="float") g.output(g.rgba(e, 1.0)) ``` ##### Shader function `subgraph` · Utility · `g.subgraph(...)` Another .pzshad used as a function: its mutable variables are inputs, its output is the color. Feed uv to move, tile or warp it. Uses another `.pzshad` as a function: its mutable variables become inputs and its color the output. Feed `uv` to move, tile or warp the whole shader. | Input | Type | Default | | --- | --- | --- | | `uv` | `vec2` | *UV* (context) | | Output | Type | | --- | --- | | `color` | `vec4` | | `rgb` | `vec3` | | `alpha` | `float` | | Setting | Values | Default | | --- | --- | --- | | `path` | `file` | — | Besides `uv`, every mutable variable of the other shader becomes an input. A Shader function needs another `.pzshad` file: `g.subgraph(path="plasma.pzshad", uv=...)`. In the designer, double-click the node to open that file. ##### Comment `comment` · Utility · `g.comment(...)` A note on the graph. A note on the canvas to explain a part of the graph. It has no effect on the shader. | Setting | Values | Default | | --- | --- | --- | | `text` | `text` | `Note` | ```python title="Example" g.comment(text="Sky: gradient from top to bottom") g.output(g.gradient(g.uv()["y"])) ``` #### Output Every shader has one Output node: what reaches it is the color of the pixel. ##### Output `output` · Output · `g.output(...)` The color of every pixel. Alpha, when connected, replaces color's alpha. The end of the graph: the color it receives is the color of the pixel. If `alpha` is connected it replaces the color's alpha. `g.output(color)` adds or finds it in Python. | Input | Type | Default | | --- | --- | --- | | `color` | `vec4` | `(0, 0, 0, 1)` | | `alpha` | `float` | — (not used until connected) | ```python title="Example" g.output([1.0, 0.4, 0.1, 1.0], alpha=g.radial_gradient(radius=0.5)) ``` **Always up to date:** This page is generated from the node catalog of `pyzed.shading`, so the ports, types and defaults are always those of this version. Each example is compiled when the docs are built. **See also:** [Getting started](https://albakore.github.io/pyzed/shading/getting-started/) · [The Shader view](https://albakore.github.io/pyzed/shading/shader-designer/) · [Recipes](https://albakore.github.io/pyzed/shading/recipes/) · [Shader graphs in Python](https://albakore.github.io/pyzed/shading/shader-graphs/) ## Theming ### Theming > Customize how a Pyzed app looks: built-in and custom themes, color tokens, CSS-like styles for any widget, component variants, form themes, icons and fonts, in code and in Pyzed Design. Source: https://albakore.github.io/pyzed/theming/ Pyzed separates **what** your interface shows from **how it looks**. Every component reads its colors from the current **theme**, a set of named colors called tokens (`background`, `text`, `border`, `accent`...). Change the theme and the whole app follows; change one widget with `style()` and only that widget changes. ```python import pyzed as pz from pyzed import ui class Notes(pz.App): theme = "Gruvbox Dark" # any name from pz.themes.names() def view(self): card = pz.Style(background="elevated", border=(1, "border"), radius=10, padding=16) return pz.column([ ui.heading("Notes"), pz.container(ui.label("Styled like CSS, colored by the theme.")).style(card), ]) ``` #### How it fits together ```mermaid flowchart LR T["Theme
named colors (tokens)"] --> C["Components
variants · tints · sizes"] C --> S["style()
one widget, like CSS"] G["Global style
App.style"] --> S ``` - **Theme**: the palette of the whole app. Pick a bundled one, follow the system's light or dark mode, or create your own from three colors. - **Components** take their colors from the theme. Their **variants** and **tints** (`variant="outline"`, `tint="error"`) choose which of those colors they use. - **`style()`** changes one widget, like CSS; named styles reuse a look; `App.style` sets the font and colors of the whole window. #### Pyzed Design follows it too The visual designer is a Pyzed app, so it has themes as well: pick one in **Settings → Appearance → Theme** and the whole editor changes at once. Each form keeps its own theme, set in its **Look** properties (see [Styling forms](https://albakore.github.io/pyzed/theming/forms/)), so the editor and the app you design can look different. **One Light:** #### In this section style() on components, named styles and theme-aware components.', href: '/pyzed/theming/components/', icon: 'puzzle' }, { title: 'Styling forms', text: 'Theme, accent, radius and fonts of a form in Pyzed Design, and what the .pzform file stores.', href: '/pyzed/theming/forms/', icon: 'laptop', tag: 'Designer' }, { title: 'Icons and fonts', text: 'About 2000 bundled icons, file-type icons, and loading your own fonts.', href: '/pyzed/theming/icons-fonts/', icon: 'star' }, { title: 'Theme cookbook', text: 'Six complete themes to copy: brand, high contrast, retro terminal, glassy, pastel and mixed.', href: '/pyzed/theming/cookbook/', icon: 'rocket', tag: 'Examples' }, { title: 'Theme API', text: 'Theme, themes and the color helpers, method by method.', href: '/pyzed/reference/theme/', icon: 'document' }, ]} /> **Use tokens, not hex colors:** Write `"accent"` or `"border"` instead of `"#3a7bc8"` where you can. Token colors change with the theme, so your app keeps looking right in light and dark mode. **Status of this section:** Everything on these pages works today, in code and in Pyzed Design. Captures of every bundled theme in light and dark are being added. Theming for mobile and web will come with those targets, which are planned but not available yet. ### Choosing a theme > Pick one of Pyzed's bundled light and dark themes, follow the system's light or dark mode, switch themes at runtime and read theme colors from Python. Source: https://albakore.github.io/pyzed/theming/themes/ A **theme** is a named set of colors and sizes. Every `pyzed.ui` component reads its colors from the **current theme**, so changing it restyles the whole interface at once, without touching your views. examples/styling.py: the same screen in any bundled theme, following the system, or generated from three colors." /> #### Set the theme of your app Give your app a `theme` with the name of a theme: ```python import pyzed as pz from pyzed import ui class MyApp(pz.App): theme = "Ayu Dark" def view(self): return pz.column([ui.heading("Hello"), ui.button("Save", self.save, variant="solid")], padding=24) def save(self): print("saved") pz.run(MyApp) ``` `theme` accepts: | Value | Example | Meaning | | --- | --- | --- | | A theme name | `"Gruvbox Light"` | Any name from `pz.themes.names()`; the lookup ignores case. | | A `Theme` object | `pz.themes.ONE_LIGHT`, or one you made | Uses exactly that theme. | | `pz.themes.auto(light, dark)` | `pz.themes.auto("One Light", "One Dark")` | Follows the operating system's light or dark mode. | | `"auto"` | `"auto"` | Shortcut for `auto("One Light", "One Dark")`. | #### The bundled themes Pyzed ships four families of themes, each with light and dark variants: | Family | Dark | Light | | --- | --- | --- | | **One** (the default) | `One Dark` | `One Light` | | **Ayu** | `Ayu Dark`, `Ayu Mirage` | `Ayu Light` | | **Gruvbox** | `Gruvbox Dark`, `Gruvbox Dark Hard`, `Gruvbox Dark Soft` | `Gruvbox Light`, `Gruvbox Light Hard`, `Gruvbox Light Soft` | | **Pyzed** | `Pyzed Midnight`, `Pyzed Graphite`, `Pyzed Forest`, `Pyzed Rosé` | `Pyzed Dawn`, `Pyzed Glacier` | The first three are the themes of the [Zed](https://zed.dev) editor (MIT license), whose interface inspired much of Pyzed's look. The Pyzed family is Pyzed's own: each one is made from a small palette with `pyzed.theme_palettes.from_palette`, which you can call to make yours (see [Custom themes](https://albakore.github.io/pyzed/theming/custom-themes/)). List them from Python: **uv:** ```bash uv run python -c "import pyzed as pz; print(pz.themes.names())" ``` **pip:** ```bash title="macOS and Linux" python3 -c "import pyzed as pz; print(pz.themes.names())" ``` ```powershell title="Windows (PowerShell)" py -c "import pyzed as pz; print(pz.themes.names())" ``` **Try them all:** Run `examples/styling.py` and pick a theme from the list: the whole window restyles as you choose. See [Running the examples](https://albakore.github.io/pyzed/examples/) for the command on your system. #### Dark, light or follow the system Most people expect an app to follow the light or dark mode of their computer. `pz.themes.auto` does it: it takes a light theme and a dark theme (names or `Theme` objects) and picks one when the system changes. ```python class MyApp(pz.App): theme = pz.themes.auto("Gruvbox Light", "Gruvbox Dark") ``` `self.system_dark` tells you which mode the system is in (`True` in dark mode), and Pyzed updates it, and the theme, when the user changes it. To let the user choose, keep their choice in a variable and set `self.theme` from it: ```python class MyApp(pz.App): def __init__(self): self.mode = "System" self.theme = pz.themes.auto() def set_mode(self, mode): self.mode = mode self.theme = {"Light": "One Light", "Dark": "One Dark"}.get(mode, pz.themes.auto()) def view(self): return ui.segmented(["System", "Light", "Dark"], self.mode, self.set_mode) ``` #### Switch themes at runtime `self.theme` is just an attribute: assign it and the next frame uses the new theme. A theme picker is one line: ```python ui.select(pz.themes.names(), self.resolved_theme().name, lambda name: setattr(self, "theme", name)) ``` A light/dark toggle: ```python def toggle_theme(self): self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark" ``` self.theme while the app runs repaints every component." /> #### Read the theme's colors `self.theme` can be a name or an `auto(...)`, so use **`self.resolved_theme()`** to get the concrete `Theme` in use right now. Its [tokens](https://albakore.github.io/pyzed/theming/tokens/) are plain attributes holding `#rrggbb` strings: ```python def view(self): t = self.resolved_theme() return pz.container(content, bg=t.surface, border_color=t.border, border_width=1, radius=t.radius) ``` Inside `view()` (and inside components) `pz.theme.current()` returns the same theme. Most of the time you do not need either: write the **token name** instead of a color, and Pyzed looks it up for you: ```python pz.text("Saved").style(color="success") pz.container(content).style(background="surface", border=(1, "border")) ``` **Hex colors do not follow the theme:** `"#1e1e1e"` stays dark when the user switches to a light theme. Prefer token names (`"surface"`, `"text_muted"`, `"accent"`...) and keep hex colors for things that really must not change, like a brand logo. #### A theme for one window or one block Each [window](https://albakore.github.io/pyzed/guides/windows/) can have its own theme (`Window.theme`); a window without one uses the app's. A `Theme` is also a context manager: components built inside a `with` block use it. This draws a light preview inside a dark app: ```python with pz.themes.get("One Light"): preview = ui.card(ui.label("This is how it looks in light mode")) ``` And every `pyzed.ui` component accepts `theme=` to draw just that component with another theme: ```python ui.button("Light button", on_press, theme=pz.themes.ONE_LIGHT) ``` See [`examples/theming/mixed.py`](https://github.com/albakore/pyzed/blob/main/examples/theming/mixed.py) for a light sidebar in a dark window, with a picker that remembers the chosen theme. #### Next - [Theme tokens](https://albakore.github.io/pyzed/theming/tokens/): what every color of a theme paints. - [Creating themes](https://albakore.github.io/pyzed/theming/custom-themes/): your own colors, in code or in a JSON file. ### Theme tokens > Every color and size token of a Pyzed theme, what it paints in the interface, and its value in One Dark and One Light. Source: https://albakore.github.io/pyzed/theming/tokens/ A theme is a list of **tokens**: names such as `accent` or `text_muted` with a color (or a size) each. Components never hard-code colors; they ask the theme for a token. That is why one change to the theme restyles everything. You use tokens in two ways: ```python pz.text("Total").style(color="text_muted") # by name, anywhere a color goes pz.container(body, bg=self.resolved_theme().surface) # as an attribute of the theme ``` Colors are `#rrggbb` or `#rrggbbaa` (the last two digits are the opacity). Token names work in `style()`, in a component's `color=`/`tint=` parameters, in [`pz.Paint`](https://albakore.github.io/pyzed/theming/styles/#gradients-opacity-and-masks) and in the designer's color fields. **Think in layers:** From back to front: the **window** (`background`), **surfaces** on it (`surface`, `panel`, `elevated`), **elements** on those (`element`, `ghost_*`), and **text and icons** on top. Pick each layer a little lighter (dark themes) or darker (light themes) than the one behind it. #### Surfaces The large areas of the window, from the back to the front. ui.card), stats and other raised areas.'], ['elevated', 'Floating things: menus, popovers, dropdowns, tooltips, dialogs, toasts and the command palette.'], ['editor', 'Where people type: text fields, the code editor, the terminal background in generated themes.'], ['panel', 'Side panels and docks of a workspace, the help panel.'], ['title_bar', 'The custom title bar (ui.title_bar, ui.window_frame).'], ['status_bar', 'The status bar at the bottom of a workspace.'], ['tab_bar', 'The strip behind the tabs of a tab bar.'], ['tab_active', 'The selected tab.'], ['tab_inactive', 'The other tabs.'], ['terminal', 'The terminal widget background.'], ]} /> #### Borders #### Elements and ghost elements **Elements** are filled controls (the default `ui.button`, chips, segmented controls). **Ghost** elements have no background until you point at them: list rows, ghost and icon buttons, menu items, tree rows. #### Text, icons and accent solid) buttons, switches, sliders, progress bars, checkboxes.'], ['on_accent', 'Text and icons drawn on top of the accent (inside a solid button or a solid badge).'], ]} /> **Keep on_accent readable:** If you change `accent` by hand, also change `on_accent`, or use `theme.with_accent(color)`, which picks a readable `on_accent` (and updates focus rings, selection and links) for you. #### Status Used by `tint=` (`ui.badge`, `ui.callout`, `ui.button`, `ui.progress`...), by toasts, validation messages and charts. ui.stat.'], ['info', 'Information callouts (the accent by default).'], ['hint', 'Hints, the quietest callout.'], ]} /> #### Version control For file trees, tabs and diffs (`ui.tree`, `ui.diff_stat`, the code editor's gutter). #### Code editor For [`pz.code_editor`](https://albakore.github.io/pyzed/library/editor/), `ui.terminal` and code blocks. #### Syntax, terminal and people These three are collections instead of single colors. - **`syntax`**: a dictionary from a highlighting name to a color, or to `(color, "italic")` / `(color, "bold")`. The main names are `primary`, `comment`, `comment.doc`, `keyword`, `function`, `type`, `string`, `string.escape`, `number`, `boolean`, `constant`, `property`, `attribute`, `operator`, `variable`, `variable.special`, `punctuation`, `punctuation.bracket`, `tag`, `title` and `link_uri`. A missing dotted name falls back to its first part (`string.escape` → `string`). - **`ansi`**: the terminal's colors: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `bright_black` and `dim`. - **`players`**: a tuple of colors for avatars and collaborators; `ui.avatar(index=n)` and `ui.facepile` take the n-th one. def, if, return...'], ['syntax.function', 'Function names.'], ['syntax.string', 'Strings.'], ['syntax.comment', 'Comments.'], ['syntax.type', 'Classes and types.'], ['syntax.number', 'Numbers.'], ['ansi.red', 'Red text in the terminal (and so on for each ANSI color).'], ['players.0', 'The first avatar color (the accent by default).'], ]} /> #### Sizes and metadata | Token | Default | What it sets | | --- | --- | --- | | `radius` | `6.0` | Corner radius of cards, dialogs and large surfaces (cards use `radius + 2`). | | `radius_sm` | `4.0` | Corner radius of buttons, inputs, chips and list rows. | | `ui_font_size` | `14.0` | The base size of interface text; a form's **Font size** and the global style's `size` set it. | | `buffer_font_size` | `14.0` | Text size in the code editor. | | `name` | | The name used by `pz.themes.get` and `theme = "..."`. | | `appearance` | | `"dark"` or `"light"`; `theme.is_dark` reads it. | #### From Python ```python t = pz.themes.get("One Dark") t.accent # '#74ade8' t.color("text_muted") # the same as t.text_muted; hex colors pass through unchanged t.tokens() # every token as a dictionary t.to_dict() # JSON-friendly, the format Theme.save writes ``` The names follow the theme format of the Zed editor, so themes made for it can be imported (see [Creating themes](https://albakore.github.io/pyzed/theming/custom-themes/#import-a-zed-theme)). The [Theme API](https://albakore.github.io/pyzed/reference/theme/) lists every method. ### Creating themes > Create your own Pyzed theme: change the accent, derive a theme, generate one from three colors, write it as JSON, import Zed themes and register it by name. Source: https://albakore.github.io/pyzed/theming/custom-themes/ There are four ways to make a theme, from the quickest to the most detailed. All of them return a `Theme`, which you can use directly (`theme = my_theme`) or [register](#give-it-a-name) to use by name. | You want to... | Use | Effort | | --- | --- | --- | | Change only the brand color | `theme.with_accent("#e11d48")` | One line | | Change a few tokens of an existing theme | `theme.derive(radius=12, success="#16a34a")` | A few lines | | A whole new theme from your colors | `pz.Theme.generate(name, background=, text=, accent=)` | Three colors | | Full control, or a theme in a file | `pz.Theme.from_dict({...})`, `Theme.load("theme.json")` | Any token | Themes are **immutable**: every method returns a new theme and leaves the original untouched, so `pz.themes.ONE_DARK` is always the real One Dark. #### 1. Change the accent The accent is the color of primary buttons, switches, sliders, focus rings, links and selections. `with_accent` changes all of them at once and picks a readable `on_accent` (the text on top of the accent): ```python class MyApp(pz.App): theme = pz.themes.ONE_DARK.with_accent("#e11d48") # One Dark, in red ``` #### 2. Derive: change a few tokens `derive` copies a theme with the [tokens](https://albakore.github.io/pyzed/theming/tokens/) you pass. The `syntax` and `ansi` dictionaries are merged with the original, so you only list what changes: ```python ocean = pz.themes.ONE_DARK.derive( name="Ocean", background="#0b1622", surface="#102131", radius=10, radius_sm=8, syntax={"keyword": "#ff79c6", "comment": ("#5c6f80", "italic")}, ).with_accent("#4fb3ff") ``` **Note:** `derive` changes exactly the tokens you name. If you change `background`, hover colors and borders that were designed for the old background stay as they were: for a new set of surfaces, `generate` (below) is usually easier. #### 3. Generate a theme from three colors `Theme.generate` builds every token from a **background**, a **text** color and an **accent**: surfaces, borders, hover states, muted text and the editor colors are blended between the background and the text. ```python custom = pz.Theme.generate( "Forest", background="#14201a", # where content sits (the editor token); the window is blended a bit lighter text="#e3efe6", # the main text color accent="#4ade80", # buttons, focus, links ) ``` Optional arguments: | Argument | Default | Meaning | | --- | --- | --- | | `appearance` | guessed | `"dark"` or `"light"`; guessed from how bright `background` is. | | `error`, `warning`, `success` | from One Dark / One Light | Status colors (they also color the version control tokens). | | `syntax` | One's colors | Code colors to change (merged). | | `radius` | `6.0` | Corner radius; `radius_sm` becomes `radius - 2`. | **Good starting colors:** For a dark theme, start with a background between `#101010` and `#2a2a2a` and a text close to white. For a light one, a background close to white and a text between `#1a1a1a` and `#3a3a3a`. Then check the contrast (below). When you have a whole palette (the chrome around the editor and six hues for status and syntax), `theme_palettes.from_palette` uses every color the way Zed's theme families do. Pyzed's own themes (Midnight, Graphite, Forest, Rosé, Dawn, Glacier) are made with it: ```python from pyzed import theme_palettes dusk = theme_palettes.from_palette( "Dusk", "dark", editor="#1b1d24", chrome="#23262f", text="#d8dbe2", accent="#7aa2f7", red="#f7768e", orange="#ff9e64", yellow="#e0af68", green="#9ece6a", cyan="#7dcfff", purple="#bb9af7", ) ``` #### 4. Every token, from a dictionary or a file `Theme.from_dict` takes any tokens you list and fills in the rest from **One Dark** (or **One Light** when `"appearance": "light"`). Unknown names raise a `ValueError`, so typos do not go unnoticed: ```python paper = pz.Theme.from_dict({ "name": "Paper", "appearance": "light", "background": "#f5f1e8", "surface": "#fbf8f1", "editor": "#fffdf8", "text": "#2b2620", "text_muted": "#6b6257", "border": "#d9d0c1", "accent": "#b45309", "on_accent": "#ffffff", }) ``` ##### Save and load a theme file `save` writes a theme as JSON and `Theme.load` reads it back. The file has the same keys as `from_dict`, so you can also write it by hand (only the tokens you change are needed): ```json title="themes/paper.json" { "name": "Paper", "appearance": "light", "background": "#f5f1e8", "text": "#2b2620", "accent": "#b45309", "on_accent": "#ffffff", "radius": 2 } ``` ```python paper = pz.Theme.load("themes/paper.json") paper.save("themes/paper-full.json") # every token, ready to edit ``` **Ship theme files with your app:** Keep theme files inside your project and list the folder in `include` of `[tool.pyzed.build]` so [`pyzed build`](https://albakore.github.io/pyzed/packaging/build/#options) puts it in the packaged app. #### Import a Zed theme Because the token names follow the Zed editor's theme format, any theme from the [Zed theme gallery](https://zed.dev/extensions?filter=themes) works in Pyzed. Download its `.json` file and: ```python mocha = pz.Theme.from_zed("catppuccin.json", name="Catppuccin Mocha") # one theme of the file pz.themes.load_zed("catppuccin.json") # every theme in it, registered ``` #### Give it a name `pz.themes.register` adds a theme to the list of themes, so it can be used by name, shows up in `pz.themes.names()` (and in any theme picker built from it), and works with `pz.themes.auto`: 1. Create the themes, usually in their own module: ```python title="mytheme.py" import pyzed as pz LIGHT = pz.themes.register(pz.Theme.generate("Acme Light", background="#ffffff", text="#1f1b2e", accent="#6d28d9")) DARK = pz.themes.register(pz.Theme.generate("Acme Dark", background="#16131f", text="#ece9f5", accent="#a78bfa")) ``` 2. Import that module before the app starts, and use the names: ```python title="main.py" import pyzed as pz import mytheme # noqa: F401 - registers the Acme themes class MyApp(pz.App): theme = pz.themes.auto("Acme Light", "Acme Dark") ``` 3. Run it: **uv:** ```bash uv run python main.py ``` **pip:** ```bash title="macOS and Linux" python3 main.py ``` ```powershell title="Windows (PowerShell)" py main.py ``` Registering a theme with the name of an existing one replaces it. #### Check the contrast `pyzed.theme` has small color helpers, handy when you build themes in code: | Function | Returns | | --- | --- | | `contrast(a, b)` | The WCAG contrast ratio of two colors, from 1 to 21. Text should be at least **4.5** (AA), ideally **7** (AAA). | | `readable_on(background)` | A light or a dark text color, whichever reads better on `background`. | | `luminance(color)` | How bright a color is, from 0 to 1. | | `mix(a, b, amount)` | A color between `a` and `b` (`amount` 0 is `a`, 1 is `b`). | | `lighten(color, amount)`, `darken(color, amount)` | The color mixed with white or black. | | `alpha(color, value)` | The color with an opacity (`alpha("#ff0000", 0.5)` is `#ff000080`). | ```python from pyzed.theme import contrast t = pz.Theme.generate("Forest", background="#14201a", text="#e3efe6", accent="#4ade80") print(round(contrast(t.text, t.background), 1)) # 11.2 -> great print(round(contrast(t.text_muted, t.background), 1)) # 6.0 -> still above 4.5 ``` #### Complete examples The [Theme cookbook](https://albakore.github.io/pyzed/theming/cookbook/) has five complete themes to copy: a brand theme in light and dark, high contrast, retro terminal, glassy dark and pastel. ### Styles > Style any Pyzed widget like CSS: colors, borders, radius, shadows and states, reusable styles and the global style of your Python desktop app. Source: https://albakore.github.io/pyzed/theming/styles/ Every widget has a `style()` method that styles it like CSS: background, borders, radius, shadow, padding, margin, size and typography. (Image: Styled buttons, a card and a masked gradient) ```python ui.button("Save", self.save).style(radius=0, border=(2, "#ef4444")) pz.text("Title").style(size=20, weight="bold", color="accent") pz.column([...]).style(background="panel", padding=16, radius=10, shadow=("#00000055", (0, 6), 18)) ``` On a button or a container, `style()` changes the widget itself (a square button stays a button). Other widgets get a box around them that carries the style. #### Properties | Property | Values | | --- | --- | | `background` | A color, a `pz.Gradient` or a `pz.Paint`. | | `color` | Text color. | | `font` | `"sans"`, `"mono"`, `"serif"` or a family loaded with `pz.load_font`. | | `weight` | `"light"`, `"normal"`, `"medium"`, `"semibold"`, `"bold"`… or 100–900. | | `italic`, `size`, `line_height` | Typography. | | `border_color`, `border_width` | The border; `border=(width, color)` sets both. | | `radius` | A number, or `(top_left, top_right, bottom_right, bottom_left)`. | | `shadow` | `(color, (x, y), blur)`. | | `padding`, `margin` | A number, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `width`, `height` | Pixels, `"fill"`, `"shrink"` or `"fill:N"`; `max_width`, `max_height`, `min_width` and `min_height` in pixels. | | `align_x`, `align_y` | Where the content sits inside the box. | | `clip` | Clips the content to the box. | | `position`, `top`, `right`, `bottom`, `left`, `z_index` | `position="absolute"` places a child of a [`pz.box`](https://albakore.github.io/pyzed/guides/layout/#boxes) by its offsets, out of the flow. | | `opacity`, `translate`, `scale`, `rotate` | Fades, moves, scales or turns how the widget is drawn, without changing the layout. | | `blur`, `backdrop_blur` | Blurs the widget, or what is behind it. | | `glow`, `bloom`, `glass` | Visual effects: a halo, light that spills from bright parts and a frosted panel. See [Animation and effects](https://albakore.github.io/pyzed/animation/effects/). | Colors can be hex strings (with alpha, `"#3b82f680"`), `(r, g, b[, a])` tuples or theme tokens such as `"accent"`, `"panel"` or `"text_muted"`, so styles follow theme switches. Text properties apply to **every text inside** the styled widget, including the labels of `pyzed.ui` components. The closest style wins. #### States and transitions Like CSS `:hover`, `:active` and `:focus`, a style can say how the widget looks while the pointer is over it (`hover`), while it is pressed (`pressed`, or `active`) and while it has the keyboard focus (`focus`). `transition` animates the change, like CSS `transition`: ```python ui.button("Save", self.save).style( background="accent", radius=6, hover={"background": "#4f8cff", "translate": (0, -1)}, pressed={"scale": 0.97}, transition=120, # milliseconds, "0.2s", True or {"duration", "easing", "delay"} ) ``` A state can change `background` (a color), `color`, `border_color`, `border_width`, `radius`, `opacity`, `translate`, `scale`, `rotate` and `blur`. These don't move the widgets around, so hovering never shifts the layout. Without `transition` the state shows at once. With one, changes of `opacity`, `translate`, `scale`, `rotate` and `blur` from one render to the next animate too. For anything more, see [Animation](https://albakore.github.io/pyzed/animation/). #### Stylesheets `pz.stylesheet` adds rules by selector, like a CSS file. `widget.classes(...)` gives a widget classes (and an `id`), and the matching rules style it: ```python pz.stylesheet({ ".card": {"background": "panel", "padding": 16, "radius": 8, "border": (1, "border")}, ".card:hover": {"border_color": "accent"}, ".card.selected, #featured": {"border": (2, "accent")}, "Rating": {"padding": 4}, # every Rating component }) pz.column([...]).classes("card", "selected" if self.chosen else None) Rating(value=3, class_="card", id="featured") # components take class_= and id= ``` - Selectors: `.class`, several classes (`.card.selected`), `#id`, a component's class name and an optional `:hover`, `:pressed` (or `:active`) or `:focus`. Commas list several selectors for one rule. - Like CSS, more specific selectors win (an id beats classes, more classes beat fewer, and classes beat a component name), then the later rule. A `style()` call after `classes()` goes on top. - Defining a selector again replaces its rule, so reloading your code doesn't pile rules up. `pz.clear_stylesheet()` removes them all. - Styles named with `pz.define_style` also work as classes. - Rules for a component's name are defaults: what its `render()` sets wins over them, and rules for the `class_` or `id` it is given win over both. #### Responsive styles Styles can follow the window's size, like CSS media queries: ```python pz.stylesheet({ ".sidebar": {"width": 260}, "@media (max_width: 700)": {".sidebar": {"width": 64}}, "@md": {".content": {"padding": 32}}, # a breakpoint: 768 px wide and up }) panel.style(padding=24, media={"max_width: 600": {"padding": 8}}) ``` - A query is `min_width`, `max_width`, `min_height` or `max_height` in pixels (`"max_width: 600"` or `"(max_width: 600px)"`), `orientation` (`portrait` or `landscape`) or `theme` (`dark` or `light`). Join several with `and`. - Breakpoints follow Tailwind and apply from that width up: `sm` (640), `md` (768), `lg` (1024), `xl` (1280) and `xxl` (1536). - The styles update when the window is resized. - In a view, `pz.viewport()` returns the window's size, and `pz.media("max_width: 600")` says whether a query matches. `pz.current_breakpoint()` returns `"xs"`, `"sm"`, `"md"` and so on. `pz.responsive(lambda w, h: ...)` builds content from the space a widget actually gets. #### Reusing styles ```python card = pz.Style(background="elevated", border=(1, "border"), radius=10, padding=16) pz.column([...]).style(card) pz.column([...]).style(card | pz.Style(shadow=("#0006", (0, 4), 12))) # the right side wins pz.define_style("pill", radius=999, padding=(4, 12), size=12, weight="semibold") pz.text("new").style("pill", background="accent") ``` `widget.style(a, b, color=...)` merges in order, and calling `style()` again merges with what the widget already has. Passing `None` for a property removes it. `Style.to_dict()` and `Style.from_dict()` turn a style into JSON-friendly data and back. Components take `style=` too: `Rating(value=3, style="card")`. #### Gradients, opacity and masks ```python sunset = pz.Gradient(["#f97316", "#db2777", "#7c3aed"], angle=120) box.style(background=sunset) box.style(background=pz.Paint("success", opacity=0.2)) # a translucent token box.style(background=pz.Paint(sunset, mask=pz.Mask([1, 0.15]))) # fades out ``` - `pz.Gradient(stops, angle=180)`: colors spread evenly, or `(offset, color)` pairs. Angles follow CSS (0 up, 90 right). Up to 8 stops. - `pz.Mask(stops)`: opacities along the fill's direction (on a plain color, along its own `angle`). - `pz.Paint(fill, opacity=, mask=)`: a color or gradient with an overall opacity and a mask. #### Color picker `ui.ColorPicker` edits any of these: a color with alpha, a gradient (click the bar to add a stop, drag stops, trash removes the selected one) or a mask. It sends back a hex string, a `pz.Gradient` or a `pz.Paint`, ready for `background`: ```python ui.ColorPicker(value=self.fill, on_change=lambda e: setattr(self, "fill", e.value)) ``` (Image: The color picker editing a gradient) `alpha=False`, `gradients=False` and `masks=False` hide those parts; `swatches=[...]` sets the quick colors. `ui.color_swatch(value)` shows a value as a small square. See [`examples/color_picker.py`](https://github.com/albakore/pyzed/blob/main/examples/color_picker.py). #### The global style `App.style` styles the whole window, and `pz.set_style(...)` sets a style for every window (each window's `style` goes on top of it): ```python class MyApp(pz.App): style = pz.Style(font="serif", size=15, color="#e6e1cf", radius=2) ``` Its text properties fill in what texts don't set themselves, and its `color`, `size`, `background`, `border_color` and `radius` also become the theme's `text`, `ui_font_size`, `background`, `border` and `radius` tokens, so `pyzed.ui` components follow them. Change `self.style` at any time to restyle the app. For other theme colors, derive a theme: `self.theme = themes.get("One Dark").with_accent("#ff6b6b")` (see [Themes](https://albakore.github.io/pyzed/theming/themes/)). See [`examples/styles.py`](https://github.com/albakore/pyzed/blob/main/examples/styles.py). ### Styling components > Change how Pyzed components look: variants, tints and sizes, style() on any component, named styles, a different theme for one component, and theme-aware components of your own. Source: https://albakore.github.io/pyzed/theming/components/ Before writing any style, check whether the component already has the look you want. Most components in [`pyzed.ui`](https://albakore.github.io/pyzed/library/) come in **variants**, **tints** and **sizes** that follow the theme. When none fits, `style()` changes anything, like CSS. ```mermaid flowchart LR T["Theme
colors for everything"] --> V["Variant · tint · size
ui.button(variant='outline')"] V --> S["style()
.style(radius=0)"] ``` Each step only overrides what it sets: the theme gives the colors, the variant picks which of them a component uses, and `style()` has the last word. #### Variants A **variant** is a ready-made look of the same component. ##### Buttons ```python ui.button("Save", save, variant="solid") # filled with the accent: the main action ui.button("Cancel", cancel) # "filled" (default): a neutral filled button ui.button("Details", details, variant="outline") # transparent with a border ui.button("More", more, variant="ghost") # no background until hovered ui.button("Skip", skip, variant="subtle") # like ghost, with muted text ui.button("Archive", archive, variant="tinted") # a translucent accent ``` | Variant | Background | Use it for | | --- | --- | --- | | `filled` (default) | `element` | Most buttons. | | `solid` | the accent (or the tint) | The main action of a screen, one per area. | | `tinted` | a translucent accent (or tint) | Secondary actions that should stand out a bit. | | `outline` | none, with a `border` | Toolbars, secondary actions on busy surfaces. | | `ghost` | none until hovered (`ghost_hover`) | Toolbars, menus, icon buttons. | | `subtle` | like `ghost`, with `text_muted` | Low-priority actions. | ##### Badges ```python ui.badge("New") # "soft" (default): translucent background ui.badge("Paid", tint="success", variant="solid") ui.badge("Failed", tint="error", variant="outline") ``` `ui.icon_button` takes `variant` too (`"ghost"` by default), and many components have their own options, such as `borderless=True` and `mono=True` on `ui.input`. Each component's page in the [Library](https://albakore.github.io/pyzed/library/) shows its variants. #### Tints A **tint** recolors a component with one of the [status tokens](https://albakore.github.io/pyzed/theming/tokens/#status): `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` (and `"hint"` for callouts): ```python ui.button("Delete", delete, variant="solid", tint="error") ui.button("Delete", delete, variant="error") # shortcut: a tinted error button ui.callout("Saved", "Everything is up to date.", tint="success") ui.progress(0.4, tint="warning") ui.spinner(tint="info") ui.count_badge(3, tint="error") ``` Since tints are tokens, they follow the theme: the same red button is a softer red in a light theme. #### Sizes ```python ui.button("Tiny", on_press, size="xs") # 18 px high ui.button("Small", on_press, size="sm") # 22 px ui.button("Normal", on_press) # "md": 28 px ui.button("Large", on_press, size="lg") ui.input(self.name, self.set_name, size="lg") ui.select(options, self.choice, self.pick, size="sm") ``` #### style() on any component Every widget, including every `pyzed.ui` component, has a `style()` method with CSS-like properties ([the full list](https://albakore.github.io/pyzed/theming/styles/#properties)). It is applied on top of the component's own look: ```python ui.button("Square", on_press).style(radius=0, border=(2, "accent")) ui.card(body, title="Usage").style(shadow=("#0006", (0, 8), 24), radius=16) ui.label("Total").style(size=20, weight="bold", color="success") ``` `ui.button` also takes the low-level colors of the button directly (`bg`, `hover_bg`, `pressed_bg`, `color`), for a fully custom button that still behaves like one: ```python ui.button("Buy", buy, bg="#16a34a", hover_bg="#15803d", pressed_bg="#166534", color="#ffffff") ``` #### Reuse a look: named styles Give a style a name once and use it anywhere. Named styles are the "CSS classes" of Pyzed: ```python pz.define_style("card", background="surface", border=(1, "border_variant"), radius=12, padding=16) pz.define_style("danger", color="error", weight="semibold") pz.column([...]).style("card") ui.label("Payment failed").style("danger") pz.column([...]).style("card", shadow=("#0005", (0, 4), 12)) # a name plus extra properties ``` A `pz.Style` object does the same without a global name, and styles combine with `|` (the right side wins): ```python CARD = pz.Style(background="surface", radius=12, padding=16) RAISED = CARD | pz.Style(shadow=("#0006", (0, 8), 24)) ``` #### A different theme for one component Every `pyzed.ui` component takes `theme=`, to draw just that component with another theme, and a `with theme:` block changes the theme of everything built inside it: ```python ui.button("Light", on_press, theme=pz.themes.ONE_LIGHT) with pz.themes.ONE_DARK.with_accent("#f97316"): warning_area = pz.column([ui.button("Go", go, variant="solid"), ui.progress(0.3)]) ``` #### Your own components A [component](https://albakore.github.io/pyzed/guides/components/) of your own should take its colors from the theme too, so it fits every theme without changes. Use token names, or read the theme with `pz.theme.current()` in `render()`: ```python class Tag(pz.Component): text: str = "" tint: str = "accent" def render(self): t = pz.theme.current() return pz.text(self.text, size=12).style( color=self.tint, background=pz.Paint(self.tint, opacity=0.15), radius=t.radius_sm, padding=(2, 8), ) Tag(text="beta", tint="warning") Tag(text="new", style={"weight": "bold"}) # every component also accepts style= ``` Components made in the designer's component view follow the same rules; see [Styling forms in the designer](https://albakore.github.io/pyzed/theming/forms/). **Which should I use?:** Variant or tint first; a named style when the same look repeats; `style()` for one-off changes; a custom theme when the change is everywhere (for example, square corners in the whole app: `radius=0` in the theme, not on every button). ### Styling forms in the designer > Give a Pyzed Design form its look: theme, accent, background, corner radius and font size, the form's global style, per-control styles, custom themes for forms and what the .pzform file stores. Source: https://albakore.github.io/pyzed/theming/forms/ Forms made in [Pyzed Design](https://albakore.github.io/pyzed/editor/) use the same themes and styles as code. You set them in the **Properties** panel instead of writing Python, and they are saved in the `.pzform` file. A form's look is built in layers, from broad to specific; each layer only changes what it sets: ```mermaid flowchart LR T["Theme
One Dark, Gruvbox..."] --> L["Look
accent · background · radius · font size"] L --> G["Global style
the form's Style section"] G --> C["Control style
each control's Style section"] ``` #### 1. The form's theme and look Click an empty spot of the canvas to select the form, then open **Properties → Look**: | Field | What it does | Saved as | | --- | --- | --- | | **Theme** | Any registered theme: the bundled ones and those your code registers. | `"theme": "Gruvbox Dark"` | | **Accent** | A preset color, **×** for the theme's own, or **Custom accent** with any `#rrggbb`. Recolors primary buttons, focus, links and selection (`with_accent`). | `"accent": "#e11d48"` | | **Background** | The window's background; empty keeps the theme's. | `"background": "#101418"` | | **Corner radius** | The radius every control uses; `0` for square corners. | `"radius": 0` | | **Font size** | The base size of every text. | `"font_size": 15` | The form's theme is independent from the theme of the editor itself (Settings → Appearance): you can design a light form in a dark editor. #### 2. The form's global style With the form selected, its **Style** section is the window's global style (the same as `App.style` in code): - **Text color, font, weight, italic, font size, line height** apply to every text that does not set its own. A serif font here changes every label, button and input of the form at once. - **Background, border color, corner radius and padding** become the window's. #### 3. A control's style Select a control and use its own **Style** section: background (a color, a gradient or a masked fill, with the color picker), text color, font, weight, borders, corner radius, shadow, padding, margin and alignment. Empty fields keep the component's own look, and **Reset style** clears them all. The [Styles and themes](https://albakore.github.io/pyzed/editor/styles-and-themes/) page of the designer describes every field and the color picker. Some controls also have **variant**, **tint** and **size** properties, the same as in code (see [Styling components](https://albakore.github.io/pyzed/theming/components/)): try a button's variant before styling it by hand. **Keep colors that follow the theme:** The color picker works with concrete colors. To keep a color that changes with the theme, type a token (`accent`, `text_muted`, `surface`...) in the field next to the swatch. The form then looks right if you switch it to a light theme later. #### What the .pzform file stores All of this is plain JSON in the form file, so it is easy to review in Git or to change by hand: ```json title="ui/main.pzform (excerpt)" { "format": "pyzed.form/1", "form": { "name": "Main", "theme": "Gruvbox Dark", "accent": "#fe8019", "background": null, "radius": 4, "font_size": 14.0, "style": { "font": "serif", "color": "text" } }, "controls": [ { "name": "save", "type": "button", "props": { "text": "Save" }, "style": { "radius": 0, "border_color": "accent", "border_width": 2 } } ] } ``` `style` holds the same properties as [`style()`](https://albakore.github.io/pyzed/theming/styles/#properties), written as `Style.to_dict()` does (a gradient becomes `{"type": "linear", "angle": ..., "stops": [...]}`). #### Use your own theme in a form The **Theme** list shows every registered theme. To use one of your own: 1. Register it in the form's code file (the `.py` next to the form, which Pyzed loads before the form runs): ```python title="ui/main.py" import pyzed as pz pz.themes.register(pz.Theme.generate("Acme", background="#16131f", text="#ece9f5", accent="#a78bfa")) def save_click(form, event): ... ``` 2. Write its name in the form file (`"theme": "Acme"`), or pick it in **Properties → Look → Theme** once the designer lists it. 3. Run the form (**Run**, or `pyzed run`). It uses the Acme theme. **Preview in the canvas:** A form whose theme name is not registered falls back to **One Dark**. If the canvas shows One Dark instead of your theme, the module that registers it has not been loaded in the designer yet; the running app uses the right theme once it imports that module. #### Change the look while the form runs **Not supported yet:** A form's handlers can change its controls (`form.save.text = "Saved"`, `form.status.color = "error"`), but not its theme: the theme is chosen when the form opens. For an app whose theme the user switches while it runs, write that window in code with `pz.App` (see [Choosing a theme](https://albakore.github.io/pyzed/theming/themes/#switch-themes-at-runtime)), or color individual controls from handlers with theme tokens. ### Icons and fonts > Use about 2000 bundled Lucide icons and file-type icons in Python desktop GUIs with Pyzed, and load your own fonts such as IBM Plex. Source: https://albakore.github.io/pyzed/theming/icons-fonts/ #### Icons Pyzed bundles the [Lucide](https://lucide.dev) icon font (~2000 icons, ISC license). Icons are drawn as text, so they are sharp at any size and take any color: ```python pz.icon("folder", size=16, color="#74ade8") ui.button("Open", self.open, icon="folder-open") ui.list_item("Settings", self.settings, icon="settings") ``` Without a `color`, `pz.icon` takes the color of the surrounding text. Icons inside `pyzed.ui` components use the theme's `icon` and `icon_muted` [tokens](https://albakore.github.io/pyzed/theming/tokens/#text-icons-and-accent), so they follow the theme. Look up names at [lucide.dev/icons](https://lucide.dev/icons) or from the terminal: **uv:** ```bash uv run pyzed icons git # git-branch, git-commit-horizontal, git-compare, git-fork, git-merge, git-pull-request, ... ``` **pip:** ```bash title="macOS and Linux" pyzed icons git # or: python3 -m pyzed icons git ``` ```powershell title="Windows (PowerShell)" pyzed icons git # or: py -m pyzed icons git ``` `pyzed.icons.names()` returns the full list and `pyzed.icons.glyph("folder")` the character, to mix it with text using `font="icons"`. ##### Searching and picking icons `icons.search` finds icons by name and by Lucide's tags, best first, and `icons.category` groups them for browsing: ```python from pyzed import icons icons.search("person") # ["person-standing", "user", "contact", ...] icons.category("arrows") # every arrow, chevron, undo... ``` To let the user choose one, use [`ui.IconPicker`](https://albakore.github.io/pyzed/library/icon-picker/), or `ui.icon_picker_dialog` in a `ui.dialog_layer`: a search box, categories and a grid of every icon. ##### File-type icons ```python from pyzed import icons icons.for_file("main.rs") # ("file-code", "#dea584") icons.for_file("Cargo.toml") # ("file-cog", "#9c4221") icons.language_for("main.py") # "Python" ``` `ui.Tab.for_file` and `ui.tree` already use them. #### Fonts Bundled fonts: | Name | Font | | --- | --- | | `"sans"` (default) | IBM Plex Sans | | `"mono"` | IBM Plex Mono | | `"icons"` | Lucide | `"serif"` uses the system's serif font. Choose a font for one text, for a block (every text inside inherits it) or for the whole window: ```python pz.text("fn main() {}").style(font="mono") # one text pz.column([...]).style(font="serif", size=15) # every text inside class MyApp(pz.App): style = pz.Style(font="mono") # the whole window ``` ##### Your own fonts Load a `.ttf` or `.otf` file before running the app and refer to it by its **family name** (the name the font declares, not the file name): ```python pz.load_font("fonts/JetBrainsMono-Regular.ttf") pz.text("fn main() {}", font="JetBrains Mono") pz.code_editor("buf", text, font="JetBrains Mono") ``` `load_font` also accepts the file's `bytes`. Load each weight you use (Regular, Bold...) as its own file. **Fonts in a packaged app:** Keep font files in your project (for example `assets/fonts/`), add the folder to `include` in `[tool.pyzed.build]`, and resolve the path from the bundle as shown in [Build and distribute](https://albakore.github.io/pyzed/packaging/build/#options), so the packaged app finds them. **Licenses:** Check that a font's license allows embedding it in an app you distribute. IBM Plex and Lucide, which Pyzed bundles, do (SIL Open Font License and ISC). ### Theme cookbook > Complete, runnable Pyzed themes to copy: a brand theme in light and dark, high contrast, retro terminal, glassy dark, playful pastel and two themes in one window. Source: https://albakore.github.io/pyzed/theming/cookbook/ Six complete themes, ready to copy into your app. Each one is a file in [`examples/theming/`](https://github.com/albakore/pyzed/tree/main/examples/theming) and draws the same screen (`preview.py`: buttons in every variant, a small form, badges, a callout and two stats), so you can compare how each theme changes it. #### Run them From a clone of the repository (see [Running the examples](https://albakore.github.io/pyzed/examples/)): **uv:** ```bash uv run python examples/theming/brand.py ``` **pip:** ```bash title="macOS and Linux" python3 examples/theming/brand.py ``` ```powershell title="Windows (PowerShell)" py examples\theming\brand.py ``` Replace `brand.py` with `high_contrast.py`, `retro_terminal.py`, `glassy_dark.py`, `pastel.py` or `mixed.py`. **Copying one into your project:** Copy the theme part (everything above `class ...App`) into a module of your project, such as `mytheme.py`, and import it before the app starts. The `preview` import is only for these examples. #### Brand theme, light and dark A company palette in two versions that follow the system's light or dark mode. `Theme.generate` builds each version from three colors; `derive` sets the brand's own green for success. `examples/theming/brand.py`: ```py """A brand theme in light and dark that follows the operating system. python examples/theming/brand.py - ``Theme.generate`` builds a whole theme from a background, a text and an accent color. - ``derive`` changes a few tokens on top (rounder corners, the brand's own green for success). - ``themes.register`` gives the themes a name, and ``themes.auto`` picks light or dark like the OS. """ import pyzed as pz from preview import preview BRAND = "#6d28d9" # the brand's purple ACME_LIGHT = pz.Theme.generate( "Acme Light", background="#ffffff", text="#1f1b2e", accent=BRAND, radius=10, ).derive(success="#15803d") ACME_DARK = pz.Theme.generate( "Acme Dark", background="#16131f", text="#ece9f5", accent="#a78bfa", radius=10, ).derive(success="#4ade80") pz.themes.register(ACME_LIGHT) pz.themes.register(ACME_DARK) class BrandApp(pz.App): title = "Acme" size = (720, 580) theme = pz.themes.auto("Acme Light", "Acme Dark") # light or dark, like the system def view(self): return pz.container(preview("Acme", "A brand theme that follows the system's light or dark mode."), padding=24, width="fill", height="fill") if __name__ == "__main__": pz.run(BrandApp) ``` **What to change:** the three colors of each `generate`, and `radius` for rounder or squarer corners. #### High contrast Pure black and white with yellow focus, for people who need strong contrast (and a good test of how your app looks with large, clear borders). `Theme.from_dict` lists every token that changes; the rest come from One Dark. The script prints the contrast ratio of the main colors when it starts. `examples/theming/high_contrast.py`: ```py """A high-contrast theme for readers who need strong colors and clear edges. python examples/theming/high_contrast.py - ``Theme.from_dict`` starts from One Dark and replaces only the tokens you list. - ``pz.theme.contrast`` checks the WCAG contrast ratio of two colors (7:1 or more is "AAA"). - A global ``style`` makes every text bigger. """ import pyzed as pz from preview import preview HIGH_CONTRAST = pz.Theme.from_dict({ "name": "High Contrast", "appearance": "dark", # surfaces: pure black, panels a touch lighter so they stay visible "background": "#000000", "surface": "#0a0a0a", "elevated": "#141414", "editor": "#000000", "panel": "#0a0a0a", "title_bar": "#000000", "status_bar": "#000000", # strong white borders "border": "#ffffff", "border_variant": "#bdbdbd", # text: white, and muted text that is still very readable "text": "#ffffff", "text_muted": "#e0e0e0", "text_placeholder": "#c2c2c2", "text_disabled": "#9e9e9e", "icon": "#ffffff", "icon_muted": "#e0e0e0", # buttons and list rows "element": "#1f1f1f", "element_hover": "#333333", "element_active": "#4d4d4d", "ghost_hover": "#262626", "ghost_active": "#3d3d3d", "ghost_selected": "#3d3d3d", # status colors that stay readable on black "error": "#ff6b6b", "warning": "#ffd600", "success": "#5cff8a", "info": "#6bc8ff", # square corners "radius": 0, "radius_sm": 0, }).with_accent("#ffd600") # yellow focus rings, selection and primary buttons pz.themes.register(HIGH_CONTRAST) def report(theme): """Prints the contrast ratio of the main text colors against the background.""" for token in ("text", "text_muted", "accent", "error"): ratio = pz.theme.contrast(theme.color(token), theme.background) print(f"{token:>10} on background: {ratio:4.1f}:1") class HighContrastApp(pz.App): title = "High contrast" size = (720, 580) theme = "High Contrast" style = pz.Style(size=15) # every text that does not set its own size def view(self): return pz.container(preview("High contrast", "Black, white and yellow, with square corners."), padding=24, width="fill", height="fill") if __name__ == "__main__": report(HIGH_CONTRAST) pz.run(HighContrastApp) ``` **Accessibility:** A theme is one part of accessibility. See [Accessibility](https://albakore.github.io/pyzed/guides/accessibility/) for keyboard navigation, screen readers and reduced motion. #### Retro terminal Green phosphor on black. One green becomes the text, the accent and the success color; the global style makes every text monospaced. `examples/theming/retro_terminal.py`: ```py """A retro terminal theme: green phosphor on black, monospaced and square. python examples/theming/retro_terminal.py - ``Theme.generate`` with a green text color and a green accent. - ``syntax`` and ``ansi`` recolor code and the terminal to match. - The global style switches every text to the monospaced font. """ import pyzed as pz from preview import preview PHOSPHOR = "#33ff66" RETRO = pz.Theme.generate( "Retro Terminal", background="#050a05", text=PHOSPHOR, accent=PHOSPHOR, error="#ff5555", warning="#ffcc33", success=PHOSPHOR, radius=0, syntax={"keyword": ("#a6ff00", "bold"), "string": "#7dffb0", "comment": ("#1f7a3a", "italic")}, ).derive(ansi={"green": PHOSPHOR, "bright_black": "#1f7a3a"}) pz.themes.register(RETRO) class RetroApp(pz.App): title = "Retro terminal" size = (720, 640) theme = "Retro Terminal" style = pz.Style(font="mono", size=13) # every text in the window is monospaced def view(self): title = pz.text("> READY_").style(size=26, weight="bold", color="accent") return pz.container(pz.column([title, preview("SYSTEM", "All colors come from one green.")], spacing=12), padding=24, width="fill", height="fill") if __name__ == "__main__": pz.run(RetroApp) ``` #### Glassy dark A gradient behind the window and a frosted glass panel on top. The theme is One Dark with a cyan accent; the look comes from the global style (`background=` a gradient) and the `glass` effect. `examples/theming/glassy_dark.py`: ```py """A glassy dark look: a gradient behind the window and a frosted panel on top. python examples/theming/glassy_dark.py - The global style paints the window with a ``pz.Gradient``. - ``glass`` turns a container into a translucent panel that blurs what is behind it. - The theme itself is One Dark with a cyan accent (``with_accent``) and rounder corners. """ import pyzed as pz from preview import preview GLASSY = pz.themes.ONE_DARK.derive(name="Glassy Dark", radius=14, radius_sm=10).with_accent("#22d3ee") pz.themes.register(GLASSY) NIGHT = pz.Gradient(["#0f172a", "#312e81", "#701a75"], angle=135) class GlassyApp(pz.App): title = "Glassy dark" size = (760, 650) theme = "Glassy Dark" style = pz.Style(background=NIGHT) # paints the whole window def view(self): panel = pz.container(preview("Glassy", "A frosted panel over a gradient."), width="fill").style( glass={"tint": "#ffffff", "strength": 0.08, "blur": 24}, radius=20, padding=24, border=(1, "#ffffff22")) return pz.container(panel, padding=28, width="fill", height="fill") if __name__ == "__main__": pz.run(GlassyApp) ``` **Effects and the GPU:** `glass`, `backdrop_blur` and `glow` are made for the GPU renderer. On machines without a GPU (some virtual machines and remote desktops) or with the CPU renderer (`ICED_BACKEND=tiny-skia`), they can look simpler than in this capture. Make sure the text stays readable without them. #### Playful pastel A cream background, candy colors and very round corners. `derive` sets pastel avatar colors (`players`), and a named style (`pz.define_style("bubble", ...)`) draws the card around the screen. `examples/theming/pastel.py`: ```py """A playful pastel theme: cream background, candy colors and very round corners. python examples/theming/pastel.py - ``Theme.generate`` for a light theme, then ``derive`` with a pastel info color and avatar colors. - ``pz.define_style`` names a reusable "bubble" style for the card around everything. """ import pyzed as pz from pyzed import ui from preview import preview PASTEL = pz.Theme.generate( "Pastel", background="#fffaf3", text="#4a3f55", accent="#f472b6", radius=16, success="#34d399", warning="#f59e0b", error="#fb7185", ).derive(info="#60a5fa", players=("#f472b6", "#a78bfa", "#60a5fa", "#34d399", "#fbbf24")) pz.themes.register(PASTEL) pz.define_style("bubble", background="surface", radius=24, padding=20, border=(2, "#f9a8d4"), shadow=("#f472b633", (0, 8), 24)) class PastelApp(pz.App): title = "Pastel" size = (720, 670) theme = "Pastel" def view(self): friends = pz.row([ui.avatar(name, index=i, size=30) for i, name in enumerate(["AL", "BO", "CY", "DI", "EV"])], spacing=6) return pz.container(pz.column([ pz.container(preview("Hello!", "Soft colors and round corners."), width="fill").style("bubble"), friends, ], spacing=16), padding=24, width="fill", height="fill") if __name__ == "__main__": pz.run(PastelApp) ``` #### Two themes in one window A light sidebar in a dark app, with a theme picker that saves the chosen theme to a JSON file and loads it on the next start (`Theme.save` and `Theme.load`). with theme: draws the sidebar with One Light." /> `examples/theming/mixed.py`: ```py """Two themes in one window, and a theme picker that remembers your choice. python examples/theming/mixed.py - ``with theme:`` draws a block with another theme (here, a light sidebar in a dark app). - ``self.theme = name`` changes the theme at runtime; the whole window restyles at once. - ``Theme.save`` and ``Theme.load`` keep the chosen theme in a JSON file. """ from pathlib import Path import pyzed as pz from pyzed import ui from preview import preview SAVED = Path(__file__).with_name("my-theme.json") class MixedApp(pz.App): title = "Mixed themes" size = (980, 580) def __init__(self): self.theme = "One Dark" if SAVED.exists(): self.theme = pz.themes.register(pz.Theme.load(SAVED)).name def pick(self, name): self.theme = name pz.themes.get(name).save(SAVED) # remembered for the next start def view(self): current = self.resolved_theme().name with pz.themes.get("One Light"): # everything built in this block uses One Light sidebar = pz.container(pz.column([ ui.heading("Settings", level=3), ui.field("Theme", ui.select(pz.themes.names(), current, self.pick, width=200)), ui.muted("This panel always uses One Light."), ], spacing=12), padding=16, width=300, height="fill").style(background="background") main = pz.container(preview(current, "The main area follows the theme you pick."), padding=24, width="fill", height="fill") return pz.row([sidebar, main], height="fill") if __name__ == "__main__": pz.run(MixedApp) ``` #### The shared preview `examples/theming/preview.py`: ```py """A small "kitchen sink" used by every example in this folder. It only uses theme tokens and ``pyzed.ui`` components, so it takes the look of whatever theme is current: run any of the examples next to it to see the same screen restyled. python examples/theming/brand.py """ import pyzed as pz from pyzed import ui def preview(title: str = "Preview", subtitle: str = "Every color on this screen comes from the theme."): """A heading, buttons in every variant, a small form, badges, a callout and two stats.""" buttons = pz.row([ ui.button("Primary", lambda: None, variant="solid"), ui.button("Filled", lambda: None), ui.button("Outline", lambda: None, variant="outline"), ui.button("Ghost", lambda: None, variant="ghost"), ui.button("Delete", lambda: None, variant="tinted", tint="error", icon="trash-2"), ], spacing=8, wrap=True) form = pz.column([ ui.field("Name", ui.input("Ada Lovelace", lambda _v: None, width=220)), ui.field("Email", ui.input("", lambda _v: None, placeholder="you@example.com", width=220)), ui.field("Notifications", ui.switch(True, lambda _v: None)), ui.field("Volume", pz.container(ui.slider(0.6, lambda _v: None), width=220)), ], spacing=10) badges = pz.row([ ui.badge("New"), ui.badge("Paid", tint="success"), ui.badge("Pending", tint="warning"), ui.badge("Failed", tint="error", variant="outline"), ui.badge("Beta", tint="info", variant="solid"), ], spacing=6, wrap=True) stats = pz.row([ ui.stat("Revenue", "$12.4k", delta=8.2, width="fill"), ui.stat("Refunds", "31", delta=-2.5, width="fill"), ], spacing=12) return pz.column([ pz.column([ui.heading(title), ui.muted(subtitle)], spacing=4), buttons, ui.card(pz.column([form, ui.progress(0.7)], spacing=14), title="Account"), badges, ui.callout("Saved", "Your changes are stored on this device.", tint="success"), stats, ], spacing=16) ``` Every color in it is a token or a tint, which is why one file looks right in six very different themes. Write your own views the same way. ## Distribution ### Distribute your app > Package a Pyzed app for Windows, macOS and Linux: a standalone executable with Python inside, built with pyzed build, plus icons, signing, installers, CI builds and self-updates. Source: https://albakore.github.io/pyzed/packaging/ When your app works on your machine, `pyzed build` (or **File → Export app…** in [Pyzed Design](https://albakore.github.io/pyzed/editor/exporting/)) packages it with PyInstaller or Nuitka: Python, Pyzed, your dependencies, your code and your forms go into one folder (or one file) that runs on its own. The people who use it do not need Python, pip or anything else. Run it from the root of your [project](https://albakore.github.io/pyzed/getting-started/new-project/): **uv:** ```bash title="Windows, macOS and Linux" uv run pyzed build ``` **pip:** ```bash title="macOS and Linux" source .venv/bin/activate pyzed build # or: python -m pyzed build ``` ```powershell title="Windows (PowerShell)" .venv\Scripts\Activate.ps1 pyzed build # or: py -m pyzed build ``` | You build on | You get | People run it by | | --- | --- | --- | | Windows | `dist\Shop\Shop.exe` | Double-clicking it, from a zip or an installer | | macOS | `dist/Shop.app` | Opening it (sign and notarize it to share it) | | Linux | `dist/Shop/Shop` | `./Shop`, or from the application menu | #### From your project to your users ```mermaid flowchart LR P["Your project
pyproject.toml · ui/ · src/"] --> B["pyzed build
(PyInstaller or Nuitka)"] B --> W["Windows
Shop.exe"] B --> M["macOS
Shop.app"] B --> L["Linux
Shop/Shop"] W --> S["Sign · zip · installer"] M --> S L --> S S --> R["GitHub Release"] R -. "pyzed.updater" .-> U["Installed apps
update themselves"] ``` **One build per platform:** A build only runs on the system it was made on: build the Windows app on Windows, the macOS app on macOS and the Linux app on Linux. A [CI workflow](https://albakore.github.io/pyzed/packaging/ci/) can do all three for you. #### In this section pyproject.toml, PyInstaller or Nuitka, data files and the splash screen.', href: '/pyzed/packaging/build/', icon: 'rocket', tag: 'Start here' }, { title: 'Export from the designer', text: 'File → Export app… in Pyzed Design: platforms, backend, icon and splash screen in one dialog.', href: '/pyzed/editor/exporting/', icon: 'pencil' }, { title: 'Windows', text: 'The .exe, folder or single file, a zip or an Inno Setup installer, and signing for SmartScreen.', href: '/pyzed/packaging/windows/', icon: 'seti:windows' }, { title: 'macOS', text: 'The .app bundle, bundle identifier, signing, notarization and a .dmg.', href: '/pyzed/packaging/macos/', icon: 'apple' }, { title: 'Linux', text: 'Which distribution to build on, archives, menu entries, and AppImage or .deb.', href: '/pyzed/packaging/linux/', icon: 'linux' }, { title: 'Icons and metadata', text: 'Name, version, publisher, the icons made from one PNG and the macOS bundle identifier.', href: '/pyzed/packaging/icons-metadata/', icon: 'star' }, { title: 'CI with GitHub Actions', text: 'Build all three systems on every tag, publish a release and sign with secrets.', href: '/pyzed/packaging/ci/', icon: 'github' }, { title: 'Self-updating apps', text: 'Check GitHub Releases, download with a checksum and swap the app on restart.', href: '/pyzed/packaging/updates/', icon: 'random' }, { title: 'Troubleshooting', text: 'Missing modules, blank windows, blocked apps, glibc errors and update messages.', href: '/pyzed/packaging/troubleshooting/', icon: 'warning' }, { title: 'CLI reference', text: 'Every option of pyzed build and the other commands.', href: '/pyzed/reference/cli/', icon: 'list-format' }, ]} /> #### What is supported today | | Windows | macOS | Linux | | --- | --- | --- | --- | | App folder (`pyzed build`) | ✅ `.exe` | ✅ `.app` | ✅ executable | | Single file (`--onefile`) | ✅ | ✅ | ✅ | | PyInstaller or Nuitka (`--backend`) | ✅ | ✅ | ✅ | | x64 and ARM | ✅ | ✅ | ✅ | | Icon from one PNG (`icon =`) | ✅ `.ico` + window | ✅ `.icns` | ✅ window; menu through a `.desktop` file | | File details (version, publisher...) | ✅ | ✅ `Info.plist` | - | | Splash screen | ✅ | ✅ | ✅ | | CI workflow (`--workflow`) and release archives | ✅ | ✅ | ✅ | | Self-update (`pyzed.updater`) | ✅ | ✅ | ✅ | | Signing | With `signtool` | With `codesign` + notarization | Not needed | | Installer | Your own (Inno Setup, WiX) | Your own `.dmg` (`hdiutil`) | Your own (archive, script, AppImage...) | | Cross-compiling | ❌ | ❌ | ❌ | **Status of this section:** Packaging for desktop is ready to use today. Installers, signing and update channels are documented with the tools that Pyzed Design itself uses, but `pyzed build` does not run them for you yet. Mobile and web targets are planned for the future and are not available yet. ### How building works > How pyzed build packages a Pyzed project as a standalone app with PyInstaller or Nuitka: what gets bundled, every option in pyproject.toml, icons, splash screen, data files and the output on each platform. Source: https://albakore.github.io/pyzed/packaging/build/ `pyzed build` turns a [Pyzed project](https://albakore.github.io/pyzed/guides/projects/) into an app that runs on machines without Python: a folder (or a single file) with the Python interpreter, `pyzed-ui`, your dependencies, your backend code and your forms. It uses [PyInstaller](https://pyinstaller.org) or [Nuitka](https://nuitka.net) underneath, run inside the project's own environment, so nothing has to be installed globally. **From the designer:** Pyzed Design does all of this from a dialog: **File → Export app…** sets every option below, previews the icon and the splash screen, builds with the output in the Console and writes the CI workflow. See [Exporting your app](https://albakore.github.io/pyzed/editor/exporting/). #### Build in three steps 1. Open a terminal in the project folder (the one with `pyproject.toml`). If you do not have a project yet, create one with `pyzed new` (see [Create a project](https://albakore.github.io/pyzed/getting-started/new-project/)). 2. Run the build: **uv:** ```bash title="Windows, macOS and Linux" uv run pyzed build ``` **pip:** ```bash title="macOS and Linux" source .venv/bin/activate pyzed build # or: python -m pyzed build ``` ```powershell title="Windows (PowerShell)" .venv\Scripts\Activate.ps1 pyzed build # or: py -m pyzed build ``` 3. Open the result in `dist/`: | Platform | Result | Run it | | --- | --- | --- | | Windows | `dist\Shop\Shop.exe` (or `dist\Shop.exe` with `--onefile`) | Double-click it. | | macOS | `dist/Shop.app` | Open it from Finder. | | Linux | `dist/Shop/Shop` (or `dist/Shop` with `--onefile`) | `./dist/Shop/Shop` | **uv is needed, even with pip:** `pyzed build` runs PyInstaller through `uv run --with pyinstaller` (and Nuitka through `uv run --with nuitka`), so [uv](https://docs.astral.sh/uv/) must be installed even if you use pip for everything else. You do not need to install PyInstaller or Nuitka yourself. Install uv with `pip install uv` or from its website. **One build per platform:** Neither PyInstaller nor Nuitka cross-compiles: build the Windows app on Windows, the macOS app on macOS and the Linux app on Linux, each on the architecture it is for (x64 or ARM). A [CI workflow](https://albakore.github.io/pyzed/packaging/ci/) builds all of them for you. #### What happens when you build ```mermaid flowchart LR P["pyproject.toml
[tool.pyzed.build]"] --> B["pyzed build"] U["ui/ forms + handlers"] --> B S["src/ backend"] --> B B --> L["launcher script
build/pyzed/"] B --> I["PyInstaller or Nuitka
(uv run --with ...)"] L --> I I --> D["dist/
app folder, .exe or .app"] ``` 1. `pyzed build` reads `[tool.pyzed]` and `[tool.pyzed.build]` from `pyproject.toml`. 2. It writes a small **launcher** (`build/pyzed/_launcher.py`) that opens the main form (`[tool.pyzed] main`) from inside the bundle, with the window icon and the splash screen. 3. It makes the icon files (`.ico`, `.icns`, `.png`) from your picture, and the Windows version file. 4. It scans the `.py` files of the UI folder with Python's `ast` module to find what they import. 5. It runs the backend in the project's environment with the right options, and the app lands in `dist/`. Last, it fills in the macOS `Info.plist` and packs the archive, if you asked for one. ##### What gets bundled Forms load their handlers from `.py` files when they open, so PyInstaller cannot follow their imports by itself. `pyzed build` fills the gaps: - **The UI folder** is shipped as data, keeping its layout (`ui/main.pzform`, `ui/main.py`, helper modules...). - **Every module imported by the UI** is passed as a hidden import, so the backend in `src/`, modules next to `pyproject.toml` and third-party packages are bundled with all their dependencies. - **The project's own package** (`src/`) is collected whole, including modules imported dynamically. - **Pyzed itself** with its data files (themes, icons, templates). - `ui/`, `src/` and the project root are added to PyInstaller's search path. Code outside the project, such as a shared library in a sibling folder, is bundled like any other dependency once it is installed in the environment: ```bash pyzed add --editable ../billing # then `from billing import invoice` in a handler ``` **Apps written in code:** `pyzed build` starts the project's main **form**. For an app written fully in code (a `pz.App` class and `pz.run(...)`), see [Apps written in code](#apps-written-in-code) below. #### Options Command line flags win over `[tool.pyzed.build]` in `pyproject.toml`: | Flag | Effect | | --- | --- | | `--backend nuitka` | Build with Nuitka instead of PyInstaller (see [Choosing a backend](#choosing-a-backend)). | | `--onefile` | A single executable instead of a folder. | | `--console` | Keep the console window (useful to see tracebacks). | | `--name Shop` | Name of the executable or bundle. | | `--archive zip` | Also pack the app in a `zip` (or `tar.gz`) named for the [self-updater](https://albakore.github.io/pyzed/packaging/updates/). | | `--target linux-x64` | Check that this computer builds that target (CI uses it); it does not cross-compile. | | `--dry-run` | Print the backend's command and stop. | | `--workflow` | Write a GitHub Actions workflow for the project's `targets` instead of building (see [CI](https://albakore.github.io/pyzed/packaging/ci/)). | All the settings of a project live in `pyproject.toml` (the [Export dialog](https://albakore.github.io/pyzed/editor/exporting/) of Pyzed Design writes the same table): ```toml title="pyproject.toml" [tool.pyzed.build] name = "Shop" # executable / bundle name (default: the project name) backend = "pyinstaller" # or "nuitka" onefile = false # one self-extracting file instead of a folder windowed = true # no console window on Windows and macOS icon = "assets/icon.png" # one square picture; .ico and .icns are made from it version = "1.2.0" # default: [project] version publisher = "Acme" # the company in the Windows file properties identifier = "com.acme.shop" # macOS bundle id and Linux app id copyright = "© 2026 Acme" optimize = 0 # 1 drops asserts, 2 also docstrings slim = true # leave out tkinter, test, idlelib... (unused by Pyzed apps) exclude = ["numpy.tests"] # more modules to leave out include = ["assets", "data/prices.csv"] # extra files and folders, same relative path in the bundle hidden-imports = ["shop.plugins.csv"] # modules imported with importlib or by name collect = ["babel"] # packages to ship whole: code, data files and binaries archive = "zip" # also pack the app: shop-windows-x86_64.zip... targets = ["windows-x64", "macos-arm64", "linux-x64"] # what the CI workflow builds args = [] # anything else, passed to the backend as is [tool.pyzed.build.splash] image = "assets/splash.png" duration = 1.5 # seconds, at least text = "Loading…" native = true # also the backend's own splash while the app unpacks [tool.pyzed.build.nuitka] # options for one backend only ([tool.pyzed.build.pyinstaller] too) args = ["--lto=yes"] ``` | Setting | Default | Notes | | --- | --- | --- | | `name` | the project name | Also the name of the folder in `dist/`. | | `backend` | `"pyinstaller"` | `"nuitka"` compiles to C: see below. | | `onefile` | `false` | A single file starts slower (it unpacks itself each time) and cannot [update itself](https://albakore.github.io/pyzed/packaging/updates/). | | `windowed` | `true` | `false` (or `--console`) shows a terminal next to the app. | | `icon` | none | A PNG becomes `.ico`, `.icns` and the window icon; see [Icons and metadata](https://albakore.github.io/pyzed/packaging/icons-metadata/). | | `version`, `publisher`, `identifier`, `description`, `copyright` | from `[project]` | Written into the Windows file properties and the macOS `Info.plist`. | | `splash` | none | A [splash screen](#splash-screen). | | `optimize` | `0` | Python's `-O` (`1`) or `-OO` (`2`). | | `slim` | `true` | Leaves out `tkinter`, `test`, `idlelib`, `lib2to3`, `ensurepip`, `pydoc_data`, `turtle` and `turtledemo`. | | `strip`, `upx` | `true`, `false` | Strip debug symbols (Linux and macOS); compress with [UPX](https://upx.github.io/) if installed. | | `exclude` | `[]` | Modules to leave out. | | `include` | `[]` | Paths relative to the project root. | | `hidden-imports` | `[]` | Modules the backend cannot see. | | `collect` | `[]` | Packages with data files or plugins. | | `archive` | none | `"zip"` or `"tar.gz"`. | | `targets` | this computer | For `--workflow`: `windows-x64`, `windows-arm64`, `macos-arm64`, `macos-x64`, `linux-x64`, `linux-arm64`. | | `args` | `[]` | Any other option of the backend ([PyInstaller](https://pyinstaller.org/en/stable/usage.html), [Nuitka](https://nuitka.net/user-documentation/)). | #### Choosing a backend | | PyInstaller | Nuitka | | --- | --- | --- | | How | Bundles your `.py` files as they are, next to Python | Compiles your code and its dependencies to C, then to machine code | | Build time | Seconds | Minutes | | Speed | Normal Python | Compiled: faster pure-Python code | | Size of a single file | Larger | About 15% smaller (better compression) | | Size of a folder | Smaller | Larger (compiled code takes more room than `.pyc`) | | Needs | Nothing (uv installs it) | A C compiler | Measured with a one-form app on Linux x64: PyInstaller made a 57 MB folder or a 25 MB single file in about 25 seconds; Nuitka a 72 MB folder or a 21 MB single file in about 3 minutes. Most of the size is Pyzed's engine (about 33 MB) and Python itself, so `slim`, `exclude` and `strip` matter more than the backend. Start with PyInstaller while you develop, and try Nuitka for releases. Nuitka needs a C compiler: on Linux `gcc` (`sudo apt install gcc`), on macOS the Xcode command line tools (`xcode-select --install`), on Windows Visual Studio's Build Tools, or Nuitka downloads a compiler the first time. `pyzed build` adds `patchelf` (Linux) and `zstandard` (single files) itself. #### Splash screen With `[tool.pyzed.build.splash]`, the app opens a frameless window with your image, centered on the screen, before the main form. It stays up for `duration` seconds at least and until the main form has loaded, then closes. `text` is written over the bottom of the image. With `native = true`, the backend's own splash also shows before Python starts (PyInstaller on Windows and Linux, which needs `tkinter` in the project's Python; Nuitka for single files on Windows), useful for single-file apps that take a moment to unpack. The same splash works in an app written in code, through `App.splash`: ```python import pyzed as pz class Shop(pz.App): splash = pz.Splash("assets/splash.png", duration=1.5, text="Loading…") ``` ##### Finding your data files Files listed in `include` end up under the bundle root, with the same relative path. Find them from your code with: ```python import sys from pathlib import Path ROOT = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parents[1])) prices = ROOT / "data" / "prices.csv" ``` `sys._MEIPASS` is the bundle folder of a PyInstaller app. In a Nuitka app and while you develop, the fallback is used: from a handler in `ui/` it points at the bundle (or the project) root; adjust `parents[1]` to where the file that runs this lives. **Do not write next to the app:** The app folder may be read-only once installed (for example in `/Applications` or `Program Files`). Save user data in the user's folders instead: the `platformdirs` package (`pyzed add platformdirs`) gives the right place on each system with `platformdirs.user_data_dir("Shop")`. ##### Where the files go The intermediate files go to `build/pyzed/` and the result to `dist/`; both are in the `.gitignore` of new projects. Each build starts clean, so delete nothing by hand. #### Apps written in code `pyzed build` starts the project's main **form**. For an app written fully in code (a `pz.App` class and `pz.run(...)`), run PyInstaller on your script yourself, collecting Pyzed's data files. `pyzed build --dry-run` prints the exact command `pyzed build` would run, which you can adapt: **uv:** ```bash uv run --with pyinstaller pyinstaller main.py --name Shop --windowed --collect-data pyzed --collect-submodules pyzed ``` **pip:** ```bash title="macOS and Linux" python3 -m pip install pyinstaller python3 -m PyInstaller main.py --name Shop --windowed --collect-data pyzed --collect-submodules pyzed ``` ```powershell title="Windows (PowerShell)" py -m pip install pyinstaller py -m PyInstaller main.py --name Shop --windowed --collect-data pyzed --collect-submodules pyzed ``` With Nuitka: ```bash uv run --with nuitka --with patchelf python -m nuitka --mode=standalone --include-package-data=pyzed main.py ``` #### Next - Per platform: [Windows](https://albakore.github.io/pyzed/packaging/windows/), [macOS](https://albakore.github.io/pyzed/packaging/macos/), [Linux](https://albakore.github.io/pyzed/packaging/linux/). - [Build every platform in CI](https://albakore.github.io/pyzed/packaging/ci/) and [ship updates](https://albakore.github.io/pyzed/packaging/updates/). - Something wrong? [Troubleshooting](https://albakore.github.io/pyzed/packaging/troubleshooting/). ### Building on Windows > Package a Pyzed app for Windows: build an .exe with pyzed build, add an icon, ship a zip or an installer made with Inno Setup, and sign it with signtool to avoid SmartScreen warnings. Source: https://albakore.github.io/pyzed/packaging/windows/ On Windows, `pyzed build` makes a folder with `Shop.exe` and everything it needs. You can share that folder as a zip, or wrap it in an installer. #### Build the .exe 1. Install [uv](https://docs.astral.sh/uv/) if you do not have it (PowerShell): ```powershell powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` 2. In the project folder, build: **uv:** ```powershell uv run pyzed build ``` **pip:** ```powershell .venv\Scripts\Activate.ps1 pyzed build # or: py -m pyzed build ``` 3. Try it: double-click `dist\Shop\Shop.exe`, or from PowerShell: ```powershell .\dist\Shop\Shop.exe ``` The result has no console window (`windowed = true`). While you are testing, build with `--console` to see errors and `print` output in a terminal next to the app. #### Folder or single file | | Folder (default) | `--onefile` | | --- | --- | --- | | Result | `dist\Shop\Shop.exe` plus an `_internal` folder | `dist\Shop.exe` | | Starts | Fast | Slower: it unpacks itself to a temporary folder each time | | Antivirus | Rarely flagged | Flagged more often (false positives) | | Self-update | Supported ([Updates](https://albakore.github.io/pyzed/packaging/updates/)) | Not supported | Prefer the folder build, and give people a zip or an installer. #### The icon Windows uses an `.ico` file. Put it in the project and point to it: ```toml title="pyproject.toml" [tool.pyzed.build] icon = "assets/icon.ico" ``` See [Icons and metadata](https://albakore.github.io/pyzed/packaging/icons-metadata/) to make an `.ico` from a PNG and to set the version shown in the file's properties. #### Share it ##### As a zip The simplest way: zip the app folder and share the zip. People unzip it anywhere and run `Shop.exe`. ```powershell Compress-Archive -Path dist\Shop -DestinationPath Shop-windows-x86_64.zip ``` Name it `-windows-.zip` if you plan to [update the app by itself](https://albakore.github.io/pyzed/packaging/updates/). ##### As an installer An installer adds the app to the Start menu, can create a desktop shortcut and registers an uninstaller. **Not built by pyzed build yet:** `pyzed build` makes the app folder, not a setup program. Create the installer with a separate tool such as [Inno Setup](https://jrsoftware.org/isinfo.php) (free) or [WiX](https://wixtoolset.org/), from the folder in `dist\`. Pyzed Design itself is installed this way: its Inno Setup script is [`installer/windows/pyzed-design.iss`](https://github.com/albakore/pyzed/blob/main/installer/windows/pyzed-design.iss) and is a good template. It installs **for the current user only, without administrator rights**, in `%LOCALAPPDATA%\Programs\`, which keeps the folder writable so the app can update itself. A minimal script: ```ini title="installer/shop.iss" #define AppName "Shop" #define AppExe "Shop.exe" [Setup] ; Generate your own AppId once (Tools -> Generate GUID in Inno Setup) and never change it. AppId={{00000000-0000-0000-0000-000000000000} AppName={#AppName} AppVersion=1.0.0 DefaultDirName={autopf}\{#AppName} PrivilegesRequired=lowest OutputBaseFilename=Shop-setup Compression=lzma2/max SolidCompression=yes WizardStyle=modern [Files] Source: "..\dist\Shop\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: "{autoprograms}\{#AppName}"; Filename: "{app}\{#AppExe}" Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExe}"; Tasks: desktopicon [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; Flags: unchecked ``` Compile it (Inno Setup 6 installs `ISCC.exe`; `choco install innosetup` also works): ```powershell & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer\shop.iss ``` With `PrivilegesRequired=lowest`, `{autopf}` is the user's own programs folder. #### Sign it Windows SmartScreen warns about executables that are not signed ("Windows protected your PC"). Unsigned apps still run (**More info → Run anyway**), but signing with a code signing certificate removes the warning over time. ```powershell signtool sign /f certificate.pfx /p $env:CERT_PASSWORD /fd sha256 ` /tr http://timestamp.digicert.com /td sha256 dist\Shop\Shop.exe ``` `signtool.exe` comes with the Windows SDK. Sign the `.exe` before zipping or building the installer, then sign the setup `.exe` too. The [CI page](https://albakore.github.io/pyzed/packaging/ci/#signing-in-ci) shows how to keep the certificate in a GitHub secret. **SmartScreen reputation:** Even signed apps can show the warning at first: SmartScreen builds reputation per certificate as people download and run your app. Extended Validation (EV) certificates start with more reputation. #### Things to know - **Architecture:** the app is built for the architecture of the Python that runs the build (usually x86-64). An x86-64 build also runs on Windows on ARM through emulation. - **Paths:** Windows paths in `pyproject.toml` use forward slashes (`assets/icon.ico`); that works on every system. - **Antivirus:** if an antivirus quarantines the app, prefer the folder build, sign it, and report the false positive to the vendor. ### Building on macOS > Package a Pyzed app for macOS: build a .app bundle with pyzed build, set its icon and bundle identifier, sign it with your Developer ID, notarize it and ship it in a .dmg. Source: https://albakore.github.io/pyzed/packaging/macos/ On macOS, `pyzed build` makes an **app bundle**, `dist/Shop.app`: a folder that Finder shows as one app, with Python, Pyzed and your code inside. #### Build the .app 1. Install [uv](https://docs.astral.sh/uv/) if you do not have it: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` 2. In the project folder, build: **uv:** ```bash uv run pyzed build ``` **pip:** ```bash source .venv/bin/activate pyzed build # or: python -m pyzed build ``` 3. Try it: ```bash open dist/Shop.app ``` **Apple silicon and Intel:** The app is built for the architecture of the Python that runs the build: `arm64` on Apple silicon, `x86_64` on Intel Macs. Build on each kind of Mac (GitHub offers both runners, see [CI](https://albakore.github.io/pyzed/packaging/ci/)) to support both. Universal (both in one) builds are not set up by `pyzed build`. #### Icon and bundle identifier macOS uses an `.icns` icon. Every Mac app also has a **bundle identifier**, a reverse domain name such as `com.acme.shop` that macOS uses to keep its settings and permissions. Set both in `pyproject.toml`; the identifier goes to PyInstaller through `args`: ```toml title="pyproject.toml" [tool.pyzed.build] icon = "assets/icon.icns" args = ["--osx-bundle-identifier", "com.acme.shop"] ``` See [Icons and metadata](https://albakore.github.io/pyzed/packaging/icons-metadata/) to make an `.icns` from a PNG. #### Sign and notarize Apps downloaded from the internet are checked by **Gatekeeper**. An app that is not signed and notarized is blocked ("Shop cannot be opened because Apple cannot check it for malicious software"). People can still open it with right-click → **Open**, or in **System Settings → Privacy & Security → Open Anyway**, but for a real release, sign and notarize it. You need an [Apple Developer Program](https://developer.apple.com/programs/) membership, a **Developer ID Application** certificate in your keychain and an [app-specific password](https://support.apple.com/en-us/102654) for your Apple ID. 1. Sign the bundle with the hardened runtime and a timestamp: ```bash codesign --force --deep --options runtime --timestamp \ --sign "Developer ID Application: Your Name (TEAMID)" dist/Shop.app ``` 2. Zip it with `ditto` (it keeps the bundle's symlinks and signature) and send it to Apple's notary service: ```bash ditto -c -k --keepParent dist/Shop.app Shop.zip xcrun notarytool submit Shop.zip --wait \ --apple-id you@example.com --team-id TEAMID --password "$APPLE_APP_PASSWORD" ``` 3. Staple the ticket to the app, so it opens even offline: ```bash xcrun stapler staple dist/Shop.app ``` Check the result with `spctl --assess --verbose dist/Shop.app` ("accepted, source=Notarized Developer ID"). **Sign after every change:** Changing anything inside the `.app` after signing (even adding a file) breaks the signature. Sign last, then zip or make the disk image. #### A disk image (.dmg) Mac users expect a `.dmg` with the app and a shortcut to Applications to drag it into. **Not built by pyzed build yet:** `pyzed build` makes the `.app`, not the disk image. Make it with `hdiutil`, which comes with macOS, as below. ```bash mkdir -p build/dmg ditto dist/Shop.app build/dmg/Shop.app ln -s /Applications build/dmg/Applications hdiutil create -volname Shop -srcfolder build/dmg -fs HFS+ -format UDZO -ov Shop-macos-arm64.dmg codesign --force --timestamp --sign "Developer ID Application: Your Name (TEAMID)" Shop-macos-arm64.dmg ``` Notarize and staple the `.dmg` the same way as the zip above. This is how Pyzed Design's own `.dmg` is made (see `macos_dmg` in [`scripts/build_designer.py`](https://github.com/albakore/pyzed/blob/main/scripts/build_designer.py)). #### Things to know - **Where it lives:** people drag the app to `/Applications`, which is writable by admin users, so the app can [update itself](https://albakore.github.io/pyzed/packaging/updates/) there. - **Quarantine:** a downloaded app carries a quarantine flag until the first launch passes Gatekeeper. For your own testing you can clear it with `xattr -dr com.apple.quarantine dist/Shop.app`; do not ask your users to do this. - **Permissions:** if the app needs the camera, microphone or other protected resources, macOS asks the user. Add the usage descriptions to the bundle's `Info.plist` with PyInstaller's spec file options; `pyzed build` does not set them for you. ### Building on Linux > Package a Pyzed app for Linux: build with pyzed build, pick the distribution to build on for glibc compatibility, ship a tarball or zip with a .desktop entry, and what about AppImage, .deb and Flatpak. Source: https://albakore.github.io/pyzed/packaging/linux/ On Linux, `pyzed build` makes a folder, `dist/Shop/`, with an executable called `Shop` and everything it needs. No signing is required. #### Build it 1. Install [uv](https://docs.astral.sh/uv/) if you do not have it: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` 2. In the project folder, build: **uv:** ```bash uv run pyzed build ``` **pip:** ```bash source .venv/bin/activate pyzed build # or: python -m pyzed build ``` 3. Try it: ```bash ./dist/Shop/Shop ``` #### Which distribution to build on The app links against the C library (glibc) of the system it was built on, and runs on that version **or newer**. Build on the **oldest** distribution you want to support. For example, an app built on Ubuntu 22.04 runs on Ubuntu 22.04, 24.04 and later, and on other distributions with glibc 2.35 or newer, but not on Ubuntu 20.04. **Building in a container:** To build for older systems from a newer one, build inside a container of the older distribution (for example `docker run -it -v "$PWD":/app -w /app ubuntu:22.04`), install uv there and run `uv run pyzed build`. Pyzed's own wheels need glibc 2.28 or newer (`manylinux_2_28`), so that is the oldest base that can work. Alpine and other musl-based systems need a build made on them. #### What the target needs The app brings Python and Pyzed with it, but uses the system's graphics and windowing libraries: a desktop session with X11 or Wayland, `libxkbcommon`, and a Vulkan or OpenGL driver (Mesa on most systems). Every desktop distribution has them; minimal server images may not. #### Share it ##### As an archive The simplest way: an archive of the folder. People unpack it anywhere and run `./Shop/Shop`. ```bash tar -czf Shop-linux-x86_64.tar.gz -C dist Shop # or a zip, which the self-updater uses: (cd dist && zip -qr ../Shop-linux-x86_64.zip Shop) ``` Name the zip `-linux-.zip` if you plan to [update the app by itself](https://albakore.github.io/pyzed/packaging/updates/). ##### Add it to the application menu A `.desktop` file makes the app appear in the desktop's application menu, with its icon. Install it per user, no root needed: ```ini title="~/.local/share/applications/com.acme.shop.desktop" [Desktop Entry] Type=Application Name=Shop Exec=/home/you/.local/share/shop/Shop Icon=/home/you/.local/share/shop/icon.png Categories=Office; Terminal=false ``` Pyzed Design's Linux installer does exactly this: it downloads the release zip, checks it against the release's `SHA256SUMS`, unpacks it into `~/.local/share/pyzed-design`, links a command into `~/.local/bin` and adds the menu entry. Its script, [`docs/public/install.sh`](https://github.com/albakore/pyzed/blob/main/docs/public/install.sh), is a good template for your own `curl ... | sh` installer. ##### AppImage, .deb, .rpm, Flatpak, Snap **Not built by pyzed build yet:** `pyzed build` makes the app folder only. Packaging it as an AppImage, a `.deb`/`.rpm` or a Flatpak/Snap is done with those formats' own tools, starting from `dist/Shop/`: - **AppImage**: put the folder in an `AppDir` with an `AppRun` that starts `Shop`, a `.desktop` file and an icon, then run `appimagetool`. - **.deb / .rpm**: tools such as [fpm](https://fpm.readthedocs.io) turn a folder into either: `fpm -s dir -t deb -n shop -v 1.0.0 dist/Shop/=/opt/shop/`. - **Flatpak / Snap**: write a manifest that copies the folder and declares the graphics and display permissions. These recipes are not tested by Pyzed. #### Things to know - **Architecture:** the app is built for the machine it is built on: `x86_64`, or `aarch64` on ARM (GitHub offers ARM runners too, see [CI](https://albakore.github.io/pyzed/packaging/ci/)). - **No GPU:** on virtual machines without a GPU, start the app with the CPU renderer if the window stays blank; see [Troubleshooting](https://albakore.github.io/pyzed/packaging/troubleshooting/). - **Executable bit:** zip files made on Windows lose the executable bit. Build and archive Linux apps on Linux. ### Icons and metadata > Give a packaged Pyzed app its name, icon (.ico, .icns and the window icon made from one PNG), version, publisher, bundle identifier and file properties on Windows, macOS and Linux. Source: https://albakore.github.io/pyzed/packaging/icons-metadata/ An app's identity is a few small things: its **name**, its **icon**, its **version**, who **publishes** it and, on macOS, its **bundle identifier**. All of them are set in `[tool.pyzed.build]` of `pyproject.toml`, by hand or from the [Export dialog](https://albakore.github.io/pyzed/editor/exporting/#general) of Pyzed Design, and `pyzed build` writes them where each system looks. #### Name, version and details ```toml title="pyproject.toml" [tool.pyzed.build] name = "Shop" # Shop.exe, Shop.app, dist/Shop/ version = "1.2.0" # default: [project] version publisher = "Acme" identifier = "com.acme.shop" # default: com.. description = "Point of sale" # default: [project] description copyright = "© 2026 Acme" ``` | What | Windows | macOS | Linux | | --- | --- | --- | --- | | `name` | `Shop.exe`, the product name | `Shop.app`, `CFBundleDisplayName` | The executable | | `version` | File and product version (Details tab) | `CFBundleShortVersionString` and `CFBundleVersion` | - | | `publisher` | Company name | - | - | | `identifier` | - | `CFBundleIdentifier` | The windows' app id (see [The window icon](#the-window-icon)) | | `description` | File description | - | - | | `copyright` | Legal copyright | `NSHumanReadableCopyright` | - | With PyInstaller, `pyzed build` writes a Windows version file (`build/pyzed/version.txt`) and fills in the bundle's `Info.plist` after the build; Nuitka gets the same values as options. Use a reverse domain you own as the `identifier`. macOS keeps the app's preferences and privacy permissions under it, so do not change it between releases. The **window title** is not here: it is the form's **Title** in the designer, or `title` of your `pz.App`. If your app shows its version or [checks for updates](https://albakore.github.io/pyzed/packaging/updates/), keep the version in your code too, for example in your package's `__init__.py`, and update it together with `pyproject.toml`: ```python title="src/shop/__init__.py" __version__ = "1.2.0" ``` **Why not importlib.metadata?:** `importlib.metadata.version("shop")` reads the installed package's metadata, which PyInstaller does not copy into the bundle unless you ask it to (`args = ["--copy-metadata", "shop"]`). A constant in your code always works. #### The icon Give `icon` one square picture, ideally a PNG of 1024 × 1024 pixels with a transparent background: ```toml title="pyproject.toml" [tool.pyzed.build] icon = "assets/icon.png" ``` `pyzed build` makes every file the systems want from it, with Pyzed's own image code (no Pillow, `sips` or `iconutil` needed), in `build/pyzed/`: | File | Sizes inside | Used for | | --- | --- | --- | | `app-icon.ico` | 16, 20, 24, 32, 40, 48, 64, 128, 256 px | The Windows `.exe`: Explorer, the taskbar, Alt+Tab | | `app-icon.icns` | 16 to 1024 px, with Retina sizes | The macOS bundle: Finder and the Dock | | `app-icon.png` | 256 px | The icon of the app's windows (shipped inside the app) | Small sizes are made by scaling the big picture down. If the 16 and 32 pixel versions look blurry, draw a simpler version for them yourself (below), or keep the design bold. The [Export dialog](https://albakore.github.io/pyzed/editor/exporting/#icon) previews every size. An `.ico` or `.icns` file given as `icon` is used as it is, for its own system. ##### Making the icon files yourself For full control over each size, make the files with your favorite tool from a 1024 × 1024 PNG: **Windows .ico (Python):** ```python from PIL import Image # pip install pillow Image.open("icon.png").save("icon.ico", sizes=[(s, s) for s in (16, 24, 32, 48, 64, 128, 256)]) ``` **macOS .icns:** ```bash mkdir icon.iconset for s in 16 32 128 256 512; do sips -z $s $s icon.png --out icon.iconset/icon_${s}x${s}.png sips -z $((s*2)) $((s*2)) icon.png --out icon.iconset/icon_${s}x${s}@2x.png done iconutil -c icns icon.iconset # writes icon.icns (macOS only) ``` #### The window icon The `.exe` and `.app` icons are the ones of the **file**. A packaged app also gives its **windows** the icon (the title bar and the taskbar on Windows and Linux): the launcher sets `pz.Window.icon` to `app-icon.png`. macOS always shows the bundle's icon in the Dock. In your own code, any window can have its icon, a path to an image or its bytes: ```python import pyzed as pz class Shop(pz.App): icon = "assets/icon.png" app_id = "com.acme.shop" # Linux: matches the .desktop file ``` On Linux, `app_id` names the app to the desktop. Wayland takes the icon of the taskbar and the dock from the `.desktop` file whose name matches it (`com.acme.shop.desktop`, see [Linux](https://albakore.github.io/pyzed/packaging/linux/#add-it-to-the-application-menu)); X11 uses `icon` directly. #### Windows file properties by hand To write a version resource of your own instead of the generated one (more fields, other languages), capture one from an existing `.exe` with `pyi-grab_version` and pass it to PyInstaller: ```toml title="pyproject.toml" [tool.pyzed.build.pyinstaller] args = ["--version-file", "assets/version.txt"] ``` Paths in `args` reach PyInstaller unchanged, and PyInstaller writes its spec file to `build/pyzed/`: if it cannot find the file, give its absolute path. ### Build in CI with GitHub Actions > Build a Pyzed app for Windows, macOS and Linux on x64 and ARM on every release with GitHub Actions: the workflow pyzed build --workflow writes, publishing a GitHub Release, and signing with secrets. Source: https://albakore.github.io/pyzed/packaging/ci/ Since each system builds its own app, the easiest way to ship them all is to let GitHub build them on its own Windows, macOS and Linux machines ("runners"), on x64 and ARM, every time you publish a version. #### Write the workflow **uv:** ```bash uv run pyzed build --workflow ``` **pip:** ```bash title="macOS and Linux" pyzed build --workflow # or: python -m pyzed build --workflow ``` ```powershell title="Windows (PowerShell)" pyzed build --workflow # or: py -m pyzed build --workflow ``` It writes `.github/workflows/build.yml`, with one job per target listed in `targets` of `[tool.pyzed.build]` (by default Windows x64, macOS on Apple silicon and Linux x64). In Pyzed Design, check the platforms in **File → Export app… → Platforms** and press **Write CI workflow** ([Exporting your app](https://albakore.github.io/pyzed/editor/exporting/#platforms-and-architectures)). ```yaml title=".github/workflows/build.yml" name: Build # Builds the app for each target with `pyzed build` and keeps the result as an # artifact (Actions -> this run -> Artifacts). The backends cannot cross-compile, so # every platform and architecture builds on its own runner. on: push: tags: ["v*"] workflow_dispatch: jobs: build: name: Build ${{ matrix.target }} runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - { target: windows-x64, runner: windows-latest } - { target: macos-arm64, runner: macos-latest } - { target: linux-x64, runner: ubuntu-22.04 } steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v6 - run: uv sync --locked - run: uv run pyzed build --target ${{ matrix.target }} - uses: actions/upload-artifact@v4 with: name: shop-${{ matrix.target }} path: dist/ ``` `--target` makes each job check that its machine builds what the matrix says. The backend, the icon, the splash screen and every other setting come from `pyproject.toml`, the same as on your computer. #### Run it 1. Commit the workflow and your `uv.lock` (the build uses `uv sync --locked`, so the lock file must be up to date). 2. Push a version tag: ```bash git tag v1.0.0 git push origin v1.0.0 ``` Or start it by hand: **Actions → Build → Run workflow**. 3. When it finishes, download the builds from the run's page, under **Artifacts**. #### Which machines | Target | Runner | Builds for | | --- | --- | --- | | `windows-x64` | `windows-latest` | Windows 10 and 11 on Intel and AMD | | `windows-arm64` | `windows-11-arm` | Windows 11 on ARM | | `macos-arm64` | `macos-latest` | Macs with Apple silicon | | `macos-x64` | `macos-15-intel` | Intel Macs | | `linux-x64` | `ubuntu-22.04` | Linux on Intel and AMD | | `linux-arm64` | `ubuntu-22.04-arm` | Linux on ARM | The Linux runners are the oldest Ubuntu GitHub offers on purpose: the app runs on the runner's glibc or newer ([Linux](https://albakore.github.io/pyzed/packaging/linux/#which-distribution-to-build-on)). With `backend = "nuitka"` every runner already has the C compiler Nuitka needs. #### Publish a GitHub Release Artifacts expire after a while and need a GitHub login to download. To give people a permanent download page, set `archive` before writing the workflow: ```toml title="pyproject.toml" [tool.pyzed.build] archive = "zip" ``` Each build then packs the app as `--.zip` (`shop-windows-x86_64.zip`, `shop-macos-arm64.zip`, `shop-linux-x86_64.zip`), the names the [self-updater](https://albakore.github.io/pyzed/packaging/updates/) looks for, and the workflow gets a last step that attaches it to the tag's release (and the permission to create it): ```yaml title=".github/workflows/build.yml (what archive adds)" jobs: build: permissions: contents: write steps: # ... the build, then: - name: Attach to the GitHub Release if: startsWith(github.ref, 'refs/tags/v') shell: bash env: GH_TOKEN: ${{ github.token }} run: | # the first build to get here creates the release; the others only upload gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --verify-tag || true gh release upload "$GITHUB_REF_NAME" dist/*.zip --clobber ``` On macOS the zip is made with `ditto`, which keeps the bundle's symlinks and signature. If you [sign](#signing-in-ci) the app, sign it before it is packed: set `archive` only after adding the signing step, or zip after signing yourself. #### Signing in CI Keep certificates in **GitHub secrets** (Settings → Secrets and variables → Actions), never in the repository. Then decode them in a step before signing. This is what Pyzed's own release workflow does for Pyzed Design: **Windows:** Store the `.pfx` file as base64 in `WINDOWS_CERTIFICATE` and its password in `WINDOWS_CERTIFICATE_PASSWORD`: ```yaml - name: Sign if: runner.os == 'Windows' shell: bash env: CERT: ${{ secrets.WINDOWS_CERTIFICATE }} PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} run: | echo "$CERT" | base64 --decode > "$RUNNER_TEMP/cert.pfx" signtool=$(ls "/c/Program Files (x86)/Windows Kits/10/bin/"*/x64/signtool.exe | tail -1) "$signtool" sign /f "$RUNNER_TEMP/cert.pfx" /p "$PASSWORD" /fd sha256 \ /tr http://timestamp.digicert.com /td sha256 dist/Shop/Shop.exe ``` **macOS:** Store the Developer ID certificate (`.p12`) as base64 in `MACOS_CERTIFICATE` with its password, and your Apple ID details for notarization: ```yaml - name: Sign and notarize if: runner.os == 'macOS' env: CERT: ${{ secrets.MACOS_CERTIFICATE }} CERT_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} run: | keychain="$RUNNER_TEMP/signing.keychain-db" echo "$CERT" | base64 --decode > "$RUNNER_TEMP/cert.p12" security create-keychain -p "" "$keychain" security unlock-keychain -p "" "$keychain" security import "$RUNNER_TEMP/cert.p12" -P "$CERT_PASSWORD" -A -t cert -f pkcs12 -k "$keychain" security set-key-partition-list -S apple-tool:,apple: -k "" "$keychain" >/dev/null security list-keychains -d user -s "$keychain" codesign --force --deep --options runtime --timestamp --sign "$IDENTITY" dist/Shop.app ditto -c -k --keepParent dist/Shop.app notarize.zip xcrun notarytool submit notarize.zip --wait \ --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_PASSWORD" xcrun stapler staple dist/Shop.app ``` Put the signing step **after** `uv run pyzed build` and **before** zipping. See [Windows](https://albakore.github.io/pyzed/packaging/windows/#sign-it) and [macOS](https://albakore.github.io/pyzed/packaging/macos/#sign-and-notarize) for what each command does. **How Pyzed Design is released:** The workflow that builds Pyzed Design itself, [`.github/workflows/release.yml`](https://github.com/albakore/pyzed/blob/main/.github/workflows/release.yml), is a complete real-world example: five platforms, optional signing and notarization, an Inno Setup installer on Windows, a `.dmg` on macOS, `SHA256SUMS`, build provenance attestations and a GitHub Release with notes. ### Self-updating apps > Let a packaged Pyzed app update itself from GitHub Releases with pyzed.updater: check for a newer version, download it with a SHA-256 check, swap the app folder on restart, and preview channels. Source: https://albakore.github.io/pyzed/packaging/updates/ `pyzed.updater` lets an app built with `pyzed build` update itself from the **GitHub Releases** of its repository: it finds the newest release, downloads the build for this system, checks it, and swaps the app folder for the new one when the app restarts. Pyzed Design updates itself with the same module. ```mermaid sequenceDiagram participant App as Your app (v1.0.0) participant GH as GitHub Releases App->>GH: latest("acme/shop") GH-->>App: v1.1.0 + shop-windows-x86_64.zip (and its SHA-256) App->>GH: download(asset) Note over App: checks the SHA-256, unpacks the zip App->>App: replace_on_exit(new folder), then quit Note over App: a small script waits, swaps the folders and starts v1.1.0 ``` #### What a release must contain 1. **A version tag** such as `v1.1.0` (pre-releases: `v1.2.0-rc.1`, `v1.2.0-beta.2`...). 2. **One zip per system**, named `--.zip`: | System | Example | | --- | --- | | Windows | `shop-windows-x86_64.zip` | | macOS | `shop-macos-arm64.zip`, `shop-macos-x86_64.zip` | | Linux | `shop-linux-x86_64.zip`, `shop-linux-aarch64.zip` | `updater.asset_name("shop")` returns the name for the machine it runs on. 3. **Inside each zip, exactly one entry**: the app folder as `pyzed build` made it (`Shop/`), or `Shop.app` on macOS (zip it with `ditto -c -k --keepParent` to keep the bundle's signature). The [CI page](https://albakore.github.io/pyzed/packaging/ci/#publish-a-github-release) has a workflow that builds and uploads these zips. **Every download is checked:** `updater.download` only installs a file whose **SHA-256** matches the one GitHub publishes for the release asset. If the checksum is missing or different, it raises `UpdateError` and deletes the file, so a corrupted or tampered download is never installed. #### Check, download, restart The functions block while they talk to the network, so run them off the UI thread, for example with `asyncio.to_thread` in a coroutine (see [Async](https://albakore.github.io/pyzed/guides/async/)). A complete app: ```python title="src/shop/app.py" import asyncio import os import pyzed as pz from pyzed import ui, updater __version__ = "1.0.0" # keep it in sync with pyproject.toml REPO = "acme/shop" # the GitHub repository with the releases APP = "shop" # the part of the zip names class Shop(pz.App): title = "Shop" def __init__(self): self.release = None # a newer release, when there is one self.ready = None # the downloaded new version, ready to swap in self.status = "" def on_start(self): if updater.is_frozen(): # only a packaged app can replace itself return self.check() async def check(self): try: release = await asyncio.to_thread(updater.latest, REPO) except updater.UpdateError as err: return ("status", f"Could not check for updates: {err}") if updater.is_newer(release.version, __version__) and release.asset_for(updater.asset_name(APP)): return ("found", release) return ("status", "You have the latest version.") async def download(self): asset = self.release.asset_for(updater.asset_name(APP)) self.status = f"Downloading {self.release.version}…" try: archive = await asyncio.to_thread(updater.download, asset) bundle = await asyncio.to_thread(updater.extract, archive) except updater.UpdateError as err: return ("status", f"The update failed: {err}") return ("ready", bundle) def restart(self): updater.replace_on_exit(self.ready) # swaps the folders once this process ends... os._exit(0) # ...so end it now; the new version starts by itself def update(self, message): kind, value = message if kind == "found": self.release, self.status = value, f"Version {value.version} is available." elif kind == "ready": self.ready, self.status = value, "Restart to finish the update." else: self.status = value def view(self): actions = [] if self.ready: actions.append(ui.button("Restart to update", self.restart, variant="solid")) elif self.release: actions.append(ui.button("Download and install", self.download, variant="solid")) return pz.column([ui.heading("Shop"), ui.muted(f"Version {__version__}"), ui.label(self.status), *actions], spacing=10, padding=24) ``` #### The functions | Function | What it does | | --- | --- | | `latest(repo, prerelease=False)` | The newest `Release` of `"owner/name"`. With `prerelease=True`, pre-releases count too (a "preview channel"). | | `release(repo, version)` | The release of one version (`"1.1.0"` or `"v1.1.0"`). | | `is_newer(candidate, current)` | Compares versions; a final release is newer than its pre-releases (`1.2.0` > `1.2.0-rc.1`). | | `asset_name(app)` | `"--.zip"` for this machine. | | `Release.asset_for(name)` | The release file with that name, or `None` if the release has no build for this system. | | `download(asset, folder=None, progress=None)` | Downloads to `folder` (a temporary one by default), checks the SHA-256 and returns the path. `progress` receives the fraction done, from 0 to 1. A file already in `folder` with the right SHA-256 is reused. | | `extract(archive)` | Unpacks the zip and returns the app inside it. | | `replace_on_exit(bundle, relaunch=True, args=(), keep=())` | Starts a small script that waits for the app to exit, swaps the old folder for `bundle` (restoring the old one if that fails) and starts the new version. `keep` lists files to carry over, such as an installer's uninstaller (`"unins*"`). Stop the app's child processes first: on Windows a folder still in use cannot move (the script retries for about 30 seconds, then starts the old version again). | | `is_frozen()` | Whether this is a packaged app. | | `install_dir()` | The folder (or `.app`) of the running app, `None` when running from Python. | | `can_update_in_place()` | Whether the app is packaged and its folder is writable, so `replace_on_exit` can work. | A `Release` has `version`, `tag`, `notes` (the release notes, Markdown: show them with [`ui.markdown`](https://albakore.github.io/pyzed/library/markdown/)), `url`, `published`, `prerelease` and `assets`. Errors are `UpdateError` with a readable message ("could not reach GitHub", "rate limit"...): network problems never crash the app. #### Things to know - **Where the app lives matters.** The app can only replace a folder it can write to. Check `updater.can_update_in_place()` before offering the update; if it is `False` (for example an app in `C:\Program Files`), point people to the download page instead. Per-user installs (`%LOCALAPPDATA%\Programs` on Windows, `~/.local/share` on Linux, `/Applications` for admin users on macOS) work. - **Folder builds only.** A `--onefile` build cannot update itself this way. - **Signed apps stay signed** on macOS as long as the zip was made with `ditto` from a signed (and notarized) `.app`. - **GitHub's API is rate limited** to 60 requests per hour per IP address without a token: check once a day, not on every start. **Remembering checks:** `updater.load_state()`, `save_state()` and `due()` keep their data in one file shared by every Pyzed app on the machine (Pyzed's own settings folder), including Pyzed Design. For your app, remember the time of the last check in your own settings instead. **Other update sources:** The updater reads GitHub Releases only. Updating from your own server, an app store or a package manager is not supported yet. ### Troubleshooting builds > Fix common problems when packaging a Pyzed app: missing modules or files, blank windows, blocked apps on macOS and Windows, glibc errors on Linux, large builds and update errors. Source: https://albakore.github.io/pyzed/packaging/troubleshooting/ #### First, see the error A packaged app has no terminal, so errors are invisible. Build it with a console and start it from a terminal to read the traceback: **Windows:** ```powershell uv run pyzed build --console .\dist\Shop\Shop.exe ``` **macOS:** ```bash uv run pyzed build --console ./dist/Shop.app/Contents/MacOS/Shop # the executable inside the bundle ``` **Linux:** ```bash uv run pyzed build --console ./dist/Shop/Shop ``` `pyzed build --dry-run` prints the PyInstaller command it would run, which helps to see what is included. #### While building | Symptom | Cause and fix | | --- | --- | | `uv: command not found`, or `uv` is not recognized | `pyzed build` runs PyInstaller and Nuitka through uv. Install it: `pip install uv`, or see [docs.astral.sh/uv](https://docs.astral.sh/uv/). | | `ui/main.pzform does not exist; set [tool.pyzed] main in pyproject.toml` | The main form moved or was renamed. Set `main = "ui/.pzform"` under `[tool.pyzed]`. | | `uv sync --locked` fails in CI | `uv.lock` is out of date: run `uv lock` (or `pyzed sync`) and commit it. | | The build is very slow the first time | uv downloads PyInstaller and PyInstaller analyzes every dependency; later builds reuse uv's cache. Nuitka compiles everything to C, which takes minutes. | | Nuitka: no C compiler found | Install one: `sudo apt install gcc` (Linux), `xcode-select --install` (macOS), Visual Studio Build Tools (Windows), or let Nuitka download MinGW when it asks. | | *PyInstaller's early splash draws with Tk, which the project's Python lacks* | The project's Python has no `tkinter`, so only the Pyzed splash shows. Use a Python from `uv python install`, or install `python3-tk`. | #### When the app starts | Symptom | Cause and fix | | --- | --- | | `ModuleNotFoundError` | A module imported by name or with `importlib`: add it to `hidden-imports`, or its package to `collect`. | | A data file of a dependency is missing (`FileNotFoundError` inside a package) | Add the package to `collect` (ships its data files and binaries too). | | Your own files are missing | Add them to `include` and resolve them from `sys._MEIPASS` ([How building works](https://albakore.github.io/pyzed/packaging/build/#finding-your-data-files)). | | The window is blank or the app closes at once on a virtual machine or remote desktop | No usable GPU. Start it with the CPU renderer: `ICED_BACKEND=tiny-skia ./Shop` (PowerShell: `$env:ICED_BACKEND="tiny-skia"; .\Shop.exe`). | | A `--onefile` app is slow to start | It unpacks itself to a temporary folder on every start. Use the folder build. | | Settings or files the app saved disappear after an update | They were written inside the app folder, which an update replaces. Save user data in the user's folders. | #### On each system | Symptom | Cause and fix | | --- | --- | | **macOS:** "Shop is damaged and can't be opened" or "cannot be opened because the developer cannot be verified" | The app is not signed and notarized. [Sign and notarize it](https://albakore.github.io/pyzed/packaging/macos/#sign-and-notarize). For your own testing only: `xattr -dr com.apple.quarantine Shop.app`. | | **macOS:** the app opens and closes immediately | Run the executable inside the bundle from a terminal (above) to see the error. | | **Windows:** "Windows protected your PC" | SmartScreen warns about unsigned apps. [Sign the .exe](https://albakore.github.io/pyzed/packaging/windows/#sign-it); people can still use **More info → Run anyway**. | | **Windows:** the antivirus deletes or blocks the app | A false positive, more common with `--onefile`. Use the folder build, sign it and report it to the vendor. | | **Linux:** `version 'GLIBC_2.xx' not found` | The app was built on a newer distribution than the one running it. [Build on an older one](https://albakore.github.io/pyzed/packaging/linux/#which-distribution-to-build-on). | | **Linux:** errors about `libxkbcommon`, X11 or Wayland | The machine has no desktop libraries (a minimal server image). Install the desktop session or `libxkbcommon`. | | **Linux:** `Permission denied` when starting it | The executable bit was lost (for example, zipped on Windows): `chmod +x Shop/Shop`. | #### Size | Symptom | Fix | | --- | --- | | The app is large | Keep build-only tools in the dev group (`pyzed add --dev ...`), keep `slim = true`, leave out modules you do not use with `exclude = [...]`, and avoid `collect` on big packages when `hidden-imports` is enough. A single file (`onefile`) is compressed. | | The zip is larger than expected | Do not include `build/`, `.venv/` or test data in `include`. | #### Self-updates | Message | Meaning | | --- | --- | | `could not reach GitHub` | No network, or a proxy blocks GitHub. Try again later. | | `GitHub refused the request (rate limit)` | More than 60 requests in an hour from this IP address. Check less often. | | `no release found` | The repository has no published release (drafts do not count), or the name is wrong. | | `the release does not publish a SHA-256 for this file` | The download cannot be checked, so it is not installed. Upload the file to the release again. | | `the download is corrupt` | The file does not match its SHA-256. Try again. | | `the update does not contain exactly one app` | The zip must hold one folder (or one `.app`) at its top level. | | `only a packaged app can replace itself` | The app runs from Python: update with `pip` or `uv` instead. | See [Self-updating apps](https://albakore.github.io/pyzed/packaging/updates/) for how releases must be laid out. **Still stuck?:** Search the [issues](https://github.com/albakore/pyzed/issues) or open one with your system, the build command, the output of `pyzed build --dry-run` and the traceback from the console build. ## Showcase ### Showcase > A gallery of Python desktop apps made with Pyzed: utilities, forms, games, GPU shaders and effects, and dashboards, each with an explanation, the full code and a command to run it. Source: https://albakore.github.io/pyzed/showcase/ Everything on these pages is a real program in the [`examples`](https://github.com/albakore/pyzed/tree/main/examples) folder of the repository, drawn by Pyzed with the GPU. Click a card to see how it is made, read the full code and run it. Want to build one yourself, step by step? The [Tutorials](https://albakore.github.io/pyzed/tutorials/) take you there. **Run any of them:** Download or clone the [repository](https://github.com/albakore/pyzed), open a terminal in its folder and run the file. Every page has a block like this one, with the commands for uv, pip and plain Python on Windows, macOS and Linux: #### Apps and utilities Small, complete programs: viewers, timers, live data, an AI chat and a code editor workspace. [See them all](https://albakore.github.io/pyzed/showcase/apps/). #### Forms Sign-in, sign-up, settings and wizards, drawn in Pyzed Design or written in code. [See them all](https://albakore.github.io/pyzed/showcase/forms/). #### Games Real-time 2D games with a frame loop, held keys, particles and a HUD made of regular widgets. [How games work](https://albakore.github.io/pyzed/games/). #### Shaders and effects GPU shaders made of nodes, and glass, bloom, blur and animations on any widget. [See them all](https://albakore.github.io/pyzed/showcase/shaders/). #### Dashboards and data Admin panels, tables, boards and media apps. [See them all](https://albakore.github.io/pyzed/showcase/dashboards/). **About this section:** The Showcase grows with every release. All the programs here are drawn off-screen by the test suite, so they run with the current version of Pyzed; short recordings of the ones that move are being added. Made something with Pyzed you would like to show? Tell us on [GitHub](https://github.com/albakore/pyzed). ### Apps and utilities > Small, complete Python desktop apps made with Pyzed: a to-do list, Markdown and image viewers, a clock, a stopwatch, a QR generator, live data, an AI chat, panes, several windows and a code editor workspace. Source: https://albakore.github.io/pyzed/showcase/apps/ Everyday programs, each a single Python file in [`examples/`](https://github.com/albakore/pyzed/tree/main/examples) that runs as it is. They are short on purpose: read one from top to bottom and you will have seen state, a view, events and a subscription working together. Several are Pyzed versions of the classic [Iced](https://iced.rs) demos, the Rust GUI library Pyzed draws with. **How to read these apps:** Every app is a class with **state** (attributes set in `__init__`), a **`view()`** that builds the screen from the state, and **methods** that change the state when something happens. Pyzed redraws after each change. New to that idea? Start with the [To-do list tutorial](https://albakore.github.io/pyzed/tutorials/basic/todo-app/). #### Todos Add, edit, complete, filter and delete tasks; they are saved as JSON in your user folder, so they are still there the next time you open the app. - A dataclass per task and a list in the state; the filter is a `ui.segmented` control. - Double-click a task to edit it in place; tasks are written to disk on every change. - A simpler version is built step by step in the [To-do list tutorial](https://albakore.github.io/pyzed/tutorials/basic/todo-app/).
Full code: examples/todos.py `examples/todos.py`: ```py """Todos, after Iced's ``todos`` demo: add, edit, complete, filter and persist tasks. python examples/todos.py Tasks are saved as JSON next to your user config, so they survive restarts. """ import json from dataclasses import asdict, dataclass, field from itertools import count from pathlib import Path import pyzed as pz from pyzed import ui STORE = Path.home() / ".config" / "pyzed-todos.json" _ids = count() @dataclass class Task: text: str done: bool = False id: int = field(default_factory=lambda: next(_ids)) class Todos(pz.App): title = "Todos" size = (560, 680) def __init__(self, store=STORE): self.store = store self.draft = "" self.filter = "All" self.editing = None # (task id, edited text) self.tasks = self.load() # -- persistence -------------------------------------------------------- def load(self): try: return [Task(t["text"], t["done"]) for t in json.loads(self.store.read_text())] except (OSError, ValueError, KeyError, TypeError): return [Task("Read the Pyzed examples"), Task("Build something nice", done=True)] def save(self): try: self.store.parent.mkdir(parents=True, exist_ok=True) self.store.write_text(json.dumps([{"text": t.text, "done": t.done} for t in self.tasks], indent=2)) except OSError: pass # -- actions -------------------------------------------------------------- def add(self): if self.draft.strip(): self.tasks.append(Task(self.draft.strip())) self.draft = "" self.save() def toggle(self, task): task.done = not task.done self.save() def remove(self, task): self.tasks.remove(task) self.save() def start_edit(self, task): self.editing = (task.id, task.text) return pz.Command.focus(f"edit-{task.id}") def finish_edit(self): task_id, text = self.editing for task in self.tasks: if task.id == task_id and text.strip(): task.text = text.strip() self.editing = None self.save() def clear_done(self): self.tasks = [t for t in self.tasks if not t.done] self.save() def on_key(self, combo): if combo == "escape": self.editing = None # -- view ------------------------------------------------------------------- def visible(self): if self.filter == "Active": return [t for t in self.tasks if not t.done] if self.filter == "Completed": return [t for t in self.tasks if t.done] return self.tasks def task_row(self, task): t = self.resolved_theme() if self.editing and self.editing[0] == task.id: return pz.row([ ui.input(self.editing[1], lambda text: setattr(self, "editing", (task.id, text)), on_submit=self.finish_edit, id=f"edit-{task.id}"), ui.icon_button("check", self.finish_edit, tooltip="Save"), ui.icon_button("trash-2", lambda: self.remove(task), tooltip="Delete", color=t.error), ], spacing=6, align="center") return pz.row([ ui.checkbox(task.done, lambda _: self.toggle(task)), pz.mouse_area( ui.label(task.text, color="text_muted" if task.done else "text", width="fill"), on_double_click=lambda: self.start_edit(task), ), ui.icon_button("pencil", lambda: self.start_edit(task), tooltip="Edit"), ], spacing=10, align="center") def view(self): t = self.resolved_theme() left = sum(not task.done for task in self.tasks) tasks = self.visible() body = (pz.column([self.task_row(task) for task in tasks], spacing=6) if tasks else ui.empty_state("Nothing here", f"No {self.filter.lower()} tasks.")) return pz.container( pz.column([ ui.heading("todos"), ui.input(self.draft, lambda text: setattr(self, "draft", text), on_submit=self.add, placeholder="What needs to be done?", icon="plus"), pz.row([ ui.muted(f"{left} {'task' if left == 1 else 'tasks'} left"), pz.hspace(), ui.segmented(["All", "Active", "Completed"], self.filter, lambda f: setattr(self, "filter", f)), ], align="center"), pz.scrollable(body, height="fill"), pz.row([pz.hspace(), ui.button("Clear completed", self.clear_done, variant="ghost", size="sm")]), ], spacing=14, max_width=520), padding=24, width="fill", height="fill", align_x="center", bg=t.editor, ) if __name__ == "__main__": pz.run(Todos) ```
#### Markdown viewer A live Markdown editor: write on the left, see the formatted result on the right, and drag the divider between them. - `ui.editor` with `on_change` keeps the text in the state; `ui.markdown` renders it. - `ui.split` makes the two panes resizable; links in the preview open in your browser.
Full code: examples/markdown_viewer.py `examples/markdown_viewer.py`: ```py """A live Markdown editor: code editor on the left, rendered preview on the right. python examples/markdown_viewer.py Shows ``ui.editor`` with ``on_change``, ``ui.markdown`` and a draggable ``ui.split``. Links in the preview open in your browser. """ import webbrowser import pyzed as pz from pyzed import ui SAMPLE = """# Pyzed Build **fast** desktop apps in *Python*, rendered by [Iced](https://iced.rs). ## Features - Zed-style components in `pyzed.ui` - Themes, including the ones from [Zed](https://zed.dev) - `async def` everywhere ```python import pyzed as pz class Hello(pz.App): def view(self): return pz.text("Hello!") ``` > Tip: drag the divider to resize the panes. """ class MarkdownViewer(pz.App): title = "Markdown" size = (1100, 720) def __init__(self): self.source = SAMPLE self.ratio = 0.5 self.wrap = True def view(self): t = self.resolved_theme() editor = pz.column([ pz.container( pz.row([ui.label("Markdown", size="sm", color="text_muted"), pz.hspace(), ui.checkbox(self.wrap, lambda v: setattr(self, "wrap", v), label="Wrap")], align="center"), padding=(6, 12), bg=t.tab_bar, width="fill", ), ui.editor("source", self.source, language="md", wrap=self.wrap, on_change=lambda text: setattr(self, "source", text)), ]) preview = pz.container( pz.scrollable(pz.container(ui.markdown(self.source, on_link=webbrowser.open), padding=24, max_width=760), height="fill"), width="fill", height="fill", bg=t.background, ) return ui.split(editor, preview, ratio=self.ratio, on_resize=lambda r: setattr(self, "ratio", r)) if __name__ == "__main__": pz.run(MarkdownViewer) ```
#### Image viewer Shows a picture with `pz.image` and in a zoomable `pz.image_viewer`: scroll to zoom, drag to pan. Pass a path to open your own image; without one it draws a picture from raw pixels made in pure Python.
Full code: examples/image_viewer.py `examples/image_viewer.py`: ```py """Bitmap images: ``pz.image`` and the zoomable ``pz.image_viewer``. python examples/image_viewer.py [picture.png] Without an argument it shows a picture generated in pure Python (raw RGBA pixels passed with ``size=(w, h)``). Scroll to zoom the viewer and drag to pan. """ import math import sys import pyzed as pz from pyzed import ui W, H = 480, 320 def generated_pixels(): """A colorful plasma pattern as raw RGBA bytes.""" out = bytearray() for y in range(H): for x in range(W): v = math.sin(x / 23) + math.sin(y / 17) + math.sin((x + y) / 31) + math.sin(math.hypot(x - W / 2, y - H / 2) / 19) out += bytes((int(128 + 127 * math.sin(v * math.pi)), int(128 + 127 * math.sin(v * math.pi + 2)), int(128 + 127 * math.sin(v * math.pi + 4)), 255)) return bytes(out) class Viewer(pz.App): title = "Image viewer" size = (1000, 640) def __init__(self, path=None): self.path = path self.pixels = None if path else generated_pixels() self.fit = "contain" self.radius = 12.0 self.opacity = 1.0 def source(self, **kwargs): if self.path: return dict(source=self.path, **kwargs) return dict(source=self.pixels, size=(W, H), **kwargs) def view(self): t = self.resolved_theme() thumbs = pz.column([ ui.label("pz.image", weight="semibold"), ui.field("Fit", ui.select(["contain", "cover", "fill", "none", "scale_down"], self.fit, lambda v: setattr(self, "fit", v), width=130)), ui.field("Radius", pz.container(ui.slider(self.radius, lambda v: setattr(self, "radius", v), min=0, max=60), width=130)), ui.field("Opacity", pz.container(ui.slider(self.opacity, lambda v: setattr(self, "opacity", v)), width=130)), pz.container(pz.image(**self.source(width="fill", height=200, content_fit=self.fit, radius=self.radius, opacity=self.opacity)), width="fill", height=200, bg=t.surface, radius=self.radius), pz.row([pz.image(**self.source(width=64, height=64, content_fit="cover", radius=32)), pz.image(**self.source(width=64, height=64, content_fit="cover", radius=8, rotation=15)), pz.image(**self.source(width=64, height=64, content_fit="cover", filter="nearest", scale=2))], spacing=12), ], spacing=12, width=320) viewer = pz.column([ ui.label("pz.image_viewer (scroll to zoom, drag to pan)", weight="semibold"), pz.container(pz.image_viewer(**self.source(width="fill", height="fill")), width="fill", height="fill", bg=t.surface, radius=t.radius, clip=True), ], spacing=12, width="fill") return pz.container(pz.row([thumbs, viewer], spacing=20), padding=20, bg=t.background, width="fill", height="fill") if __name__ == "__main__": pz.run(Viewer(sys.argv[1] if len(sys.argv) > 1 else None)) ```
#### Clock An analog clock drawn on a `pz.canvas`. A subscription, `pz.every(1, ...)`, redraws it every second, and the hands are paths rotated around the center.
Full code: examples/clock.py `examples/clock.py`: ```py """An analog clock drawn on a canvas, after Iced's ``clock`` demo. python examples/clock.py ``pz.every(1, ...)`` redraws it every second; the hands are rotated paths. """ import math import time import pyzed as pz from pyzed import draw SIZE = 320 def hand(cx, cy, angle, length, width, color): a = math.radians(angle - 90) return draw.line(cx, cy, cx + length * math.cos(a), cy + length * math.sin(a), stroke=color, width=width, cap="round") class Clock(pz.App): title = "Clock" size = (SIZE + 40, SIZE + 80) def __init__(self): self.now = time.localtime() def subscriptions(self): return [pz.every(1.0, lambda: setattr(self, "now", time.localtime()))] def view(self): t = self.resolved_theme() c = SIZE / 2 r = c - 8 shapes = [draw.circle(c, c, r, fill=t.surface, stroke=t.border, width=2)] for i in range(60): a = math.radians(i * 6) outer, inner = r - 6, r - (18 if i % 5 == 0 else 11) shapes.append(draw.line(c + inner * math.sin(a), c - inner * math.cos(a), c + outer * math.sin(a), c - outer * math.cos(a), stroke=t.text if i % 5 == 0 else t.text_muted, width=2.5 if i % 5 == 0 else 1)) for h in range(1, 13): a = math.radians(h * 30) shapes.append(draw.text(c + (r - 38) * math.sin(a), c - (r - 38) * math.cos(a), h, color=t.text, size=16, align_x="center", align_y="center")) hours, minutes, seconds = self.now.tm_hour % 12, self.now.tm_min, self.now.tm_sec shapes += [ hand(c, c, hours * 30 + minutes / 2, r * 0.5, 6, t.text), hand(c, c, minutes * 6 + seconds / 10, r * 0.75, 4, t.text), hand(c, c, seconds * 6, r * 0.82, 2, t.error), draw.circle(c, c, 5, fill=t.error), ] return pz.container( pz.column([pz.canvas(shapes, width=SIZE, height=SIZE), pz.text(time.strftime("%H:%M:%S", self.now), size=20, font="mono", color=t.text)], spacing=12, align="center"), center=True, bg=t.background, ) if __name__ == "__main__": pz.run(Clock) ```
#### Stopwatch A stopwatch with laps. Space starts and stops it, L records a lap and R resets it. - The timer subscription only exists **while the watch runs**: `subscriptions()` returns an empty list otherwise, so an idle app uses no CPU.
Full code: examples/stopwatch.py `examples/stopwatch.py`: ```py """A stopwatch with laps, after Iced's ``stopwatch`` demo. python examples/stopwatch.py The timer subscription only exists while the watch runs, so an idle app uses no CPU. Space starts/stops, ``l`` records a lap, ``r`` resets. """ import time import pyzed as pz from pyzed import ui def fmt(seconds): minutes, rest = divmod(seconds, 60) return f"{int(minutes):02d}:{rest:05.2f}" class Stopwatch(pz.App): title = "Stopwatch" size = (420, 520) def __init__(self): self.elapsed = 0.0 self.started = None self.laps = [] @property def running(self): return self.started is not None def now(self): return self.elapsed + (time.monotonic() - self.started if self.running else 0.0) def toggle(self): if self.running: self.elapsed = self.now() self.started = None else: self.started = time.monotonic() def reset(self): self.elapsed, self.started, self.laps = 0.0, None, [] def lap(self): if self.running: self.laps.insert(0, self.now()) def subscriptions(self): # An empty message still triggers a redraw; ``now()`` does the rest. return [pz.every(0.03, lambda: None)] if self.running else [] def on_key(self, combo): {"space": self.toggle, "l": self.lap, "r": self.reset}.get(combo, lambda: None)() def view(self): t = self.resolved_theme() laps = [ pz.row([ui.muted(f"Lap {len(self.laps) - i}"), pz.hspace(), pz.text(fmt(v), font="mono", color=t.text)]) for i, v in enumerate(self.laps) ] return pz.container( pz.column([ pz.text(fmt(self.now()), size=56, font="mono", color=t.text), pz.row([ ui.button("Stop" if self.running else "Start", self.toggle, variant="solid", tint="error" if self.running else "success", keys="space"), ui.button("Lap", self.lap, disabled=not self.running, keys="l"), ui.button("Reset", self.reset, variant="ghost", keys="r"), ], spacing=8), pz.scrollable(pz.column(laps, spacing=6, width="fill"), height="fill"), ], spacing=20, align="center", max_width=340), padding=28, width="fill", height="fill", align_x="center", bg=t.editor, ) if __name__ == "__main__": pz.run(Stopwatch) ```
#### QR code Type a text or a link and get its QR code, in the theme's colors or in black and white. The whole app is about 30 lines: `pz.qr_code(data, cell_size=7)` does the work.
Full code: examples/qr_code.py `examples/qr_code.py`: ```py """Type something and get its QR code, after Iced's ``qr_code`` demo.""" import pyzed as pz from pyzed import ui class QR(pz.App): title = "QR code" size = (420, 520) def __init__(self): self.data = "https://github.com/albakore/pyzed" self.dark = True def view(self): t = self.resolved_theme() fg, bg = (t.text, t.surface) if self.dark else ("#000000", "#ffffff") return pz.container( pz.column([ ui.heading("QR code", level=2), ui.input(self.data, lambda v: setattr(self, "data", v), placeholder="Text or URL"), ui.checkbox(self.dark, lambda v: setattr(self, "dark", v), label="Use theme colors"), pz.container(pz.qr_code(self.data, cell_size=7, color=fg, bg=bg) if self.data else ui.muted("Empty"), padding=16, bg=bg, radius=t.radius), ], spacing=14, align="center"), padding=24, width="fill", height="fill", align_x="center", bg=t.background, ) if __name__ == "__main__": pz.run(QR) ```
#### Pomodoro A focus timer with work and break periods: the app that the [Prompting](https://albakore.github.io/pyzed/ai/prompting/) guide builds with an AI assistant, one request at a time.
Full code: examples/pomodoro.py `examples/pomodoro.py`: ```py """A Pomodoro timer: the app built step by step in the AI prompting guide.""" import pyzed as pz from pyzed import ui WORK, BREAK = 25 * 60, 5 * 60 class Pomodoro(pz.App): title = "Pomodoro" size = (420, 380) def __init__(self): self.mode = "Focus" self.left = WORK self.running = False self.done = 0 def subscriptions(self): return [pz.every(1, self.tick, key="timer")] if self.running else [] def tick(self): self.left -= 1 if self.left <= 0: if self.mode == "Focus": self.done += 1 self.mode = "Break" if self.mode == "Focus" else "Focus" self.left = BREAK if self.mode == "Break" else WORK def switch(self, mode): self.mode, self.running = mode, False self.left = WORK if mode == "Focus" else BREAK def toggle(self): self.running = not self.running def reset(self): self.running, self.left = False, WORK if self.mode == "Focus" else BREAK def view(self): total = WORK if self.mode == "Focus" else BREAK return pz.center(pz.column([ ui.segmented(["Focus", "Break"], self.mode, self.switch), pz.text(f"{self.left // 60:02d}:{self.left % 60:02d}", size=64, weight="bold"), ui.progress(1 - self.left / total, height=8, width=280), pz.row([ ui.button("Pause" if self.running else "Start", self.toggle, icon="pause" if self.running else "play", variant="solid", size="lg"), ui.icon_button("rotate-ccw", self.reset, tooltip="Reset"), ], spacing=8, align="center"), ui.muted(f"{self.done} sessions today", size="sm"), ], spacing=18, align="center")).style(background="editor") if __name__ == "__main__": pz.run(Pomodoro) ```
#### Color picker `ui.ColorPicker` in its three modes: a color with transparency, a linear gradient, and a fill with an opacity mask. It reports a `"#rrggbbaa"` string, a `pz.Gradient` or a `pz.Paint`, and all three work as a `background`.
Full code: examples/color_picker.py `examples/color_picker.py`: ```py """Color picker: a color with alpha, a linear gradient, or a fill with an opacity mask. python examples/color_picker.py ``ui.ColorPicker`` reports a ``"#rrggbbaa"`` string, a ``pz.Gradient`` or a ``pz.Paint``; all of them work as a ``background`` in ``widget.style``. """ import pyzed as pz from pyzed import ui class ColorPickerDemo(pz.App): title = "Color picker" size = (640, 560) def __init__(self): self.fill = pz.Gradient(["#6366f1", "#ec4899"], angle=135) def pick(self, event): self.fill = event.value def view(self): preview = pz.container(None, width="fill", height="fill").style(background=self.fill) return pz.row([ ui.ColorPicker(value=self.fill, on_change=self.pick), pz.column([ pz.stack([ui.checkerboard(320, 360, 12), preview], width=320, height=360).style(border=(1, "border")), pz.text(repr(self.fill)).style(font="mono", size=12, color="text_muted"), ], spacing=10, width="fill"), ], spacing=20).style(padding=24, width="fill", height="fill") if __name__ == "__main__": pz.run(ColorPickerDemo) ```
#### Game of Life Conway's Game of Life: click or drag to paint cells, Space to play or pause, N to advance one generation. Thousands of cells are drawn as a single shape on a `pz.canvas`, and `pz.every` drives the simulation.
Full code: examples/game_of_life.py `examples/game_of_life.py`: ```py """Conway's Game of Life, after Iced's ``game_of_life`` demo. python examples/game_of_life.py Click or drag on the grid to paint cells, Space to play/pause, ``n`` to step once. Shows ``pz.canvas`` drawing thousands of cells in one shape and ``pz.every`` driving the simulation. """ import random import pyzed as pz from pyzed import draw, ui CELL = 10 COLS, ROWS = 80, 56 def step(alive): counts = {} for x, y in alive: for dx in (-1, 0, 1): for dy in (-1, 0, 1): if dx or dy: key = ((x + dx) % COLS, (y + dy) % ROWS) counts[key] = counts.get(key, 0) + 1 return {cell for cell, n in counts.items() if n == 3 or (n == 2 and cell in alive)} class Life(pz.App): title = "Game of Life" size = (COLS * CELL + 48, ROWS * CELL + 120) def __init__(self): self.alive = set() self.playing = False self.speed = 10 self.generation = 0 self.painting = None # True paints cells, False erases them self.randomize() def randomize(self): self.alive = {(x, y) for x in range(COLS) for y in range(ROWS) if random.random() < 0.18} self.generation = 0 def clear(self): self.alive.clear() self.generation = 0 def tick(self): self.alive = step(self.alive) self.generation += 1 def subscriptions(self): return [pz.every(1 / self.speed, self.tick, key="life")] if self.playing else [] def on_key(self, combo): if combo == "space": self.playing = not self.playing elif combo == "n": self.tick() def cell_at(self, x, y): return int(x // CELL) % COLS, int(y // CELL) % ROWS def press(self, x, y): cell = self.cell_at(x, y) self.painting = cell not in self.alive self.paint(x, y) def paint(self, x, y): if self.painting is None: return cell = self.cell_at(x, y) (self.alive.add if self.painting else self.alive.discard)(cell) def view(self): t = self.resolved_theme() grid = [draw.background(t.editor)] grid.append(draw.rects(((x * CELL, y * CELL, CELL - 1, CELL - 1) for x, y in self.alive), fill=t.accent)) board = pz.canvas( grid, width=COLS * CELL, height=ROWS * CELL, on_press=self.press, on_move=self.paint, on_release=lambda: setattr(self, "painting", None), cursor="crosshair", ) controls = pz.row([ ui.button("Pause" if self.playing else "Play", lambda: setattr(self, "playing", not self.playing), icon="pause" if self.playing else "play", variant="solid", keys="space"), ui.button("Step", self.tick, icon="step-forward", disabled=self.playing), ui.button("Random", self.randomize, icon="shuffle"), ui.button("Clear", self.clear, icon="eraser"), pz.hspace(), ui.muted(f"Speed {self.speed}/s"), pz.container(ui.slider(self.speed, lambda v: setattr(self, "speed", int(v)), min=1, max=60, step=1), width=160), ui.muted(f"Generation {self.generation} · {len(self.alive)} alive"), ], spacing=8, align="center") return pz.container(pz.column([controls, board], spacing=12), padding=16, bg=t.background, width="fill", height="fill") if __name__ == "__main__": pz.run(Life) ```
#### Downloads with progress Several downloads at once, each with its own progress bar, and a window that never freezes. Each download is an `async def` generator: every value it `yield`s goes to `update` while it keeps running on Pyzed's event loop. Swap the simulated transfer for streaming with `httpx` to download real files.
Full code: examples/download_progress.py `examples/download_progress.py`: ```py """Concurrent downloads with live progress, after Iced's ``download_progress`` demo. python examples/download_progress.py Each download is an ``async def`` generator: every value it yields goes to ``update`` while it keeps running on Pyzed's asyncio loop, so the UI never blocks. Swap the simulated transfer for ``aiohttp``/``httpx`` streaming to download real files. """ import asyncio import random import pyzed as pz from pyzed import ui async def download(index, size): """Simulates a transfer that sometimes fails; yields ``(index, event, value)``.""" received = 0 while received < size: await asyncio.sleep(random.uniform(0.02, 0.08)) received = min(size, received + random.randint(20_000, 120_000)) if random.random() < 0.004: yield index, "failed", "connection reset" return yield index, "progress", received / size yield index, "finished", size class Download: def __init__(self, index): self.index = index self.size = random.randint(2, 12) * 1_000_000 self.state = "idle" # idle | running | finished | failed self.progress = 0.0 self.error = "" class Downloads(pz.App): title = "Downloads" size = (560, 520) def __init__(self): self.downloads = [Download(0)] def start(self, item): item.state, item.progress = "running", 0.0 # Returning an async generator runs it in the background. return download(item.index, item.size) def add(self): self.downloads.append(Download(len(self.downloads))) def start_all(self): return [pz.Command.perform(self.start(d)) for d in self.downloads if d.state != "running"] def update(self, message): index, event, value = message item = self.downloads[index] if event == "progress": item.progress = value elif event == "finished": item.state, item.progress = "finished", 1.0 elif event == "failed": item.state, item.error = "failed", value def row(self, item): t = self.resolved_theme() mb = item.size / 1_000_000 if item.state == "idle": status, action = ui.muted(f"{mb:.0f} MB"), ui.button("Start", lambda: self.start(item), size="sm") elif item.state == "running": status, action = ui.muted(f"{item.progress * mb:.1f} / {mb:.0f} MB"), ui.spinner(size=14) elif item.state == "finished": status = ui.label("Done", color="success", size="sm") action = ui.icon_button("rotate-ccw", lambda: self.start(item), tooltip="Again") else: status = ui.label(f"Failed: {item.error}", color="error", size="sm") action = ui.button("Retry", lambda: self.start(item), size="sm", tint="error") return ui.card(pz.column([ pz.row([ui.label(f"file-{item.index + 1}.bin", weight="medium"), pz.hspace(), status, action], spacing=10, align="center"), ui.progress(item.progress, tint="error" if item.state == "failed" else "accent", theme=t), ], spacing=8), padding=12) def subscriptions(self): # Keep the spinners turning while something is downloading. busy = any(d.state == "running" for d in self.downloads) return [pz.every(1 / 30, lambda: None)] if busy else [] def view(self): t = self.resolved_theme() return pz.container( pz.column([ pz.row([ui.heading("Downloads", level=2), pz.hspace(), ui.button("Add", self.add, icon="plus"), ui.button("Start all", self.start_all, icon="download", variant="solid")], spacing=8, align="center"), pz.scrollable(pz.column([self.row(d) for d in self.downloads], spacing=8), height="fill"), ], spacing=16), padding=24, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(Downloads) ```
The same idea with threads (for blocking code) is explained in the [Background tasks](https://albakore.github.io/pyzed/tutorials/intermediate/background-tasks/) tutorial. #### Live data A price ticker fed by `pz.listen`: a subscription backed by an async generator. It runs while `subscriptions()` returns it and sends every value it yields to `update`. ```python async def feed(): # a real app would read a websocket here prices = {s: random.uniform(50, 150) for s in SYMBOLS} while True: await asyncio.sleep(0.25) symbol = random.choice(SYMBOLS) prices[symbol] *= 1 + random.gauss(0, 0.01) yield symbol, prices[symbol] # each value goes to update() class Ticker(pz.App): def subscriptions(self): return [pz.listen(feed)] if self.connected else [] # the switch pauses the feed def update(self, message): symbol, price = message self.history[symbol] = (self.history[symbol] + [price])[-60:] ```
Full code: examples/websocket_stream.py `examples/websocket_stream.py`: ```py """Live data with ``pz.listen``: a subscription backed by an async generator. python examples/websocket_stream.py ``listen`` runs the generator for as long as it is returned from ``subscriptions()`` and sends every value it yields to ``update``. Here it simulates a price feed; replace the body with ``async for message in websocket:`` to consume a real socket. """ import asyncio import random import pyzed as pz from pyzed import ui SYMBOLS = ["PYZ", "ICE", "ZED", "RST"] async def feed(): prices = {s: random.uniform(50, 150) for s in SYMBOLS} while True: await asyncio.sleep(0.25) symbol = random.choice(SYMBOLS) prices[symbol] *= 1 + random.gauss(0, 0.01) yield symbol, prices[symbol] class Ticker(pz.App): title = "Live feed" size = (720, 460) def __init__(self): self.connected = True self.history = {s: [] for s in SYMBOLS} def subscriptions(self): return [pz.listen(feed)] if self.connected else [] def update(self, message): symbol, price = message self.history[symbol] = (self.history[symbol] + [price])[-60:] def view(self): t = self.resolved_theme() cards = [] for symbol, values in self.history.items(): if len(values) < 2: cards.append(ui.stat(symbol, "…", width="fill")) continue delta = (values[-1] / values[0] - 1) * 100 cards.append(ui.stat(symbol, f"{values[-1]:.2f}", delta=delta, trend=values, width="fill")) return pz.container( pz.column([ pz.row([ui.heading("Live feed", level=2), pz.hspace(), ui.switch(self.connected, lambda v: setattr(self, "connected", v)), ui.muted("connected" if self.connected else "paused")], spacing=8, align="center"), pz.row(cards[:2], spacing=12), pz.row(cards[2:], spacing=12), ], spacing=16), padding=24, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(Ticker) ```
#### AI chat A chat whose answers appear word by word. It talks to any OpenAI-compatible server (Ollama, LM Studio, OpenAI...) through `httpx`, and has a built-in demo mode with canned answers, so it runs offline. To use a real model, set three environment variables before running it, for example with [Ollama](https://ollama.com) on your machine: `CHAT_BASE_URL=http://localhost:11434/v1` and `CHAT_MODEL=llama3.2` (plus `CHAT_API_KEY` for a hosted service). The [AI section](https://albakore.github.io/pyzed/ai/) explains the rest.
Full code: examples/ai_chat.py `examples/ai_chat.py`: ```py """A streaming AI chat: any OpenAI-compatible server through httpx, with a built-in demo stream. python examples/ai_chat.py # demo: canned answers, no network CHAT_BASE_URL=http://localhost:11434/v1 CHAT_MODEL=llama3.2 python examples/ai_chat.py # Ollama CHAT_BASE_URL=https://api.openai.com/v1 CHAT_API_KEY=sk-... CHAT_MODEL= python examples/ai_chat.py The answer streams token by token: ``send`` returns an async generator, Pyzed runs it on its asyncio loop and every value it yields goes to ``update``, so the window keeps responding while the model writes. """ import asyncio import json import os import time import pyzed as pz from pyzed import ui BASE_URL = os.environ.get("CHAT_BASE_URL", "") # empty: demo mode API_KEY = os.environ.get("CHAT_API_KEY", "") MODEL = os.environ.get("CHAT_MODEL", "demo" if not BASE_URL else "llama3.2") SYSTEM = "You are a concise assistant inside a desktop app. Answer in Markdown." SUGGESTIONS = [ ("code", "Write a Python function that removes duplicates but keeps the order"), ("lightbulb", "Explain async/await like I'm new to Python"), ("table", "Compare SQLite, PostgreSQL and DuckDB in a table"), ] DEMO_ANSWER = """Sure! A `dict` keeps insertion order, so it does the work for you: ```python def unique(items): \"\"\"Removes duplicates and keeps the first occurrence of each item.\"\"\" return list(dict.fromkeys(items)) unique([3, 1, 3, 2, 1]) # [3, 1, 2] ``` - **O(n)**: one pass, no nested loops. - Works with any *hashable* item (numbers, strings, tuples). - For unhashable items such as lists, keep a `seen` list instead. """ # -- the model --------------------------------------------------------------------------------- async def openai_stream(messages, *, model=MODEL, base_url=BASE_URL, api_key=API_KEY): """Yields the answer's text pieces from an OpenAI-compatible ``/chat/completions`` endpoint.""" import httpx # only needed when talking to a real server headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} body = {"model": model, "messages": messages, "stream": True} async with httpx.AsyncClient(timeout=httpx.Timeout(60, connect=10)) as client: async with client.stream("POST", f"{base_url.rstrip('/')}/chat/completions", json=body, headers=headers) as response: if response.status_code >= 400: detail = (await response.aread()).decode(errors="replace")[:300] raise RuntimeError(f"HTTP {response.status_code}: {detail}") async for line in response.aiter_lines(): # server-sent events: "data: {...}" if not line.startswith("data:"): continue data = line[5:].strip() if data == "[DONE]": break delta = json.loads(data)["choices"][0].get("delta", {}).get("content") if delta: yield delta async def demo_stream(messages, *, delay=0.015): """A stand-in for the model: streams a canned answer a few characters at a time.""" text = DEMO_ANSWER for i in range(0, len(text), 4): await asyncio.sleep(delay) yield text[i:i + 4] def model_stream(messages): return demo_stream(messages) if not BASE_URL else openai_stream(messages) # -- the app ----------------------------------------------------------------------------------- class AIChat(pz.App): title = "AI chat" size = (980, 720) def __init__(self, stream=model_stream): self.stream = stream # a function: messages -> async iterator of text self.messages = [] # [{"role": "user" | "assistant", "content": str}] self.draft = "" self.busy = False self.error = "" self.turn = 0 # tags each reply, so pieces of a stopped one are ignored self.started = 0.0 self.tokens = 0 # -- actions -------------------------------------------------------------------------------- def send(self, text=None): text = (text if text is not None else self.draft).strip() if not text or self.busy: return None self.messages += [{"role": "user", "content": text}, {"role": "assistant", "content": ""}] self.draft, self.error, self.busy = "", "", True self.turn += 1 self.started, self.tokens = time.monotonic(), 0 history = [{"role": "system", "content": SYSTEM}] + self.messages[:-1] return [self.reply(self.turn, history), pz.Command.scroll_to_end("thread")] async def reply(self, turn, history): """Runs on Pyzed's asyncio loop; each yield goes to update() on the UI thread.""" try: async for piece in self.stream(history): if turn != self.turn: # stopped or superseded return yield ("delta", turn, piece) except Exception as error: # network errors, bad keys... shown in the thread yield ("error", turn, str(error) or type(error).__name__) yield ("done", turn, None) def stop(self): self.turn += 1 # the running reply sees a new turn and ends self.busy = False def clear(self): self.stop() self.messages, self.error = [], "" def update(self, message): kind, turn, value = message if turn != self.turn: return None if kind == "delta": self.messages[-1]["content"] += value self.tokens += 1 return pz.Command.scroll_to_end("thread") if kind == "error": self.error = value elif kind == "done": self.busy = False return None # -- view ----------------------------------------------------------------------------------- def bubble(self, index, message): last = index == len(self.messages) - 1 if message["role"] == "user": return pz.row([pz.hspace(), pz.container(ui.label(message["content"]), padding=(10, 14), max_width=560).style(background="element", radius=14)]) content = message["content"] if last and self.busy: content += " ▍" body = [ui.markdown(content) if content.strip() else pz.row([ui.spinner(size=14), ui.muted("Thinking…")], spacing=8, align="center")] if not (last and self.busy) and content: body.append(pz.row([ ui.icon_button("copy", pz.Command.copy(message["content"]), size="sm", tooltip="Copy"), ui.icon_button("refresh-cw", self.retry, size="sm", tooltip="Regenerate") if last else None, ui.icon_button("thumbs-up", None, size="sm"), ui.icon_button("thumbs-down", None, size="sm"), ], spacing=2)) avatar = pz.container(pz.icon("sparkles", size=15, color="#ffffff"), width=30, height=30, center=True).style( background=pz.Gradient(["#8b5cf6", "#ec4899"], angle=135), radius=15) return pz.row([avatar, pz.column(body, spacing=6, width="fill")], spacing=12) def retry(self): if self.messages and not self.busy: question = self.messages[-2]["content"] self.messages = self.messages[:-2] return self.send(question) return None def empty(self): cards = [pz.button(pz.row([pz.icon(icon, size=16), ui.label(text, size="sm")], spacing=10, align="center"), lambda text=text: self.send(text), width="fill", padding=(12, 14), radius=10, bg=self.resolved_theme().surface, hover_bg=self.resolved_theme().element_hover, border_color=self.resolved_theme().border_variant, border_width=1) for icon, text in SUGGESTIONS] return pz.container(pz.column([ pz.container(pz.icon("sparkles", size=26, color="#ffffff"), width=56, height=56, center=True).style( background=pz.Gradient(["#8b5cf6", "#ec4899"], angle=135), radius=16, glow=("#8b5cf6", 18)), ui.heading("How can I help?"), ui.muted(f"Model: {MODEL}" + (" · demo stream, no network" if not BASE_URL else "")), pz.space(height=8), pz.column(cards, spacing=8, width=480), ], spacing=10, align="center"), width="fill", height="fill", center=True) def composer(self): action = (ui.icon_button("square", self.stop, variant="solid", tooltip="Stop") if self.busy else ui.icon_button("arrow-up", self.send, variant="solid" if self.draft.strip() else "filled", tooltip="Send", disabled=not self.draft.strip())) return pz.container(pz.row([ ui.input(self.draft, lambda v: setattr(self, "draft", v), placeholder="Ask anything…", on_submit=self.send, borderless=True, id="prompt", size="lg"), action, ], spacing=8, align="center"), padding=(6, 8, 6, 12), max_width=760).style( background="surface", border=(1, "border"), radius=16, shadow=("#00000026", (0, 4), 16)) def view(self): speed = "" if self.tokens and self.busy: speed = f"{self.tokens / max(time.monotonic() - self.started, 0.001):.0f} chunks/s" header = ui.toolbar( [pz.icon("sparkles", size=14), ui.label("Assistant", weight="semibold"), ui.badge(MODEL, tint="info", variant="soft")], [ui.muted(speed, size="sm") if speed else None, ui.icon_button("trash-2", self.clear, size="sm", tooltip="New chat")], height=44) if self.messages: thread = pz.column([self.bubble(i, m) for i, m in enumerate(self.messages)], spacing=22, max_width=760, padding=(24, 24, 8, 24)) if self.error: thread = pz.column([thread, pz.container(ui.callout("The model failed", self.error, tint="error", icon="circle-alert"), max_width=760, padding=(0, 24))]) middle = pz.scrollable(pz.container(thread, width="fill", align_x="center"), height="fill", id="thread", anchor_bottom=True) else: middle = self.empty() return pz.column([ header, ui.divider(), middle, pz.container(self.composer(), width="fill", align_x="center", padding=(8, 24, 20, 24)), ], height="fill").style(background="editor") if __name__ == "__main__": pz.run(AIChat) ```
#### Several windows Open as many windows as you like: each `pz.Window` has its own view, title, size and theme, but all of them share the app's state, so the click counter is the same in every window. Closing the main window quits the app.
Full code: examples/multi_window.py `examples/multi_window.py`: ```py """Several windows in one app, after Iced's ``multi_window`` demo. python examples/multi_window.py Every ``pz.Window`` has its own view, title, size and theme, but they all share the app's state: the counter below is the same in every window. Closing the main window quits the app. """ import pyzed as pz from pyzed import ui class Note(pz.Window): size = (360, 260) def __init__(self, number, theme): self.number = number self.theme = theme self.title = f"Window {number}" self.text = "" def on_close(self): # The user clicked the window's close button. self.app.windows.remove(self) def dismiss(self): self.app.windows.remove(self) return self.close() def view(self): t = self.resolved_theme() return pz.container( pz.column([ ui.heading(self.title, level=3), ui.muted(f"Theme: {t.name}"), ui.input(self.text, lambda v: setattr(self, "text", v), placeholder="Type something…"), pz.row([ ui.button(f"Shared clicks: {self.app.clicks}", self.app.click, icon="mouse-pointer-click"), pz.hspace(), ui.button("Close", self.dismiss, variant="ghost"), ], align="center"), ], spacing=12), padding=20, width="fill", height="fill", bg=t.background, ) class Main(pz.App): title = "Multi window" size = (480, 360) def __init__(self): self.clicks = 0 self.windows = [] self.opened = 0 def click(self): self.clicks += 1 def open_window(self): self.opened += 1 themes = pz.themes.all() window = Note(self.opened, themes[self.opened % len(themes)]) self.windows.append(window) return window.open() def close_all(self): commands = [w.close() for w in self.windows] self.windows = [] return commands def view(self): t = self.resolved_theme() items = [ui.list_item(w.title, None, icon="app-window", end=ui.muted(w.text or "empty")) for w in self.windows] return pz.container( pz.column([ ui.heading("Windows", level=2), pz.row([ui.button("Open window", self.open_window, icon="plus", variant="solid"), ui.button("Close all", self.close_all, disabled=not self.windows), ui.button(f"Clicks: {self.clicks}", self.click)], spacing=8), pz.column(items) if items else ui.muted("No extra windows yet."), ], spacing=14), padding=24, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(Main) ```
#### Panes Resizable panes like an editor's: split any pane to the right or down, drag the dividers, close panes. The layout is a small tree of lists kept in the state and drawn with nested `ui.split`.
Full code: examples/panes.py `examples/panes.py`: ```py """Resizable panes, after Iced's ``pane_grid`` demo, using nested ``ui.split``. python examples/panes.py Drag the dividers; each pane can be split again or closed. The layout is a small tree of ``("h" | "v", ratio, first, second)`` tuples kept in your state. """ from itertools import count import pyzed as pz from pyzed import ui _ids = count(1) class Panes(pz.App): title = "Panes" size = (1000, 680) def __init__(self): self.layout = next(_ids) # a leaf is a pane number self.focus = self.layout def replace(self, node, target, new): if node == target: return new if isinstance(node, list): node[2] = self.replace(node[2], target, new) node[3] = self.replace(node[3], target, new) return node def remove(self, node, target): if isinstance(node, list): if node[2] == target: return node[3] if node[3] == target: return node[2] node[2] = self.remove(node[2], target) node[3] = self.remove(node[3], target) return node def split(self, pane, vertical): new = next(_ids) self.layout = self.replace(self.layout, pane, ["v" if vertical else "h", 0.5, pane, new]) self.focus = new def close(self, pane): if self.layout != pane: self.layout = self.remove(self.layout, pane) self.focus = None def pane(self, number): t = self.resolved_theme() focused = number == self.focus header = pz.container( pz.row([ ui.label(f"Pane {number}", weight="medium", color="text" if focused else "text_muted"), pz.hspace(), ui.icon_button("columns-2", lambda: self.split(number, False), size="sm", tooltip="Split right"), ui.icon_button("rows-2", lambda: self.split(number, True), size="sm", tooltip="Split down"), ui.icon_button("x", lambda: self.close(number), size="sm", tooltip="Close", disabled=self.layout == number), ], spacing=2, align="center"), padding=(4, 8), width="fill", bg=t.tab_active if focused else t.tab_inactive, ) body = pz.mouse_area(pz.center(ui.muted("Click to focus")), on_press=lambda: setattr(self, "focus", number)) return pz.container(pz.column([header, body]), width="fill", height="fill", bg=t.editor, border_color=t.border_focused if focused else t.border_variant, border_width=1) def render(self, node): if not isinstance(node, list): return self.pane(node) direction, ratio, first, second = node def resize(r, node=node): node[1] = r return ui.split(self.render(first), self.render(second), ratio=ratio, on_resize=resize, vertical=direction == "v", min_size=120) def view(self): return pz.container(self.render(self.layout), padding=8, width="fill", height="fill", bg=self.resolved_theme().background) if __name__ == "__main__": pz.run(Panes) ```
#### Context menu A right-click menu built from `ui.popover` and `ui.menu`. `mouse_area(on_move=...)` remembers where the pointer is, so the menu opens right there; clicking elsewhere or pressing Esc closes it.
Full code: examples/context_menu.py `examples/context_menu.py`: ```py """A right-click context menu built from ``ui.popover`` and ``ui.menu``. python examples/context_menu.py The pointer position is remembered with ``mouse_area(on_move=...)`` so the menu opens where you clicked. Clicking anywhere else (or pressing Escape) closes it. """ import pyzed as pz from pyzed import ui class ContextMenu(pz.App): title = "Context menu" size = (640, 420) def __init__(self): self.pointer = (0, 0) self.menu_at = None self.log = ["Right-click anywhere in the box."] self.soft_wrap = True def act(self, what): def run(): self.log.append(what) self.menu_at = None return run def toggle_wrap(self): self.soft_wrap = not self.soft_wrap self.menu_at = None def on_key(self, combo): if combo == "escape": self.menu_at = None def view(self): t = self.resolved_theme() area = pz.mouse_area( pz.container(pz.column([ui.muted(line) for line in self.log[-12:]], spacing=4), padding=16, width="fill", height="fill", bg=t.editor, border_color=t.border_variant, border_width=1, radius=t.radius), on_move=lambda x, y: setattr(self, "pointer", (x, y)), on_right_press=lambda: setattr(self, "menu_at", self.pointer), ) menu = None if self.menu_at: menu = ui.menu([ ui.MenuItem("Cut", self.act("cut"), icon="scissors", keys="ctrl-x"), ui.MenuItem("Copy", self.act("copy"), icon="copy", keys="ctrl-c"), ui.MenuItem("Paste", self.act("paste"), icon="clipboard", keys="ctrl-v"), ui.SEPARATOR, ui.MenuItem("Soft wrap", self.toggle_wrap, checked=self.soft_wrap), ui.SEPARATOR, ui.MenuItem("Delete", self.act("delete"), icon="trash-2", danger=True), ], width=220) x, y = self.menu_at or (0, 0) return pz.container( ui.popover(area, menu, x=x + 16, y=y + 16, on_dismiss=lambda: setattr(self, "menu_at", None)), padding=16, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(ContextMenu) ```
#### Custom components Three reusable pieces written in code: a star rating and a tag editor (`pz.Component` classes with properties, their own state and their own events), and a note you can drag, double-click and type on, which logs the common events every widget has.
Full code: examples/components.py `examples/components.py`: ```py """Custom components and events: a star rating, a tag editor and a draggable note. python examples/components.py - ``Rating`` and ``TagInput`` are ``pz.Component`` classes: props, their own state, their own events (``change``, ``add``, ``remove``), reusable anywhere. - The note uses the common events every widget has: drag it around, double-click to pin it, hover it, click it and press keys (they show up in the log). """ import pyzed as pz from pyzed import ui class Rating(pz.Component): """Stars the user can click; shows a preview while hovering.""" value: int = 0 max: int = 5 events = {"change": "A star was picked (``event.value`` is the new rating)."} icon = "star" size = (140, 28) def setup(self): self.hover = None # state: which star is under the pointer def render(self): t = pz.theme.current() shown = self.hover or self.value stars = [] for i in range(1, self.max + 1): color = t.warning if i <= shown else t.text_disabled stars.append( pz.icon("star", size=20, color=color).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), ) ) return pz.row(stars, spacing=2) class TagInput(pz.Component): """A list of tags with a box to add more.""" tags: list = () placeholder: str = "Add a tag…" events = {"add": "Enter was pressed (``event.value`` is the text).", "remove": "A tag's x was clicked (``event.value`` is the tag)."} icon = "tags" size = (280, 64) def setup(self): self.draft = "" def submit(self): text, self.draft = self.draft.strip(), "" return self.emit("add", text) if text else None def render(self): chips = [ui.chip(tag, on_remove=lambda tag=tag: self.emit("remove", tag)) for tag in self.tags] return pz.column([ pz.row(chips, spacing=6, wrap=True, width="fill") if chips else ui.label("No tags yet", color="text_muted", size="sm"), ui.input(self.draft, lambda text: setattr(self, "draft", text), placeholder=self.placeholder, on_submit=self.submit, icon="tag"), ], spacing=8) class Demo(pz.App): title = "Components and events" size = (720, 460) def __init__(self): self.rating = 3 self.tags = ["python", "rust"] self.note = (20, 40) self.pinned = False self.log = ["Drag the note, double-click it, click it and type."] def record(self, event): detail = event.key or (f"{event.dx:+.0f}, {event.dy:+.0f}" if event.type == "drag" else "") self.log = [f"{event.type} {detail}".strip(), *self.log][:9] def move(self, event): if not self.pinned: x, y = self.note self.note = (x + event.dx, y + event.dy) def view(self): t = self.resolved_theme() form = ui.card(pz.column([ ui.field("Rating", Rating(value=self.rating, on_change=lambda e: setattr(self, "rating", e.value))), ui.label(f"{self.rating} of 5", color="text_muted", size="sm"), ui.label("Tags"), TagInput( tags=self.tags, on_add=lambda e: setattr(self, "tags", [*self.tags, e.value]), on_remove=lambda e: setattr(self, "tags", [x for x in self.tags if x != e.value]), ), ], spacing=14), title="Components", width=320) note = pz.container( pz.column([ pz.row([pz.icon("pin" if self.pinned else "grip", size=14, color=t.text_muted), ui.label("Pinned" if self.pinned else "Drag me", weight="semibold")], spacing=6, align="center"), ui.label("Double-click to pin", size="sm", color="text_muted"), ], spacing=4), width=180, padding=12, bg=t.elevated, border_color=t.accent if self.pinned else t.border, border_width=1, radius=t.radius, ).on( drag=self.move, drag_end=self.record, double_click=lambda: setattr(self, "pinned", not self.pinned), click=self.record, key_down=self.record, focus=self.record, blur=self.record, cursor="pointer" if self.pinned else "grab", ) log = ui.card(pz.column([ui.label(line, mono=True, size="sm") for line in self.log], spacing=2), title="Event log", width=320) x, y = self.note canvas = pz.stack([ pz.container(width="fill", height="fill"), pz.pin(note, x=x, y=y), ], width="fill", height="fill") return pz.row([pz.column([form, log], spacing=16), canvas], spacing=24, padding=24) if __name__ == "__main__": pz.run(Demo) ```
To draw components with the mouse instead, follow [A custom component](https://albakore.github.io/pyzed/tutorials/intermediate/custom-component/). #### Component gallery A tour of the ready-made components in `pyzed.ui`: buttons, inputs, tabs, a code editor, menus, trees, notifications, forms, feedback and data, with a theme picker to see them all in any theme.
Full code: examples/gallery.py `examples/gallery.py`: ```py """A tour of the ready-made components in ``pyzed.ui``. python examples/gallery.py """ import pyzed as pz from pyzed import ui SNIPPET = '''from pyzed import ui def view(self): return ui.button("Deploy", self.deploy) ''' class Gallery(pz.App): title = "Pyzed components" size = (1440, 900) def __init__(self, theme=pz.themes.ONE_DARK): self.theme = theme self.name = "Pyzed" self.query = "" self.format_on_save = True self.vim_mode = False self.telemetry = True self.font_size = 0.55 self.view_mode = "Split" self.active_tab = "main.py" self.selected_file = "src/app.py" self.expanded = {"src"} self.language = "Python" self.size_choice = "Medium" self.tab_size = 4 self.tags = {"rust", "python", "ui"} self.advanced_open = True self.sort_by = "Size" self.descending = True self.selected_row = None def set(self, field): return lambda value: setattr(self, field, value) def section(self, title, *children, spacing=10): return ui.card(pz.column(list(children), spacing=spacing), title=title, width="fill") def view(self): t = self.theme buttons = self.section( "Buttons", pz.row([ ui.button("Filled", lambda: None), ui.button("Ghost", lambda: None, variant="ghost"), ui.button("Outline", lambda: None, variant="outline"), ui.button("Solid", lambda: None, variant="solid"), ui.button("Disabled", disabled=True), ], spacing=8), pz.row([ ui.button("Error", lambda: None, variant="error", icon="circle-x"), ui.button("Warning", lambda: None, variant="warning", icon="triangle-alert"), ui.button("Success", lambda: None, variant="success", icon="circle-check"), ui.button("Save", lambda: None, icon="save", keys="ctrl-s"), ], spacing=8), pz.row([ ui.button("XS", lambda: None, size="xs"), ui.button("Small", lambda: None, size="sm"), ui.button("Medium", lambda: None, size="md"), ui.button("Large", lambda: None, size="lg", icon="rocket", variant="solid"), ], spacing=8, align="center"), pz.row([ ui.icon_button("files", lambda: None, tooltip="Project"), ui.icon_button("search", lambda: None, selected=True), ui.icon_button("git-branch", lambda: None), ui.icon_button("bug", lambda: None), ui.icon_button("settings", lambda: None, variant="filled"), pz.space(width=12), ui.avatar("KV", index=0), ui.avatar("AL", index=3), ui.avatar("MZ", index=7), pz.space(width=12), ui.indicator("success"), ui.indicator("warning"), ui.indicator("error"), ], spacing=6, align="center"), ) badges = self.section( "Badges & key bindings", pz.row([ ui.badge("New"), ui.badge("Beta", tint="warning"), ui.badge("Stable", tint="success", icon="check"), ui.badge("Deprecated", tint="error", variant="outline"), ui.badge("v0.1.0", tint="accent", variant="solid"), ], spacing=6), pz.row([ui.kbd("ctrl-shift-p"), ui.kbd("ctrl-k ctrl-s"), ui.kbd("alt-enter"), ui.kbd("escape")], spacing=12, align="center"), ) inputs = self.section( "Inputs", ui.input(self.name, self.set("name"), placeholder="Project name"), ui.search_input(self.query, self.set("query"), placeholder="Search symbols…"), ui.field("Format on save", ui.switch(self.format_on_save, self.set("format_on_save")), description="Run the formatter whenever a buffer is saved"), ui.field("Vim mode", ui.switch(self.vim_mode, self.set("vim_mode"))), ui.checkbox(self.telemetry, self.set("telemetry"), label="Send anonymous usage data"), pz.row([ui.label("Font size", size="sm", color="text_muted"), ui.slider(self.font_size, self.set("font_size"))], spacing=12, align="center"), pz.row([ui.label("Indexing", size="sm", color="text_muted"), ui.progress(0.68)], spacing=12, align="center"), ui.segmented(["Unified", "Split", "Inline"], self.view_mode, self.set("view_mode")), spacing=12, ) menu = self.section( "Context menu", ui.menu([ ui.MenuItem("Cut", lambda: None, icon="scissors", keys="ctrl-x"), ui.MenuItem("Copy", lambda: None, icon="copy", keys="ctrl-c"), ui.MenuItem("Paste", lambda: None, icon="clipboard", keys="ctrl-v"), ui.SEPARATOR, ui.MenuItem("Soft wrap", lambda: None, checked=True), ui.MenuItem("Show minimap", lambda: None, checked=False), ui.SEPARATOR, ui.MenuItem("Delete file", lambda: None, icon="trash-2", danger=True), ], width=280), ) files = ui.TreeNode.from_paths( ["src/app.py", "src/views/editor.py", "src/views/panel.py", "tests/test_app.py", "pyproject.toml", "README.md"], {"src/app.py": "modified", "tests/test_app.py": "created"}, ) tree = self.section( "Tree & list", ui.tree(files, expanded=self.expanded, selected=self.selected_file, on_toggle=lambda f: self.expanded.symmetric_difference_update({f}), on_select=self.set("selected_file")), ui.divider(), ui.list_item("Recent projects", icon="history", end=ui.badge("3"), on_press=lambda: None), ui.list_item("Extensions", icon="blocks", end=ui.kbd("ctrl-shift-x"), on_press=lambda: None), spacing=6, ) tabs = self.section( "Tabs, breadcrumbs & editor", pz.container( pz.column([ ui.tab_bar( [ui.Tab.for_file("main.py"), ui.Tab.for_file("theme.rs", dirty=True), ui.Tab.for_file("README.md", preview=True)], self.active_tab, on_select=self.set("active_tab"), on_close=lambda _: None, end=[ui.icon_button("plus", lambda: None, size="sm")], ), ui.toolbar([ui.breadcrumbs(["examples", self.active_tab, "def view"])]), pz.container(ui.editor("gallery-snippet", SNIPPET, language="py"), height=150), ]), border_color=t.border, border_width=1, radius=t.radius, clip=True, ), ) toasts = self.section( "Notifications", ui.toast("Update available", "Pyzed 0.2 is ready to install.", kind="info", actions=[ui.button("Restart", lambda: None, size="sm", variant="solid"), ui.button("Later", lambda: None, size="sm", variant="ghost")], width=None), ui.toast("Saved 3 files", kind="success", on_dismiss=lambda: None, width=None), ui.toast("Language server crashed", "rust-analyzer exited with code 101.", kind="error", on_dismiss=lambda: None, width=None), ) forms = self.section( "Forms", ui.field("Language", ui.select(["Python", "Rust", "TypeScript", "Go"], self.language, self.set("language"), width=160)), ui.field("Tab size", ui.number_field(self.tab_size, self.set("tab_size"), min=1, max=8)), ui.radio_group(["Small", "Medium", "Large"], self.size_choice, self.set("size_choice"), horizontal=True), pz.row([ui.chip(tag, on_remove=lambda tag=tag: self.tags.discard(tag)) for tag in sorted(self.tags)], spacing=6), ui.disclosure("Advanced", ui.muted("Settings that most people never need."), open=self.advanced_open, on_toggle=lambda: setattr(self, "advanced_open", not self.advanced_open)), pz.row([ui.split_button("Run", lambda: None, lambda: None, icon="play"), ui.toggle_group([("list", "list"), ("grid", "layout-grid")], "list", lambda v: None)], spacing=8), ) feedback = self.section( "Feedback", ui.callout("Update available", "Pyzed 0.2 is ready.", tint="info", actions=[ui.button("Update", lambda: None, size="sm", variant="solid")]), ui.callout("Build failed", "3 errors in src/main.rs", tint="error"), pz.row([ui.count_badge(3), ui.count_badge(128, tint="error"), ui.facepile(["KV", "AL", "MZ", "JD", "RS", "TT"]), ui.diff_stat(42, 7)], spacing=12, align="center"), ui.keybinding_hint("Open file", "ctrl-p"), ) files = [dict(name="main.rs", size=1200, lang="Rust"), dict(name="app.py", size=840, lang="Python"), dict(name="README.md", size=310, lang="Markdown"), dict(name="theme.json", size=96, lang="JSON")] def sort(column): self.descending = not self.descending if self.sort_by == column else False self.sort_by = column data = self.section( "Data", ui.table([ui.Column("Name", "name"), ui.Column("Size", "size", width=70, align="end"), ui.Column("Language", "lang")], files, sort_by=self.sort_by, descending=self.descending, on_sort=sort, on_row_press=self.set("selected_row"), selected=self.selected_row, height=156), pz.row([ui.stat("Requests", "12.4k", delta=4.2, trend=[3, 5, 4, 6, 8, 7, 9]), ui.donut([5, 3, 2], label="62%", size=88)], spacing=12, align="center"), ui.bar_chart([4, 8, 6, 9, 3], labels=["Mon", "Tue", "Wed", "Thu", "Fri"], width=300, height=120), ) theme_picker = ui.select(pz.themes.names(), t.name, lambda name: setattr(self, "theme", pz.themes.get(name)), width=200) header = pz.row([ pz.icon("layers", size=22, color=t.accent), ui.heading("Pyzed components", level=2), pz.hspace(), theme_picker, ], spacing=10, align="center") grid = pz.row([ pz.column([buttons, badges, menu], spacing=16, width="fill"), pz.column([inputs, tree], spacing=16, width="fill"), pz.column([tabs, toasts], spacing=16, width="fill"), ], spacing=16) more = pz.row([ pz.column([forms], width="fill"), pz.column([feedback], width="fill"), pz.column([data], width="fill"), ], spacing=16) return pz.container( pz.scrollable(pz.column([header, grid, more], spacing=20, padding=24), height="fill"), width="fill", height="fill", bg=t.background if not t.is_dark else t.editor, color=t.text, ) if __name__ == "__main__": pz.run(Gallery) ```
#### Counter The smallest Pyzed app: one number, two buttons. It is the best file to start from when you make something new.
Full code: examples/counter.py `examples/counter.py`: ```py """The smallest Pyzed app: state + update + view.""" import pyzed as pz from pyzed import ui class Counter(pz.App): title = "Counter" size = (360, 220) def __init__(self): self.count = 0 def update(self, message): if message == "increment": self.count += 1 elif message == "decrement": self.count -= 1 def view(self): return pz.center( pz.column( [ ui.heading(str(self.count)), pz.row( [ ui.button("1", "decrement", icon="minus"), ui.button("1", "increment", icon="plus", variant="solid"), ui.icon_button("rotate-ccw", lambda: setattr(self, "count", 0), tooltip="Reset"), ], spacing=8, ), ], spacing=16, align="center", ), bg=self.theme.editor, ) if __name__ == "__main__": pz.run(Counter) ```
#### Workspace editor A full code editor workspace in about 450 lines: a project panel with the files of a folder, tabs, a syntax-highlighted editor (edit, undo, save with Ctrl+S), git status colors, an outline panel, theme switching, a command palette (Ctrl+Shift+P) and a file finder (Ctrl+P). Its look recreates the [Zed](https://zed.dev) editor, whose interface inspired Pyzed's default theme. The terminal panel shows sample output; it does not run a shell. Pass a folder to open it: `python examples/zed_editor.py path/to/project`. A smaller version is built step by step in the [mini code editor](https://albakore.github.io/pyzed/tutorials/advanced/code-editor/) tutorial.
Full code: examples/zed_editor.py `examples/zed_editor.py`: ```py """A recreation of the Zed editor written with Pyzed. python examples/zed_editor.py [folder] Real features: project panel over the folder's files, tabs, a syntax-highlighted editor (edit, undo, save with ctrl-s), git status colors, an outline panel, theme switching, the command palette (ctrl-shift-p) and the file finder (ctrl-p). The terminal panel shows static sample output; it does not run a shell. """ from __future__ import annotations import re import subprocess import sys from pathlib import Path from typing import Dict, List, Optional, Tuple import pyzed as pz from pyzed import Command, ui IGNORED_SUFFIXES = (".so", ".pyd", ".dll", ".dylib", ".pyc") IGNORED = {".git", "target", ".venv", "venv", "__pycache__", "node_modules", ".pytest_cache", "dist", ".mypy_cache"} MAX_FILES = 3000 TERMINAL_SAMPLE = ( "\x1b[32m~/pyzed\x1b[0m \x1b[34mmain\x1b[0m $ maturin develop --release\n" "🔗 Found pyo3 bindings with abi3 support\n" "\x1b[32m Compiling\x1b[0m pyzed v0.1.0 (~/pyzed)\n" "\x1b[32m Finished\x1b[0m `release` profile [optimized] target(s) in 41.20s\n" "📦 Built wheel for abi3 Python ≥ 3.9\n" "🛠 Installed pyzed-0.1.0\n" "\x1b[32m~/pyzed\x1b[0m \x1b[34mmain\x1b[0m $ python examples/zed_editor.py\n" ) # --------------------------------------------------------------------------- # Project helpers # --------------------------------------------------------------------------- def scan(root: Path) -> List[str]: files: List[str] = [] for path in sorted(root.rglob("*")): rel = path.relative_to(root) if any(part in IGNORED for part in rel.parts) or path.suffix in IGNORED_SUFFIXES or not path.is_file(): continue files.append(rel.as_posix()) if len(files) >= MAX_FILES: break return files def git(root: Path, *args: str) -> str: try: return subprocess.run(["git", *args], cwd=root, capture_output=True, text=True, timeout=5).stdout except (OSError, subprocess.SubprocessError): return "" def git_status(root: Path) -> Dict[str, str]: status = {} for line in git(root, "status", "--porcelain").splitlines(): code, path = line[:2], line[3:].strip() if "D" in code: status[path] = "deleted" elif "?" in code or "A" in code: status[path] = "created" elif code.strip(): status[path] = "modified" return status def git_markers(root: Path, path: str) -> Dict[int, str]: markers: Dict[int, str] = {} for hunk in re.finditer(r"^@@ -\d+(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", git(root, "diff", "-U0", "--", path), re.M): removed, start, added = int(hunk.group(1) or 1), int(hunk.group(2)), int(hunk.group(3) or 1) if added == 0: markers[max(start, 1)] = "deleted" for line in range(start, start + added): markers[line] = "created" if removed == 0 else "modified" return markers SYMBOLS = re.compile( r"^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?Pfn|struct|enum|trait|impl|mod|def|class)\s+(?P[A-Za-z_]\w*)", ) def outline(text: str) -> List[Tuple[int, str, str]]: out = [] for number, line in enumerate(text.splitlines(), start=1): match = SYMBOLS.match(line) if match: out.append((number, match["kind"], match["name"].strip())) return out # --------------------------------------------------------------------------- # The app # --------------------------------------------------------------------------- class Zed(pz.App): title = "Pyzed — a Zed-like editor in Python" size = (1440, 900) min_size = (800, 500) def __init__(self, root: str = ".", *, status: Optional[Dict[str, str]] = None): self.root = Path(root).resolve() self.files = scan(self.root) self.status = git_status(self.root) if status is None else status self.nodes = [ui.TreeNode(self.root.name, children=ui.TreeNode.from_paths(self.files, self.status), id="")] self.expanded = {""} self.selected: Optional[str] = None self.tabs: List[str] = [] self.active: Optional[str] = None self.texts: Dict[str, str] = {} self.markers: Dict[str, Dict[int, str]] = {} self.dirty: set = set() self.cursor = (1, 1) self.show_left, self.show_bottom, self.show_right = True, False, False self.palette: Optional[str] = None # "commands" | "files" self.query, self.choice = "", 0 self.notices: List[Tuple[str, str]] = [] self.theme = pz.themes.ONE_DARK def on_start(self): commands = [] if self.active and self.cursor != (1, 1): commands.append(Command.set_editor_cursor(self.active, *self.cursor)) if self.palette: commands.append(Command.focus("palette")) return commands # -- files ------------------------------------------------------------- def open_file(self, path: str): if path not in self.texts: try: self.texts[path] = (self.root / path).read_text(encoding="utf-8") except (UnicodeDecodeError, OSError): self.notify("Can't open binary file", "warning") return self.markers.setdefault(path, git_markers(self.root, path)) if path not in self.tabs: self.tabs.append(path) self.active = self.selected = path parts = path.split("/") self.expanded.update("/".join(parts[:i]) for i in range(1, len(parts))) self.cursor = (1, 1) def close_tab(self, path: str): if path in self.tabs: index = self.tabs.index(path) self.tabs.remove(path) if self.active == path: self.active = self.tabs[min(index, len(self.tabs) - 1)] if self.tabs else None def edited(self, path: str, text: str): self.texts[path] = text self.dirty.add(path) def save(self): if self.active: (self.root / self.active).write_text(self.texts[self.active], encoding="utf-8") self.dirty.discard(self.active) self.notify(f"Saved {self.active}", "success") def toggle_folder(self, folder: str): self.expanded.symmetric_difference_update({folder}) self.selected = folder def notify(self, text: str, kind: str = "info"): self.notices = (self.notices + [(text, kind)])[-3:] # -- palette ----------------------------------------------------------- def commands(self) -> List[ui.PaletteItem]: def run(action): def handler(): self.palette = None return action() return handler def set_theme(theme): return run(lambda: setattr(self, "theme", theme)) return [ ui.PaletteItem("file finder: toggle", run(self.open_finder), keys="ctrl-p"), ui.PaletteItem("workspace: toggle left dock", run(self.toggle_left), keys="ctrl-b"), ui.PaletteItem("workspace: toggle bottom dock", run(self.toggle_bottom), keys="ctrl-j"), ui.PaletteItem("workspace: toggle right dock", run(self.toggle_right), keys="ctrl-alt-b"), ui.PaletteItem("workspace: save", run(self.save), keys="ctrl-s"), ui.PaletteItem("pane: close active item", run(lambda: self.active and self.close_tab(self.active)), keys="ctrl-w"), ui.PaletteItem("theme selector: one dark", set_theme(pz.themes.ONE_DARK)), ui.PaletteItem("theme selector: one light", set_theme(pz.themes.ONE_LIGHT)), ui.PaletteItem("theme selector: gruvbox dark", set_theme(pz.themes.GRUVBOX_DARK)), ui.PaletteItem("terminal panel: toggle focus", run(self.toggle_bottom), keys="ctrl-`"), ui.PaletteItem("outline panel: toggle", run(self.toggle_right)), ui.PaletteItem("zed: about", run(lambda: self.notify("Pyzed 0.1 · Iced 0.14 · Python " + sys.version.split()[0]))), ui.PaletteItem("zed: quit", run(Command.exit), keys="ctrl-q"), ] def palette_matches(self): if self.palette == "files": items = [ ui.PaletteItem(path, (lambda p=path: self.pick_file(p)), icon=pz.icons.for_file(path)[0]) for path in self.files ] return ui.fuzzy_filter(self.query, items, key=lambda i: i.label)[:200] return ui.fuzzy_filter(self.query, self.commands(), key=lambda i: i.label) def pick_file(self, path: str): self.palette = None self.open_file(path) def open_palette(self, kind: str = "commands"): self.palette, self.query, self.choice = kind, "", 0 return Command.focus("palette") def open_finder(self): return self.open_palette("files") def set_query(self, query: str): self.query, self.choice = query, 0 def submit(self): matches = self.palette_matches() if matches: return matches[min(self.choice, len(matches) - 1)][0].on_press() def toggle_left(self): self.show_left = not self.show_left def toggle_bottom(self): self.show_bottom = not self.show_bottom def toggle_right(self): self.show_right = not self.show_right def on_key(self, combo: str): combo = combo.replace("cmd-", "ctrl-") if combo == "ctrl-shift-p": if self.palette == "commands": self.palette = None return None return self.open_palette() if combo == "ctrl-p": return self.open_finder() if self.palette: if combo == "escape": self.palette = None elif combo in ("down", "ctrl-n"): self.choice += 1 elif combo in ("up", "ctrl-p"): self.choice = max(0, self.choice - 1) return None actions = { "ctrl-b": self.toggle_left, "ctrl-j": self.toggle_bottom, "ctrl-`": self.toggle_bottom, "ctrl-alt-b": self.toggle_right, "ctrl-s": self.save, "ctrl-w": lambda: self.active and self.close_tab(self.active), "ctrl-q": Command.exit, } if combo in actions: return actions[combo]() # -- view -------------------------------------------------------------- def view(self): t = self.theme layout = ui.workspace( self.center(), title_bar=ui.title_bar( [ ui.title_button(self.root.name, chevron=True), ui.title_button("main", icon="git-branch", muted=True), ], [ ui.icon_button("bell", tooltip="Notifications"), ui.icon_button("sparkles", tooltip="Agent Panel"), pz.space(width=4), ui.avatar("KV", index=0), pz.space(width=4), ], ), left=self.project_panel() if self.show_left else None, right=self.outline_panel() if self.show_right else None, bottom=self.terminal_panel() if self.show_bottom else None, status_bar=self.status_bar(), left_width=250, right_width=260, ) if self.notices: layout = ui.toasts(layout, [ ui.toast(text, kind=kind, on_dismiss=(lambda i=i: self.notices.pop(i))) for i, (text, kind) in enumerate(self.notices) ]) if self.palette: matches = self.palette_matches() self.choice = min(self.choice, max(len(matches) - 1, 0)) overlay = ui.picker( self.query, self.set_query, matches, selected=self.choice, input_id="palette", on_submit=self.submit, placeholder="Execute a command…" if self.palette == "commands" else "Search project files…", ) layout = ui.layer(layout, overlay, on_dismiss=lambda: setattr(self, "palette", None), backdrop=pz.theme.alpha("#000000", 0.12 if t.is_dark else 0.04)) return layout def project_panel(self): return pz.scrollable( pz.container( ui.tree(self.nodes, expanded=self.expanded, selected=self.selected, on_toggle=self.toggle_folder, on_select=self.open_file), padding=(4, 0), ), height="fill", scroller=self.theme.scrollbar, scrollbar_width=6, ) def outline_panel(self): symbols = outline(self.texts.get(self.active or "", "")) rows = [ ui.list_item( pz.row([ui.label(kind, color=self.theme.syntax["keyword"], size="sm", mono=True), ui.label(name, mono=True, size="sm")], spacing=6), lambda: None, height=24, indent=8, ) for _, kind, name in symbols ] return pz.column([ ui.panel_header("Outline", [ui.icon_button("x", self.toggle_right, size="sm")], icon="list-tree"), pz.scrollable(pz.column(rows or [ui.muted("No symbols", size="sm")], padding=(0, 0, 8, 0)), height="fill"), ]) def terminal_panel(self): tabs = pz.row([ pz.container( pz.row([pz.icon("terminal", size=14), ui.label("zsh", size="sm")], spacing=6, align="center"), padding=(3, 10), bg=self.theme.editor, radius=self.theme.radius_sm, ), pz.hspace(), ui.icon_button("plus", size="sm"), ui.icon_button("maximize-2", size="sm"), ui.icon_button("x", self.toggle_bottom, size="sm"), ], align="center", padding=(4, 6)) return pz.column([tabs, ui.terminal(TERMINAL_SAMPLE)]) def center(self): if not self.active: return self.welcome() symbols = outline(self.texts.get(self.active, "")) scope = [f"{kind} {name}" for line, kind, name in symbols if line <= self.cursor[0]][-1:] tabs = ui.tab_bar( [ui.Tab.for_file(p, dirty=p in self.dirty, status=self.status.get(p)) for p in self.tabs], self.active, on_select=self.select_tab, on_close=self.close_tab, start=[ui.icon_button("arrow-left", size="sm"), ui.icon_button("arrow-right", size="sm")], end=[ui.icon_button("plus", size="sm"), ui.icon_button("columns-2", size="sm")], ) toolbar = ui.toolbar( [ui.breadcrumbs([self.active, *scope])], [ ui.icon_button("sparkles", size="sm", tooltip="Inline Assist"), ui.icon_button("text-search", size="sm", tooltip="Buffer Search"), ui.icon_button("ellipsis", size="sm"), ], ) editor = ui.editor( self.active, self.texts[self.active], path=self.active, markers=self.markers.get(self.active), on_change=lambda text, p=self.active: self.edited(p, text), on_cursor=self.move_cursor, ) return pz.column([tabs, toolbar, editor]) def select_tab(self, path: str): self.active = self.selected = path def move_cursor(self, line: int, column: int): self.cursor = (line, column) def welcome(self): rows = [ ("Open a File", "file", "ctrl-p", self.open_finder), ("Command Palette", "command", "ctrl-shift-p", self.open_palette), ("Toggle Project Panel", "panel-left", "ctrl-b", self.toggle_left), ("Toggle Terminal", "terminal", "ctrl-j", self.toggle_bottom), ] return pz.center( pz.column( [ pz.row([pz.icon("zap", size=30, color=self.theme.accent), ui.heading("Pyzed")], spacing=10, align="center"), ui.muted("Code at the speed of thought — now in Python"), pz.space(height=18), *[ ui.button(label, action, icon=icon, variant="ghost", full_width=True, keys=keys) for label, icon, keys, action in rows ], ], spacing=4, width=340, align="center", ) ) def status_bar(self): language = pz.icons.language_for(self.active) if self.active else None return ui.status_bar( [ ui.status_item(icon="panel-left", on_press=self.toggle_left, selected=self.show_left, tooltip="Project Panel", keys="ctrl-b"), ui.status_item(icon="git-branch", on_press=lambda: None), ui.status_item(icon="search", on_press=self.open_finder, tooltip="File Finder", keys="ctrl-p"), pz.space(width=6), ui.status_item("0", icon="circle-x", on_press=lambda: None), ui.status_item("0", icon="triangle-alert", on_press=lambda: None), ], [ ui.status_item(f"{self.cursor[0]}:{self.cursor[1]}", on_press=lambda: None) if self.active else None, ui.status_item(language, on_press=lambda: None) if language else None, ui.status_item(icon="sparkles", on_press=lambda: None), ui.status_item(icon="bell", on_press=lambda: None), ui.status_item(icon="panel-bottom", on_press=self.toggle_bottom, selected=self.show_bottom, tooltip="Terminal", keys="ctrl-j"), ui.status_item(icon="panel-right", on_press=self.toggle_right, selected=self.show_right, tooltip="Outline", keys="ctrl-alt-b"), ], ) if __name__ == "__main__": Zed(sys.argv[1] if len(sys.argv) > 1 else ".").run() ```
**About this page:** Every app here is drawn off-screen by the test suite, so they run with the current version of Pyzed. Short recordings are being added for the apps that move (Game of Life, downloads, several windows). ### Forms > Python desktop forms made with Pyzed: sign-in, sign-up with validation, a contact form, onboarding and settings screens, dialogs, Pydantic validation and forms drawn in the Pyzed Design visual designer. Source: https://albakore.github.io/pyzed/showcase/forms/ Forms are where most desktop apps start: fields, buttons, rules and messages. Pyzed gives you two ways to make them, and you can mix both in one app: - **Draw them** in [Pyzed Design](https://albakore.github.io/pyzed/editor/), the visual designer: drag controls onto a window, set their properties, and write only the Python that runs when something happens (a `.pzform` file plus a `.py` file with the handlers). If you have used Visual Basic, Qt Designer or the editors of Godot and Unreal Engine, it will feel familiar. - **Write them** in Python with the [components library](https://albakore.github.io/pyzed/library/): `ui.field`, `ui.input`, `ui.select`, `ui.switch`... in a `view()` method. **Open a form in the designer:** Every `.pzform` on this page opens in Pyzed Design: `pyzed design path/to/form.pzform`. Press F5 there to run it, and look at the **Events** panel to see which handler each control calls. #### Sign-in form Drawn in the designer: an email and a password, a "remember me" check box, and a button that shows a progress bar while it "signs in". The Python file is just four small functions, one per event. - Controls are attributes of `form`: `form.email.text`, `form.status.color = "error"`, `form.progress.show()`. - A **Timer** control (`ticker`) calls `ticker_tick` every few milliseconds while it runs; that is how the progress bar fills up without blocking the window.
Event handlers: examples/designer/login.py `examples/designer/login.py`: ```py """Event handlers for the login form. Each function receives the running ``form`` (controls are attributes: ``form.button1.text``) and an ``event`` with ``event.sender``, ``event.name`` and ``event.value``. """ def sign_in_click(form, event): """Runs when sign_in is clicked.""" if "@" not in form.email.text: form.status.text = "Enter a valid email address." form.status.color = "error" return form.email.focus() form.status.text = "" form.sign_in.enabled = False form.progress.value = 0 form.progress.show() form.ticker.running = True def ticker_tick(form, event): """Runs every ``interval`` milliseconds while ticker is enabled.""" form.progress.value += 4 if form.progress.value >= 100: form.ticker.running = False form.progress.hide() form.status.color = "success" form.status.text = f"Signed in as {form.email.text}" form.sign_in.enabled = True def password_submit(form, event): """Runs when Enter is pressed in password.""" return sign_in_click(form, event) def form_load(form, event): """Runs once when login opens.""" form.status.text = "Tip: any email works in this demo." return form.email.focus() ```
#### Sign-up with validation Every field is **bound** to `form.data` and has **rules** set in the Properties panel (required, an email, at least 8 characters, the same as another field). One line, `form.validate()`, checks them all and shows the messages next to the fields; the greeting and the summary update by themselves as you type.
Event handlers: examples/tutorial/designer/signup.py `examples/tutorial/designer/signup.py`: ```py """Event handlers for the sign-up form (tutorial: Data binding and validation). pyzed run examples/tutorial/designer/signup.pzform The controls are bound to fields of ``form.data`` (Properties → Data → Bind to) and carry their validation rules, so the handler only has to call ``form.validate()`` and read ``form.data``. """ def create_click(form, event): """Runs when create is clicked.""" if not form.validate(): # checks every rule and shows the messages under the controls form.status.color = "error" form.status.text = f"{len(form.errors)} field(s) need your attention." return data = form.data # {"name": ..., "email": ..., "password": ..., "age": ..., "terms": True} print("New account:", {k: v for k, v in data.items() if k != "password"}) form.status.color = "success" form.status.text = f"Welcome aboard, {data['name']}!" ```
Built step by step in [Data binding and validation](https://albakore.github.io/pyzed/tutorials/intermediate/data-binding/). #### Contact form Name, email, a topic drop-down and a message with a live character counter. The **Send** button checks the fields and writes the message to the console.
Event handlers: examples/tutorial/designer/contact.py `examples/tutorial/designer/contact.py`: ```py """Event handlers for the contact form (tutorial: A form with inputs and a button). pyzed run examples/tutorial/designer/contact.pzform Each function receives the running ``form`` and an ``event`` (``event.value`` holds the new value of a ``change`` event). """ LIMIT = 500 def form_load(form, event): """Runs once when contact opens: put the cursor in the first box.""" return form.your_name.focus() def message_change(form, event): """Runs on every keystroke in message: update the character counter.""" count = len(event.value) form.counter.text = f"{count} / {LIMIT}" form.counter.color = "error" if count > LIMIT else "text_muted" def send_click(form, event): """Runs when send is clicked: check the fields, then 'send' the message.""" if not form.your_name.text.strip(): form.status.color = "error" form.status.text = "Please tell us your name." return form.your_name.focus() if "@" not in form.email.text: form.status.color = "error" form.status.text = "That email does not look right." return form.email.focus() if len(form.message.text) > LIMIT: form.status.color = "error" form.status.text = f"Keep the message under {LIMIT} characters." return None # print() shows up in the designer's console (and in the terminal) print(f"New {form.topic.selected.lower()} from {form.email.text}: {form.message.text!r}") form.status.color = "success" form.status.text = f"Thanks, {form.your_name.text.split()[0]}! We got your message." form.message.text = "" form.counter.text = f"0 / {LIMIT}" return None ```
Built step by step in [A form that does something](https://albakore.github.io/pyzed/tutorials/basic/first-form/). #### Reviews with custom components Two profile cards, each with a five-star **Rating** component, side by side in a flex row that grows and shrinks with the window. `Rating`, `ProfileCard` and `TeamCard` are components drawn in the designer's component mode (`.pzcomp` files): they have their own properties, events and code, and `TeamCard` inherits everything from `ProfileCard`.
Event handlers: examples/designer/components/reviews.py `examples/designer/components/reviews.py`: ```py """Event handlers for the Reviews form. Each function receives the running ``form`` (controls are attributes: ``form.button1.text``) and an ``event`` with ``event.sender``, ``event.name`` and ``event.value``. """ def ada_rated(form, event): """Runs when the card of ada is rated.""" event.sender.score = event.value form.status.text = f"{event.sender.person} now has {event.value} stars" def grace_rated(form, event): """Runs when the card of grace is rated.""" event.sender.score = event.value form.status.text = f"{event.sender.person} now has {event.value} stars" ```
Built step by step in [A custom component](https://albakore.github.io/pyzed/tutorials/intermediate/custom-component/). #### Login screen A full sign-in screen written in code: a brand panel, single sign-on buttons, a passkey option and an email form. It is one of the [Blocks](https://albakore.github.io/pyzed/blocks/), screens you can copy into your app and adapt. The code and what to change in it: [Login block](https://albakore.github.io/pyzed/blocks/login/). #### Onboarding wizard A multi-step setup: a stepper at the top, choice cards, per-step validation and a summary before finishing. The code: [Onboarding block](https://albakore.github.io/pyzed/blocks/onboarding/). #### Settings page Sections on the left, grouped rows on the right: profile fields, a language select, light, dark or system theme, accent swatches, a font size slider, notification switches, and a danger zone that asks before deleting. The code: [Settings block](https://albakore.github.io/pyzed/blocks/settings/). #### Dialogs and languages Each button opens one of the ready-made dialogs of `pyzed.ui` (an alert, a confirmation, a prompt, a choice and "unsaved changes"). Pick a language in the toolbar and every text, including the dialogs' buttons, changes while the app runs.
Full code: examples/dialogs.py `examples/dialogs.py`: ```py """Dialogs and languages: ready-made dialogs and switching the app's language while it runs. python examples/dialogs.py - Pick a language in the toolbar; every text (and the dialogs' buttons) follows it. - Each button opens one of the dialogs in ``pyzed.ui``. """ import pyzed as pz from pyzed import i18n, ui i18n.add("en", { "title": "Dialogs", "language": "Language", "files": {"zero": "No files", "one": "{count} file", "other": "{count} files"}, "actions": {"alert": "Show an alert", "confirm": "Delete files", "prompt": "Rename", "choice": "Export", "unsaved": "Close document"}, "delete_question": "Delete {files}? This cannot be undone.", "saved": "Your changes were saved.", "rename": "Rename file", "new_name": "New name", "export": "Export the report as…", "last": "Last action: {action}", }) i18n.add("es", { "title": "Diálogos", "language": "Idioma", "files": {"zero": "Ningún archivo", "one": "{count} archivo", "other": "{count} archivos"}, "actions": {"alert": "Mostrar un aviso", "confirm": "Eliminar archivos", "prompt": "Renombrar", "choice": "Exportar", "unsaved": "Cerrar documento"}, "delete_question": "¿Eliminar {files}? No se puede deshacer.", "saved": "Tus cambios se guardaron.", "rename": "Renombrar archivo", "new_name": "Nombre nuevo", "export": "Exportar el informe como…", "last": "Última acción: {action}", }) LANGUAGES = {"en": "English", "es": "Español"} class DialogsDemo(pz.App): size = (720, 460) def __init__(self): self.dialog = None self.files = 3 self.name = "report.md" self.last = "-" def title(self): return pz.t("title") def open(self, kind): self.dialog = kind def done(self, action): self.last = action self.dialog = None def delete(self): self.files = 0 self.done("delete") def dialog_view(self): close = lambda: self.done("cancel") # noqa: E731 if self.dialog == "alert": return ui.alert(pz.t("saved"), close, kind="success") if self.dialog == "confirm": return ui.confirm(pz.t("delete_question", files=pz.t("files", count=self.files)), self.delete, close, danger=True) if self.dialog == "prompt": return ui.prompt(pz.t("rename"), self.name, lambda v: setattr(self, "name", v), lambda: self.done(f"rename → {self.name}"), close, message=pz.t("new_name")) if self.dialog == "choice": return ui.choice(pz.t("export"), [(f, lambda f=f: self.done(f"export {f}")) for f in ("CSV", "PDF")], close) if self.dialog == "unsaved": return ui.unsaved_changes(self.name, lambda: self.done("save"), lambda: self.done("discard"), close) return None def view(self): toolbar = pz.row([ ui.heading(pz.t("title")), pz.space(width="fill"), ui.label(pz.t("language"), color="text_muted"), ui.segmented([LANGUAGES[k] for k in LANGUAGES], LANGUAGES[i18n.language()[:2]], lambda name: i18n.set_language(next(k for k, v in LANGUAGES.items() if v == name))), ], align="center", spacing=10) buttons = pz.grid( [ui.button(pz.t(f"actions.{kind}"), lambda kind=kind: self.open(kind), full_width=True) for kind in ("alert", "confirm", "prompt", "choice", "unsaved")], columns=["1fr", "1fr"], spacing=8, ) body = pz.column([ toolbar, ui.label(f"{self.name} · {pz.t('files', count=self.files)}", color="text_muted"), buttons, ui.label(pz.t("last", action=self.last), mono=True, size="sm"), ], spacing=16, padding=24) return ui.dialog_layer(body, self.dialog_view(), on_dismiss=lambda: self.done("cancel")) if __name__ == "__main__": pz.run(DialogsDemo) ```
For field errors, confirmations and toasts in your own forms, see [Dialogs, notifications and validation](https://albakore.github.io/pyzed/tutorials/intermediate/dialogs-feedback/). #### Sign-up validated by Pydantic The rules of this form live in a [Pydantic](https://docs.pydantic.dev) model, the same kind of model a web API would use. The window keeps what the user types as plain text and hands it to `SignUp.model_validate`; Pydantic converts the values (a date, an email, a yes/no), checks every rule, and `pz.validation_errors` turns its errors into a message per field. Accounts are saved to `accounts.json`. It is a small project with its own dependencies, so run it from its folder: **uv:** ```bash title="Windows, macOS and Linux" cd examples/integrations/pydantic_signup uv run main.py ``` **pip:** ```bash title="macOS / Linux" cd examples/integrations/pydantic_signup python3 -m venv .venv source .venv/bin/activate python -m pip install pyzed-ui "pydantic[email]>=2.6" python main.py ``` ```powershell title="Windows (PowerShell)" cd examples\integrations\pydantic_signup py -m venv .venv .venv\Scripts\Activate.ps1 python -m pip install pyzed-ui "pydantic[email]>=2.6" python main.py ``` **python:** ```bash title="With pyzed-ui and pydantic[email] already installed" cd examples/integrations/pydantic_signup python main.py ```
Full code: main.py and models.py `examples/integrations/pydantic_signup/models.py`: ```py """The form's rules, written as Pydantic models (https://docs.pydantic.dev/latest/concepts/models/).""" from __future__ import annotations from datetime import date from typing import Literal from pydantic import BaseModel, EmailStr, Field, SecretStr, TypeAdapter, field_validator, model_validator class SignUp(BaseModel): username: str = Field(min_length=3, max_length=20, pattern=r"^[a-z0-9_]+$") email: EmailStr birthday: date plan: Literal["free", "pro", "team"] = "free" password: SecretStr # repr() and str() show ******** password_repeat: SecretStr newsletter: bool = False @field_validator("username", mode="before") @classmethod def lowercase(cls, value: str) -> str: return value.strip().lower() @field_validator("birthday") @classmethod def old_enough(cls, value: date) -> date: today = date.today() age = today.year - value.year - ((today.month, today.day) < (value.month, value.day)) if age < 13: raise ValueError("You must be at least 13 years old") return value @field_validator("password") @classmethod def long_enough(cls, value: SecretStr) -> SecretStr: if len(value.get_secret_value()) < 8: raise ValueError("Use 8 characters or more") return value @model_validator(mode="after") def passwords_match(self) -> SignUp: if self.password.get_secret_value() != self.password_repeat.get_secret_value(): raise ValueError("The passwords don't match") return self class Account(BaseModel): """What gets saved: the sign-up without the passwords.""" username: str email: EmailStr birthday: date plan: Literal["free", "pro", "team"] newsletter: bool Accounts = TypeAdapter(list[Account]) # validates and serializes a whole list at once ``` `examples/integrations/pydantic_signup/main.py`: ```py """A sign-up form whose rules live in a Pydantic model; accounts are saved to accounts.json. uv run main.py The window keeps what the user types as plain strings and hands them to ``SignUp.model_validate``: Pydantic converts them (a date, an email, a bool), checks every rule and reports the errors per field, which ``pz.validation_errors`` turns into ``{field: message}``. Walkthrough: https://albakore.github.io/pyzed/integrations/pydantic/ """ from __future__ import annotations from pathlib import Path from pydantic import ValidationError import pyzed as pz from pyzed import ui from models import Account, Accounts, SignUp FILE = Path(__file__).with_name("accounts.json") EMPTY = {"username": "", "email": "", "birthday": "", "plan": "free", "password": "", "password_repeat": "", "newsletter": False} def load() -> list[Account]: return Accounts.validate_json(FILE.read_bytes()) if FILE.exists() else [] class SignUpForm(pz.App): title = "Sign up · Pydantic" size = (860, 600) def __init__(self): self.draft = dict(EMPTY) self.errors: dict[str, str] = {} self.accounts = load() self.preview = "" # the JSON Pydantic produced for the last account def edit(self, key): return lambda value: self.draft.__setitem__(key, value) def submit(self, *_): try: signup = SignUp.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) # the model validator's error has the key "" return account = Account.model_validate(signup.model_dump(exclude={"password", "password_repeat"})) if any(a.username == account.username for a in self.accounts): self.errors = {"username": "That username is taken"} return self.accounts.append(account) FILE.write_bytes(Accounts.dump_json(self.accounts, indent=2)) self.preview = account.model_dump_json(indent=2) self.draft, self.errors = dict(EMPTY), {} def form(self): e, d = self.errors.get, self.draft def text(key, title, placeholder="", **kwargs): return ui.field(title, ui.input(d[key], self.edit(key), placeholder=placeholder, on_submit=self.submit, **kwargs), error=e(key)) return pz.column([ ui.heading("Create an account", level=2), text("username", "Username", "lowercase letters, digits and _"), text("email", "Email", "you@example.com"), text("birthday", "Birthday", "YYYY-MM-DD"), ui.field("Plan", ui.segmented(["free", "pro", "team"], d["plan"], self.edit("plan"))), text("password", "Password", "8 characters or more", secure=True), text("password_repeat", "Repeat the password", secure=True), ui.checkbox(d["newsletter"], self.edit("newsletter"), label="Send me the newsletter"), ui.callout(e(""), tint="error") if e("") else None, ui.button("Sign up", self.submit, icon="user-plus", variant="solid", full_width=True), ], spacing=12, padding=24, width=420) def side(self, t): rows = [ui.list_item(a.username, None, icon="user", end=ui.badge(a.plan), inset=True, height=30) for a in reversed(self.accounts)] return pz.container(pz.column([ ui.muted("accounts.json", size="xs", mono=True), *rows, ui.card(ui.label(self.preview, mono=True, size="sm"), title="model_dump_json()") if self.preview else None, ], spacing=6), padding=16, width="fill", height="fill", bg=t.panel) def view(self): t = self.resolved_theme() return pz.row([pz.container(pz.scrollable(self.form()), height="fill", bg=t.editor), self.side(t)]) if __name__ == "__main__": pz.run(SignUpForm) ```
The walkthrough is in [Pydantic](https://albakore.github.io/pyzed/integrations/pydantic/). **About this page:** The designer forms and the apps on this page are loaded and drawn by the test suite with the current version of Pyzed. Recordings of them being filled in are on the way. ### Dashboards and data apps > Data-heavy Python desktop apps made with Pyzed: an admin dashboard with KPIs and charts, a data table with filters and paging, a kanban board, a team chat, a music player, an IDE layout and a contacts app over SQLite. Source: https://albakore.github.io/pyzed/showcase/dashboards/ Screens full of numbers, tables and lists: the kind of internal tool, admin panel or media app that many desktop programs are. The first six are [Blocks](https://albakore.github.io/pyzed/blocks/), complete screens written with the [components library](https://albakore.github.io/pyzed/library/) that you can copy and adapt; each block's page has its full code and notes on what to change. Every capture follows this site's theme: switch between light and dark at the top of the page to see them in both. #### Dashboard An admin dashboard: sidebar navigation, KPI cards with trends, a revenue chart, traffic sources and a table of recent orders. - KPI cards are `ui.stat` with a `delta` and a small `trend` line. The charts are ready-made components too: `ui.bar_chart` for revenue (its range is a `ui.segmented` control) and `ui.donut` for traffic sources, with a `ui.progress` bar for the monthly goal. - The sidebar switches pages by changing one attribute of the state: see [A multi-view app](https://albakore.github.io/pyzed/tutorials/intermediate/navigation/). The code: [Dashboard block](https://albakore.github.io/pyzed/blocks/dashboard/). #### Data table A team members table with search, status filters, sortable columns, row selection, paging and an export action. The code: [Data table block](https://albakore.github.io/pyzed/blocks/data-table/). To learn the parts one by one (search, sort, select, show the details), follow [Lists and tables with data](https://albakore.github.io/pyzed/tutorials/intermediate/lists-tables/), which builds this small book library: #### Kanban board A project board with columns, labelled cards, assignees, progress, and buttons to move cards along. The code: [Kanban block](https://albakore.github.io/pyzed/blocks/kanban/). #### Team chat A messenger with channels, direct messages, a message thread and a composer. The code: [Chat block](https://albakore.github.io/pyzed/blocks/chat/). For a chat that streams answers from an AI model, see the [AI chat](https://albakore.github.io/pyzed/showcase/apps/#ai-chat). #### Music player A library sidebar, an album page with its track list, and a player bar whose progress moves while a song plays. The code: [Music player block](https://albakore.github.io/pyzed/blocks/music-player/). #### IDE layout The layout of a code editor: a project tree, editor tabs with breadcrumbs, a terminal, an outline panel and a status bar. The code: [File explorer block](https://albakore.github.io/pyzed/blocks/file-explorer/). A working editor on the same idea is the [Workspace editor](https://albakore.github.io/pyzed/showcase/apps/#workspace-editor). #### Contacts over SQLite A contacts app on [SQLAlchemy](https://www.sqlalchemy.org) 2.0 with **20,000 rows** in a virtual table. Searching and sorting run as SQL queries in a worker thread (`Command.perform`), so the window never waits for the database. **uv:** ```bash title="Windows, macOS and Linux" cd examples/integrations/sqlalchemy_contacts uv run main.py ``` **pip:** ```bash title="macOS / Linux" cd examples/integrations/sqlalchemy_contacts python3 -m venv .venv source .venv/bin/activate python -m pip install pyzed-ui "sqlalchemy>=2.0" python main.py ``` ```powershell title="Windows (PowerShell)" cd examples\integrations\sqlalchemy_contacts py -m venv .venv .venv\Scripts\Activate.ps1 python -m pip install pyzed-ui "sqlalchemy>=2.0" python main.py ``` **python:** ```bash title="With pyzed-ui and SQLAlchemy already installed" cd examples/integrations/sqlalchemy_contacts python main.py ```
Full code: db.py and main.py `examples/integrations/sqlalchemy_contacts/db.py`: ```py """The database: SQLAlchemy 2.0 models on SQLite, plus the queries the window runs.""" from __future__ import annotations import random from datetime import datetime from pathlib import Path from sqlalchemy import String, create_engine, func, or_, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column DATABASE = Path(__file__).with_name("contacts.db") engine = create_engine(f"sqlite:///{DATABASE}") class Base(DeclarativeBase): pass class Contact(Base): __tablename__ = "contacts" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(80), index=True) email: Mapped[str] = mapped_column(String(120), unique=True) company: Mapped[str] = mapped_column(String(80), default="") city: Mapped[str] = mapped_column(String(60), default="") created: Mapped[datetime] = mapped_column(default=datetime.now) # Sorting by a column title of the table is an ORDER BY on this column. COLUMNS = {"Name": Contact.name, "Email": Contact.email, "Company": Contact.company, "City": Contact.city} def setup(rows: int = 20_000) -> None: """Creates the table and fills it with made-up contacts the first time.""" Base.metadata.create_all(engine) with Session(engine) as session: if session.scalar(select(func.count()).select_from(Contact)): return rng = random.Random(7) first = ["Ada", "Alan", "Grace", "Linus", "Guido", "Margaret", "Ken", "Barbara", "Dennis", "Radia", "Tim", "Frances", "Bjarne", "Anita", "John", "Hedy", "Yukihiro", "Sophie", "Brendan", "Katherine"] last = ["Lovelace", "Turing", "Hopper", "Torvalds", "van Rossum", "Hamilton", "Thompson", "Liskov", "Ritchie", "Perlman", "Berners-Lee", "Allen", "Stroustrup", "Borg", "McCarthy", "Lamarr", "Matsumoto", "Wilson", "Eich", "Johnson"] companies = ["Acme", "Globex", "Initech", "Umbrella", "Hooli", "Stark", "Wayne", "Tyrell", "Cyberdyne", "Aperture"] cities = ["Buenos Aires", "Madrid", "Mexico City", "Bogotá", "Lima", "Santiago", "Montevideo", "Barcelona", "Berlin", "Tokyo", "Toronto", "Lisbon"] weights = [12, 10, 9, 7, 6, 5, 4, 4, 3, 3, 2, 2] session.add_all( Contact(name=f"{rng.choice(first)} {rng.choice(last)}", email=f"user{i}@example.com", company=rng.choice(companies), city=rng.choices(cities, weights)[0]) for i in range(rows) ) session.commit() def search(text: str, sort: str, descending: bool) -> list[Contact]: """Contacts whose name, email or company contains ``text``, sorted in SQL.""" column = COLUMNS[sort] query = select(Contact).order_by(column.desc() if descending else column, Contact.id) if text: pattern = f"%{text}%" query = query.where(or_(Contact.name.ilike(pattern), Contact.email.ilike(pattern), Contact.company.ilike(pattern))) with Session(engine) as session: return list(session.scalars(query)) # plain objects keep their loaded values after the session closes def stats() -> dict: """Totals and the five cities with more contacts, for the header.""" with Session(engine) as session: total = session.scalar(select(func.count()).select_from(Contact)) companies = session.scalar(select(func.count(func.distinct(Contact.company)))) cities = session.execute(select(Contact.city, func.count()).group_by(Contact.city) .order_by(func.count().desc()).limit(5)).all() return {"total": total, "companies": companies, "cities": [(city, count) for city, count in cities]} def save(contact_id: int | None, values: dict) -> Contact | dict: """Inserts or updates a contact. Returns it, or ``{field: message}`` when it can't be saved.""" errors = {} if not values["name"].strip(): errors["name"] = "A contact needs a name" if "@" not in values["email"]: errors["email"] = "Not an email address" if errors: return errors with Session(engine, expire_on_commit=False) as session: contact = session.get(Contact, contact_id) if contact_id else Contact() for key, value in values.items(): setattr(contact, key, value.strip()) session.add(contact) try: session.commit() except IntegrityError: return {"email": "Another contact already uses this email"} return contact def delete(contact_id: int) -> None: with Session(engine) as session: contact = session.get(Contact, contact_id) if contact is not None: session.delete(contact) session.commit() ``` `examples/integrations/sqlalchemy_contacts/main.py`: ```py """A contacts manager on SQLAlchemy 2.0: 20,000 rows in a virtual table, searched and sorted in SQL. uv run main.py The database (contacts.db) is created next to this file on the first run. Walkthrough: https://albakore.github.io/pyzed/integrations/sqlalchemy/ """ from __future__ import annotations import pyzed as pz from pyzed import Command, ui import db FIELDS = [("name", "Name", "Ada Lovelace"), ("email", "Email", "ada@example.com"), ("company", "Company", "Analytical Engines"), ("city", "City", "London")] class Contacts(pz.App): title = "Contacts · SQLAlchemy" size = (1180, 720) def __init__(self): self.rows: list[db.Contact] = [] self.stats = {"total": 0, "companies": 0, "cities": []} self.query = "" self.sort, self.descending = "Name", False self.loading = True self.editing = None # a Contact, "new" or None self.draft: dict[str, str] = {} self.errors: dict[str, str] = {} # -- database work runs in a thread with Command.perform ------------------------------ def on_start(self): return Command.perform(db.setup, lambda _: self.reload()) def reload(self): self.loading = True query, sort, descending = self.query, self.sort, self.descending return [ Command.perform(lambda: db.search(query, sort, descending), lambda rows: self.loaded(query, rows)), Command.perform(db.stats, lambda stats: setattr(self, "stats", stats)), ] def loaded(self, query: str, rows: list[db.Contact]): if query == self.query: # a newer search is on its way: keep waiting for it self.rows, self.loading = rows, False def search(self, text: str): self.query = text return self.reload() def sort_by(self, title: str): self.descending = not self.descending if title == self.sort else False self.sort = title return self.reload() # -- the edit panel ------------------------------------------------------------------ def open(self, contact): self.editing, self.errors = contact, {} self.draft = {key: "" if contact == "new" else getattr(contact, key) for key, _, _ in FIELDS} def save(self): contact_id = None if self.editing == "new" else self.editing.id values = dict(self.draft) return Command.perform(lambda: db.save(contact_id, values), self.saved) def saved(self, result): if isinstance(result, dict): self.errors = result return None self.editing = None return self.reload() def delete(self): contact_id = self.editing.id self.editing = None return Command.perform(lambda: db.delete(contact_id), lambda _: self.reload()) def on_key(self, combo): if combo == "escape": self.editing = None elif combo == "ctrl-n": self.open("new") # -- view ---------------------------------------------------------------------------- def header(self): cities = self.stats["cities"] return pz.row([ ui.stat("Contacts", f"{self.stats['total']:,}"), ui.stat("Companies", self.stats["companies"]), ui.card(pz.column([ui.muted("Top cities", size="sm"), ui.bar_chart([count for _, count in cities], labels=[c.split()[0] for c, _ in cities], width=360, height=90)], spacing=6)), ], spacing=12) def panel(self, t): if self.editing is None: return None rows = [ui.field(title, ui.input(self.draft[key], lambda v, k=key: self.draft.__setitem__(k, v), placeholder=hint, width=220), error=self.errors.get(key)) for key, title, hint in FIELDS] new = self.editing == "new" return pz.container( pz.column([ pz.row([ui.heading("New contact" if new else "Edit contact", level=3), pz.hspace(), ui.icon_button("x", lambda: setattr(self, "editing", None), tooltip="Close (Esc)")], align="center"), *rows, pz.row([None if new else ui.button("Delete", self.delete, icon="trash-2", variant="ghost", tint="error"), pz.hspace(), ui.button("Save", self.save, icon="check", variant="solid")], spacing=8), ], spacing=14), padding=16, width=420, height="fill", bg=t.panel, ) def view(self): t = self.resolved_theme() columns = [ ui.Column("Name", "name", width="fill:3"), ui.Column("Email", "email", width="fill:3"), ui.Column("Company", "company", width="fill:2"), ui.Column("City", "city", width="fill:2"), ] toolbar = pz.row([ ui.search_input(self.query, self.search, placeholder="Search by name, email or company…", width=360), ui.spinner() if self.loading else ui.muted(f"{len(self.rows):,} found", size="sm"), pz.hspace(), ui.button("New contact", lambda: self.open("new"), icon="user-plus", keys="ctrl-n"), ], spacing=10, align="center") # Rows come sorted from SQL; sort_by only draws the arrow (re-sorting a sorted list is cheap). table = ui.table(columns, self.rows, sort_by=self.sort, descending=self.descending, on_sort=self.sort_by, on_row_press=self.open, selected=self.editing, striped=True, height="fill") main = pz.column([self.header(), toolbar, table], spacing=14, padding=16) return pz.container(pz.row([main, self.panel(t)]), width="fill", height="fill", bg=t.editor) if __name__ == "__main__": pz.run(Contacts) ```
The walkthrough is in [SQLAlchemy](https://albakore.github.io/pyzed/integrations/sqlalchemy/). Without any library at all, the [Connecting a backend](https://albakore.github.io/pyzed/tutorials/advanced/backend/) tutorial builds a smaller contacts app over the `sqlite3` module that comes with Python, with a repository class, a spinner and friendly errors: #### More - [Live data](https://albakore.github.io/pyzed/showcase/apps/#live-data): a price ticker fed by an async generator. - [Glass control center](https://albakore.github.io/pyzed/showcase/glass-dashboard/): a dashboard with frosted-glass cards over a moving shader. - [Integrations](https://albakore.github.io/pyzed/integrations/overview/): FastAPI, Django, SQLModel, httpx and more, each with a desktop app. **About this page:** The blocks are regenerated from `examples/blocks` and drawn in both themes by the docs tooling, so the captures match the current code. ### Shaders and effects > GPU effects in Python desktop apps: eleven shader graphs made of nodes in Pyzed Design (plasma, fire, nebula, synthwave, radar...), and the glass, bloom, glow, blur and animation effects every widget has. Source: https://albakore.github.io/pyzed/showcase/shaders/ Pyzed draws everything with the GPU, so a desktop app can have what games have: moving backgrounds, light that spills out of bright things, frosted glass. There are two layers to it: - **Shaders**: a small program that the graphics card runs for every pixel of a widget, every frame. In Pyzed you make them as **graphs of nodes** in the Shader view of [Pyzed Design](https://albakore.github.io/pyzed/shading/), saved as `.pzshad` files, or in Python with `pyzed.shading.Graph`. No shader language to learn. - **Effects**: `glass`, `bloom`, `glow`, `blur`, rotation and animations, which any widget has through `widget.style(...)` and `widget.animate(...)`. #### The shader gallery Six graphs side by side, with a slider that changes the `speed` variable of all of them at once. Variables (uniforms) are just values, so changing them never recompiles the shader. ```python from pyzed import shading plasma = shading.load("shaders/plasma.pzshad") # compile once pz.shader(plasma, uniforms={"speed": self.speed}, # variables by name, from your state width=340, height=210, fallback=pz.Gradient(["#7c3aed", "#ec4899"])) # shown where shaders are not available ```
Full code: examples/showcase/shaders.py `examples/showcase/shaders.py`: ```py """A gallery of shader graphs: plasma, a ripple that follows the pointer, a tunnel, a CRT screen, Voronoi cells and a fire. Each is a ``.pzshad`` file in ``shaders/`` made of nodes (no WGSL to write): open one in Pyzed Design (``pyzed design shaders/fire.pzshad``) to see and change its graph. python examples/showcase/shaders.py ``pz.shader`` compiles the graph and runs it on the GPU for every pixel, every frame:: pz.shader("shaders/plasma.pzshad", uniforms={"speed": 2.0}, width=340, height=210) ``uniforms`` sets the shader's mutable variables by name; the slider below changes ``speed`` on every shader without recompiling. """ from pathlib import Path import pyzed as pz from pyzed import shading HERE = Path(__file__).parent / "shaders" NAMES = {"Plasma": "plasma", "Ripple": "ripple", "Tunnel": "tunnel", "CRT": "crt", "Voronoi": "voronoi", "Fire": "fire"} SHADERS = {name: shading.load(HERE / f"{file}.pzshad") for name, file in NAMES.items()} FALLBACKS = { "Plasma": pz.Gradient(["#7c3aed", "#ec4899", "#f59e0b"], angle=45), "Ripple": pz.Gradient(["#0c4a6e", "#0891b2"], angle=180), "Tunnel": pz.Gradient(["#1e1b4b", "#0e7490"], angle=135), "CRT": pz.Gradient(["#ef4444", "#22c55e", "#3b82f6"], angle=90), "Voronoi": pz.Gradient(["#111827", "#4c1d95"], angle=135), "Fire": pz.Gradient(["#000000", "#b91c1c", "#fbbf24"], angle=180), } TINTS = {"Ripple": "#7dd3fc", "Voronoi": "#a78bfa"} class Shaders(pz.App): title = "Shaders" size = (1140, 720) theme = pz.themes.ONE_DARK def __init__(self): self.speed = 1.0 def set_speed(self, value): self.speed = value def tile(self, name): shader = SHADERS[name] uniforms = {"speed": self.speed} if name in TINTS: uniforms["tint"] = TINTS[name] art = pz.shader(shader, uniforms=uniforms, width=340, height=210, fallback=FALLBACKS[name]) label = pz.row([ pz.text(name, size=14, weight="semibold", color="#ffffff"), pz.space(width="fill"), pz.text(f"{len(shader.graph.nodes)} nodes", size=11, color="#ffffff88", font="mono"), ], width=340, align="center") return pz.column([art, label], spacing=8).animate("lift", on="hover") def view(self): names = list(SHADERS) grid = pz.column([pz.row([self.tile(n) for n in names[i:i + 3]], spacing=20) for i in (0, 3)], spacing=22) header = pz.row([ pz.column([ pz.text("Shaders", size=28, weight="bold", color="#ffffff"), pz.text("Node graphs from Pyzed Design, running on the GPU", size=13, color="#ffffffaa"), ], spacing=2), pz.space(width="fill"), pz.text("Speed", size=13, color="#ffffffaa"), pz.slider(0.0, 3.0, self.speed, step=0.05, on_change=self.set_speed, width=200), pz.text(f"{self.speed:.2f}×", size=13, color="#ffffff", font="mono", width=48), ], spacing=12, align="center", width=1060) return pz.container(pz.column([header, pz.row(pz.stagger([grid], "fade_in_up"))], spacing=24), width="fill", height="fill", align_x="center", align_y="center").style( background="#0b0d14") if __name__ == "__main__": pz.run(Shaders) ```
#### Every graph All eleven graphs live in [`examples/showcase/shaders`](https://github.com/albakore/pyzed/tree/main/examples/showcase/shaders). Click one to see its file, or open it in the designer to see and change the nodes: `pyzed design examples/showcase/shaders/fire.pzshad`. .pzshad drawn with pz.shader." size="wide" /> speed.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/plasma.pzshad', image: plasma, tag: '28 nodes' }, { title: 'Ripple', text: 'Rings of waves around the pointer. Variables: speed, tint.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/ripple.pzshad', image: ripple, tag: 'Mouse' }, { title: 'Tunnel', text: 'A checkered tunnel rushing towards you. Variable: speed.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/tunnel.pzshad', image: tunnel, tag: '29 nodes' }, { title: 'CRT', text: 'Color bars on a curved screen, with scanlines and a rolling band. Variable: speed.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/crt.pzshad', image: crt, tag: '28 nodes' }, { title: 'Voronoi', text: 'Wandering cells with glowing borders. Variables: speed, tint.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/voronoi.pzshad', image: voronoi, tag: '14 nodes' }, { title: 'Fire', text: 'Fractal noise flames rising from the bottom. Variable: speed.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/fire.pzshad', image: fire, tag: '23 nodes' }, { title: 'Nebula', text: 'Warped noise clouds and twinkling stars; the HUD\'s backdrop. Variable: drift.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/nebula.pzshad', image: nebula, tag: '40 nodes' }, { title: 'Radar', text: 'A sweep with a fading trail, rings and a cross; the HUD\'s minimap. Variables: accent, sweep.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/radar.pzshad', image: radar, tag: '41 nodes' }, { title: 'Embers', text: 'Sparks drifting up from a warm glow; the inventory\'s backdrop. Variable: rise.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/embers.pzshad', image: embers, tag: '100 nodes' }, { title: 'Synthwave', text: 'Dusk sky, stars, a striped sun and a grid that leans towards the pointer; the main menu\'s backdrop.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/synthwave.pzshad', image: synthwave, tag: 'Mouse' }, { title: 'Blobs', text: 'Soft color blobs drifting slowly, made to sit behind glass panels. Variable: speed.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/blobs.pzshad', image: blobs, tag: '74 nodes' }, ]} /> **Make your own:** Start from a template in Pyzed Design (Ctrl+N → **Shader**) and follow [A shader effect from the Shader view](https://albakore.github.io/pyzed/tutorials/intermediate/shader-effect/), or build one node by node in Python with [A custom shader graph](https://albakore.github.io/pyzed/tutorials/advanced/shader-graph/). The [Shading](https://albakore.github.io/pyzed/shading/) section documents every node. #### Effects on any widget Rotation, frosted glass, bloom and blur, applied with `style(...)` and animated. The buttons at the bottom animate blur and size; size, padding and font size reflow the layout as they move. ```python card.style(rotate=-8) # turn it card.style(glass=True) # a tinted panel that blurs what is behind it card.style(bloom=("#fbbf24", 0.8)) # light around its bright parts card.style(blur=4) # blur the widget itself spinner.animate("spin") # a looping preset ```
Full code: examples/effects.py `examples/effects.py`: ```py """Effects drawn by the renderer: rotation, blur, backdrop blur, glass and bloom, animated. python examples/effects.py - ``style(rotate=-8)`` turns a widget, ``blur=4`` blurs it and ``backdrop_blur=12`` blurs what is behind it; ``glass=True`` is a tinted panel with a backdrop blur. - ``bloom=(color, intensity)`` lights up the area around the bright parts of a widget. - ``animate("spin")`` and ``transition(rotate=..., blur=..., width=..., font_size=...)`` animate them; size, padding and font size reflow the layout as they move. """ import pyzed as pz from pyzed import ui BACKDROP = pz.Gradient(["#ff006e", "#3a86ff", "#06d6a0"], angle=45) class Effects(pz.App): title = "Effects" size = (900, 460) def __init__(self): self.focused = True self.wide = False def toggle_focus(self): self.focused = not self.focused def toggle_wide(self): self.wide = not self.wide def card(self, title, **style): return pz.container(pz.text(title, size=18), padding=20, width=150, height=100, align_x="center", align_y="center").style(radius=12, **style) def view(self): cards = pz.row([ self.card("Rotate", background="panel", rotate=-10).animate("tilt", on="hover"), self.card("Glass", glass=True), self.card("Bloom", background="#ffffff", color="#1f2937", bloom=("#ffbe0b", 1.0)), self.card("Blur", background="panel").transition(blur=0 if self.focused else 6), ], spacing=32, align="center") loader = pz.container(pz.icon("loader", size=28), padding=4).animate("spin") bar = pz.container(pz.text("Resizable", size=14), padding=10).style(background="panel", radius=8) \ .transition(width=420 if self.wide else 160, font_size=20 if self.wide else 14, duration=260, easing="ease_out_cubic") controls = pz.row([ ui.button("Blur" if self.focused else "Focus", self.toggle_focus, variant="outline"), ui.button("Resize", self.toggle_wide, variant="outline"), loader, bar, ], spacing=16, align="center") return pz.container(pz.column([cards, controls], spacing=48, align="center"), width="fill", height="fill", align_x="center", align_y="center").style(background=BACKDROP) if __name__ == "__main__": Effects().run() ```
#### Animations Every preset, played on click or on hover; glass, glow, bloom and opacity over a gradient; and transitions that animate a panel, a page and a wrong password. ```python button.animate("lift", on="hover").animate("shrink", on="press") # on events title.animate("fade_in_up") # when it appears panel.transition(x=0 if self.open else -260, opacity=1 if self.open else 0) # when a value changes pz.presence(page, key=self.page) # out and in when the key changes ```
Full code: examples/animations.py `examples/animations.py`: ```py """Animations and effects: presets, hover and press motion, transitions, presence and glass. python examples/animations.py - ``widget.animate("fade_in_up")`` plays a preset when the widget appears; ``on="hover"``, ``"press"``, ``"focus"``, ``"click"`` and ``"active"`` play it on events. - ``widget.transition(opacity=...)`` animates a value whenever it changes, like CSS. - ``pz.presence(content, key=...)`` animates content out and in when ``key`` changes. - ``style(glass=True)``, ``glow=``, ``bloom=`` and ``opacity=`` are visual effects. """ import pyzed as pz from pyzed import ui AURORA = pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=135) PAGES = ("Overview", "Activity", "Settings") class Animations(pz.App): title = "Animations" size = (980, 700) transition = "zoom" def __init__(self): self.page = 0 self.panel = True self.errors = 0 self.recording = False def toggle_panel(self): self.panel = not self.panel def next_page(self): self.page = (self.page + 1) % len(PAGES) def fail(self): self.errors += 1 def toggle_recording(self): self.recording = not self.recording def tile(self, name): preset = pz.animation.PRESETS[name] on = preset.on if preset.on != "mount" else "click" repeat = 1 if preset.category == "loop" and on == "click" else None return pz.column([ pz.text(name.replace("_", " ")).style(weight="semibold"), pz.text(f"{preset.category} · {on}").style(size=11, color="text_muted"), ], spacing=2).style(background="elevated", border=(1, "border"), radius=8, padding=(10, 12), width=150).animate(name, on=on, repeat=repeat) def effects(self): card = lambda label, **style: pz.column([ # noqa: E731 pz.text(label).style(weight="semibold", color="#ffffff"), pz.text(", ".join(f"{k}={v!r}" for k, v in style.items())).style(size=11, color="#ffffffaa"), ], spacing=4).style(padding=14, width=200, radius=12, **style) return pz.row([ card("Glass", glass=True), card("Glow", glow=("#22d3ee", 18), background="#0b1220"), card("Bloom", bloom=("#f472b6", 0.8), background="#1a0b16"), card("Opacity", opacity=0.55, background="#0b1220", border=(1, "#ffffff33")), ], spacing=18).style(padding=24, background=AURORA, radius=14, width="fill") def view(self): tiles = [self.tile(n) for n in ("pulse", "shake", "bounce", "tada", "lift", "grow", "glow", "bloom", "float", "glow_pulse")] side = pz.column([ pz.text("Side panel").style(weight="semibold"), pz.text("transition(x=..., opacity=...)").style(size=11, color="text_muted"), ], spacing=4).style(background="elevated", radius=8, padding=12, width=220).transition( x=0 if self.panel else -24, opacity=1 if self.panel else 0, duration=220, easing="ease_out_cubic") page = pz.presence( pz.text(PAGES[self.page]).style(size=20, weight="bold"), key=self.page, enter="fade_in_right", exit="fade_out_left", id="pages") controls = pz.row([ ui.button("Toggle panel", self.toggle_panel), ui.button("Next page", self.next_page), ui.button("Wrong password", self.fail).animate("shake", key=self.errors), ui.button("Record" if not self.recording else "Stop", self.toggle_recording).animate( "glow_pulse", active=self.recording).animate("shrink"), ], spacing=10) return pz.column([ pz.text("Animations").style(size=24, weight="bold").animate("fade_in_down"), pz.row(pz.stagger(tiles, "fade_in_up", step=40), spacing=10, wrap=True, line_spacing=10), self.effects().animate("fade_in_up", delay=200), controls, pz.row([side, page], spacing=20), ], spacing=18).style(padding=24, background="background", width="fill", height="fill") if __name__ == "__main__": pz.run(Animations) ```
#### Styles The CSS-like side: borders, radius, gradients, shadows, typography, named styles with `pz.define_style`, and paints with an opacity mask. More in [Animation and effects](https://albakore.github.io/pyzed/animation/) and the [Theming](https://albakore.github.io/pyzed/theming/) section. #### Where they are used - [Game HUD](https://albakore.github.io/pyzed/showcase/hud/): the Nebula and Radar graphs, glass panels and bloom. - [Game main menu](https://albakore.github.io/pyzed/showcase/main-menu/): the Synthwave graph and a glowing title. - [RPG inventory](https://albakore.github.io/pyzed/showcase/inventory/): the Embers graph, bloom by rarity and animated stats. - [Glass control center](https://albakore.github.io/pyzed/showcase/glass-dashboard/): the Blobs graph behind frosted-glass cards. **When there is no GPU:** On machines without a supported graphics card (some virtual machines and remote desktops) shaders cannot run, and Pyzed draws the widget's `fallback` instead, a gradient or a color. Always give one to shaders that sit behind your content. ### Game HUD > A game heads-up display in Python: health and mana bars, a radar minimap drawn by a shader, an ability bar with cooldowns and glass panels. Source: https://albakore.github.io/pyzed/showcase/hud/ A complete heads-up display over an animated space nebula. Everything is ordinary Pyzed widgets: the nebula and the radar sweep are shader graphs (`.pzshad` files made of nodes in Pyzed Design), the panels are `glass`, and the ready ability and the level-up banner use `bloom`. (Image: Game HUD) Press 1 to 6 (or click a slot) to cast an ability. #### How it is made ##### Shader backdrop The nebula is a graph of domain-warped noise, redrawn every frame by the GPU. The radar sweep is a second graph inside a round glass panel; its `accent` variable sets the color from Python. ```python SHADERS = Path(__file__).parent / "shaders" pz.shader(SHADERS / "nebula.pzshad", width="fill", height="fill", fallback=pz.Gradient(["#05040f", "#2a0c46", "#0b3a4a"], angle=135)) pz.shader(SHADERS / "radar.pzshad", uniforms={"accent": "#34d399"}, width=176, height=176) ``` See both graphs in [Shaders and effects](https://albakore.github.io/pyzed/showcase/shaders/#every-graph). ##### Glass panels Each panel blurs what is behind it and tints it. ```python pz.row([...]).style(glass={"tint": "#0f172a", "strength": 0.35, "blur": 18}, radius=16) ``` ##### Light and motion Low health pulses, the ultimate ability blooms and pulses when it is ready. ```python health.animate("glow_pulse", repeat="infinite") # low health tile.animate("pulse", repeat="infinite", duration=1400) # the ultimate, when ready ``` ##### Cooldowns Press 1 to 6: `pz.every` counts the cooldowns down and the overlay shrinks with them. ```python def subscriptions(self): return [pz.every(0.1, self.tick, key="cooldowns")] if self.cooldowns else [] ``` #### Try this - Change the radar's `accent` to `"#f472b6"` and its `sweep` variable to `1.0`. - Open `examples/showcase/shaders/nebula.pzshad` in Pyzed Design and change the colors of its Mix node. - Make every ability's cooldown twice as long. #### Full code `examples/showcase/hud.py`: ```py """A game HUD: health and mana bars, a radar minimap, an ability bar and a quest tracker. python examples/showcase/hud.py - The space background and the minimap's radar sweep are shader graphs made of nodes in Pyzed Design (``shaders/nebula.pzshad`` and ``shaders/radar.pzshad``): ``pz.shader`` runs them on the GPU for every pixel, every frame. - Panels are ``glass=True`` (a tinted panel with a real backdrop blur), the ready ability and the level-up banner use ``bloom``, low health pulses with ``animate("glow_pulse")``. - Press 1 to 6 (or click a slot) to cast an ability; it goes on cooldown. """ from pathlib import Path import pyzed as pz SHADERS = Path(__file__).parent / "shaders" ABILITIES = ( ("sword", "Slash", 2.0), ("flame", "Fireball", 6.0), ("shield", "Ward", 10.0), ("zap", "Chain bolt", 8.0), ("wind", "Dash", 4.0), ("sparkles", "Ultimate", 30.0), ) QUESTS = ( ("Reach the Sunken Gate", True), ("Collect 3 star shards", False), ("Defeat the Warden", False), ) GOLD = "#fbbf24" CYAN = "#22d3ee" class Hud(pz.App): title = "Game HUD" size = (1280, 720) theme = pz.themes.ONE_DARK def __init__(self): self.health = 0.28 self.mana = 0.72 self.cooldowns = {1: 3.0} # slot -> seconds left self.casts = 0 def subscriptions(self): return [pz.every(0.1, self.tick, key="cooldowns")] if self.cooldowns else [] def tick(self): self.cooldowns = {slot: left - 0.1 for slot, left in self.cooldowns.items() if left > 0.1} def cast(self, slot): if slot in self.cooldowns: return self.cooldowns[slot] = ABILITIES[slot][2] self.mana = max(0.0, self.mana - 0.08) self.casts += 1 def on_key(self, combo): if combo in ("1", "2", "3", "4", "5", "6"): self.cast(int(combo) - 1) return True # -- pieces --------------------------------------------------------------------- def bar(self, value, colors, glow, width=260, height=14, label=None): fill = pz.container(None, width=max(height, width * value), height=height).style( background=pz.Gradient(colors, angle=90), radius=height / 2, glow=(glow, 10)) track = pz.container(fill, width=width, height=height).style( background="#ffffff14", radius=height / 2, border=(1, "#ffffff22")) if label is None: return track return pz.stack([track, pz.container(pz.text(label, size=10, weight="bold", color="#ffffffdd"), width=width, height=height, align_x="center", align_y="center")]) def player(self): portrait = pz.container(pz.icon("swords", size=34, color="#ffffff"), width=72, height=72, align_x="center", align_y="center").style( background=pz.Gradient(["#7c3aed", "#db2777"], angle=135), radius=36, border=(2, "#ffffffaa"), bloom=("#a855f7", 0.5)) health = self.bar(self.health, ["#991b1b", "#ef4444"], "#ef4444", label=f"{round(self.health * 1840)} / 1840") if self.health < 0.3: health = health.animate("glow_pulse", repeat="infinite") stats = pz.column([ pz.row([pz.text("Aria", size=18, weight="bold", color="#ffffff"), pz.text("Lv 27 · Stormblade", size=12, color="#ffffffaa")], spacing=8, align="end"), health, self.bar(self.mana, ["#1d4ed8", "#38bdf8"], "#38bdf8", label=f"{round(self.mana * 960)} / 960"), self.bar(0.64, ["#b45309", GOLD], GOLD, height=5), ], spacing=6) return pz.row([portrait, stats], spacing=14, align="center").style( glass={"tint": "#0f172a", "strength": 0.35, "blur": 18}, radius=16, padding=14) def minimap(self): radar = pz.shader(SHADERS / "radar.pzshad", uniforms={"accent": "#34d399"}, width=176, height=176) blips = pz.stack([ pz.container(pz.container(None, width=8, height=8).style(background="#f87171", radius=4, bloom=("#f87171", 1.0)), width=176, height=176, padding=(46, 0, 0, 120)), pz.container(pz.container(None, width=8, height=8).style(background=GOLD, radius=4, bloom=(GOLD, 1.0)), width=176, height=176, padding=(118, 0, 0, 52)), pz.container(pz.icon("navigation", size=16, color="#ffffff"), width=176, height=176, align_x="center", align_y="center"), ]) dial = pz.stack([radar, blips], width=176, height=176).style( radius=88, glass={"tint": "#022c22", "strength": 0.25, "blur": 14}, clip=True) return pz.column([ dial, pz.text("Sunken Coast · 412, -88", size=11, color="#ffffffaa"), ], spacing=6, align="center") def objective(self): return pz.row([ pz.icon("compass", size=16, color=GOLD), pz.text("Reach the Sunken Gate", size=14, weight="semibold", color="#ffffff"), pz.text("120 m", size=12, color="#ffffffaa"), ], spacing=10, align="center").style(glass={"tint": "#0f172a", "strength": 0.3}, radius=20, padding=(8, 16)).animate("fade_in_down") def quests(self): rows = [pz.text("QUESTS", size=11, weight="bold", color=GOLD)] for name, done in QUESTS: rows.append(pz.row([ pz.icon("circle-check" if done else "circle", size=14, color="#34d399" if done else "#ffffff88"), pz.text(name, size=13, color="#ffffff88" if done else "#ffffff"), ], spacing=8, align="center")) return pz.column(rows, spacing=8).style(glass={"tint": "#0f172a", "strength": 0.3}, radius=14, padding=14, width=230) def slot(self, index): icon, name, cooldown = ABILITIES[index] left = self.cooldowns.get(index) ready = left is None face = pz.container(pz.icon(icon, size=28, color="#ffffff" if ready else "#ffffff55"), width=64, height=64, align_x="center", align_y="center") layers = [face] if not ready: shade = 64 * left / cooldown layers.append(pz.container(pz.container(None, width=64, height=shade).style(background="#000000aa"), width=64, height=64, align_y="end")) layers.append(pz.container(pz.text(f"{left:.0f}" if left >= 1 else f"{left:.1f}", size=18, weight="bold", color="#ffffff"), width=64, height=64, align_x="center", align_y="center")) layers.append(pz.container(pz.text(str(index + 1), size=10, weight="bold", color="#ffffffcc"), width=64, height=64, padding=(3, 6), align_x="end")) style = dict(radius=12, clip=True, border=(1, "#ffffff33")) if ready and index == 5: style.update(background=pz.Gradient(["#7c2d12", "#ea580c"], angle=135), bloom=(GOLD, 0.9), border=(2, GOLD)) elif ready: style.update(background=pz.Gradient(["#1e293b", "#334155"], angle=135)) else: style.update(background="#0f172a") tile = pz.stack(layers, width=64, height=64).style(**style) tile = pz.mouse_area(tile, on_press=lambda: self.cast(index)) tile = tile.animate("shrink", on="press") if ready and index == 5: tile = tile.animate("pulse", repeat="infinite", duration=1400) return pz.column([tile, pz.text(name, size=10, color="#ffffffaa")], spacing=4, align="center") def abilities(self): return pz.row([self.slot(i) for i in range(len(ABILITIES))], spacing=10).style( glass={"tint": "#020617", "strength": 0.4, "blur": 16}, radius=18, padding=(12, 16, 8, 16)) def log(self): lines = ( ("You hit Tide Wraith for 312", "#ffffff"), ("Critical! Chain bolt 1,204", GOLD), ("Tide Wraith hits you for 188", "#f87171"), ("Ward absorbed 140 damage", CYAN), ) return pz.column([pz.text(t, size=12, color=c, font="mono") for t, c in lines], spacing=4).style( glass={"tint": "#020617", "strength": 0.3}, radius=12, padding=12, width=300) def level_up(self): return pz.column([ pz.text("LEVEL UP", size=40, weight="black", color="#fff7d6").style(bloom=(GOLD, 1.0)), pz.text("Stormblade · Level 27", size=14, color="#fde68a"), ], spacing=2, align="center").animate("pop_in").animate("float", repeat="infinite") def view(self): def at(widget, x, y, pad): return pz.container(widget, width="fill", height="fill", align_x=x, align_y=y, padding=pad) return pz.stack([ pz.shader(SHADERS / "nebula.pzshad", width="fill", height="fill", fallback=pz.Gradient(["#05040f", "#2a0c46", "#0b3a4a"], angle=135)), at(self.player(), "start", "start", 20), at(self.objective(), "center", "start", 24), at(self.minimap(), "end", "start", 20), at(self.quests(), "end", "center", 20), at(self.level_up(), "center", "center", (0, 0, 120, 0)), at(self.log(), "start", "end", 20), at(self.abilities(), "center", "end", 20), ]) if __name__ == "__main__": pz.run(Hud) ``` ### Game main menu > A synthwave game main menu in Python with a shader backdrop that follows the mouse, a glowing title and glass buttons that lift on hover. Source: https://albakore.github.io/pyzed/showcase/main-menu/ A title screen in the style of a synthwave racer. The sun, the stars and the endless grid are one shader graph, `shaders/synthwave.pzshad`, that scrolls with time and leans towards the pointer. (Image: Game main menu) Use ↑ and ↓ (or W and S) to choose. #### How it is made ##### Interactive shader The graph has a **Mouse** node: the pointer position over the widget, from 0 to 1. A few math nodes move the horizon a little with it, so the scene leans as you move. In Python it is one widget behind the content: ```python pz.stack([ pz.shader(SHADER, width="fill", height="fill", fallback=pz.Gradient(["#08021a", "#4a0d5e", "#d9468f"], angle=180)), content, ]) ``` ##### Glowing title `bloom` on a bare text lights it from its own strokes. ```python pz.text("NEON DRIFT", size=72, weight="black").style(bloom=("#f472b6", 1.0)) ``` ##### Menu motion Buttons lift on hover, shrink on press and the selected one slides with a transition. ```python button.animate("lift", on="hover").animate("shrink", on="press").transition(x=6 if selected else 0) ``` #### Try this - Add an item to the menu and give it its own action. - Change the title's `bloom` color and intensity. - Open `shaders/synthwave.pzshad` in Pyzed Design and change the sun's colors. #### Full code `examples/showcase/main_menu.py`: ```py """A game's main menu: a synthwave shader backdrop, a glowing title and glass buttons. python examples/showcase/main_menu.py - The sun, the grid and the stars are one shader graph, ``shaders/synthwave.pzshad``, made of nodes in Pyzed Design: it scrolls with Time and follows the pointer with Mouse. - The title uses ``bloom``; the menu is a ``glass`` panel whose buttons lift and glow on hover (``animate("lift", on="hover")``) and play ``shrink`` on press. - The arrow keys (or W and S) move the selection. """ from pathlib import Path import pyzed as pz SHADER = Path(__file__).parent / "shaders" / "synthwave.pzshad" ITEMS = ( ("play", "Continue", "Chapter 4 · The Neon Pier"), ("plus", "New game", "Start a fresh run"), ("users", "Multiplayer", "3 friends online"), ("settings", "Settings", "Graphics, audio, controls"), ("log-out", "Quit", "See you soon"), ) PINK = "#f472b6" class MainMenu(pz.App): title = "Main menu" size = (1280, 720) theme = pz.themes.ONE_DARK def __init__(self): self.selected = 0 def pick(self, index): self.selected = index def on_key(self, combo): if combo in ("down", "s"): self.selected = (self.selected + 1) % len(ITEMS) return True if combo in ("up", "w"): self.selected = (self.selected - 1) % len(ITEMS) return True def item(self, index): icon, label, hint = ITEMS[index] selected = index == self.selected body = pz.row([ pz.icon(icon, size=20, color="#ffffff" if selected else "#ffffffbb"), pz.column([ pz.text(label, size=18, weight="bold" if selected else "semibold", color="#ffffff"), pz.text(hint, size=12, color="#ffffffaa"), ], spacing=1), ], spacing=14, align="center") style = dict(radius=12, padding=(12, 18), width=330) if selected: style.update(background=pz.Gradient(["#db2777cc", "#7c3aedcc"], angle=90), border=(1, "#ffffff66"), bloom=(PINK, 0.6)) else: style.update(background="#ffffff0d", border=(1, "#ffffff1a")) button = pz.mouse_area(body.style(**style), on_press=lambda: self.pick(index)) return button.animate("lift", on="hover").animate("shrink", on="press").transition( x=6 if selected else 0, duration=160, easing="ease_out_cubic") def view(self): title = pz.column([ pz.text("NEON DRIFT", size=72, weight="black", color="#ffe4f3").style(bloom=(PINK, 1.0)), pz.text("A pyzed showcase", size=16, color="#fbcfe8").style(opacity=0.8), ], spacing=0).animate("fade_in_down", duration=600) menu = pz.column([self.item(i) for i in range(len(ITEMS))], spacing=10).style( glass={"tint": "#1e0b2e", "strength": 0.35, "blur": 22}, radius=20, padding=16) footer = pz.row([ pz.text("v1.4.2", size=12, color="#ffffff88", font="mono"), pz.space(width="fill"), pz.text("Press ↑ ↓ to choose", size=12, color="#ffffffaa").animate( "blink", repeat="infinite", duration=2200), ], width="fill") content = pz.column([ title, pz.row(pz.stagger([menu], "fade_in_left", step=80)), pz.space(height="fill"), footer, ], spacing=28, width="fill", height="fill", padding=(48, 56)) return pz.stack([ pz.shader(SHADER, width="fill", height="fill", fallback=pz.Gradient(["#08021a", "#4a0d5e", "#d9468f"], angle=180)), content, ]) if __name__ == "__main__": pz.run(MainMenu) ``` ### RPG inventory > An RPG inventory screen in Python where item rarity glows and blooms, with an animated glass details panel over a shader of embers. Source: https://albakore.github.io/pyzed/showcase/inventory/ An inventory where rarity is light: rare items glow, epic and legendary items bloom, and legendary ones pulse. Clicking an item swaps the glass details panel with `presence`, and its stat bars grow with `transition(width=...)`. (Image: RPG inventory) The embers behind it are the `embers.pzshad` graph; see it in [Shaders and effects](https://albakore.github.io/pyzed/showcase/shaders/#every-graph). #### How it is made ##### Rarity as light Glow for rare, bloom for epic and legendary. ```python style.update(bloom=(color, 0.8)) tile.animate("glow_pulse", repeat="infinite", duration=2000) ``` ##### Animated stats Bar widths are layout properties, so they animate and push nothing out of place. ```python bar.transition(width=240 * value, duration=320, easing="ease_out_cubic") ``` ##### Panel swaps `pz.presence` plays the old panel out and the new one in. ```python pz.presence(panel, key=self.selected, enter="fade_in_right", exit="fade_out") ``` #### Try this - Add an item with a new rarity, and a color and light for it in the rarity table. - Make the stat bars grow slower by raising the transition's `duration`. - Change the `rise` variable of the embers shader to make the sparks faster. #### Full code `examples/showcase/inventory.py`: ```py """An RPG inventory: a grid of items whose rarity glows, and a glass details panel. python examples/showcase/inventory.py - Rarity is light: rare items ``glow``, epic and legendary ones ``bloom`` (and legendary ones pulse with ``animate("glow_pulse")``). - The details panel is ``glass`` over an ember backdrop, a shader graph made of nodes in Pyzed Design (``shaders/embers.pzshad``); stat bars and the selected slot animate with ``transition``. - Click an item to inspect it. """ from pathlib import Path import pyzed as pz SHADER = Path(__file__).parent / "shaders" / "embers.pzshad" RARITY = { "common": ("#9ca3af", None), "rare": ("#60a5fa", "glow"), "epic": ("#c084fc", "bloom"), "legendary": ("#fb923c", "bloom"), } ITEMS = ( ("sword", "Dawnbreaker", "legendary", "Weapon", {"Attack": 0.92, "Speed": 0.64, "Crit": 0.71}), ("shield", "Aegis of Tides", "epic", "Off-hand", {"Armor": 0.81, "Block": 0.66, "Weight": 0.40}), ("wand", "Ember Wand", "rare", "Weapon", {"Attack": 0.58, "Speed": 0.80, "Crit": 0.35}), ("gem", "Star Shard", "epic", "Material", {"Purity": 0.88, "Power": 0.62, "Value": 0.74}), ("flask-conical", "Health Potion", "common", "Consumable", {"Heal": 0.45, "Duration": 0.10, "Value": 0.08}), ("scroll", "Scroll of Return", "common", "Consumable", {"Range": 0.30, "Value": 0.12, "Weight": 0.05}), ("crown", "Crown of Ash", "legendary", "Helm", {"Armor": 0.55, "Wisdom": 0.95, "Charm": 0.86}), ("key", "Old Key", "common", "Quest", {"Value": 0.02, "Weight": 0.04, "Rust": 0.90}), ("feather", "Phoenix Plume", "rare", "Material", {"Power": 0.70, "Purity": 0.52, "Value": 0.61}), ("bow-arrow", "Windstring", "rare", "Weapon", {"Attack": 0.66, "Speed": 0.87, "Crit": 0.50}), ("shirt", "Silk Tunic", "common", "Chest", {"Armor": 0.20, "Speed": 0.60, "Weight": 0.10}), ("apple", "Golden Apple", "epic", "Consumable", {"Heal": 0.95, "Duration": 0.60, "Value": 0.70}), ) #: Each rarity's slot color. TINT = {"common": "#1f1d24", "rare": "#16233a", "epic": "#2a1a3d", "legendary": "#3a2112"} COLUMNS, ROWS = 6, 4 SLOT = 76 class Inventory(pz.App): title = "Inventory" size = (1180, 640) theme = pz.themes.ONE_DARK def __init__(self): self.selected = 0 def select(self, index): self.selected = index def slot(self, index): if index >= len(ITEMS): return pz.container(None, width=SLOT, height=SLOT).style( background="#ffffff08", radius=12, border=(1, "#ffffff12")) icon, name, rarity, _, _ = ITEMS[index] color, light = RARITY[rarity] selected = index == self.selected # A plain color, not a gradient: the GPU renderer draws halos only around solid boxes. style = dict(radius=12, background=TINT[rarity], border=(2 if selected else 1, color if selected else color + "66")) if light == "glow": style.update(glow=(color + "88", 10)) elif light == "bloom": style.update(bloom=(color, 0.55 if rarity == "epic" else 0.8)) tile = pz.container(pz.icon(icon, size=32, color="#ffffff"), width=SLOT, height=SLOT, align_x="center", align_y="center").style(**style) if rarity == "legendary": tile = tile.animate("glow_pulse", repeat="infinite", duration=2000) tile = pz.mouse_area(tile, on_press=lambda: self.select(index)) return tile.animate("grow", on="hover").animate("shrink", on="press") def grid(self): rows = [pz.row([self.slot(r * COLUMNS + c) for c in range(COLUMNS)], spacing=10) for r in range(ROWS)] gold = pz.row([pz.icon("coins", size=16, color="#fbbf24"), pz.text("12,480", size=14, weight="bold", color="#fde68a"), pz.space(width="fill"), pz.text(f"{len(ITEMS)} / {COLUMNS * ROWS}", size=12, color="#ffffff88")], spacing=8, align="center", width=COLUMNS * SLOT + (COLUMNS - 1) * 10) return pz.column([ pz.text("INVENTORY", size=13, weight="bold", color="#fdba74"), pz.column(rows, spacing=10), gold, ], spacing=14).style(glass={"tint": "#140b0b", "strength": 0.35, "blur": 16}, radius=20, padding=20) def details(self): icon, name, rarity, kind, stats = ITEMS[self.selected] color, _ = RARITY[rarity] bars = [] for label, value in stats.items(): bars.append(pz.column([ pz.row([pz.text(label, size=12, color="#ffffffbb"), pz.space(width="fill"), pz.text(str(round(value * 100)), size=12, weight="bold", color="#ffffff")], width=240), pz.container(pz.container(None, height=8).style( background=pz.Gradient([color + "88", color], angle=90), radius=4).transition( width=240 * value, duration=320, easing="ease_out_cubic"), width=240).style(background="#ffffff14", radius=4), ], spacing=4)) hero = pz.container(pz.icon(icon, size=56, color="#ffffff").style(bloom=(color, 0.9)), width=240, height=150, align_x="center", align_y="center").style( background=pz.Gradient([color + "44", "#00000000"], angle=180), radius=16) panel = pz.column([ hero, pz.text(name, size=22, weight="bold", color="#ffffff"), pz.row([pz.text(rarity.upper(), size=11, weight="bold", color=color), pz.text("·", size=11, color="#ffffff66"), pz.text(kind, size=11, color="#ffffffaa")], spacing=6), *bars, pz.row([ pz.text("Equip", size=13, weight="bold", color="#1c1917").style( background=color, radius=8, padding=(8, 18)).animate("lift", on="hover"), pz.text("Drop", size=13, color="#ffffffcc").style( background="#ffffff14", radius=8, padding=(8, 18), border=(1, "#ffffff22")), ], spacing=10), ], spacing=12, width=240) return pz.presence(panel.style(glass={"tint": "#1c1012", "strength": 0.3, "blur": 18}, radius=20, padding=20), key=self.selected, enter="fade_in_right", exit="fade_out", id="details") def view(self): body = pz.row([self.grid(), self.details()], spacing=24, align="start") return pz.stack([ pz.shader(SHADER, width="fill", height="fill", fallback=pz.Gradient(["#0f0707", "#2a0e05"], angle=180)), pz.container(body, width="fill", height="fill", align_x="center", align_y="center"), ]) if __name__ == "__main__": pz.run(Inventory) ``` ### Glass control center > A professional frosted-glass dashboard in Python: music player, smart lights, climate and schedule cards with real backdrop blur over moving color. Source: https://albakore.github.io/pyzed/showcase/glass-dashboard/ A smart-home control center in the frosted-glass style of modern operating systems. The colored blobs behind the cards are a shader graph (`shaders/blobs.pzshad`), so the blur you see through each card moves with them. (Image: Glass control center) #### How it is made ##### Real backdrop blur `glass` blurs what is behind the card, not a fake gradient. ```python GLASS = {"tint": "#ffffff", "strength": 0.10, "blur": 28} card.style(glass=GLASS, radius=22, padding=20) ``` ##### Living details The album cover spins while music plays; lit bulbs bloom. ```python cover.animate("spin", duration=12000) ``` ##### Real controls Sliders and switches update the state like in any Pyzed app. ```python pz.slider(0, 100, self.volume, on_change=self.set_volume) ``` #### Try this - Change the `tint` and `strength` in `GLASS` and see how much of the background shows through. - Turn every light on and watch the bloom. - Swap the blobs for another graph from [Shaders and effects](https://albakore.github.io/pyzed/showcase/shaders/#every-graph), such as `nebula.pzshad`. **Tip:** Glass looks best over something that moves and has color. Over a flat background it is just a tinted panel. #### Full code `examples/showcase/glass_dashboard.py`: ```py """A frosted-glass control center: music, lights, climate and a schedule over moving color. python examples/showcase/glass_dashboard.py - The background is a shader graph of soft color blobs that drift with time (``shaders/blobs.pzshad``, made of nodes in Pyzed Design). - Every card is ``glass``: a tinted panel that blurs what moves behind it (``backdrop_blur``). - Switches, sliders and the play button update the cards; values move with ``transition``. """ from pathlib import Path import pyzed as pz SHADER = Path(__file__).parent / "shaders" / "blobs.pzshad" from pyzed import ui GLASS = {"tint": "#ffffff", "strength": 0.10, "blur": 28} WHITE = "#ffffff" MUTED = "#ffffffaa" class Dashboard(pz.App): title = "Control center" size = (1180, 700) theme = pz.themes.ONE_DARK def __init__(self): self.playing = True self.volume = 64.0 self.lights = {"Living room": True, "Kitchen": False, "Studio": True} self.brightness = 72.0 self.temperature = 21.5 def toggle_play(self): self.playing = not self.playing def set_volume(self, value): self.volume = value def toggle_light(self, name): self.lights[name] = not self.lights[name] def set_brightness(self, value): self.brightness = value def warmer(self): self.temperature = min(28.0, self.temperature + 0.5) def cooler(self): self.temperature = max(16.0, self.temperature - 0.5) def card(self, title, icon, body, width): head = pz.row([pz.icon(icon, size=16, color=WHITE), pz.text(title, size=13, weight="semibold", color=WHITE)], spacing=8, align="center") return pz.column([head, body], spacing=14).style(glass=GLASS, radius=22, padding=20, width=width) def music(self): cover = pz.container(pz.icon("disc-3", size=44, color="#ffffffdd"), width=112, height=112, align_x="center", align_y="center").style( background=pz.Gradient(["#f472b6", "#8b5cf6", "#22d3ee"], angle=135), radius=18, rotate=0, bloom=("#c084fc", 0.4)) if self.playing: cover = cover.animate("spin", duration=12000) controls = pz.row([ pz.icon("skip-back", size=20, color=WHITE), pz.mouse_area(pz.container(pz.icon("pause" if self.playing else "play", size=22, color="#111827"), width=48, height=48, align_x="center", align_y="center").style( background=WHITE, radius=24, glow=("#ffffff88", 14)), on_press=self.toggle_play).animate( "shrink", on="press"), pz.icon("skip-forward", size=20, color=WHITE), ], spacing=22, align="center") info = pz.column([ pz.text("Midnight City", size=20, weight="bold", color=WHITE), pz.text("Neon Avenue · Afterglow", size=13, color=MUTED), pz.container(pz.container(None, height=4).style(background=WHITE, radius=2).transition( width=230 * (0.42 if self.playing else 0.41), duration=400), width=230).style( background="#ffffff33", radius=2), pz.row([pz.text("1:48", size=11, color=MUTED), pz.space(width="fill"), pz.text("4:16", size=11, color=MUTED)], width=230), controls, ], spacing=8) volume = pz.row([ pz.icon("volume-1", size=16, color=MUTED), pz.slider(0, 100, self.volume, on_change=self.set_volume, width="fill", accent=WHITE, bg="#ffffff33"), pz.icon("volume-2", size=16, color=MUTED), ], spacing=10, align="center") return self.card("Now playing", "music", pz.column([pz.row([cover, info], spacing=20, align="center"), volume], spacing=16), 440) def lights_card(self): rows = [] for name, on in self.lights.items(): rows.append(pz.row([ pz.container(pz.icon("lightbulb", size=16, color="#1f2937" if on else MUTED), width=34, height=34, align_x="center", align_y="center").style( background="#fde68a" if on else "#ffffff1a", radius=17, bloom=("#fbbf24", 0.6) if on else None), pz.column([pz.text(name, size=14, weight="semibold", color=WHITE), pz.text(f"{round(self.brightness)}%" if on else "Off", size=11, color=MUTED)], spacing=1, width="fill"), ui.switch(on, lambda _v, name=name: self.toggle_light(name)), ], spacing=12, align="center")) rows.append(pz.slider(0, 100, self.brightness, on_change=self.set_brightness, width="fill", accent="#fde68a", bg="#ffffff33")) return self.card("Lights", "sun", pz.column(rows, spacing=12), 330) def climate(self): ring = pz.column([ pz.text(f"{self.temperature:.1f}°", size=44, weight="bold", color=WHITE).transition( scale=1.0, duration=200), pz.text("Heating to 22°", size=12, color=MUTED), ], spacing=0, align="center") buttons = pz.row([ pz.mouse_area(pz.container(pz.icon("minus", size=18, color=WHITE), width=44, height=44, align_x="center", align_y="center").style( background="#ffffff1f", radius=22), on_press=self.cooler).animate("shrink", on="press"), pz.mouse_area(pz.container(pz.icon("plus", size=18, color=WHITE), width=44, height=44, align_x="center", align_y="center").style( background="#ffffff1f", radius=22), on_press=self.warmer).animate("shrink", on="press"), ], spacing=16) dial = pz.container(ring, width=170, height=170, align_x="center", align_y="center").style( radius=85, border=(6, "#fb923c"), bloom=("#fb923c", 0.5), background="#ffffff0d") return self.card("Climate", "thermometer", pz.column([dial, buttons], spacing=14, align="center", width="fill"), 330) def schedule(self): events = ( ("09:30", "Design review", "#22d3ee"), ("12:00", "Lunch with Sam", "#a3e635"), ("15:00", "Ship 1.2", "#f472b6"), ("18:30", "Climbing", "#fbbf24"), ) rows = [pz.row([ pz.text(time, size=12, color=MUTED, font="mono", width=48), pz.container(None, width=4, height=28).style(background=color, radius=2, bloom=(color, 0.6)), pz.text(title, size=14, color=WHITE), ], spacing=12, align="center") for time, title, color in events] return self.card("Today", "calendar", pz.column(rows, spacing=10), 440) def header(self): return pz.row([ pz.column([pz.text("Good evening, Kim", size=28, weight="bold", color=WHITE), pz.text("Saturday · 3 rooms on · 21.5° inside", size=14, color=MUTED)], spacing=2), pz.space(width="fill"), pz.row([pz.icon("search", size=16, color=MUTED), pz.text("Search devices", size=13, color=MUTED)], spacing=8, align="center").style(glass=GLASS, radius=20, padding=(10, 16), width=240), ], align="center", width=1124) def view(self): grid = pz.row([ pz.column([self.music(), self.schedule()], spacing=20), self.lights_card(), self.climate(), ], spacing=20, align="start") content = pz.column([self.header(), pz.row(pz.stagger([grid], "fade_in_up", step=60))], spacing=24) return pz.stack([ pz.shader(SHADER, width="fill", height="fill", fallback=pz.Gradient(["#0b1026", "#5b1a4a", "#0f4c5c"], angle=135)), pz.container(content, width="fill", height="fill", align_x="center", align_y="center"), ]) if __name__ == "__main__": pz.run(Dashboard) ``` ## Games ### Making games > Learn to make 2D games in Python with Pyzed, from your first game loop to complete platformers, shooters and puzzles: a step-by-step learning path with animated examples, tips and three finished games. Source: https://albakore.github.io/pyzed/games/ Pyzed is a UI library, but everything a 2D game needs is already in it: a GPU-drawn `pz.canvas`, a loop that runs once per displayed frame, the keys held down right now, and the whole widget library to build menus and HUDs on top of the action. Every game on these pages is a complete, playable program in [`examples/games/`](https://github.com/albakore/pyzed/tree/main/examples/games), tested by the test suite on every change. #### Learn to make games A path from zero to complete games. Each step is a tutorial with a goal, the time it takes, what you need to know first, the full code and ideas to keep going. If you have never written a game, start at 1; if you have, jump to what you need. ```mermaid flowchart LR A["1 · Loop and keys"] --> B["2 · Sprites and animation"] --> C["3 · Collisions and physics"] C --> D["4 · Platformer"] C --> E["5 · Asteroids"] B --> F["6 · Puzzle: 2048"] D --> G["7 · Menus, HUD, settings"] E --> G F --> G G --> H["8 · Saves and sound"] ``` **Never programmed a game?:** Start with [1. Your first game](https://albakore.github.io/pyzed/games/first-game/): it explains every line, and the game is playable after the third step. [A game loop](https://albakore.github.io/pyzed/tutorials/advanced/game-loop/) (Dodge, in the Tutorials) is a good second read. #### Complete games Three bigger games to read, play and take apart. They grow in difficulty: - [Snake](https://albakore.github.io/pyzed/games/snake/): Grid movement on a fixed timer, a key queue, game over and restart. ~150 lines. - [Breakout](https://albakore.github.io/pyzed/games/breakout/): A 60 fps loop with pz.frames, held keys and the mouse, ball physics, particles and shake. - [Neon Invaders](https://albakore.github.io/pyzed/games/neon-invaders/): Pixel-art sprites, waves, power-ups, neon glow and a glass HUD with bloom. Run any game from the repository folder; change the file name for the others: #### The building blocks | What | API | Used for | | --- | --- | --- | | Frame loop | `pz.frames(callback)` in `subscriptions()` | `callback(dt)` once per frame (in sync with the screen); `dt` is the seconds since the last one. | | Fixed-rate timer | `pz.every(seconds, callback)` | Turn-based or grid games that step at a set pace (Snake). | | Key presses | `on_key(combo)` | One-off actions: jump, pause, restart (`"space"`, `"enter"`, `"ctrl-r"`). | | Key releases | `on_key_up(key)` | Charge-and-release mechanics, stopping a sound. | | Held keys | `pz.pressed()`, `pz.is_pressed("left", "a")` | Continuous movement and auto-fire, polled every frame. | | Movement | `game.axis(neg, pos)`, `game.vector()` | -1/0/1 from two key groups; a normalized direction from arrows/WASD. | | Drawing | `pz.canvas([...])` with `pyzed.draw` | Rectangles, circles, paths, text, transforms. | | Sprites | `draw.image(src, x, y, w, h)` and `game.sprite(rows, palette)` | Image files (decoded once and cached) or pixel art written as text. | | Collisions | `game.Rect`, `game.circle_rect` | Box overlaps; where a ball hits a box, to bounce it. | | Effects | `game.glow`, `game.Particles`, `game.Shake` | Neon lights, explosions and sparks, screen shake. | | HUD and menus | `pz.stack([canvas, widgets])` | Scores, health bars and pause screens with `glass`, `bloom` and animations. | ##### The loop A game is a regular `pz.App`. Return `pz.frames(self.tick)` from `subscriptions()` and the runtime calls `tick(dt)` before every frame. Move things by *speed × dt* so they go just as fast at 60, 120 or 144 Hz: ```python import pyzed as pz from pyzed import draw, game class Ship(pz.App): size = (480, 320) def __init__(self): self.x, self.y = 240, 160 def subscriptions(self): return [pz.frames(self.tick)] def tick(self, dt): dx, dy = game.vector() # arrows / WASD, diagonals normalized self.x += dx * 300 * dt self.y += dy * 300 * dt def view(self): return pz.canvas([draw.background("#0b0e14"), *game.glow(self.x, self.y, 14, "#22d3ee")], width=480, height=320) pz.run(Ship) ``` `dt` is capped at a quarter of a second, so dragging the window or hitting a breakpoint never makes the game jump ahead. Where the display does not wait for vsync (software rendering), `max_fps` (240 by default) keeps the loop from spinning the CPU. **Tip:** Stop the loop when nothing moves: `subscriptions()` is evaluated after every update, so `return [] if self.paused else [pz.frames(self.tick)]` pauses the game and the CPU. ##### Input `on_key` is for actions that happen once per press; `pz.pressed()` is for things that happen *while* a key is down. Both see every key, whatever widget has focus. ```python def on_key(self, combo): # "space", "p", "shift-r"... if combo == "p": self.paused = not self.paused def tick(self, dt): if pz.is_pressed("space") and self.cooldown <= 0: self.fire() # auto-fire while Space is held ``` Mouse input comes from the canvas itself: `on_move(x, y)`, `on_press(x, y)`, `on_release()`, `on_right_press(x, y)` and `on_scroll(dx, dy)`, in canvas coordinates. ##### Sprites `draw.image` draws a PNG/JPEG (or raw RGBA bytes) into a box, optionally rotated and faded. It is decoded once per source and cached, so drawing the same sprite hundreds of times per frame is cheap. Use `pixelated=True` for pixel art scaled up. ```python draw.image("assets/ship.png", self.x - 16, self.y - 16, 32, 32, rotation=self.angle) ``` For retro games you don't even need files: `game.sprite` turns text into pixel art, with one shape per color however big the sprite is. ```python INVADER = ["..#.....#..", "...#...#...", "..#######..", ".##.###.##.", "###########"] game.sprite(INVADER, {"#": "#a3e635"}, x, y, scale=4) ``` ##### HUD and menus are just widgets Stack widgets over the canvas with `pz.stack`. Scores, health bars, badges and pause menus are ordinary Pyzed components, so they get themes, `glass` panels, `bloom`, and entrance animations for free — the hard part of game UI is already done. ```python hud = pz.container(pz.row([score, pz.hspace(), ui.badge(f"Wave {self.wave}")]), padding=(8, 14)).style(glass=True, radius=12) return pz.stack([self.scene(), hud], width=W, height=H) ``` #### Performance tips More in [Game dev tips](https://albakore.github.io/pyzed/games/tips/). - Keep the per-frame work in Python small: plain lists and numbers, no allocations of big structures every frame. - Batch shapes: `draw.rects` draws many rectangles of one color as a single shape (that is what `game.sprite` uses). - Cap particles (`game.Particles(limit=...)`) and drop bullets that leave the screen. - Only the canvas changes every frame; widgets in the HUD are cheap to rebuild, but `pz.memo` can cache expensive ones. - Test game logic without a window: call `tick(1 / 60)` in a loop and assert on the state, as [`tests/test_game.py`](https://github.com/albakore/pyzed/blob/main/tests/test_game.py) does. `App.screenshot` renders any moment of the game to a PNG. ### 1. Your first game: the loop and the keys > Make a complete little game in Python with Pyzed: a frame loop with pz.frames, movement with speed × dt, held keys, a timer, a score and a pause, in about 100 lines. Source: https://albakore.github.io/pyzed/games/first-game/ A game is a program that **keeps running whether you press a key or not**. Every few milliseconds it looks at the keys, moves things a little and draws everything again. In this first step of the path you build *Coin Chase*: a glowing orb you steer with the arrows, a coin to grab, a 30-second clock and a score. It is small, but it has every part a real game has. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A complete game with a loop, held keys, a timer and a score | 25 minutes | Beginner | Python installed and Pyzed ([Installation](https://albakore.github.io/pyzed/getting-started/installation/)); no game experience | #### What you'll learn - The three parts of every game: **state**, **update** and **draw**. - Run code **once per frame** with `pz.frames(self.tick)`. - Move things by **speed × `dt`**, so the game runs at the same speed on every screen. - Read the keys that are **held down** (`game.vector`) and the ones that are **pressed once** (`on_key`). - Pause the game (and the CPU) by returning no subscriptions. Play it first: #### The three parts ```mermaid flowchart LR S["State
x, y, score, time_left"] --> U["Update: tick(dt)
read keys · move · collide"] U --> D["Draw: view()
canvas + HUD widgets"] D -->|next frame| U ``` Pyzed calls `tick(dt)` before every frame the screen shows (60, 120 or 144 times a second), then calls `view()` to draw. You never write the loop yourself; you only say what happens in one frame. 1. **The state.** Everything the game needs to remember lives in attributes of the app. `reset()` sets them, so the same method starts the first round and every new one: ```python W, H = 560, 380 SPEED = 260 # pixels per second PLAYER_R = 14 COIN_R = 9 ROUND = 30.0 # seconds per round class CoinChase(pz.App): def __init__(self, seed=None): self.random = random.Random(seed) self.held = None # the keys held down; None reads the keyboard self.best = 0 self.reset() def reset(self): self.x, self.y = W / 2, H / 2 self.score = 0 self.time_left = ROUND self.paused = self.over = False self.pops = [] # [x, y, age]: rings that grow and fade where a coin was taken self.clock = 0.0 self.coin = self.new_coin() ``` `held` is for tests and demos: they set it to `{"right"}` to steer without a keyboard. When it is `None` the game reads the real keys. 2. **Turn the loop on.** `subscriptions()` returns what should be running right now. `pz.frames(self.tick)` means "call `tick` on every frame". Pyzed asks again after every change, so returning nothing pauses the game, and the CPU rests: ```python def subscriptions(self): if self.paused or self.over: return [] return [pz.frames(self.tick)] ``` 3. **Move by speed × dt.** `dt` is the number of seconds since the last frame (about `0.0167` at 60 Hz). If you moved by a fixed 4 pixels per frame, the orb would go twice as fast on a 120 Hz screen. Multiplying by `dt` gives *pixels per second* instead: ```python def tick(self, dt): self.clock += dt self.time_left = max(0.0, self.time_left - dt) dx, dy = game.vector(held=self.held) # arrows / WASD, diagonals normalized self.x = game.clamp(self.x + dx * SPEED * dt, PLAYER_R, W - PLAYER_R) self.y = game.clamp(self.y + dy * SPEED * dt, PLAYER_R, H - PLAYER_R) ``` `game.vector()` turns the arrow keys and WASD into a direction: `(1, 0)` for right, `(0.71, -0.71)` for up-right (so diagonals are not faster). `game.clamp` keeps the orb inside the board. 4. **Did it touch the coin?** Two circles touch when the distance between their centers is smaller than their two radii added together. `math.hypot` measures that distance: ```python cx, cy = self.coin if math.hypot(cx - self.x, cy - self.y) < PLAYER_R + COIN_R: self.score += 1 self.pops.append([cx, cy, 0.0]) self.coin = self.new_coin() for pop in self.pops: pop[2] += dt self.pops = [p for p in self.pops if p[2] < 0.4] if self.time_left <= 0: self.over = True self.best = max(self.best, self.score) ``` Each "pop" is a ring that lives for 0.4 seconds. Effects like this are the cheapest way to make a game feel good; the [tips page](https://albakore.github.io/pyzed/games/tips/#juice) has more. 5. **Keys pressed once.** Holding a key moves the orb; *pressing* one does something once. `on_key` receives every press as a string: ```python def on_key(self, combo): if combo == "space" and not self.over: self.paused = not self.paused elif combo == "enter" and self.over: self.reset() ``` 6. **Draw the frame.** `view()` turns the state into a picture. The board is a `pz.canvas` with shapes from `pyzed.draw`; the coin squashes its width with `cos` so it seems to spin, and `game.glow` adds a soft light: ```python def board(self): shapes = [draw.background("#0b0e14")] ... for x, y, age in self.pops: k = age / 0.4 # 0 -> 1 over the ring's life shapes.append(draw.circle(x, y, COIN_R + k * 26, stroke=game.rgba("#fde68a", 1 - k), width=3)) shapes += game.glow(self.x, self.y, PLAYER_R, "#22d3ee", layers=4, strength=0.5) return pz.canvas(shapes, width=W, height=H) ``` 7. **The HUD is just widgets.** The score, the clock and the pause card are regular Pyzed components stacked over the canvas with `pz.stack`, so they get the theme, icons and the `glass` effect for free: ```python hud = pz.row([ ui.label("Coin Chase", weight="semibold"), pz.hspace(), ui.badge(f"{self.score} coins", icon="circle-dollar-sign", tint="warning"), ui.badge(f"{math.ceil(self.time_left)} s", icon="timer", tint="error" if self.time_left < 5 else "accent"), ], spacing=8, align="center") play = pz.stack([self.board(), self.overlay()], width=W, height=H) ``` **Why not move 4 pixels per frame?:** Many first games do, and they run at half speed on a 30 fps laptop and double speed on a 144 Hz monitor. Always multiply speeds by `dt`. Pyzed also caps `dt` at a quarter of a second, so a breakpoint or a dragged window never makes the orb teleport. **What about frames per second?:** `pz.frames` follows the screen's refresh rate (vsync). Where the display cannot wait for vsync, it stops at `max_fps` (240 by default): `pz.frames(self.tick, max_fps=60)` caps it lower. #### Try this - Make the orb faster for a second after each coin (a `boost` timer that counts down in `tick`). - Add a second, rarer coin worth 5 points that disappears after 3 seconds. - Save the best score to a file; [step 8](https://albakore.github.io/pyzed/games/high-scores/) shows how. #### Full code `examples/games/coin_chase.py`: ```py """Coin Chase: your first game. A frame loop, keys held down and a 30-second timer. python examples/games/coin_chase.py Arrows or WASD move the orb, collect as many coins as you can in 30 seconds. Space pauses, Enter plays again. Shows the three parts of every real-time game: 1. **State**: plain attributes on the app (position, score, time left). 2. **Update**: ``tick(dt)``, called by ``pz.frames`` once per displayed frame, moves things by *speed x dt* and checks what happened. 3. **Draw**: ``view()`` turns the state into a canvas, with the HUD as ordinary widgets. Tutorial: https://albakore.github.io/pyzed/games/first-game/ """ import math import random import pyzed as pz from pyzed import draw, game, ui W, H = 560, 380 SPEED = 260 # pixels per second PLAYER_R = 14 COIN_R = 9 ROUND = 30.0 # seconds per round class CoinChase(pz.App): title = "Coin Chase" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) #: The keys held down; ``None`` reads the keyboard. Tests and demos set it to steer. self.held = None self.best = 0 self.reset() # -- 1. state --------------------------------------------------------------------------- def reset(self): self.x, self.y = W / 2, H / 2 self.score = 0 self.time_left = ROUND self.paused = self.over = False self.pops = [] # [x, y, age]: rings that grow and fade where a coin was taken self.clock = 0.0 # seconds since the round started (for the coin's spin) self.coin = self.new_coin() def new_coin(self): """A coin somewhere not too close to the player.""" while True: x = self.random.uniform(30, W - 30) y = self.random.uniform(30, H - 30) if math.hypot(x - self.x, y - self.y) > 120: return (x, y) # -- 2. update -------------------------------------------------------------------------- def subscriptions(self): # No frames while paused or over: the game stops and so does the CPU. if self.paused or self.over: return [] return [pz.frames(self.tick)] def tick(self, dt): self.clock += dt self.time_left = max(0.0, self.time_left - dt) # Move by speed x dt: the same speed at 60, 120 or 144 frames per second. dx, dy = game.vector(held=self.held) # arrows / WASD, diagonals normalized self.x = game.clamp(self.x + dx * SPEED * dt, PLAYER_R, W - PLAYER_R) self.y = game.clamp(self.y + dy * SPEED * dt, PLAYER_R, H - PLAYER_R) # Two circles touch when the distance between centers is less than the radii added. cx, cy = self.coin if math.hypot(cx - self.x, cy - self.y) < PLAYER_R + COIN_R: self.score += 1 self.pops.append([cx, cy, 0.0]) self.coin = self.new_coin() for pop in self.pops: pop[2] += dt self.pops = [p for p in self.pops if p[2] < 0.4] if self.time_left <= 0: self.over = True self.best = max(self.best, self.score) def on_key(self, combo): if combo == "space" and not self.over: self.paused = not self.paused elif combo == "enter" and self.over: self.reset() # -- 3. draw ---------------------------------------------------------------------------- def board(self): shapes = [draw.background("#0b0e14")] for gx in range(0, W, 40): # a faint grid, to feel the motion shapes.append(draw.line(gx, 0, gx, H, stroke="#ffffff08")) for gy in range(0, H, 40): shapes.append(draw.line(0, gy, W, gy, stroke="#ffffff08")) cx, cy = self.coin spin = abs(math.cos(self.clock * 4)) # squash the coin's width: it seems to spin shapes += game.glow(cx, cy, COIN_R * 0.6, "#fbbf24", layers=3, strength=0.5, core=False) shapes.append(draw.path([("M", cx, cy - COIN_R)] + [ ("L", cx + math.sin(a) * COIN_R * max(spin, 0.15), cy - math.cos(a) * COIN_R) for a in (i * math.pi / 8 for i in range(1, 17))], fill="#fbbf24", stroke="#fde68a", width=1.5)) for x, y, age in self.pops: k = age / 0.4 # 0 -> 1 over the ring's life shapes.append(draw.circle(x, y, COIN_R + k * 26, stroke=game.rgba("#fde68a", 1 - k), width=3)) shapes += game.glow(self.x, self.y, PLAYER_R, "#22d3ee", layers=4, strength=0.5) return pz.canvas(shapes, width=W, height=H) def overlay(self): if self.over: title, lines = "Time!", [f"{self.score} coins · best {self.best}", "Enter plays again"] elif self.paused: title, lines = "Paused", ["Space continues"] else: return None card = pz.container(pz.column([ui.heading(title, level=2), *[ui.muted(t) for t in lines]], spacing=8, align="center"), padding=(20, 32)).style(glass=True, radius=14) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def view(self): t = self.resolved_theme() hud = pz.row([ ui.label("Coin Chase", weight="semibold"), pz.hspace(), ui.badge(f"{self.score} coins", icon="circle-dollar-sign", tint="warning"), ui.badge(f"{math.ceil(self.time_left)} s", icon="timer", tint="error" if self.time_left < 5 else "accent"), ui.badge(f"Best {self.best}", icon="trophy", tint="text_muted"), ], spacing=8, align="center") play = pz.stack([self.board(), self.overlay()], width=W, height=H) return pz.container(pz.column([hud, pz.container(play, radius=10, clip=True), ui.muted("Arrows / WASD move · Space pauses · Enter restarts", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(CoinChase) ``` #### Next - [2. Sprites and animation](https://albakore.github.io/pyzed/games/sprites-animation/): Pixel-art characters from text, frame animation, flipping and tweens. ### 2. Sprites and animation > Animate a pixel-art character in Python with Pyzed: sprites written as text with game.sprite, a sprite sheet played frame by frame, flipping, tweens with easing curves, a jump arc and squash and stretch. Source: https://albakore.github.io/pyzed/games/sprites-animation/ Things in a game rarely just slide: characters walk, blink and land, coins bob and pop. Two techniques do almost all of it. **Frame animation** shows a series of pictures one after another, like a flip book. **Tweening** moves a number (a position, a size, an opacity) from one value to another over time, shaped by an *easing curve*. This step builds *Walker*, a pixel-art hero that uses both. | Goal | Time | Level | You need | | --- | --- | --- | --- | | An animated character with walk, idle and jump states | 35 minutes | Beginner | [1. Your first game](https://albakore.github.io/pyzed/games/first-game/) | #### What you'll learn - Draw pixel art **from text** with `game.sprite`, and mirror it with `flip=True`. - Keep a **sprite sheet** (a list of frames) and play it at a few frames per second. - Switch animations with **states** (`idle`, `walk`, `jump`). - Write a tiny **`Tween`** class and use **easing curves** for the jump, a landing squash, a "+1" and a title. 1. **A sprite is text.** Each string is a row of pixels; each character picks a color from a palette, and any character that is not in the palette (like `.`) is transparent. `game.sprite` turns it into a handful of shapes, one per color, however big the sprite is: ```python PALETTE = {"h": "#7c4a2d", "s": "#f5c99b", "S": "#e9b385", "e": "#1b1f27", "b": "#3b82f6", "B": "#60a5fa", "p": "#334155", "k": "#111827"} HEAD = [ "...hhhh...", "..hhhhhh..", "..hhssss..", "..hsssess.", "..hsssssS.", "...ssss...", ] BODY = ["..bbBBbb..", ".sbbBBbbs.", ".sbbbbbbs.", "..bbbbbb.."] game.sprite(HEAD + BODY + LEGS["stand"], PALETTE, x, y, scale=4) # every pixel becomes 4x4 ``` 2. **A sprite sheet is a list of frames.** The walk cycle reuses the head and body and changes the legs; the idle animation is the same frame six times and a blink once, so the hero blinks every couple of seconds: ```python LEGS = { "stand": ["..pp..pp..", "..pp..pp..", "..kk..kk.."], "apart": ["..pp..pp..", ".pp....pp.", ".kk.....kk"], "pass": ["...pp.pp..", "...pp.pp..", "...kkkkk.."], "air": ["..pp..pp..", "..ppp.ppp.", "....k...k."], } IDLE = [HEAD + BODY + LEGS["stand"]] * 6 + [BLINK + BODY + LEGS["stand"]] WALK = [HEAD + BODY + LEGS["apart"], HEAD + BODY + LEGS["pass"], HEAD + BODY + LEGS["apart"], HEAD + BODY + LEGS["pass"]] JUMP = [HEAD + BODY + LEGS["air"]] ``` 3. **Play the frames.** An animation only needs to count time; the frame to show is *time × frames per second*, wrapped around with `%` when it loops: ```python class FrameAnimation: def __init__(self, frames, fps, loop=True): self.frames, self.fps, self.loop = frames, fps, loop self.time = 0.0 def update(self, dt): self.time += dt @property def index(self): i = int(self.time * self.fps) return i % len(self.frames) if self.loop else min(i, len(self.frames) - 1) @property def frame(self): return self.frames[self.index] ``` 4. **Pick the animation from the state.** Every frame the hero is idle, walking or jumping. When the state changes, the new animation starts from its first frame, and the facing direction mirrors the sprite: ```python self.animations = {"idle": FrameAnimation(IDLE, 3), "walk": FrameAnimation(WALK, 10), "jump": FrameAnimation(JUMP, 1)} def set_state(self, state): if state != self.state: self.state = state self.animations[state].time = 0.0 # in tick(dt): move = game.axis(("left", "a"), ("right", "d"), held=self.held) if move: self.facing = move self.set_state("jump" if self.jump else "walk" if move else "idle") self.animation.update(dt) # in board(): game.sprite(self.animation.frame, PALETTE, x, y, scale=PIXEL, flip=self.facing < 0) ``` 5. **Tweens: from A to B over time.** A tween remembers when it started and computes its value from the *progress* (0 to 1), passed through an easing function. Easing is what makes motion feel natural: things rarely start and stop at full speed. ```python def ease_out_cubic(t): return 1 - (1 - t) ** 3 def ease_out_back(t): # goes a little past the end, then settles c = 1.70158 return 1 + (c + 1) * (t - 1) ** 3 + c * (t - 1) ** 2 class Tween: def __init__(self, start, end, duration, ease=ease_out_cubic, delay=0.0): self.start, self.end, self.duration, self.ease = start, end, duration, ease self.time = -delay def update(self, dt): self.time += dt @property def progress(self): return game.clamp(self.time / self.duration, 0.0, 1.0) @property def done(self): return self.time >= self.duration @property def value(self): return game.lerp(self.start, self.end, self.ease(self.progress)) ``` 6. **A jump is a tween too.** The jump's progress goes from 0 to 1 in `JUMP_TIME` seconds; the height is a parabola, `4t(1 − t)`, which is 0 at both ends and 1 in the middle: ```python def start_jump(self): if not self.jump: self.jump = Tween(0.0, 1.0, JUMP_TIME, linear) @property def height(self): if not self.jump: return 0.0 t = self.jump.value return 4 * t * (1 - t) * JUMP_HEIGHT ``` In [step 3](https://albakore.github.io/pyzed/games/collisions-physics/) you will replace it with real gravity, which can land on platforms; a tween is perfect when the motion is always the same. 7. **Squash and stretch.** When the jump ends, a second tween goes from 1 to 0 in a quarter of a second. While it runs, the sprite is drawn wider and shorter around its feet, with a `scale` transform: ```python s = self.squash.value if self.squash else 0.0 sx, sy = 1 + 0.25 * s, 1 - 0.2 * s feet_x, feet_y = self.x + SPRITE_W / 2, GROUND - self.height shapes += [draw.save(), draw.translate(feet_x, feet_y), draw.scale(sx, sy), *game.sprite(self.animation.frame, PALETTE, -SPRITE_W / 2, -SPRITE_H, scale=PIXEL, flip=self.facing < 0), draw.restore()] ``` Moving the origin to the feet first (`translate`) makes the sprite squash *onto the ground* instead of toward its top-left corner. 8. **Little tweens everywhere.** The "+1" over a coin rises 40 pixels and fades out in 0.7 s; the title slides in with `ease_out_back` 0.2 s after the game starts; the coins bob with a sine wave: ```python self.popups.append([cx, cy, Tween(0.0, 1.0, 0.7, ease_out_cubic)]) ... for x, y, tween in self.popups: shapes.append(draw.text(x, y - 40 * tween.value, "+1", size=18, weight="bold", color=game.rgba("#fde68a", 1 - tween.progress), align_x="center")) ``` **Image files and real sprite sheets:** `draw.image("hero.png", x, y, w, h, pixelated=True)` draws a PNG instead of text pixels, decoded once and cached, so drawing it every frame is cheap; `rotation=` and `opacity=` turn and fade it. `draw.image` draws a whole image, so for a sprite sheet in one file, cut it into frames once when the game starts (for example with Pillow's `Image.crop`, saving each frame's bytes) and keep the list, exactly like `WALK` above. **Widgets animate on their own:** Menus and HUD widgets have their own animation system: `.animate("fade_in_up")`, `.transition(...)` and `pz.AnimationPlayer` play natively, without your loop (see [Animation](https://albakore.github.io/pyzed/guides/animation/)). Inside a `pz.canvas` there are no widgets, so you animate the numbers yourself, as here. #### Try this - Add a run animation (6 frames at 14 fps) used when Shift is held, with a higher `WALK_SPEED`. - Make the coins spin by squashing their width with `abs(math.cos(time))`, as Coin Chase does. - Give the title an `ease_in_out_sine` bob after it lands. #### Full code `examples/games/walker.py`: ```py """Walker: sprites, frame animation and tweening. python examples/games/walker.py Arrows or A/D walk, Space jumps, F shows or hides the sprite sheet. A pixel-art hero walks, blinks, jumps and squashes when it lands; coins bob and pop with a "+1" that eases up and fades. Shows: - **Sprites from text** with ``game.sprite`` (one string per row, one character per pixel), flipped with ``flip=True`` when the hero faces left. - **Frame animation**: a list of frames (a sprite sheet) played at a few frames per second by ``FrameAnimation``, which only counts time. - **Tweening**: a value going from A to B over a duration with an easing curve (``Tween``), for the jump, the landing squash, the "+1" and the title that slides in. Tutorial: https://albakore.github.io/pyzed/games/sprites-animation/ """ import math import pyzed as pz from pyzed import draw, game, ui W, H = 640, 300 GROUND = 236 PIXEL = 4 WALK_SPEED = 170 JUMP_HEIGHT = 70 JUMP_TIME = 0.55 PALETTE = {"h": "#7c4a2d", "s": "#f5c99b", "S": "#e9b385", "e": "#1b1f27", "b": "#3b82f6", "B": "#60a5fa", "p": "#334155", "k": "#111827"} # "." (or any character not here) is transparent # -- the sprite sheet: every frame is a list of rows, every character a pixel --------------- HEAD = [ "...hhhh...", "..hhhhhh..", "..hhssss..", "..hsssess.", "..hsssssS.", "...ssss...", ] BLINK = HEAD[:3] + ["..hsssshs."] + HEAD[4:] # the eye closed for a frame BODY = [ "..bbBBbb..", ".sbbBBbbs.", ".sbbbbbbs.", "..bbbbbb..", ] LEGS = { "stand": ["..pp..pp..", "..pp..pp..", "..kk..kk.."], "apart": ["..pp..pp..", ".pp....pp.", ".kk.....kk"], "pass": ["...pp.pp..", "...pp.pp..", "...kkkkk.."], "air": ["..pp..pp..", "..ppp.ppp.", "....k...k."], } IDLE = [HEAD + BODY + LEGS["stand"]] * 6 + [BLINK + BODY + LEGS["stand"]] # blink once in a while WALK = [HEAD + BODY + LEGS["apart"], HEAD + BODY + LEGS["pass"], HEAD + BODY + LEGS["apart"], HEAD + BODY + LEGS["pass"]] JUMP = [HEAD + BODY + LEGS["air"]] SPRITE_W = len(HEAD[0]) * PIXEL SPRITE_H = len(IDLE[0]) * PIXEL # -- easing: how a value moves from 0 to 1 ---------------------------------------------------- def linear(t): return t def ease_out_cubic(t): return 1 - (1 - t) ** 3 def ease_out_back(t): c = 1.70158 return 1 + (c + 1) * (t - 1) ** 3 + c * (t - 1) ** 2 def ease_in_out_sine(t): return -(math.cos(math.pi * t) - 1) / 2 class Tween: """A number going from ``start`` to ``end`` in ``duration`` seconds, shaped by ``ease``.""" def __init__(self, start, end, duration, ease=ease_out_cubic, delay=0.0): self.start, self.end, self.duration, self.ease = start, end, duration, ease self.time = -delay def update(self, dt): self.time += dt @property def progress(self): return game.clamp(self.time / self.duration, 0.0, 1.0) @property def done(self): return self.time >= self.duration @property def value(self): return game.lerp(self.start, self.end, self.ease(self.progress)) class FrameAnimation: """Plays a list of frames at ``fps`` frames per second, looping or not.""" def __init__(self, frames, fps, loop=True): self.frames, self.fps, self.loop = frames, fps, loop self.time = 0.0 def update(self, dt): self.time += dt @property def index(self): i = int(self.time * self.fps) return i % len(self.frames) if self.loop else min(i, len(self.frames) - 1) @property def frame(self): return self.frames[self.index] class Walker(pz.App): title = "Walker" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None): self.held = None # the keys held down; None reads the keyboard self.show_sheet = True self.x = 120.0 self.facing = 1 # 1 right, -1 left self.clock = 0.0 self.animations = {"idle": FrameAnimation(IDLE, 3), "walk": FrameAnimation(WALK, 10), "jump": FrameAnimation(JUMP, 1)} self.state = "idle" self.jump = None # a Tween from 0 to 1 while in the air self.squash = None # a Tween from 1 back to 0 after landing self.coins = [{"x": 260 + i * 70, "high": i % 2 == 1, "taken": None} for i in range(5)] self.popups = [] # [x, y, Tween] "+1" labels self.title_in = Tween(-80, 18, 0.8, ease_out_back, delay=0.2) self.score = 0 @property def animation(self): return self.animations[self.state] def set_state(self, state): if state != self.state: self.state = state self.animations[state].time = 0.0 # start the new animation from its first frame # -- update ------------------------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] def tick(self, dt): self.clock += dt self.title_in.update(dt) move = game.axis(("left", "a"), ("right", "d"), held=self.held) if move: self.facing = move self.x = game.clamp(self.x + move * WALK_SPEED * dt, 10, W - SPRITE_W - 10) if self.held is not None and "space" in self.held: self.start_jump() if self.jump: self.jump.update(dt) if self.jump.done: self.jump = None self.squash = Tween(1.0, 0.0, 0.25, ease_out_cubic) # land: squash, then recover if self.squash: self.squash.update(dt) if self.squash.done: self.squash = None self.set_state("jump" if self.jump else "walk" if move else "idle") self.animation.update(dt) self.collect() for popup in self.popups: popup[2].update(dt) self.popups = [p for p in self.popups if not p[2].done] def start_jump(self): if not self.jump: self.jump = Tween(0.0, 1.0, JUMP_TIME, linear) @property def height(self): """How high the hero is: a parabola, 4t(1-t), peaks at JUMP_HEIGHT halfway through.""" if not self.jump: return 0.0 t = self.jump.value return 4 * t * (1 - t) * JUMP_HEIGHT def collect(self): hero = game.Rect(self.x + 8, GROUND - SPRITE_H - self.height, SPRITE_W - 16, SPRITE_H) for coin in self.coins: if coin["taken"] is not None: if self.clock - coin["taken"] > 3: # coins come back after three seconds coin["taken"] = None continue cx, cy = coin["x"], self.coin_y(coin) if hero.overlaps(game.Rect(cx - 9, cy - 9, 18, 18)): coin["taken"] = self.clock self.score += 1 self.popups.append([cx, cy, Tween(0.0, 1.0, 0.7, ease_out_cubic)]) def coin_y(self, coin): base = GROUND - (110 if coin["high"] else 40) return base + math.sin(self.clock * 3 + coin["x"] * 0.05) * 5 # bob with a sine wave def on_key(self, combo): if combo in ("space", "up", "w"): self.start_jump() elif combo == "f": self.show_sheet = not self.show_sheet # -- draw --------------------------------------------------------------------------------- def board(self): shapes = [draw.background("#101521")] for i, (color, y) in enumerate((("#1a2236", 170), ("#202a40", 196))): # far hills shapes += [draw.circle(80 + j * 190 + i * 90, y + 70, 110, fill=color) for j in range(4)] shapes.append(draw.rect(0, GROUND, W, H - GROUND, fill="#1f2a1d")) shapes.append(draw.rect(0, GROUND, W, 6, fill="#4d7c3a")) for coin in self.coins: if coin["taken"] is None: cx, cy = coin["x"], self.coin_y(coin) shapes += game.glow(cx, cy, 8, "#fbbf24", layers=3, strength=0.45) shapes.append(draw.circle(cx, cy, 4, fill="#fde68a")) # shadow: smaller and fainter the higher the hero is k = 1 - self.height / JUMP_HEIGHT * 0.5 shapes.append(draw.rect(self.x + SPRITE_W / 2 - 18 * k, GROUND - 3, 36 * k, 6, radius=3, fill=game.rgba("#000000", 0.35 * k))) # squash and stretch: wider and shorter right after landing, around the feet s = self.squash.value if self.squash else 0.0 sx, sy = 1 + 0.25 * s, 1 - 0.2 * s feet_x, feet_y = self.x + SPRITE_W / 2, GROUND - self.height shapes += [draw.save(), draw.translate(feet_x, feet_y), draw.scale(sx, sy), *game.sprite(self.animation.frame, PALETTE, -SPRITE_W / 2, -SPRITE_H, scale=PIXEL, flip=self.facing < 0), draw.restore()] for x, y, tween in self.popups: shapes.append(draw.text(x, y - 40 * tween.value, "+1", size=18, weight="bold", color=game.rgba("#fde68a", 1 - tween.progress), align_x="center")) shapes.append(draw.text(W / 2, self.title_in.value, "WALKER", size=26, weight="bold", color="#e2e8f0", align_x="center")) if self.show_sheet: shapes += self.sheet() return pz.canvas(shapes, width=W, height=H) def sheet(self): """The current animation's frames, small, with the one on screen highlighted.""" frames = self.animation.frames unique = [] for f in frames: if not unique or unique[-1] is not f: unique.append(f) scale, gap = 2, 6 fw, fh = len(HEAD[0]) * scale, len(IDLE[0]) * scale x0, y0 = 12, 12 shapes = [draw.rect(x0 - 6, y0 - 6, max(len(frames) * (fw + gap) + 6, 160), fh + 30, radius=8, fill="#00000066")] for i, frame in enumerate(frames): x = x0 + i * (fw + gap) current = i == self.animation.index shapes.append(draw.rect(x - 2, y0 - 2, fw + 4, fh + 4, radius=3, stroke="#fbbf24" if current else "#ffffff22", width=1.5 if current else 1)) shapes += game.sprite(frame, PALETTE, x, y0, scale=scale) shapes.append(draw.text(x0, y0 + fh + 8, f"{self.state} · frame {self.animation.index + 1}/{len(frames)}", size=11, color="#cbd5e1", font="mono")) return shapes def view(self): t = self.resolved_theme() hud = pz.row([ui.label("Walker", weight="semibold"), pz.hspace(), ui.badge(self.state, icon="film", tint="accent"), ui.badge(f"{self.score} coins", icon="coins", tint="warning")], spacing=8, align="center") return pz.container(pz.column([hud, pz.container(self.board(), radius=10, clip=True), ui.muted("←/→ or A/D walk · Space jumps · F shows the sprite sheet", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(Walker) ``` #### Next - [3. Collisions and physics](https://albakore.github.io/pyzed/games/collisions-physics/): Velocity, gravity, box collisions and a jump that feels good. ### 3. Collisions and physics > Simple game physics in Python with Pyzed: velocity and gravity, AABB box collisions resolved one axis at a time, circles bouncing off boxes, and a platformer jump with coyote time and jump buffering. Source: https://albakore.github.io/pyzed/games/collisions-physics/ You don't need a physics engine to make a platformer. A few lines give you **velocity** (how fast something moves), **gravity** (a constant pull down), and **collisions** with boxes. This step builds *Jump Lab*, a playground where a box runs and jumps between platforms while balls bounce around, with a debug view that shows the hitboxes, the velocity and the arc of every jump. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A box that runs, jumps and lands on platforms, and balls that bounce | 40 minutes | Intermediate | [2. Sprites and animation](https://albakore.github.io/pyzed/games/sprites-animation/) | #### What you'll learn - **Velocity and gravity**: position changes by velocity × dt, velocity changes by gravity × dt. - **AABB collisions** (axis-aligned bounding boxes) with `game.Rect.overlaps`, resolved **one axis at a time**. - **Circles against boxes** with `game.circle_rect`, which tells you which side was hit. - **Game feel**: acceleration, variable jump height, *coyote time* and *jump buffering*. - A **debug view** you can switch on and off. 1. **Numbers that feel right.** All the physics is a handful of constants, in pixels and seconds. Changing them is how you *tune* the feel: ```python RUN = 250 # top running speed, px/s ACCEL = 2200 # px/s² on the ground (how fast you reach RUN) AIR_ACCEL = 1300 # less control in the air GRAVITY = 1900 # px/s², added to vy every second JUMP_SPEED = 660 # vy right after a jump (upwards, so negative) LOW_JUMP = 2.6 # gravity multiplier while rising without the jump key: short hops MAX_FALL = 900 COYOTE = 0.10 # seconds after leaving a ledge when a jump still works BUFFER = 0.12 # seconds a jump press is remembered before landing ``` With these, the highest jump is `JUMP_SPEED² / (2 × GRAVITY)` ≈ 115 pixels. The test suite checks that. 2. **Velocity and gravity.** `vx` and `vy` are speeds in pixels per second (`y` grows downwards, so up is negative). Instead of setting `vx` to the target speed at once, it moves toward it by at most `ACCEL × dt` per frame, which gives a short, smooth start and stop: ```python move = game.axis(("left", "a"), ("right", "d"), held=self.held) jump_held = game.is_pressed(*JUMP_KEYS, held=self.held) target = move * RUN accel = (ACCEL if self.on_ground else AIR_ACCEL) * dt self.vx += game.clamp(target - self.vx, -accel, accel) g = GRAVITY * (LOW_JUMP if self.vy < 0 and not jump_held else 1) self.vy = min(self.vy + g * dt, MAX_FALL) ``` The second line of gravity is **variable jump height**: while rising, letting go of the key makes gravity stronger, so a tap is a hop and a long press is a full jump. 3. **Move one axis at a time.** Move horizontally and push out of anything you now overlap; then do the same vertically. Resolving each axis on its own is what lets the box slide along a wall while falling, and land exactly on a floor: ```python def move_x(self, dx): self.player.x += dx for i, p in self.solids_hit(self.player): self.player.x = p.left - self.player.w if dx > 0 else p.right self.vx = 0.0 def move_y(self, dy): self.player.y += dy for i, p in self.solids_hit(self.player): if dy > 0: # falling: land on top self.player.y = p.top - self.player.h self.on_ground = True else: # rising: bump the head self.player.y = p.bottom self.vy = 0.0 # in tick: self.move_x(self.vx * dt) self.on_ground = False self.move_y(self.vy * dt) ``` `on_ground` is only true when the vertical move hit something below, so it is always up to date. 4. **Coyote time and jump buffering.** Two tiny timers make jumps forgiving. *Coyote time* lets you still jump for 0.1 s after running off a ledge (like the cartoon coyote who only falls when he looks down). The *jump buffer* remembers a press made just before landing, so it is not lost: ```python def press_jump(self): # called by on_key for Space, Up and W self.buffer = BUFFER # in tick: self.coyote = COYOTE if self.on_ground else max(0.0, self.coyote - dt) self.buffer = max(0.0, self.buffer - dt) if self.buffer > 0 and self.coyote > 0: self.vy = -JUMP_SPEED self.buffer = self.coyote = 0.0 ``` Players never notice these, but they notice when they're missing: the game feels like it "eats" jumps. 5. **Balls bounce off boxes.** `game.circle_rect(cx, cy, r, rect)` returns `None`, or the direction from the box to the circle along the side it hit: `(0, -1)` from above, `(1, 0)` from the right. Flip the speed to point that way, push the ball out, and lose a little energy: ```python normal = game.circle_rect(ball.x, ball.y, ball.r, p) if normal: nx, ny = normal if nx: ball.vx = abs(ball.vx) * nx ball.x = (p.right + ball.r) if nx > 0 else (p.left - ball.r) if ny: ball.vy = abs(ball.vy) * ny * 0.82 ball.y = (p.bottom + ball.r) if ny > 0 else (p.top - ball.r) ``` 6. **A debug view.** Drawing what the code "sees" saves hours. With H the game outlines the hitboxes, draws the velocity as a line, keeps the last 60 positions as fading dots (the jump arc) and prints the numbers: ```python self.trail.append(self.player.center) del self.trail[:-60] ... if self.debug: shapes.append(p.shape(stroke="#34d399", width=1.5)) shapes.append(draw.line(cx, cy, cx + self.vx * 0.15, cy + self.vy * 0.15, stroke="#f472b6", width=2.5)) ``` **Tunneling:** If something moves more than its own size in one frame, it can pass through a thin wall without ever overlapping it. Jump Lab caps `dt` at 1/30 s; for fast objects, split the move into smaller steps, as [Breakout](https://albakore.github.io/pyzed/games/breakout/) does for its ball (`steps = ceil(speed × dt / radius)`). **When to use what:** Boxes (`Rect.overlaps`) for characters, platforms, bullets and pickups; circles (`math.hypot`) for round things and quick "is it close enough" checks; `circle_rect` when a ball must bounce off a box. Most 2D games need nothing else. #### Try this - Add a moving platform: a `Rect` whose `x` follows a sine wave; when the player stands on it, add its movement to the player's `x`. - Add a double jump: allow one extra jump in the air, reset when landing. - Tune `GRAVITY` and `JUMP_SPEED` to make the jump floatier or snappier, and watch the trail change. #### Full code `examples/games/jump_lab.py`: ```py """Jump Lab: velocity, gravity, box collisions and a jump that feels good. python examples/games/jump_lab.py Arrows or A/D run, Space (or Up/W) jumps, hold it to jump higher, H hides the debug view. A box runs and jumps between platforms while three balls bounce around. The debug view draws the hitboxes, the velocity and the trail of the last second, so you can see the jump arc. Shows: - **Velocity and gravity**: ``vx``/``vy`` in pixels per second; gravity adds to ``vy`` every frame, and the position moves by ``velocity x dt``. - **AABB collisions** (axis-aligned bounding boxes, ``game.Rect.overlaps``), resolved one axis at a time so the box slides along walls and lands on floors. - **Circle against box** with ``game.circle_rect``, which says which side was hit, to bounce. - **Game feel**: acceleration, coyote time, jump buffering and variable jump height. Tutorial: https://albakore.github.io/pyzed/games/collisions-physics/ """ import random import pyzed as pz from pyzed import draw, game, ui W, H = 720, 400 RUN = 250 # top running speed, px/s ACCEL = 2200 # px/s² on the ground (how fast you reach RUN) AIR_ACCEL = 1300 # less control in the air GRAVITY = 1900 # px/s², added to vy every second JUMP_SPEED = 660 # vy right after a jump (upwards, so negative) LOW_JUMP = 2.6 # gravity multiplier while rising without the jump key: short hops MAX_FALL = 900 COYOTE = 0.10 # seconds after leaving a ledge when a jump still works BUFFER = 0.12 # seconds a jump press is remembered before landing JUMP_KEYS = ("space", "up", "w") PLATFORMS = [ game.Rect(0, H - 36, W, 36), # the floor game.Rect(120, 280, 140, 16), game.Rect(330, 214, 130, 16), game.Rect(520, 150, 150, 16), game.Rect(560, H - 116, 40, 80), # a wall to bump into game.Rect(0, 0, 12, H), game.Rect(W - 12, 0, 12, H), # side walls ] class Ball: def __init__(self, x, y, vx, vy, r, color): self.x, self.y, self.vx, self.vy, self.r, self.color = x, y, vx, vy, r, color class JumpLab(pz.App): title = "Jump Lab" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None): rnd = random.Random(seed) self.held = None # the keys held down; None reads the keyboard self.debug = True self.player = game.Rect(60, H - 36 - 34, 26, 34) self.vx = self.vy = 0.0 self.on_ground = False self.coyote = 0.0 # time left to jump after walking off a ledge self.buffer = 0.0 # time left of a remembered jump press self.trail = [] # the player's center, one point per frame self.hits = {} # platform index -> seconds to keep it highlighted self.balls = [Ball(rnd.uniform(100, 600), rnd.uniform(40, 120), rnd.choice((-1, 1)) * rnd.uniform(120, 220), 0.0, r, color) for r, color in ((10, "#f472b6"), (14, "#a78bfa"), (8, "#34d399"))] # -- input ------------------------------------------------------------------------------ def on_key(self, combo): if combo in JUMP_KEYS: self.press_jump() elif combo == "h": self.debug = not self.debug def press_jump(self): self.buffer = BUFFER # remember the press for a moment (jump buffering) # -- update ----------------------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] def tick(self, dt): dt = min(dt, 1 / 30) # big steps would let things pass through walls move = game.axis(("left", "a"), ("right", "d"), held=self.held) jump_held = game.is_pressed(*JUMP_KEYS, held=self.held) # Horizontal: accelerate toward the target speed instead of jumping to it. target = move * RUN accel = (ACCEL if self.on_ground else AIR_ACCEL) * dt self.vx += game.clamp(target - self.vx, -accel, accel) # Vertical: gravity, stronger while rising without the key held (variable jump height). g = GRAVITY * (LOW_JUMP if self.vy < 0 and not jump_held else 1) self.vy = min(self.vy + g * dt, MAX_FALL) # Coyote time and jump buffer: both are small timers counting down. self.coyote = COYOTE if self.on_ground else max(0.0, self.coyote - dt) self.buffer = max(0.0, self.buffer - dt) if self.buffer > 0 and self.coyote > 0: self.vy = -JUMP_SPEED self.buffer = self.coyote = 0.0 # Move one axis at a time and push out of whatever we hit. self.move_x(self.vx * dt) self.on_ground = False self.move_y(self.vy * dt) self.trail.append(self.player.center) del self.trail[:-60] self.hits = {i: t - dt for i, t in self.hits.items() if t - dt > 0} for ball in self.balls: self.bounce(ball, dt) def solids_hit(self, rect): return [(i, p) for i, p in enumerate(PLATFORMS) if rect.overlaps(p)] def move_x(self, dx): self.player.x += dx for i, p in self.solids_hit(self.player): self.player.x = p.left - self.player.w if dx > 0 else p.right self.vx = 0.0 self.hits[i] = 0.2 def move_y(self, dy): self.player.y += dy for i, p in self.solids_hit(self.player): if dy > 0: # falling: land on top self.player.y = p.top - self.player.h self.on_ground = True else: # rising: bump the head self.player.y = p.bottom self.vy = 0.0 self.hits[i] = 0.2 def bounce(self, ball, dt): ball.vy += GRAVITY * 0.5 * dt ball.x += ball.vx * dt ball.y += ball.vy * dt for i, p in enumerate(PLATFORMS): normal = game.circle_rect(ball.x, ball.y, ball.r, p) if normal: nx, ny = normal if nx: ball.vx = abs(ball.vx) * nx # reflect the speed on that side ball.x = (p.right + ball.r) if nx > 0 else (p.left - ball.r) if ny: ball.vy = abs(ball.vy) * ny * 0.82 # lose a little energy on each bounce ball.y = (p.bottom + ball.r) if ny > 0 else (p.top - ball.r) if ny < 0 and abs(ball.vy) < 120: # nearly still: kick it again ball.vy = -620 self.hits[i] = 0.2 # -- draw ------------------------------------------------------------------------------- def board(self): shapes = [draw.background("#0f141c")] for i, p in enumerate(PLATFORMS): lit = i in self.hits and self.debug shapes.append(p.shape(fill="#2a3446", radius=3)) shapes.append(draw.rect(p.x, p.y, p.w, 3, fill="#4b5d7c" if not lit else "#fbbf24")) if self.debug: shapes.append(p.shape(stroke="#fbbf24" if lit else "#ffffff1f", width=1.5 if lit else 1)) for ball in self.balls: shapes += game.glow(ball.x, ball.y, ball.r, ball.color, layers=2, strength=0.35) if self.debug: shapes.append(draw.line(ball.x, ball.y, ball.x + ball.vx * 0.08, ball.y + ball.vy * 0.08, stroke=game.rgba(ball.color, 0.7), width=1.5)) if self.debug and len(self.trail) > 1: # the trail: fading dots, one per frame n = len(self.trail) for i, (x, y) in enumerate(self.trail[::2]): a = (i * 2 + 1) / n shapes.append(draw.circle(x, y, 2.2, fill=game.rgba("#22d3ee", a * 0.8))) p = self.player squash = 1 + game.clamp(abs(self.vy) / MAX_FALL, 0, 1) * 0.15 if not self.on_ground else 1 w, h = p.w / squash, p.h * squash # stretch while flying fast shapes.append(draw.rect(p.center[0] - w / 2, p.bottom - h, w, h, radius=6, fill="#22d3ee")) eye = 1 if self.vx >= 0 else -1 shapes.append(draw.rect(p.center[0] + eye * 4 - 3, p.bottom - h + 9, 6, 7, radius=2, fill="#0b0e14")) if self.debug: cx, cy = p.center shapes.append(p.shape(stroke="#34d399", width=1.5)) shapes.append(draw.line(cx, cy, cx + self.vx * 0.15, cy + self.vy * 0.15, stroke="#f472b6", width=2.5, cap="round")) info = (f"vx {self.vx:6.0f} vy {self.vy:6.0f} ground {'yes' if self.on_ground else 'no '} " f"coyote {self.coyote:.2f} buffer {self.buffer:.2f}") shapes.append(draw.rect(16, 16, 520, 24, radius=6, fill="#00000088")) shapes.append(draw.text(26, 21, info, size=12, font="mono", color="#cbd5e1")) return pz.canvas(shapes, width=W, height=H) def view(self): t = self.resolved_theme() hud = pz.row([ui.label("Jump Lab", weight="semibold"), pz.hspace(), ui.badge("on the ground" if self.on_ground else "in the air", icon="footprints" if self.on_ground else "wind", tint="success" if self.on_ground else "accent"), ui.badge("debug on" if self.debug else "debug off", icon="bug", tint="text_muted")], spacing=8, align="center") return pz.container(pz.column([hud, pz.container(self.board(), radius=10, clip=True), ui.muted("←/→ run · Space jumps (hold for higher) · H toggles the debug view", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(JumpLab) ``` #### Next - [4. A platformer](https://albakore.github.io/pyzed/games/platformer/): A tile map, a camera that follows you, enemies to stomp, coins and a goal. ### 4. A platformer > Build a side-scrolling platformer in Python with Pyzed: a tile map written as text, collisions with nearby tiles only, a smooth camera with parallax, patrolling enemies you can stomp, coins, spikes, lives and a goal flag. Source: https://albakore.github.io/pyzed/games/platformer/ With the physics of Jump Lab you have a character that runs and jumps. Now give it a world. This step builds *Pixel Run*: a level three thousand pixels wide, written as a few lines of text, a camera that follows the player, slimes that patrol and can be stomped, coins, spikes and a flag at the end. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A complete level with a camera, enemies, coins, lives and a win screen | 60 minutes | Intermediate | [3. Collisions and physics](https://albakore.github.io/pyzed/games/collisions-physics/) | #### What you'll learn - Write a level as a **tile map** in text and turn it into objects. - Collide only with the **tiles near the player**, and draw only the **tiles on screen**. - A **camera** that eases toward the player and stops at the level's edges, plus **parallax**. - **Enemies** that patrol, turn at walls and ledges, and can be **stomped**. - **Lives, respawn and a win screen**. 1. **The level is text.** Each character is a 32 × 32 tile: `#` ground, `=` a platform, `o` a coin, `E` a slime, `^` spikes, `P` the start and `F` the flag. Editing the level is editing these strings: ```python TILE = 32 LEVEL = [ "................................................................................................", ... "...P.ooo......ooo.......Eoo.######............Eoo...##...^^.ooE...............E.ooo.......F.....", "##################...####################...######################...###########################", "##################...####################...######################...###########################", ] COLS, ROWS = len(LEVEL[0]), len(LEVEL) SOLID = "#=" ``` When a game starts, `new_game` reads it once: coins go into a set of `(col, row)`, each `E` becomes an `Enemy`, `P` sets the start and `F` the flag's box. 2. **Only the tiles that matter.** The level has more than a thousand tiles, but the player can only touch the few under its box. `tiles_near` turns the player's box into a range of columns and rows and returns the solid tiles there; the collision code is the same as in Jump Lab, one axis at a time: ```python def solid(col, row): if col < 0 or col >= COLS: return True # the sides of the level are walls if row < 0 or row >= ROWS: return False # above and below are open (falling out is a lost life) return LEVEL[row][col] in SOLID def tiles_near(rect): out = [] for row in range(int(rect.top // TILE), int(rect.bottom // TILE) + 1): for col in range(int(rect.left // TILE), int(rect.right // TILE) + 1): if solid(col, row): out.append(game.Rect(col * TILE, row * TILE, TILE, TILE)) return out ``` 3. **A camera.** The camera is one number: how far the world is scrolled. It aims a little ahead of the player (40% of the screen), stays inside the level, and eases toward its target with `lerp`, so it glides instead of jerking: ```python target = game.clamp(self.player.center[0] - W * 0.4, 0, LEVEL_W - W) self.camera = game.lerp(self.camera, target, min(1.0, 8 * dt)) ``` Drawing the world through the camera is one transform: `draw.translate(-cam, 0)` before the world, inside `save()`/`restore()` so the sky is not moved. Hills move by `cam × 0.2` and `cam × 0.45` instead, which makes them look far away (**parallax**). 4. **Draw only what is on screen.** From the camera you know the first and last visible columns. Tiles of one color are collected and drawn with a single `draw.rects`, so the whole level costs three shapes: ```python first, last = cam // TILE, (cam + W) // TILE + 1 dirt, grass, stone = [], [], [] for row, line in enumerate(LEVEL): for col in range(max(0, first), min(COLS, last + 1)): ... shapes += [draw.rects(dirt, fill="#5b3a29"), draw.rects(grass, fill="#65a30d"), draw.rects(stone, fill="#64748b")] ``` 5. **Enemies that patrol.** A slime walks until the tile ahead is a wall or the tile below-ahead is empty; then it steps back and turns around. That one rule keeps it on its platform: ```python r.x += e.vx * dt ahead = r.right if e.vx > 0 else r.left col = int(ahead // TILE) row = int((r.bottom - 1) // TILE) if solid(col, row) or not solid(col, row + 1): r.x -= e.vx * dt e.vx = -e.vx ``` 6. **Stomp or get hurt.** Touching a slime is fine *if you are falling onto it*: the player's bottom was above the slime's top in the previous frame. Then the slime is squashed and the player bounces. Any other touch costs a life: ```python if self.vy > 0 and p.bottom - self.vy / 60 <= e.rect.top + 8: # coming down on it: stomp e.squashed = 0.001 self.vy = -JUMP_SPEED * 0.6 self.sparks.burst(e.rect.center[0], e.rect.bottom, count=14, color="#a3e635", angle=-90, spread=160) self.shake.kick(3) elif not self.invincible: return self.hurt() ``` After a hit the player respawns at the start and blinks for a second (`invincible`), so one mistake never costs two lives. 7. **Win and lose.** Falling below the level, spikes and slimes call `hurt()`; reaching the flag sets `won`. The cards are the same glass widgets as before, and Enter calls `new_game()`. **Design levels for the jump you have:** With `JUMP_SPEED = 680` and `GRAVITY = 1900` the hero rises about 120 pixels (almost four tiles) and covers about five tiles in a full jump at top speed. Keep gaps at three tiles and walls at three tiles high and every jump is fair. Change the physics and the level changes with it. **Bigger worlds:** Text maps are perfect for a few levels. For many, keep each level in its own `.txt` file and read it with `Path("levels/1.txt").read_text().splitlines()`, or export from a tile editor such as Tiled as CSV. #### Try this - Add a checkpoint flag halfway: when touched, `respawn()` uses its position. - Make `=` platforms one-way: only collide when falling and the player's bottom was above the platform's top. - Add a second level: a list of maps, and `level += 1` when the flag is reached. #### Full code `examples/games/platformer.py`: ```py """Pixel Run: a side-scrolling platformer with a tile map, a camera, enemies and coins. python examples/games/platformer.py Arrows or A/D run, Space (or Up/W) jumps, hold it to jump higher. Stomp the slimes, grab the coins, avoid the spikes and reach the flag. Enter plays again. Builds on the physics of Jump Lab and adds what turns it into a level: - **A tile map written as text**: ``#`` ground, ``=`` platforms, ``o`` coins, ``E`` enemies, ``^`` spikes, ``P`` the start and ``F`` the flag. Only the tiles near the player are checked for collisions, and only the ones on screen are drawn. - **A camera** that follows the player smoothly and stops at the edges of the level, with hills that scroll slower than the ground (parallax). - **Enemies** that patrol and turn at walls and ledges, and can be stomped from above. - **Lives, respawn, a win screen** and a HUD made of ordinary widgets. Tutorial: https://albakore.github.io/pyzed/games/platformer/ """ import math import pyzed as pz from pyzed import draw, game, ui TILE = 32 LEVEL = [ "................................................................................................", "................................................................................................", "................................................................................................", "................................................................................................", "....................................ooo.........................................................", "...................................=====........................................................", ".........ooo.............................................................ooo....................", "........=====...........................................................=====...................", "...................oo.....................oo........##.............oo...........................", "...............................###..................##..........................................", "...P.ooo......ooo.......Eoo.######............Eoo...##...^^.ooE...............E.ooo.......F.....", "##################...####################...######################...###########################", "##################...####################...######################...###########################", ] COLS, ROWS = len(LEVEL[0]), len(LEVEL) LEVEL_W, LEVEL_H = COLS * TILE, ROWS * TILE W, H = 704, LEVEL_H # the window shows 22 of the 96 columns RUN, ACCEL, AIR_ACCEL = 240, 2200, 1400 GRAVITY, JUMP_SPEED, LOW_JUMP, MAX_FALL = 1900, 680, 2.6, 900 COYOTE, BUFFER = 0.1, 0.12 JUMP_KEYS = ("space", "up", "w") ENEMY_SPEED = 55 SOLID = "#=" SLIME = ["..####..", ".######.", "##w##w##", "##k##k##", "########", "########", ".#.##.#."] SLIME_FLAT = ["........", "........", "........", "..####..", "##w##w##", "########", "########"] def solid(col, row): """Whether the tile at ``(col, row)`` blocks; outside the map, the sides block and the bottom is open.""" if col < 0 or col >= COLS: return True if row < 0 or row >= ROWS: return False return LEVEL[row][col] in SOLID def tiles_near(rect): """The solid tiles ``rect`` could touch: only the few cells under it, never the whole map.""" out = [] for row in range(int(rect.top // TILE), int(rect.bottom // TILE) + 1): for col in range(int(rect.left // TILE), int(rect.right // TILE) + 1): if solid(col, row): out.append(game.Rect(col * TILE, row * TILE, TILE, TILE)) return out class Enemy: def __init__(self, x, y): self.rect = game.Rect(x + 2, y + TILE - 24, 28, 24) self.vx = -ENEMY_SPEED self.squashed = 0.0 # seconds since it was stomped (0 while alive) class Platformer(pz.App): title = "Pixel Run" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None): self.held = None # the keys held down; None reads the keyboard self.sparks = game.Particles(gravity=600, seed=seed) self.shake = game.Shake(seed=seed) self.best_time = None self.new_game() # -- state ------------------------------------------------------------------------------ def new_game(self): self.coins, self.enemies = set(), [] for row, line in enumerate(LEVEL): for col, ch in enumerate(line): if ch == "o": self.coins.add((col, row)) elif ch == "E": self.enemies.append(Enemy(col * TILE, row * TILE)) elif ch == "P": self.start = (col * TILE + 4, row * TILE + TILE - 30) elif ch == "F": self.flag = game.Rect(col * TILE + 10, row * TILE - 4 * TILE, 12, 5 * TILE) self.total_coins = len(self.coins) self.lives, self.time = 3, 0.0 self.won = self.over = False self.camera = 0.0 self.respawn() def respawn(self): self.player = game.Rect(*self.start, 22, 30) self.vx = self.vy = 0.0 self.on_ground = False self.coyote = self.buffer = 0.0 self.facing = 1 self.invincible = 1.0 # a second of blinking after (re)spawning @property def collected(self): return self.total_coins - len(self.coins) # -- input ------------------------------------------------------------------------------ def on_key(self, combo): if combo in JUMP_KEYS: self.buffer = BUFFER elif combo == "enter" and (self.won or self.over): self.new_game() # -- update ----------------------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] def tick(self, dt): dt = min(dt, 1 / 30) self.sparks.update(dt) self.shake.update(dt) if self.won or self.over: return self.time += dt self.invincible = max(0.0, self.invincible - dt) self.move_player(dt) self.move_enemies(dt) self.pick_coins() self.check_dangers() # The camera looks a little ahead of the player and eases toward it. target = game.clamp(self.player.center[0] - W * 0.4, 0, LEVEL_W - W) self.camera = game.lerp(self.camera, target, min(1.0, 8 * dt)) def move_player(self, dt): move = game.axis(("left", "a"), ("right", "d"), held=self.held) if move: self.facing = move accel = (ACCEL if self.on_ground else AIR_ACCEL) * dt self.vx += game.clamp(move * RUN - self.vx, -accel, accel) rising_without_key = self.vy < 0 and not game.is_pressed(*JUMP_KEYS, held=self.held) self.vy = min(self.vy + GRAVITY * (LOW_JUMP if rising_without_key else 1) * dt, MAX_FALL) self.coyote = COYOTE if self.on_ground else max(0.0, self.coyote - dt) self.buffer = max(0.0, self.buffer - dt) if self.buffer > 0 and self.coyote > 0: self.vy, self.buffer, self.coyote = -JUMP_SPEED, 0.0, 0.0 p = self.player p.x += self.vx * dt for tile in tiles_near(p): if p.overlaps(tile): p.x = tile.left - p.w if self.vx > 0 else tile.right self.vx = 0.0 p.y += self.vy * dt self.on_ground = False for tile in tiles_near(p): if p.overlaps(tile): if self.vy > 0: p.y, self.on_ground = tile.top - p.h, True else: p.y = tile.bottom self.vy = 0.0 def move_enemies(self, dt): for e in self.enemies: if e.squashed: e.squashed += dt continue r = e.rect r.x += e.vx * dt ahead = r.right if e.vx > 0 else r.left col = int(ahead // TILE) row = int((r.bottom - 1) // TILE) if solid(col, row) or not solid(col, row + 1): # a wall, or no floor ahead: r.x -= e.vx * dt # step back and turn around e.vx = -e.vx self.enemies = [e for e in self.enemies if e.squashed < 0.6] def pick_coins(self): p = self.player for col, row in list(self.coins): if p.overlaps(game.Rect(col * TILE + 8, row * TILE + 8, 16, 16)): self.coins.discard((col, row)) self.sparks.burst(col * TILE + 16, row * TILE + 16, count=12, color=["#fde68a", "#fbbf24"], speed=(40, 160), life=(0.2, 0.5)) def check_dangers(self): p = self.player for e in self.enemies: if e.squashed or not p.overlaps(e.rect): continue if self.vy > 0 and p.bottom - self.vy / 60 <= e.rect.top + 8: # coming down on it: stomp e.squashed = 0.001 self.vy = -JUMP_SPEED * 0.6 self.sparks.burst(e.rect.center[0], e.rect.bottom, count=14, color="#a3e635", angle=-90, spread=160) self.shake.kick(3) elif not self.invincible: return self.hurt() for row in range(int(p.top // TILE), int(p.bottom // TILE) + 1): for col in range(int(p.left // TILE), int(p.right // TILE) + 1): if 0 <= row < ROWS and 0 <= col < COLS and LEVEL[row][col] == "^": spike = game.Rect(col * TILE + 4, row * TILE + 14, TILE - 8, TILE - 14) if p.overlaps(spike) and not self.invincible: return self.hurt() if p.top > LEVEL_H + 40: return self.hurt() if p.overlaps(self.flag): self.won = True self.best_time = self.time if self.best_time is None else min(self.best_time, self.time) self.sparks.burst(self.flag.center[0], self.flag.top + 20, count=60, color=["#f472b6", "#fbbf24", "#22d3ee", "#a3e635"], speed=(80, 320)) def hurt(self): self.lives -= 1 self.shake.kick(10) self.sparks.burst(*self.player.center, count=30, color=["#f87171", "#fecaca"], speed=(80, 260)) if self.lives <= 0: self.over = True else: self.respawn() # -- draw ------------------------------------------------------------------------------- def board(self): cam = round(self.camera) shapes = [draw.background("#1e1b3a")] for i, color in enumerate(("#2a2550", "#352d63", "#43367a")): # sky bands shapes.append(draw.rect(0, 70 + i * 60, W, 60, fill=color)) shapes.append(draw.circle(560, 90, 34, fill="#fde68a")) for layer, (speed, color, base) in enumerate(((0.2, "#2c2a55", 300), (0.45, "#24344d", 340))): off = -(cam * speed) % 240 # parallax hills for i in range(-1, W // 240 + 2): cx = i * 240 + off + layer * 120 shapes.append(draw.circle(cx, base, 150 - layer * 30, fill=color)) shapes += [draw.save(), *self.shake.shapes(), draw.translate(-cam, 0)] first, last = cam // TILE, (cam + W) // TILE + 1 dirt, grass, stone = [], [], [] for row, line in enumerate(LEVEL): for col in range(max(0, first), min(COLS, last + 1)): # only the visible columns ch = line[col] x, y = col * TILE, row * TILE if ch == "#": dirt.append((x, y, TILE, TILE)) if row == 0 or LEVEL[row - 1][col] not in SOLID: grass.append((x, y, TILE, 8)) elif ch == "=": stone.append((x, y, TILE, TILE / 2)) elif ch == "^": for k in range(2): sx = x + 2 + k * 14 shapes.append(draw.polygon([(sx, y + TILE), (sx + 7, y + 12), (sx + 14, y + TILE)], fill="#cbd5e1")) shapes += [draw.rects(dirt, fill="#5b3a29"), draw.rects(grass, fill="#65a30d"), draw.rects(stone, fill="#64748b")] for col, row in self.coins: cx, cy = col * TILE + 16, row * TILE + 16 + math.sin(self.time * 4 + col) * 3 spin = 0.35 + 0.65 * abs(math.cos(self.time * 3 + col * 0.7)) shapes += game.glow(cx, cy, 6, "#fbbf24", layers=2, strength=0.35, core=False) shapes.append(draw.rect(cx - 7 * spin, cy - 8, 14 * spin, 16, radius=7 * spin, fill="#fbbf24")) fx, fy = self.flag.x, self.flag.y shapes.append(draw.rect(fx + 4, fy, 4, self.flag.h, fill="#e2e8f0")) wave = math.sin(self.time * 5) * 4 shapes.append(draw.polygon([(fx + 8, fy + 2), (fx + 52, fy + 14 + wave), (fx + 8, fy + 28)], fill="#f472b6")) for e in self.enemies: frame = SLIME_FLAT if e.squashed else SLIME bob = 0 if e.squashed else abs(math.sin(self.time * 8 + e.rect.x * 0.1)) * -2 shapes += game.sprite(frame, {"#": "#84cc16", "w": "#f8fafc", "k": "#111827"}, e.rect.x - 2, e.rect.y - 4 + bob, scale=4, flip=e.vx > 0) if not self.over and (not self.invincible or int(self.time * 14) % 2): p = self.player shapes.append(p.shape(fill="#22d3ee", radius=5)) ex = p.center[0] + self.facing * 4 shapes.append(draw.rect(ex - 2, p.y + 8, 4, 7, radius=2, fill="#0b0e14")) shapes += self.sparks.shapes(glow=True) shapes.append(draw.restore()) return pz.canvas(shapes, width=W, height=H) def overlay(self): if self.won: title, lines = "Level clear!", [f"{self.collected}/{self.total_coins} coins in {self.time:.1f} s", "Enter plays again"] elif self.over: title, lines = "Game over", [f"{self.collected}/{self.total_coins} coins", "Enter plays again"] else: return None card = pz.container(pz.column([ui.heading(title, level=2), *[ui.muted(t) for t in lines]], spacing=8, align="center"), padding=(20, 32)).style(glass=True, radius=14) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def view(self): t = self.resolved_theme() hearts = pz.row([pz.icon("heart", size=15, color="#f87171" if i < self.lives else t.border) for i in range(3)], spacing=3) hud = pz.row([ ui.label("Pixel Run", weight="semibold"), pz.hspace(), ui.badge(f"{self.collected}/{self.total_coins}", icon="coins", tint="warning"), ui.badge(f"{self.time:.1f} s", icon="timer", tint="accent"), ui.badge(f"Best {self.best_time:.1f} s", icon="trophy", tint="text_muted") if self.best_time else None, hearts, ], spacing=8, align="center") play = pz.stack([self.board(), self.overlay()], width=W, height=H) return pz.container(pz.column([hud, pz.container(play, radius=10, clip=True), ui.muted("←/→ run · Space jumps · stomp the slimes · reach the flag", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(Platformer) ``` #### Next - [5. Asteroids with particles](https://albakore.github.io/pyzed/games/asteroids/): Rotation and thrust, wrap-around space, circle collisions and lots of particles. ### 5. Asteroids with particles > Make an Asteroids-style space shooter in Python with Pyzed: rotation and thrust with sin and cos, drag, wrap-around space, circle collisions, rocks that split, engine trails, explosions with game.Particles and screen shake. Source: https://albakore.github.io/pyzed/games/asteroids/ *Rock Storm* is a shooter in the spirit of the 1979 classic: a ship that turns and thrusts, drifts with inertia, and shoots rocks that split into smaller rocks. It is a good place to learn a bit of trigonometry (it is only `cos` and `sin`) and to make the screen come alive with **particles**. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A shooter with inertia, splitting rocks, waves, lives and particle effects | 50 minutes | Intermediate | [1. Your first game](https://albakore.github.io/pyzed/games/first-game/); [3. Collisions](https://albakore.github.io/pyzed/games/collisions-physics/) helps | #### What you'll learn - Turn an **angle** into a direction with `cos` and `sin`; thrust and **drag**. - **Wrap** things around the edges of the screen. - **Circle collisions** for rocks, bullets and the ship. - **Particles** with `game.Particles`: an engine trail, muzzle flashes, debris and explosions. - **Screen shake**, invulnerability after a hit and waves. 1. **An angle becomes a direction.** The ship stores where it points in degrees. `cos` and `sin` of that angle give a direction of length 1: at 0° it is `(1, 0)` (right), at -90° it is `(0, -1)` (up). Thrust adds that direction to the velocity; drag takes away a fraction every second: ```python def fly(self, dt): self.angle += game.axis(("left", "a"), ("right", "d"), held=self.held) * TURN * dt rad = math.radians(self.angle) fx, fy = math.cos(rad), math.sin(rad) if game.is_pressed("up", "w", held=self.held): self.vx += fx * THRUST * dt self.vy += fy * THRUST * dt damping = max(0.0, 1 - DRAG * dt) self.vx, self.vy = self.vx * damping, self.vy * damping ``` The ship keeps drifting after you let go: that inertia is what makes Asteroids feel like space. 2. **Wrap around.** What leaves through one edge comes back through the other. The modulo operator `%` does it in one line; the margin lets big rocks leave the screen completely before they reappear: ```python def wrap(x, y, margin=0): return ((x + margin) % (W + 2 * margin) - margin, (y + margin) % (H + 2 * margin) - margin) ``` 3. **Rocks with character.** A rock is a circle for collisions, but drawn as a jagged polygon: points around a circle at slightly random distances, turned by its own angle every frame: ```python n = 9 + size * 2 self.outline = [(math.cos(i / n * math.tau), math.sin(i / n * math.tau), rnd.uniform(0.75, 1.1)) for i in range(n)] ``` 4. **Circle collisions.** A bullet hits a rock when it is closer to the rock's center than its radius; the ship hits a rock when the distance is less than both radii added: ```python for b in self.bullets[:]: for rock in self.rocks: if math.hypot(b[0] - rock.x, b[1] - rock.y) < rock.r: self.bullets.remove(b) self.break_rock(rock) break ``` Iterate over a copy (`self.bullets[:]`) when you remove items from the list inside the loop. 5. **Split the rock.** A big rock (size 3) becomes two medium ones, a medium becomes two small, a small one is gone. Each piece flies off in a random direction, a little faster than its parent: ```python def break_rock(self, rock): self.rocks.remove(rock) self.score += POINTS[rock.size] self.sparks.burst(rock.x, rock.y, count=10 + rock.size * 10, color=["#cbd5e1", "#94a3b8", "#fbbf24"], speed=(40, 120 + rock.size * 60), life=(0.3, 0.6 + rock.size * 0.25), size=(1, 3)) self.shake.kick(rock.size * 2.5) if rock.size > 1: for _ in range(2): self.rocks.append(Rock(self.random, rock.x, rock.y, rock.size - 1, math.hypot(rock.vx, rock.vy) * 1.3 + self.random.uniform(20, 60))) ``` 6. **Particles everywhere.** `game.Particles` is a list of short-lived dots. `burst` adds some, `update(dt)` moves them and removes the dead ones, `shapes()` draws them fading out. The engine trail is a small burst *every frame* while thrusting, aimed backwards: ```python self.trail = game.Particles(drag=3.0, limit=400, seed=seed) ... self.trail.burst(self.x - fx * SHIP_R, self.y - fy * SHIP_R, count=3, angle=self.angle + 180, spread=30, speed=(80, 180), life=(0.15, 0.4), size=(1.5, 3), color=["#fbbf24", "#f97316", "#fde68a"]) ``` `angle` and `spread` aim a burst (`angle=-90, spread=40` is a fountain), `gravity` and `drag` shape the fall, `life` and `size` are ranges picked at random per particle, and `color` can be a list to mix. 7. **Getting hit.** A crash costs a life, shakes the screen hard and scatters the ship as particles. Then the ship comes back in the middle and blinks for two seconds (`safe`), when rocks can't hurt it. **Particles are cheap, but not free:** Each particle is a circle (two with `glow=True`). A few hundred alive at once is nothing; tens of thousands is a lot of Python per frame. Use `limit=` to cap each emitter, and shorter `life` before smaller `count`. #### Try this - Add a UFO that crosses the screen now and then and shoots at the ship. - Add a hyperspace key that moves the ship to a random place (with a burst where it left). - Give rocks a color per size and make small ones flash white for a frame when hit. #### Full code `examples/games/asteroids.py`: ```py """Rock Storm: an Asteroids-style shooter with thrust, wrap-around space and lots of particles. python examples/games/asteroids.py Left/Right (or A/D) turn, Up (or W) thrusts, hold Space to fire, P pauses, Enter plays again. Big rocks split in two when hit, small ones burst into sparks. Shows: - **Rotation and thrust**: the ship has an angle; thrust adds ``cos``/``sin`` of it to the velocity, and a little drag slows it down. Everything wraps around the screen edges. - **Circle collisions**: two circles touch when the distance between their centers is smaller than the sum of their radii. - **Particles** everywhere: an engine trail emitted every frame, explosions, debris and sparks, with ``game.Particles`` (``burst``, ``update``, ``shapes``). - **Screen shake**, invulnerability after a hit, waves that get harder. Tutorial: https://albakore.github.io/pyzed/games/asteroids/ """ import math import random import pyzed as pz from pyzed import draw, game, ui W, H = 720, 480 TURN = 260 # degrees per second THRUST = 380 # px/s² while thrusting DRAG = 0.6 # fraction of the speed lost per second MAX_SPEED = 420 BULLET_SPEED = 620 BULLET_LIFE = 0.9 FIRE_DELAY = 0.18 SHIP_R = 13 SIZES = {3: 46, 2: 26, 1: 14} # rock size -> radius POINTS = {3: 20, 2: 50, 1: 100} def wrap(x, y, margin=0): """The point moved back onto the screen when it leaves through one edge.""" return ((x + margin) % (W + 2 * margin) - margin, (y + margin) % (H + 2 * margin) - margin) class Rock: def __init__(self, rnd, x, y, size, speed): self.x, self.y, self.size = x, y, size a = rnd.uniform(0, math.tau) self.vx, self.vy = math.cos(a) * speed, math.sin(a) * speed self.angle, self.spin = rnd.uniform(0, 360), rnd.uniform(-60, 60) # A jagged outline: points around a circle at slightly random distances. n = 9 + size * 2 self.outline = [(math.cos(i / n * math.tau), math.sin(i / n * math.tau), rnd.uniform(0.75, 1.1)) for i in range(n)] @property def r(self): return SIZES[self.size] def points(self): rad = math.radians(self.angle) c, s = math.cos(rad), math.sin(rad) return [(self.x + (ux * c - uy * s) * k * self.r, self.y + (ux * s + uy * c) * k * self.r) for ux, uy, k in self.outline] class Asteroids(pz.App): title = "Rock Storm" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) self.held = None # the keys held down; None reads the keyboard self.sparks = game.Particles(drag=1.2, seed=seed) # explosions and debris self.trail = game.Particles(drag=3.0, limit=400, seed=seed) # the engine's exhaust self.shake = game.Shake(seed=seed) self.stars = [(self.random.uniform(0, W), self.random.uniform(0, H), self.random.uniform(0.4, 1.4)) for _ in range(90)] self.best = 0 self.new_game() # -- state ------------------------------------------------------------------------------ def new_game(self): self.score, self.lives, self.wave = 0, 3, 0 self.over = self.paused = False self.clock = 0.0 self.reset_ship() self.next_wave() def reset_ship(self): self.x, self.y, self.vx, self.vy = W / 2, H / 2, 0.0, 0.0 self.angle = -90.0 # pointing up self.bullets = [] # [x, y, vx, vy, age] self.cooldown = 0.0 self.safe = 2.0 # seconds of invulnerability def next_wave(self): self.wave += 1 self.rocks = [] for _ in range(2 + self.wave): # Spawn on the edges, never on top of the ship. x, y = (self.random.uniform(0, W), -40) if self.random.random() < 0.5 else (-40, self.random.uniform(0, H)) self.rocks.append(Rock(self.random, x, y, 3, self.random.uniform(30, 60) + self.wave * 8)) # -- input ------------------------------------------------------------------------------ def on_key(self, combo): if combo == "p" and not self.over: self.paused = not self.paused elif combo == "enter" and self.over: self.new_game() # -- update ----------------------------------------------------------------------------- def subscriptions(self): return [] if self.paused else [pz.frames(self.tick)] def tick(self, dt): dt = min(dt, 1 / 30) self.clock += dt self.sparks.update(dt) self.trail.update(dt) self.shake.update(dt) for rock in self.rocks: rock.x, rock.y = wrap(rock.x + rock.vx * dt, rock.y + rock.vy * dt, rock.r) rock.angle += rock.spin * dt if self.over: return self.safe = max(0.0, self.safe - dt) self.fly(dt) self.move_bullets(dt) self.collide() if not self.rocks: self.next_wave() def fly(self, dt): self.angle += game.axis(("left", "a"), ("right", "d"), held=self.held) * TURN * dt rad = math.radians(self.angle) fx, fy = math.cos(rad), math.sin(rad) if game.is_pressed("up", "w", held=self.held): self.vx += fx * THRUST * dt self.vy += fy * THRUST * dt # Exhaust: a few particles every frame, shot backwards from the tail. self.trail.burst(self.x - fx * SHIP_R, self.y - fy * SHIP_R, count=3, angle=self.angle + 180, spread=30, speed=(80, 180), life=(0.15, 0.4), size=(1.5, 3), color=["#fbbf24", "#f97316", "#fde68a"]) damping = max(0.0, 1 - DRAG * dt) self.vx, self.vy = self.vx * damping, self.vy * damping speed = math.hypot(self.vx, self.vy) if speed > MAX_SPEED: self.vx, self.vy = self.vx / speed * MAX_SPEED, self.vy / speed * MAX_SPEED self.x, self.y = wrap(self.x + self.vx * dt, self.y + self.vy * dt) self.cooldown -= dt if game.is_pressed("space", held=self.held) and self.cooldown <= 0: self.fire() def fire(self): self.cooldown = FIRE_DELAY rad = math.radians(self.angle) fx, fy = math.cos(rad), math.sin(rad) self.bullets.append([self.x + fx * SHIP_R, self.y + fy * SHIP_R, self.vx + fx * BULLET_SPEED, self.vy + fy * BULLET_SPEED, 0.0]) self.sparks.burst(self.x + fx * SHIP_R, self.y + fy * SHIP_R, count=4, angle=self.angle, spread=40, speed=(60, 140), life=(0.05, 0.15), color="#a5f3fc") def move_bullets(self, dt): for b in self.bullets: b[0], b[1] = wrap(b[0] + b[2] * dt, b[1] + b[3] * dt) b[4] += dt self.bullets = [b for b in self.bullets if b[4] < BULLET_LIFE] def collide(self): for b in self.bullets[:]: for rock in self.rocks: if math.hypot(b[0] - rock.x, b[1] - rock.y) < rock.r: # a point inside a circle self.bullets.remove(b) self.break_rock(rock) break if self.safe <= 0: for rock in self.rocks: if math.hypot(self.x - rock.x, self.y - rock.y) < rock.r + SHIP_R * 0.8: # circle vs circle self.crash() break def break_rock(self, rock): self.rocks.remove(rock) self.score += POINTS[rock.size] self.sparks.burst(rock.x, rock.y, count=10 + rock.size * 10, color=["#cbd5e1", "#94a3b8", "#fbbf24"], speed=(40, 120 + rock.size * 60), life=(0.3, 0.6 + rock.size * 0.25), size=(1, 3)) self.shake.kick(rock.size * 2.5) if rock.size > 1: for _ in range(2): child = Rock(self.random, rock.x, rock.y, rock.size - 1, math.hypot(rock.vx, rock.vy) * 1.3 + self.random.uniform(20, 60)) self.rocks.append(child) def crash(self): self.lives -= 1 self.shake.kick(16) self.sparks.burst(self.x, self.y, count=70, color=["#22d3ee", "#a5f3fc", "#f472b6", "#ffffff"], speed=(60, 340), life=(0.4, 1.2), size=(1.5, 4)) if self.lives <= 0: self.over = True self.best = max(self.best, self.score) else: self.reset_ship() # -- draw ------------------------------------------------------------------------------- def board(self): shapes = [draw.background("#05060d"), *self.shake.shapes()] for x, y, r in self.stars: shapes.append(draw.circle(x, y, r, fill=game.rgba("#e2e8f0", 0.25 + r * 0.3))) shapes += self.trail.shapes(glow=True) for rock in self.rocks: shapes.append(draw.polygon(rock.points(), fill="#1e2433", stroke="#94a3b8", width=2)) for x, y, vx, vy, age in self.bullets: speed = math.hypot(vx, vy) or 1 tx, ty = x - vx / speed * 10, y - vy / speed * 10 shapes.append(draw.line(tx, ty, x, y, stroke=game.rgba("#22d3ee", 0.35), width=6, cap="round")) shapes.append(draw.line(tx, ty, x, y, stroke="#e0fbff", width=2, cap="round")) if not self.over and (self.safe <= 0 or int(self.clock * 12) % 2): rad = math.radians(self.angle) pts = [] for a, d in ((0, 1.4), (140, 1.0), (180, 0.45), (220, 1.0)): # nose, wing, tail notch, wing pa = rad + math.radians(a) pts.append((self.x + math.cos(pa) * SHIP_R * d, self.y + math.sin(pa) * SHIP_R * d)) shapes += game.glow(self.x, self.y, SHIP_R, "#22d3ee", layers=3, strength=0.18, core=False) shapes.append(draw.polygon(pts, fill="#0b1220", stroke="#22d3ee", width=2.5)) shapes += self.sparks.shapes(glow=True) return pz.canvas(shapes, width=W, height=H) def overlay(self): if self.over: title, lines = "Game over", [f"Score {self.score:,} · wave {self.wave}", "Enter plays again"] elif self.paused: title, lines = "Paused", ["P continues"] else: return None card = pz.container(pz.column([ui.heading(title, level=2), *[ui.muted(t) for t in lines]], spacing=8, align="center"), padding=(20, 32)).style(glass=True, radius=14) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def view(self): t = self.resolved_theme() ships = pz.row([pz.icon("rocket", size=15, color="#22d3ee" if i < self.lives else t.border) for i in range(3)], spacing=3) hud = pz.row([ ui.label("Rock Storm", weight="semibold"), pz.hspace(), ui.badge(f"Wave {self.wave}", icon="orbit", tint="accent"), ui.badge(f"{self.score:,}", icon="star", tint="warning"), ui.badge(f"Best {self.best:,}", icon="trophy", tint="text_muted"), ships, ], spacing=8, align="center") play = pz.stack([self.board(), self.overlay()], width=W, height=H) return pz.container(pz.column([hud, pz.container(play, radius=10, clip=True), ui.muted("←/→ turn · ↑ thrust · hold Space to fire · P pauses", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(Asteroids) ``` #### Next - [6. A puzzle game: 2048](https://albakore.github.io/pyzed/games/puzzle-2048/): Rules as plain functions, tweened tiles, swipes and a widget overlay. ### 6. A puzzle game: 2048 > Make 2048 in Python with Pyzed: the rules as plain testable functions, tiles that slide and pop with tweens, a frame loop that only runs while something moves, mouse swipes, and score boxes and dialogs made of widgets. Source: https://albakore.github.io/pyzed/games/puzzle-2048/ Not every game is a race against the clock. In a puzzle game nothing happens until you move, and then a lot happens at once: tiles slide, merge, pop and a new one appears. This step builds **2048** and shows how to keep the rules separate from the animation, and how a game can use **no CPU at all** while the player is thinking. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A complete 2048 with animations, swipes, win and lose dialogs | 45 minutes | Intermediate | [2. Sprites and animation](https://albakore.github.io/pyzed/games/sprites-animation/) (tweens) | #### What you'll learn - Write the **rules as plain functions** that are easy to read and test. - Separate the **game state** (the numbers) from its **animation** (where the tiles are drawn). - Run the frame loop **only while something moves**. - Read **arrow keys and mouse swipes**. - Put **score boxes, buttons and a dialog** made of widgets around and over a canvas. 1. **The rule, as a function.** Every move slides each row toward one side and merges equal neighbors, once. That is a function of a list of four numbers, with no window and no drawing: ```python def slide_line(values): """[2, 2, 4, 0] -> ([4, 4, 0, 0], 4): the new row and the points gained.""" tiles = [v for v in values if v] out, gained, i = [], 0, 0 while i < len(tiles): if i + 1 < len(tiles) and tiles[i] == tiles[i + 1]: out.append(tiles[i] * 2) gained += tiles[i] * 2 i += 2 # a tile merges only once per move else: out.append(tiles[i]) i += 1 return out + [0] * (len(values) - len(out)), gained ``` Sliding right, up or down is the same function on the cells in another order: `lines(direction)` returns each row or column starting from the edge the tiles move toward. The tests check the tricky cases: `[2, 2, 2, 2]` gives `[4, 4, 0, 0]`, not `[8, 0, 0, 0]`. 2. **Tiles remember where they came from.** To animate, each tile is an object with its value, its cell and the cell it came from. A tile that merges into another becomes a *ghost*: it slides to the same cell and disappears when the slide ends: ```python class Tile: def __init__(self, value, cell): self.value = value self.cell = self.came_from = cell self.merged = False # doubled in the last move: shows the old value while sliding self.new = True # just appeared: grows from nothing ``` `move()` walks every line exactly like `slide_line`, moves the tiles, and double-checks itself against `slide_line` with an `assert`. Then it adds the score, adds a new tile and restarts the animation clock: `self.anim = 0.0`. 3. **Animation from one clock.** Everything is computed from `anim`, the seconds since the last move: tiles are drawn between `came_from` and `cell` with an ease-out for the first 0.11 s, then merged tiles grow 18% and back, and new tiles grow from nothing: ```python slide = game.clamp(self.anim / SLIDE_TIME, 0, 1) ease = 1 - (1 - slide) ** 3 x, y = game.lerp(x0, x1, ease), game.lerp(y0, y1, ease) pop = (self.anim - SLIDE_TIME) / POP_TIME # 0 -> 1 after the slide if tile.new: scale = 0.0 if pop < 0 else min(1.0, 0.3 + pop * 0.9) elif tile.merged and 0 <= pop < 1: scale = 1 + 0.18 * (1 - abs(2 * pop - 1)) # up and back down ``` The rules already moved the numbers when you pressed the key; the animation only draws the way there. That is why you can press keys faster than the animation and never lose a move. 4. **No loop while you think.** `subscriptions()` returns the frame loop only while something is animating. Between moves the game draws nothing and uses no CPU, like any desktop app: ```python @property def animating(self): return self.anim < SLIDE_TIME + POP_TIME or bool(self.floats) def subscriptions(self): return [pz.frames(self.tick)] if self.animating else [] ``` 5. **Keys and swipes.** Arrow keys and WASD map to directions in `on_key`. For the mouse, the canvas reports where a drag starts (`on_press`), where it goes (`on_move`) and when it ends (`on_release`); a long enough drag is a swipe in its main direction: ```python def release(self): x0, y0, x1, y1 = self.swipe self.swipe = None dx, dy = x1 - x0, y1 - y0 if max(abs(dx), abs(dy)) > 30: # a short click is not a swipe self.move((0, 1 if dx > 0 else -1) if abs(dx) > abs(dy) else (1 if dy > 0 else -1, 0)) pz.canvas(shapes, width=BOARD, height=BOARD, on_press=self.press, on_move=self.drag, on_release=self.release, cursor="grab") ``` 6. **Widgets around the board.** The score boxes are containers with the theme's `surface` color; the points you just won rise and fade with `.style(opacity=..., translate=(0, ...))`. When you win or run out of moves, a glass card with buttons appears over the board with a native `.animate("zoom_in")`: ```python card = pz.container(pz.column([ui.heading(title, level=2), ui.muted(line), pz.row(buttons, spacing=8)], spacing=10, align="center"), padding=(22, 30)) card = card.style(glass=True, radius=14).animate("zoom_in", duration=250) ``` **Rules first, pixels later:** Keeping the rules in plain functions (`slide_line`, `can_move`) means you can test them in a fraction of a second, change the drawing without breaking the game, and even write a solver or an AI player against the same functions. #### Try this - Add **Undo**: keep the previous `values` and score before each move, and a button that restores them. - Make the board size a setting (`SIZE = 5`) and check that everything still works. - Save the best score between sessions, as in [step 8](https://albakore.github.io/pyzed/games/high-scores/). #### Full code `examples/games/game_2048.py`: ```py """2048: the sliding-tiles puzzle, with smooth moves, merges that pop and a widget overlay. python examples/games/game_2048.py Arrows or WASD (or a swipe with the mouse) slide every tile; two equal tiles that meet merge into one. Reach 2048. Shows how a turn-based game differs from an action game: - **The rules are plain functions** (``slide_line``), easy to read and to test without a window. - **Animation only between turns**: ``pz.frames`` runs while tiles slide and pop, and ``subscriptions()`` returns nothing the rest of the time, so the game uses no CPU while you think. - **Tweens** for sliding (each tile remembers where it came from), popping merges and the "+points" that float up. - **UI on top of the game**: score boxes, a New game button and a glass card with buttons when you win or lose. Tutorial: https://albakore.github.io/pyzed/games/puzzle-2048/ """ import random import pyzed as pz from pyzed import draw, game, ui SIZE = 4 CELL, GAP = 96, 12 BOARD = SIZE * CELL + (SIZE + 1) * GAP SLIDE_TIME = 0.11 POP_TIME = 0.16 DIRECTIONS = {"left": (0, -1), "a": (0, -1), "right": (0, 1), "d": (0, 1), "up": (-1, 0), "w": (-1, 0), "down": (1, 0), "s": (1, 0)} COLORS = {2: ("#3b4252", "#e5e9f0"), 4: ("#434c5e", "#e5e9f0"), 8: ("#d08770", "#ffffff"), 16: ("#e07a5f", "#ffffff"), 32: ("#e76f51", "#ffffff"), 64: ("#ef4444", "#ffffff"), 128: ("#e9c46a", "#1f2937"), 256: ("#f4d35e", "#1f2937"), 512: ("#facc15", "#1f2937"), 1024: ("#a3e635", "#1f2937"), 2048: ("#22d3ee", "#0b1220")} # -- the rules: plain functions ----------------------------------------------------------------- def slide_line(values): """Slides one row toward its start and merges equal neighbours once. ``[2, 2, 4, 0] -> ([4, 4, 0, 0], 4)``: the new row and the points gained. """ tiles = [v for v in values if v] out, gained, i = [], 0, 0 while i < len(tiles): if i + 1 < len(tiles) and tiles[i] == tiles[i + 1]: out.append(tiles[i] * 2) gained += tiles[i] * 2 i += 2 # both tiles are used up: a tile merges only once per move else: out.append(tiles[i]) i += 1 return out + [0] * (len(values) - len(out)), gained def lines(direction): """The cells of each row (or column) in the order tiles slide, the leading edge first.""" dr, dc = direction out = [] for i in range(SIZE): cells = [(i, j) if dr == 0 else (j, i) for j in range(SIZE)] if dr == 1 or dc == 1: cells.reverse() out.append(cells) return out def can_move(values): """Whether any move changes the board (``values`` maps ``(row, col)`` to a number).""" for r in range(SIZE): for c in range(SIZE): v = values.get((r, c), 0) if not v or (c + 1 < SIZE and values.get((r, c + 1)) == v) or (r + 1 < SIZE and values.get((r + 1, c)) == v): return True return False class Tile: def __init__(self, value, cell): self.value = value self.cell = self.came_from = cell self.merged = False # doubled in the last move: shows the old value while sliding self.new = True # just appeared: grows from nothing class Game2048(pz.App): title = "2048" size = (BOARD + 48, BOARD + 150) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) self.best = 0 self.new_game() # -- state ------------------------------------------------------------------------------ def new_game(self): self.tiles = {} # (row, col) -> Tile self.ghosts = [] # tiles that merged into another: they slide, then vanish self.score = 0 self.won = self.keep_going = False self.anim = 1.0 # seconds since the last move (animations end at SLIDE + POP) self.floats = [] # [text, age]: "+8" rising from the score self.swipe = None self.add_tile() self.add_tile() def add_tile(self): free = [(r, c) for r in range(SIZE) for c in range(SIZE) if (r, c) not in self.tiles] if free: cell = self.random.choice(free) self.tiles[cell] = Tile(4 if self.random.random() < 0.1 else 2, cell) @property def values(self): return {cell: tile.value for cell, tile in self.tiles.items()} @property def over(self): return not can_move(self.values) @property def animating(self): return self.anim < SLIDE_TIME + POP_TIME or bool(self.floats) # -- a move ----------------------------------------------------------------------------- def move(self, direction): if self.over or (self.won and not self.keep_going): return False for tile in self.tiles.values(): tile.came_from, tile.merged, tile.new = tile.cell, False, False self.ghosts = [] moved, gained, new_tiles = False, 0, {} for cells in lines(direction): row = [self.tiles[c] for c in cells if c in self.tiles] new_values, points = slide_line([self.tiles[c].value if c in self.tiles else 0 for c in cells]) gained += points # Walk the row the same way slide_line did, to know where each tile goes. k = 0 i = 0 while i < len(row): target = cells[k] tile = row[i] if i + 1 < len(row) and row[i + 1].value == tile.value: other = row[i + 1] other.cell = target # it slides to the same cell, then disappears self.ghosts.append(other) tile.value *= 2 tile.merged = True i += 2 else: i += 1 moved |= tile.cell != target or tile.merged tile.cell = target new_tiles[target] = tile k += 1 assert [new_tiles[c].value if c in new_tiles else 0 for c in cells] == new_values if not moved: return False self.tiles = new_tiles self.score += gained self.best = max(self.best, self.score) if gained: self.floats.append([f"+{gained}", 0.0]) if any(t.value >= 2048 for t in self.tiles.values()) and not self.keep_going: self.won = True self.add_tile() self.anim = 0.0 return True # -- input ------------------------------------------------------------------------------ def on_key(self, combo): if combo in DIRECTIONS: self.move(DIRECTIONS[combo]) def press(self, x, y): self.swipe = (x, y, x, y) def drag(self, x, y): if self.swipe: self.swipe = (*self.swipe[:2], x, y) def release(self): if not self.swipe: return x0, y0, x1, y1 = self.swipe self.swipe = None dx, dy = x1 - x0, y1 - y0 if max(abs(dx), abs(dy)) > 30: # a short click is not a swipe self.move((0, 1 if dx > 0 else -1) if abs(dx) > abs(dy) else (1 if dy > 0 else -1, 0)) def continue_playing(self): self.keep_going = True # -- animation -------------------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] if self.animating else [] # idle between moves def tick(self, dt): self.anim += dt for f in self.floats: f[1] += dt self.floats = [f for f in self.floats if f[1] < 0.8] if self.anim >= SLIDE_TIME: self.ghosts = [] # -- draw ------------------------------------------------------------------------------- @staticmethod def cell_xy(cell): r, c = cell return GAP + c * (CELL + GAP), GAP + r * (CELL + GAP) def tile_shapes(self, tile, value): slide = game.clamp(self.anim / SLIDE_TIME, 0, 1) ease = 1 - (1 - slide) ** 3 (x0, y0), (x1, y1) = self.cell_xy(tile.came_from), self.cell_xy(tile.cell) x, y = game.lerp(x0, x1, ease), game.lerp(y0, y1, ease) scale = 1.0 pop = (self.anim - SLIDE_TIME) / POP_TIME # 0 -> 1 after the slide if tile.new: scale = 0.0 if pop < 0 else min(1.0, 0.3 + pop * 0.9) elif tile.merged and 0 <= pop < 1: scale = 1 + 0.18 * (1 - abs(2 * pop - 1)) # up and back down if scale <= 0: return [] bg, fg = COLORS.get(value, ("#22d3ee", "#0b1220")) size = CELL * scale cx, cy = x + CELL / 2, y + CELL / 2 shapes = [] if value >= 128: shapes += game.glow(cx, cy, CELL * 0.42, bg, layers=2, strength=0.2, core=False) shapes.append(draw.rect(cx - size / 2, cy - size / 2, size, size, radius=10 * scale, fill=bg)) font = (40 if value < 100 else 34 if value < 1000 else 28) * scale shapes.append(draw.text(cx, cy, value, size=font, weight="bold", color=fg, align_x="center", align_y="center")) return shapes def board(self): shapes = [draw.background("#1b1f27")] for r in range(SIZE): for c in range(SIZE): x, y = self.cell_xy((r, c)) shapes.append(draw.rect(x, y, CELL, CELL, radius=10, fill="#262b35")) sliding = self.anim < SLIDE_TIME for ghost in self.ghosts: shapes += self.tile_shapes(ghost, ghost.value) for tile in self.tiles.values(): shown = tile.value // 2 if tile.merged and sliding else tile.value shapes += self.tile_shapes(tile, shown) return pz.canvas(shapes, width=BOARD, height=BOARD, on_press=self.press, on_move=self.drag, on_release=self.release, cursor="grab") def score_box(self, label, value, floats=()): texts = [pz.text(f"{value:,}", size=20, weight="bold")] box = pz.container(pz.column([ui.muted(label, size="sm"), *texts], spacing=0, align="center"), padding=(6, 16)).style(background="surface", radius=8) risers = [pz.container(pz.text(text, size=14, weight="bold", color="#fbbf24"), padding=(0, 0, 0, 0)).style(opacity=1 - age / 0.8, translate=(0, -28 * age / 0.8)) for text, age in floats] return pz.stack([box, *[pz.container(r, width="fill", align_x="center") for r in risers]]) def overlay(self): if self.won and not self.keep_going: title, line = "You made 2048!", f"Score {self.score:,}" buttons = [ui.button("Keep going", self.continue_playing, variant="solid", icon="play"), ui.button("New game", self.new_game, icon="rotate-ccw")] elif self.over and not self.animating: title, line = "No more moves", f"Score {self.score:,} · best {self.best:,}" buttons = [ui.button("Try again", self.new_game, variant="solid", icon="rotate-ccw")] else: return None card = pz.container(pz.column([ui.heading(title, level=2), ui.muted(line), pz.row(buttons, spacing=8)], spacing=10, align="center"), padding=(22, 30)) card = card.style(glass=True, radius=14).animate("zoom_in", duration=250) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def view(self): t = self.resolved_theme() header = pz.row([ pz.text("2048", size=40, weight="bold"), pz.hspace(), self.score_box("SCORE", self.score, self.floats), self.score_box("BEST", self.best), ui.button("New game", self.new_game, icon="rotate-ccw"), ], spacing=10, align="center") play = pz.stack([self.board(), self.overlay()], width=BOARD, height=BOARD) return pz.container(pz.column([header, pz.container(play, radius=12, clip=True), ui.muted("Arrows / WASD or swipe with the mouse · join the numbers to get to 2048", size="sm")], spacing=12), padding=24, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(Game2048) ``` #### Next - [7. Menus, HUD, pause and settings](https://albakore.github.io/pyzed/games/menus-hud/): A title screen, transitions, a pause menu and a settings screen with Pyzed's UI components. ### 7. Menus, HUD, pause and settings > Wrap a Python game in a real interface with Pyzed: scenes, a title menu that works with keys and mouse, animated transitions, a glass HUD with bloom, a pause menu and a settings screen with sliders, switches and segmented controls. Source: https://albakore.github.io/pyzed/games/menus-hud/ Players see your menus before they see your game. This is where Pyzed shines: it is a UI library, so buttons, sliders, switches, glass panels and glowing text are already there and already look good. This step wraps a tiny game, *Orb Rush*, in a title screen, a HUD, a pause menu and a settings screen, with transitions between them. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A title menu, HUD, pause menu and settings screen around a game | 45 minutes | Intermediate | [1. Your first game](https://albakore.github.io/pyzed/games/first-game/); the [component library](https://albakore.github.io/pyzed/library/) helps | #### What you'll learn - Organize a game in **scenes** (`title`, `play`, `pause`, `settings`, `over`). - Animate **transitions** with one clock per scene and `.style(opacity=..., translate=...)`. - Build menus that work with **arrow keys and Enter** and with **clicks**. - A **HUD** with bloom, a health bar, a timer and an FPS counter. - A **settings screen** with `ui.slider`, `ui.switch` and `ui.segmented` whose values the game reads. ```mermaid stateDiagram-v2 [*] --> title title --> play: Play title --> settings: Settings play --> pause: Esc / P pause --> play: Resume / Esc pause --> settings: Settings settings --> title: Back (from title) settings --> pause: Back (from pause) pause --> title: Main menu play --> over: health = 0 over --> play: Play again over --> title: Main menu ``` 1. **One attribute decides the scene.** `go()` switches scene and restarts `since`, the seconds since the scene began, which drives every transition. Settings remembers where it was opened from, so Back returns to the title or to the pause menu: ```python def go(self, scene): if scene == "settings": self.came_from = self.scene self.scene = scene self.since = 0.0 self.selected = 0 ``` `tick` only runs the game in its scene, so pausing is free: `if self.scene == "play": self.play(dt)`. The stars keep moving behind the menus, so the screen never looks frozen. 2. **Menus as data.** Each scene's menu is a list of `(label, action)`. One method does what an action means, whether it came from a key or a click, and *Quit* returns `self.close()`, a command that closes the window: ```python MENUS = { "title": [("Play", "play"), ("Settings", "settings"), ("Quit", "quit")], "pause": [("Resume", "resume"), ("Settings", "settings"), ("Main menu", "menu")], "over": [("Play again", "play"), ("Main menu", "menu")], "settings": [("Back", "back")], } def choose(self, action): if action == "play": self.reset_round() self.go("play") ... elif action == "quit": return self.close() ``` 3. **Keyboard navigation.** Up and Down move `selected` (wrapping around with `%`), Enter chooses. The selected button is drawn `solid` with an arrow; the others are `ghost`. Clicking any of them calls the same `choose`: ```python items = MENUS[self.scene] if combo in ("down", "s"): self.selected = (self.selected + 1) % len(items) elif combo in ("up", "w"): self.selected = (self.selected - 1) % len(items) elif combo in ("enter", "space"): return self.choose(items[self.selected][1]) ``` 4. **Transitions from one clock.** `appear(delay)` goes from 0 to 1 over 0.35 s, `delay` seconds after the scene started, with an ease-out. Any widget can fade and slide in with it, and giving each menu item a slightly bigger delay makes them arrive one after another: ```python def appear(self, delay=0.0): return ease_out(game.clamp((self.since - delay) / FADE, 0.0, 1.0)) def slide_in(self, widget, delay=0.0, distance=24): a = self.appear(delay) return widget.style(opacity=a, translate=(0, (1 - a) * distance)) # each item of a menu: items.append(self.slide_in(pz.container(button, width=220), delay + i * 0.06, 16)) ``` Because the values come from your own clock, the transition is exactly the same every time, in screenshots and in tests too. 5. **A glowing title and a glass HUD.** `bloom` makes the bright parts of a widget glow; with the border and shadow removed, only the text glows. The HUD is a row in a `glass` container (a blurred, tinted panel), and it slides down from the top when the game starts: ```python def title_text(self, text, color, size=48): return pz.container(pz.text(text, size=size, weight="bold", color="#ffffff"), padding=(4, 16)).style( bloom=(color, 0.9), border_width=0, shadow=("#00000000", (0, 0), 0)) bits = [ self.title_text(f"{self.score:05d}", NEON["cyan"], size=22), pz.hspace(), pz.row([pz.icon("heart", size=14, color=NEON["red"]), pz.container(ui.progress(self.health, max=3, tint=NEON["red"], height=6), width=90)], spacing=6, align="center"), ui.badge(f"{self.time:4.1f} s", icon="timer", tint="text_muted"), ui.badge(f"{self.fps:.0f} fps", icon="gauge", tint="success") if self.settings["fps"] else None, pz.row([ui.kbd("Esc"), ui.muted("pause", size="sm")], spacing=4, align="center"), ] ``` `None` in a row is skipped, which is the easy way to show something only sometimes (the FPS counter). 6. **The pause menu.** The game is still drawn underneath; on top go the HUD, a dark layer that fades in and the menu panel. `pz.stack` needs the size of the board so that the panel can center itself: ```python return pz.stack([self.hud(), pz.container(width="fill", height="fill").style( background=game.rgba("#000000", 0.35 * self.appear())), self.panel(body)], width=W, height=H) ``` 7. **Settings the game reads.** Settings are a plain dict. Each control writes one key, and the game reads them where they matter: the difficulty scales the speed, the shake switch decides whether a hit shakes the screen: ```python self.settings = {"music": 0.6, "effects": 0.8, "difficulty": "Normal", "shake": True, "fps": False} rows = [ ui.field("Music", pz.container(ui.slider(s["music"], setter("music"), show_value=True), width=180)), ui.field("Effects", pz.container(ui.slider(s["effects"], setter("effects"), show_value=True), width=180)), ui.field("Difficulty", ui.segmented(list(DIFFICULTY), s["difficulty"], setter("difficulty"), size="sm")), ui.field("Screen shake", ui.switch(s["shake"], setter("shake"))), ui.field("Show FPS", ui.switch(s["fps"], setter("fps"))), ] ``` **Volume sliders:** Here the Music and Effects sliders only store a value, since Orb Rush makes no sound. [Step 8](https://albakore.github.io/pyzed/games/high-scores/#sound-effects) adds sound effects with `pyzed.audio`, shows how a slider sets their volume, and saves these settings to disk. **Accessibility is a setting too:** Offer a switch for screen shake and flashes; some players get motion sick. `pz.system_reduced_motion()` tells you whether the system asks for less motion, a good default for that switch. **See also:** The [Game main menu](https://albakore.github.io/pyzed/showcase/main-menu/) showcase is a title screen with an interactive shader backdrop, and the [Game HUD](https://albakore.github.io/pyzed/showcase/hud/) one is a full sci-fi HUD. #### Try this - Add a **Controls** page in Settings that lists the keys with `ui.kbd`. - Add a 3-2-1 countdown scene between the title and the game. - Animate the Game over title with `.animate("pulse", repeat="infinite")`. #### Full code `examples/games/menus.py`: ```py """Orb Rush: a title screen, a HUD, a pause menu and a settings screen around a small game. python examples/games/menus.py Up/Down (or W/S) choose, Enter confirms, Escape (or P) pauses and goes back. In the game, arrows or WASD fly; collect the blue orbs and dodge the red mines. Shows the parts of a game that are not the game itself: - **Scenes**: one ``scene`` attribute (``"title"``, ``"play"``, ``"pause"``, ``"settings"``, ``"over"``) decides what ``tick`` updates and what ``view`` shows. - **Transitions**: every scene change restarts a clock; panels fade and slide in from it (``.style(opacity=..., translate=...)``) and menu items follow one after another. - **Menus that work with the keyboard and the mouse**: a selected index for the keys, ``ui.button`` for clicks. - **A HUD** of widgets (score with bloom, a health bar, a timer, an FPS counter) and a **settings screen** with ``ui.slider``, ``ui.switch`` and ``ui.segmented`` whose values the game reads. Tutorial: https://albakore.github.io/pyzed/games/menus-hud/ """ import math import random import pyzed as pz from pyzed import draw, game, ui W, H = 720, 460 FADE = 0.35 # seconds a scene takes to come in NEON = {"cyan": "#22d3ee", "pink": "#f472b6", "violet": "#a78bfa", "red": "#f87171", "amber": "#fbbf24"} DIFFICULTY = {"Easy": 0.7, "Normal": 1.0, "Hard": 1.45} #: The menu of each scene: (label, action) pairs. MENUS = { "title": [("Play", "play"), ("Settings", "settings"), ("Quit", "quit")], "pause": [("Resume", "resume"), ("Settings", "settings"), ("Main menu", "menu")], "over": [("Play again", "play"), ("Main menu", "menu")], "settings": [("Back", "back")], } def ease_out(t): return 1 - (1 - t) ** 3 class OrbRush(pz.App): title = "Orb Rush" size = (W + 32, H + 32) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) self.held = None # the keys held down; None reads the keyboard self.settings = {"music": 0.6, "effects": 0.8, "difficulty": "Normal", "shake": True, "fps": False} self.stars = [(self.random.uniform(0, W), self.random.uniform(0, H), self.random.choice((1, 2, 3))) for _ in range(110)] self.sparks = game.Particles(drag=1.5, seed=seed) self.shake = game.Shake(seed=seed) self.clock, self.fps = 0.0, 60.0 self.best = 0 self.reset_round() self.go("title") # -- scenes ----------------------------------------------------------------------------- def go(self, scene): """Switches scene and restarts the transition clock.""" if scene == "settings": self.came_from = self.scene self.scene = scene self.since = 0.0 # seconds since this scene started: drives every transition self.selected = 0 def choose(self, action): if action == "play": self.reset_round() self.go("play") elif action == "resume": self.go("play") elif action == "settings": self.go("settings") elif action == "back": self.go(self.came_from) elif action == "menu": self.go("title") elif action == "quit": return self.close() def reset_round(self): self.x, self.y = W / 2, H - 80 self.orbs, self.mines = [], [] self.score, self.health, self.time = 0, 3, 0.0 self.spawn = 0.0 # -- input ------------------------------------------------------------------------------ def on_key(self, combo): if self.scene == "play": if combo in ("escape", "p"): self.go("pause") return None if combo in ("escape", "p") and self.scene == "pause": return self.go("play") if combo == "escape" and self.scene == "settings": return self.go(self.came_from) items = MENUS[self.scene] if combo in ("down", "s"): self.selected = (self.selected + 1) % len(items) elif combo in ("up", "w"): self.selected = (self.selected - 1) % len(items) elif combo in ("enter", "space"): return self.choose(items[self.selected][1]) return None # -- update ----------------------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] # the stars move in every scene def tick(self, dt): dt = min(dt, 1 / 30) self.clock += dt self.since += dt self.fps = game.lerp(self.fps, 1 / max(dt, 1e-3), 0.1) # smoothed, so the number is readable self.stars = [(x, (y + 18 * layer * dt * (3 if self.scene == "play" else 1)) % H, layer) for x, y, layer in self.stars] self.sparks.update(dt) self.shake.update(dt) if self.scene == "play": # the game only runs in its scene: pausing is free self.play(dt) def play(self, dt): self.time += dt speed = DIFFICULTY[self.settings["difficulty"]] dx, dy = game.vector(held=self.held) self.x = game.clamp(self.x + dx * 320 * dt, 16, W - 16) self.y = game.clamp(self.y + dy * 320 * dt, 80, H - 16) self.spawn -= dt if self.spawn <= 0: self.spawn = 0.45 / speed kind = self.mines if self.random.random() < 0.35 else self.orbs kind.append([self.random.uniform(20, W - 20), -20.0]) for item in self.orbs + self.mines: item[1] += 170 * speed * dt for orb in self.orbs[:]: if math.hypot(orb[0] - self.x, orb[1] - self.y) < 24: self.orbs.remove(orb) self.score += 10 self.sparks.burst(orb[0], orb[1], count=14, color=[NEON["cyan"], "#e0fbff"]) for mine in self.mines[:]: if math.hypot(mine[0] - self.x, mine[1] - self.y) < 24: self.mines.remove(mine) self.health -= 1 self.sparks.burst(mine[0], mine[1], count=30, color=[NEON["red"], NEON["amber"]], speed=(80, 300)) if self.settings["shake"]: self.shake.kick(10) if self.health <= 0: self.best = max(self.best, self.score) self.go("over") self.orbs = [o for o in self.orbs if o[1] < H + 20] self.mines = [m for m in self.mines if m[1] < H + 20] # -- transitions ------------------------------------------------------------------------ def appear(self, delay=0.0): """0 -> 1 over FADE seconds, ``delay`` seconds after the scene started.""" return ease_out(game.clamp((self.since - delay) / FADE, 0.0, 1.0)) def slide_in(self, widget, delay=0.0, distance=24): a = self.appear(delay) return widget.style(opacity=a, translate=(0, (1 - a) * distance)) # -- draw ------------------------------------------------------------------------------- def world(self): shapes = [draw.background("#070817"), *self.shake.shapes()] for x, y, layer in self.stars: shapes.append(draw.circle(x, y, layer * 0.6, fill=game.rgba("#c7d2fe", 0.25 * layer))) if self.scene != "title": for x, y in self.orbs: shapes += game.glow(x, y, 7, NEON["cyan"], layers=3, strength=0.5) for x, y in self.mines: spikes = [(x + math.cos(a) * (13 if i % 2 else 7), y + math.sin(a) * (13 if i % 2 else 7)) for i, a in enumerate(i * math.pi / 6 + self.clock * 3 for i in range(12))] shapes += game.glow(x, y, 6, NEON["red"], layers=2, strength=0.3, core=False) shapes.append(draw.polygon(spikes, fill=NEON["red"])) if self.scene != "over": shapes += game.glow(self.x, self.y, 12, NEON["violet"], layers=4, strength=0.4) shapes += self.sparks.shapes(glow=True) return pz.canvas(shapes, width=W, height=H) def title_text(self, text, color, size=48): return pz.container(pz.text(text, size=size, weight="bold", color="#ffffff"), padding=(4, 16)).style( bloom=(color, 0.9), border_width=0, shadow=("#00000000", (0, 0), 0)) def menu(self, delay=0.15): """The current scene's menu: a button per item, the selected one solid, each sliding in a little later.""" items = [] for i, (label, action) in enumerate(MENUS[self.scene]): selected = i == self.selected button = ui.button(label, lambda a=action: self.choose(a), variant="solid" if selected else "ghost", full_width=True, icon="chevron-right" if selected else None) items.append(self.slide_in(pz.container(button, width=220), delay + i * 0.06, 16)) return pz.column(items, spacing=6, align="center") def panel(self, content, delay=0.0): card = pz.container(content, padding=(24, 32)).style(glass=True, radius=16, border=(1, "#ffffff22")) return pz.container(self.slide_in(card, delay, 30), width="fill", height="fill", align_x="center", align_y="center") def hud(self): bits = [ self.title_text(f"{self.score:05d}", NEON["cyan"], size=22), pz.hspace(), pz.row([pz.icon("heart", size=14, color=NEON["red"]), pz.container(ui.progress(self.health, max=3, tint=NEON["red"], height=6), width=90)], spacing=6, align="center"), ui.badge(f"{self.time:4.1f} s", icon="timer", tint="text_muted"), ui.badge(f"{self.fps:.0f} fps", icon="gauge", tint="success") if self.settings["fps"] else None, pz.row([ui.kbd("Esc"), ui.muted("pause", size="sm")], spacing=4, align="center"), ] bar = pz.container(pz.row(bits, spacing=10, align="center"), padding=(6, 14)).style( glass=True, radius=12, border=(1, "#ffffff1f")) a = self.appear() return pz.container(bar.style(opacity=a, translate=(0, -(1 - a) * 40)), padding=12, width="fill") def settings_panel(self): s = self.settings def setter(key): return lambda value: s.__setitem__(key, value) rows = [ ui.field("Music", pz.container(ui.slider(s["music"], setter("music"), show_value=True), width=180)), ui.field("Effects", pz.container(ui.slider(s["effects"], setter("effects"), show_value=True), width=180)), ui.field("Difficulty", ui.segmented(list(DIFFICULTY), s["difficulty"], setter("difficulty"), size="sm")), ui.field("Screen shake", ui.switch(s["shake"], setter("shake"))), ui.field("Show FPS", ui.switch(s["fps"], setter("fps"))), ] body = pz.column([ ui.heading("Settings", level=2), *[self.slide_in(r, 0.08 + i * 0.05, 12) for i, r in enumerate(rows)], self.menu(0.4), ], spacing=10, width=380) return self.panel(body) def overlay(self): if self.scene == "title": pulse = 0.85 + 0.15 * math.sin(self.clock * 2.5) body = pz.column([ self.slide_in(self.title_text("ORB RUSH", NEON["violet"]).style(scale=pulse), 0.0, -30), self.slide_in(ui.muted("Collect the orbs. Dodge the mines."), 0.08, 12), pz.vspace(12), self.menu(), self.slide_in(ui.muted(f"Best {self.best}", size="sm"), 0.4, 8), ], spacing=6, align="center") return pz.container(body, width="fill", height="fill", align_x="center", align_y="center") if self.scene == "play": return self.hud() if self.scene == "pause": body = pz.column([ui.heading("Paused", level=2), ui.muted(f"Score {self.score}"), self.menu()], spacing=10, align="center") return pz.stack([self.hud(), pz.container(width="fill", height="fill").style( background=game.rgba("#000000", 0.35 * self.appear())), self.panel(body)], width=W, height=H) if self.scene == "settings": return self.settings_panel() body = pz.column([self.title_text("GAME OVER", NEON["pink"], size=34), ui.muted(f"Score {self.score} · best {self.best}"), self.menu()], spacing=10, align="center") return self.panel(body) def view(self): board = pz.stack([self.world(), self.overlay()], width=W, height=H) return pz.container(pz.container(board, radius=12, clip=True), padding=16, width="fill", height="fill", bg="#05060d", align_x="center", align_y="center") if __name__ == "__main__": pz.run(OrbRush) ``` #### Next - [8. High scores, saves and sound](https://albakore.github.io/pyzed/games/high-scores/): Save a leaderboard safely, enter a name, and add sound effects. ### 8. High scores, saves and sound > Save a game's leaderboard and settings in Python with Pyzed: a JSON file in the right folder on Windows, macOS and Linux, atomic writes, name entry with ui.input, a leaderboard, sound effects synthesized with pyzed.audio, and packaging the game. Source: https://albakore.github.io/pyzed/games/high-scores/ A score means more when it is still there tomorrow. This last step of the path builds *Target Rush*, a 20-second clicking game, and gives it what finished games have: a **leaderboard saved on disk**, a **name entry** when you beat a record, **sound effects**, and a way to **ship it** to your friends. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A game with sound that saves a top-10 leaderboard | 35 minutes | Intermediate | [7. Menus, HUD and settings](https://albakore.github.io/pyzed/games/menus-hud/) | #### What you'll learn - Find the **right folder** for saves on Windows, macOS and Linux. - **Save safely**: write a temporary file and swap it in, and survive a damaged file. - A **name entry** with `ui.input` (Enter submits) and a **leaderboard** made of rows. - **Sound effects**: one method called wherever a sound belongs, playing drums synthesized by `pyzed.audio`. - **Package** the game as an app. 1. **Where saves go.** Each system has a folder for app data, and games should not write next to their code (that folder may be read-only once installed): ```python def data_dir(app_name): if sys.platform == "win32": base = Path(os.environ.get("APPDATA") or Path.home() / "AppData" / "Roaming") elif sys.platform == "darwin": base = Path.home() / "Library" / "Application Support" else: base = Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share") return base / app_name ``` | System | Folder for "Target Rush" | | --- | --- | | Windows | `C:\Users\you\AppData\Roaming\Target Rush` | | macOS | `~/Library/Application Support/Target Rush` | | Linux | `~/.local/share/Target Rush` | 2. **Load without fear.** The first time there is no file, and a file can get damaged (a disk full, someone editing it by hand). Both should just mean "no scores yet", never a crash: ```python def load(self): try: data = json.loads(self.path.read_text(encoding="utf-8")) entries = [e for e in data.get("scores", []) if isinstance(e.get("score"), int)] except (OSError, ValueError, AttributeError): return [] return sorted(entries, key=lambda e: -e["score"])[:KEEP] ``` 3. **Save safely.** If the game crashes while writing, a half-written file would lose every score. So write a temporary file in the same folder and replace the real one in a single step; `os.replace` is atomic on every system: ```python def save(self): self.path.parent.mkdir(parents=True, exist_ok=True) fd, tmp = tempfile.mkstemp(dir=self.path.parent, prefix=".scores-", suffix=".json") with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump({"version": 1, "scores": self.entries}, f, indent=2) os.replace(tmp, self.path) ``` The `"version": 1` costs nothing and lets a future version of your game read old saves differently. 4. **Does it make the table?** A score qualifies when the table is not full yet or it beats the last entry. `add` inserts it, keeps the best 10, saves, and returns the place: ```python def qualifies(self, score): return score > 0 and (len(self.entries) < KEEP or score > self.entries[-1]["score"]) def add(self, name, score): entry = {"name": name.strip()[:16] or "Player", "score": score, "date": time.strftime("%Y-%m-%d")} self.entries.append(entry) self.entries.sort(key=lambda e: -e["score"]) del self.entries[KEEP:] self.save() return self.entries.index(entry) + 1 if entry in self.entries else None ``` 5. **Name entry.** When a round ends with a qualifying score the scene becomes `"name"`. `ui.input` keeps the text in your state through `on_input`, and `on_submit` runs on Enter, so typing a name and pressing Enter is all it takes: ```python ui.input(self.name, self.set_name, placeholder="Your name", icon="user", on_submit=self.submit_name, id="name", max_length=16) ``` The leaderboard is a column of rows; the row you just added gets a tinted background, and the top three get gold, silver and bronze numbers. 6. **Sound hooks.** Decide *where* sounds go before deciding *how* they play. Target Rush calls `self.sound("hit")`, `"miss"`, `"combo"`, `"start"`, `"end"` and `"record"` at the right moments, and only this method knows what to do with them: ```python def sound(self, name): """Called wherever a sound belongs: "start", "hit", "miss", "combo", "end", "record".""" self.last_sound, self.sound_age = name, 0.0 if self.sounds is None and self.sound_backend: self.sounds = Sounds(self.sound_backend) # the first sound starts the engine if self.sounds: self.sounds.play(name) ``` It also shows the name in the HUD for a moment, a handy way to check the timing of your sounds. 7. **Make the sounds.** `Sounds` builds a tiny patch with [`pyzed.audio`](https://albakore.github.io/pyzed/audio/): four `drum` voices into a `mixer` and the speakers. Each effect is one voice with its own tuning, listed in a table at the top of the file, so there are no sound files to ship: ```python SOUNDS = { # effect: (drum voice, tune in semitones, decay in seconds) "hit": ("tom", 7, 0.12), "miss": ("kick", -6, 0.25), ... } def play(self, name): kind, tune, decay = SOUNDS[name] voice = self.voices[kind] voice.set(tune=tune, decay=decay) voice.trigger() # returns at once: the sound plays on the audio thread ``` #### Sound effects `pyzed.audio` computes the sound in Rust on its own real-time thread, so `trigger()` never slows the frame loop, and with no sound card the engine plays silently instead of failing. A few ways to go further: - **Recorded sounds**: a `sampler` module plays a WAV file. Call `node.load("coin.wav")` once at start and `node.trigger()` in `sound()`. - **Volume settings**: the `audio_out` module has a `volume` knob, and the mixer a `level_1`…`level_4` per channel. Set them from the sliders of [Orb Rush](https://albakore.github.io/pyzed/games/menus-hud/): `self.sounds.out["volume"] = value`. - **Music**: a `clock` and a `sequencer` play a loop on their own; see the [audio engine](https://albakore.github.io/pyzed/audio/engine/getting-started/). - **Tests**: `sound_backend="offline"` never opens a device; the tests call `engine.render(seconds)` to check that each effect makes a sound, and `None` mutes the game. **Tip:** Keep sounds short and a little different each time (vary `tune` by a semitone or two), so a sound heard a hundred times in a round does not get tiring. Save the settings the same way as the scores: a `settings.json` next to `scores.json`, written with the same `save()` when the settings screen closes, and loaded at start with defaults for any missing key. #### Ship your game A game is finished when someone else can play it. Pyzed packages an app as a double-clickable program for Windows, macOS and Linux, with its icon and name: - [Package your app](https://albakore.github.io/pyzed/tutorials/advanced/packaging/): A step-by-step tutorial: build, icon, and builds for all three systems from CI. - [Distribute](https://albakore.github.io/pyzed/packaging/): Everything about building, signing and updating apps for each system. **Tip:** Once packaged, the game's own folder may be read-only, which is one more reason to keep saves in `data_dir`. #### Try this - Save the settings of [Orb Rush](https://albakore.github.io/pyzed/games/menus-hud/) with the same pattern. - Keep a separate leaderboard per difficulty: one key per difficulty in the JSON. - Add a "Clear scores" button behind a `ui.confirm` dialog. #### Full code `examples/games/high_scores.py`: ```py """Target Rush: a 20-second clicking game that keeps a top-10 table on disk. python examples/games/high_scores.py Click the targets before they shrink away: the sooner, the more points, and hits in a row build a combo. A good score asks for your name and goes into the leaderboard, which is saved as JSON in your user data folder and is still there next time. Shows: - **Saving data** the safe way: a JSON file in the right folder for each system, written to a temporary file first and then swapped in, so a crash never leaves half a file. A missing or damaged file just means an empty table. - **Name entry** with ``ui.input`` (Enter submits) and a leaderboard made of rows. - **Sound effects** from one ``sound(name)`` method called wherever a sound belongs. The sounds are synthesized live by ``pyzed.audio`` (drum voices, no files); with no sound card the game simply stays silent. Tutorial: https://albakore.github.io/pyzed/games/high-scores/ """ import json import math import os import random import sys import tempfile import time from pathlib import Path import pyzed as pz from pyzed import audio, draw, game, ui W, H = 640, 420 ROUND = 20.0 TARGET_LIFE = 1.4 # seconds a target lives (it shrinks meanwhile) TARGET_R = 34 KEEP = 10 # entries in the leaderboard SOUNDS = { # effect: (drum voice, tune in semitones, decay in seconds) "start": ("tom", 0, 0.3), "hit": ("tom", 7, 0.12), "combo": ("clap", 0, 0.25), "miss": ("kick", -6, 0.25), "end": ("tom", -7, 0.6), "record": ("hihat", 5, 0.5), } # -- sound ---------------------------------------------------------------------------------------- class Sounds: """The game's sound effects, synthesized live by ``pyzed.audio``: there are no files to load. Four drum voices go through a mixer to the speakers, and each effect strikes one voice with its own tuning. Without a sound card the engine plays silently, so the game runs anywhere. """ def __init__(self, backend="auto", volume=0.8): self.engine = audio.Engine(backend) mixer = self.engine.add("mixer") self.out = self.engine.add("audio_out", volume=volume) self.engine.connect(mixer, "left", self.out, "left") self.engine.connect(mixer, "right", self.out, "right") self.voices = {} for i, kind in enumerate(("tom", "clap", "kick", "hihat"), start=1): self.voices[kind] = self.engine.add("drum", sound=kind) self.engine.connect(self.voices[kind], "out", mixer, f"in_{i}") if backend != "offline": # "offline" never plays: tests render() it instead self.engine.start() def play(self, name): kind, tune, decay = SOUNDS[name] voice = self.voices[kind] voice.set(tune=tune, decay=decay) voice.trigger() # returns at once: the sound plays on the audio thread # -- saving -------------------------------------------------------------------------------------- def data_dir(app_name): """The folder where a game keeps its saves, following each system's convention.""" if sys.platform == "win32": base = Path(os.environ.get("APPDATA") or Path.home() / "AppData" / "Roaming") elif sys.platform == "darwin": base = Path.home() / "Library" / "Application Support" else: base = Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share") return base / app_name class HighScores: """The best ``KEEP`` scores, as a list of ``{"name", "score", "date"}`` saved as JSON.""" def __init__(self, path): self.path = Path(path) self.entries = self.load() def load(self): try: data = json.loads(self.path.read_text(encoding="utf-8")) entries = [e for e in data.get("scores", []) if isinstance(e.get("score"), int)] except (OSError, ValueError, AttributeError): return [] # no file yet, or a damaged one: start empty return sorted(entries, key=lambda e: -e["score"])[:KEEP] def qualifies(self, score): return score > 0 and (len(self.entries) < KEEP or score > self.entries[-1]["score"]) def add(self, name, score): """Adds a score, saves, and returns its place (1 is the best).""" entry = {"name": name.strip()[:16] or "Player", "score": score, "date": time.strftime("%Y-%m-%d")} self.entries.append(entry) self.entries.sort(key=lambda e: -e["score"]) # a stable sort: older equal scores stay first del self.entries[KEEP:] self.save() return self.entries.index(entry) + 1 if entry in self.entries else None def save(self): self.path.parent.mkdir(parents=True, exist_ok=True) # Write next to the real file, then replace it in one step (atomic on every system). fd, tmp = tempfile.mkstemp(dir=self.path.parent, prefix=".scores-", suffix=".json") with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump({"version": 1, "scores": self.entries}, f, indent=2) os.replace(tmp, self.path) @property def best(self): return self.entries[0]["score"] if self.entries else 0 # -- the game ------------------------------------------------------------------------------------ class TargetRush(pz.App): title = "Target Rush" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None, path=None, sound_backend="auto"): self.random = random.Random(seed) self.scores = HighScores(path or data_dir("Target Rush") / "scores.json") self.sparks = game.Particles(gravity=500, seed=seed) self.scene = "ready" # ready -> play -> name (if it made the table) -> board self.name = "" self.rank = None self.last_sound, self.sound_age = None, 9.0 self.sound_backend = sound_backend # "auto", "offline" (tests) or None (muted) self.sounds = None # made on the first sound, so a menu costs nothing self.reset() def reset(self): self.targets = [] # [x, y, age] self.score, self.combo, self.hits, self.misses = 0, 0, 0, 0 self.time_left = ROUND self.spawn = 0.0 self.clock = 0.0 def start(self): self.reset() self.scene = "play" self.sound("start") # -- sound hooks -------------------------------------------------------------------------- def sound(self, name): """Called wherever a sound belongs: "start", "hit", "miss", "combo", "end", "record". Plays the effect and shows its name in the HUD for a moment. """ self.last_sound, self.sound_age = name, 0.0 if self.sounds is None and self.sound_backend: self.sounds = Sounds(self.sound_backend) if self.sounds: self.sounds.play(name) # -- update ------------------------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] if self.scene == "play" or len(self.sparks) or self.sound_age < 1 else [] def tick(self, dt): dt = min(dt, 1 / 30) self.sparks.update(dt) self.sound_age += dt if self.scene != "play": return self.clock += dt self.time_left = max(0.0, self.time_left - dt) self.spawn -= dt if self.spawn <= 0: self.spawn = max(0.35, 0.8 - self.clock * 0.02) self.targets.append([self.random.uniform(50, W - 50), self.random.uniform(50, H - 50), 0.0]) for t in self.targets: t[2] += dt expired = [t for t in self.targets if t[2] >= TARGET_LIFE] if expired: self.combo = 0 self.misses += len(expired) self.targets = [t for t in self.targets if t[2] < TARGET_LIFE] if self.time_left <= 0: self.finish() def finish(self): self.targets = [] if self.scores.qualifies(self.score): self.scene = "name" self.sound("record") else: self.scene = "board" self.rank = None self.sound("end") # -- input -------------------------------------------------------------------------------- def click(self, x, y): if self.scene != "play": return for t in reversed(self.targets): # the newest (on top) first r = self.radius(t) if math.hypot(x - t[0], y - t[1]) <= r: self.targets.remove(t) self.combo += 1 self.hits += 1 freshness = 1 - t[2] / TARGET_LIFE # earlier clicks are worth more self.score += int(10 + 40 * freshness) * min(self.combo, 5) self.sparks.burst(t[0], t[1], count=18, color=["#f472b6", "#fbbf24", "#ffffff"], speed=(60, 260)) self.sound("combo" if self.combo in (5, 10, 15) else "hit") return self.combo = 0 self.misses += 1 self.sound("miss") def set_name(self, value): self.name = value def submit_name(self, *_): self.rank = self.scores.add(self.name, self.score) self.scene = "board" def on_key(self, combo): if combo in ("enter", "space") and self.scene in ("ready", "board"): self.start() # -- draw --------------------------------------------------------------------------------- @staticmethod def radius(t): return TARGET_R * (1 - t[2] / TARGET_LIFE * 0.75) def board(self): shapes = [draw.background("#12101c")] for i in range(0, W, 32): shapes.append(draw.line(i, 0, i, H, stroke="#ffffff06")) for t in self.targets: r = self.radius(t) appear = min(1.0, t[2] / 0.12) # a quick grow-in r *= appear for k, color in enumerate(("#f472b6", "#ffffff", "#f472b6")): shapes.append(draw.circle(t[0], t[1], r * (1 - k * 0.3), fill=color)) # the remaining time as an arc around the target left = 1 - t[2] / TARGET_LIFE shapes.append(draw.arc(t[0], t[1], TARGET_R + 6, -90, -90 + 360 * left, stroke="#fbbf24", width=3, cap="round")) shapes += self.sparks.shapes(glow=True) return pz.canvas(shapes, width=W, height=H, on_press=self.click, cursor="crosshair") def leaderboard(self): rows = [] for i, e in enumerate(self.scores.entries): mine = self.rank == i + 1 medal = {0: "#fbbf24", 1: "#cbd5e1", 2: "#d97706"}.get(i) rows.append(pz.container(pz.row([ pz.container(pz.text(f"{i + 1}", weight="bold", color=medal) if medal else ui.muted(f"{i + 1}"), width=28), pz.text(e["name"], weight="semibold" if mine else None), pz.hspace(), ui.muted(e["date"], size="sm"), pz.container(pz.text(f"{e['score']:,}", font="mono"), width=70, align_x="end"), ], spacing=10, align="center"), padding=(4, 10)).style(background="#fbbf2430" if mine else None, radius=6)) if not rows: rows = [ui.muted("No scores yet. Be the first!")] return pz.column(rows, spacing=2, width=340) def card(self, *children): card = pz.container(pz.column(list(children), spacing=10, align="center"), padding=(20, 28)).style( glass=True, radius=14).animate("fade_in_up", duration=260) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def overlay(self): if self.scene == "ready": return self.card(ui.heading("Target Rush", level=2), ui.muted("Click the targets before they vanish."), self.leaderboard(), ui.button("Start", self.start, variant="solid", icon="play")) if self.scene == "name": return self.card( ui.heading("New high score!", level=2), pz.text(f"{self.score:,}", size=28, weight="bold"), pz.container(ui.input(self.name, self.set_name, placeholder="Your name", icon="user", on_submit=self.submit_name, id="name", max_length=16), width=240), ui.button("Save", self.submit_name, variant="solid", icon="save")) if self.scene == "board": place = f"You placed #{self.rank}!" if self.rank else f"Score {self.score:,}" return self.card(ui.heading("Leaderboard", level=2), ui.muted(place), self.leaderboard(), ui.button("Play again", self.start, variant="solid", icon="rotate-ccw")) return None def view(self): t = self.resolved_theme() sound = (ui.badge(f"♪ {self.last_sound}", icon="volume-2", tint="hint") if self.sound_age < 0.8 and self.last_sound else None) hud = pz.row([ ui.label("Target Rush", weight="semibold"), pz.hspace(), sound, ui.badge(f"x{self.combo}", icon="zap", tint="warning") if self.combo > 1 else None, ui.badge(f"{self.score:,}", icon="target", tint="accent"), ui.badge(f"{math.ceil(self.time_left)} s", icon="timer", tint="error" if self.time_left < 5 else "text_muted"), ui.badge(f"Best {self.scores.best:,}", icon="trophy", tint="text_muted"), ], spacing=8, align="center") play = pz.stack([self.board(), self.overlay()], width=W, height=H) return pz.container(pz.column([hud, pz.container(play, radius=10, clip=True), ui.muted(f"Scores are saved in {self.scores.path}", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(TargetRush) ``` #### Next - [Game dev tips](https://albakore.github.io/pyzed/games/tips/): Performance, fixed timesteps, input buffering, juice, organizing state and testing headlessly. ### Game dev tips > Practical tips for making games with Pyzed in Python: delta time and fixed timesteps, performance, input buffering, juice and screen shake, organizing game state, debugging views and testing games headlessly. Source: https://albakore.github.io/pyzed/games/tips/ Short, practical advice gathered from the games of this section. Each tip says *why*, with the few lines of code that do it in Pyzed. #### Time ##### Always multiply by dt `pz.frames(self.tick)` calls `tick(dt)` once per displayed frame, and `dt` is the seconds since the last one. Speeds are *per second* and every change is `speed × dt`, so the game runs the same on a 60 Hz laptop and a 144 Hz monitor. Timers count down by `dt` too: ```python self.x += self.vx * dt self.cooldown = max(0.0, self.cooldown - dt) ``` ##### Cap dt After a pause (a dragged window, a breakpoint, a slow frame) `dt` can be large, and a big step lets things pass through walls. Pyzed already caps it at 0.25 s; physics-heavy games cap it lower: ```python def tick(self, dt): dt = min(dt, 1 / 30) ``` ##### A fixed timestep, when you need one Some games need every step to be the same size: physics that must not depend on the frame rate, replays, networking, or a simulation that must give exactly the same result twice. Accumulate time and step in fixed slices: ```python STEP = 1 / 120 def tick(self, dt): self.acc += dt while self.acc >= STEP: self.simulate(STEP) # the game's logic, always with the same dt self.acc -= STEP self.blend = self.acc / STEP # 0..1: how far between two steps, to interpolate drawing ``` **Caution:** Don't call that method `update`: on a `pz.App`, `update(message)` is where widget messages arrive. For turn-based or grid games, use `pz.every(seconds, callback)` instead of a frame loop, as [Snake](https://albakore.github.io/pyzed/games/snake/) does. ##### Stop the loop when nothing moves `subscriptions()` is evaluated after every update. Return `[]` while paused, on a menu with nothing animated, or between moves of a puzzle, and the game uses no CPU: ```python def subscriptions(self): return [pz.frames(self.tick)] if self.animating else [] ``` #### Performance - **Keep `tick` small.** Plain numbers, lists and small classes are fast; avoid building big structures every frame, and avoid reading files or the network in the loop. - **Batch shapes.** `draw.rects([...], fill=color)` draws many rectangles of one color as one shape; that is how `game.sprite` and the platformer's tiles stay cheap. - **Draw only what is on screen**, and collide only with what is near (see the [platformer](https://albakore.github.io/pyzed/games/platformer/)). - **Cap particles** with `game.Particles(limit=...)`, and remove bullets and enemies that leave the screen. - **Images are cached.** `draw.image("ship.png", ...)` decodes a file once, so drawing it hundreds of times per frame is fine. - **The HUD is cheap**, but `pz.memo` can cache a widget that is expensive to build and rarely changes. - **Measure.** Show an FPS counter (see [Orb Rush](https://albakore.github.io/pyzed/games/menus-hud/)) and read the [Performance](https://albakore.github.io/pyzed/guides/performance/) guide. #### Input - **Held vs pressed.** Use `game.axis`, `game.vector` and `pz.is_pressed` in `tick` for things that happen *while* a key is down (moving, auto-fire), and `on_key` for things that happen *once* per press (jump, pause). - **Buffer inputs.** Remember a press for a short time so it is not lost: a jump pressed just before landing, or two quick turns in Snake. A small timer or queue is enough: ```python def on_key(self, combo): if combo == "space": self.jump_buffer = 0.12 # seconds; tick counts it down and jumps when possible ``` - **Be forgiving.** Coyote time, slightly smaller hitboxes for the player than its sprite, and slightly bigger ones for pickups make a game feel fair. - **Let people choose.** Accept arrows and WASD (`game.vector` does by default), and the mouse where it makes sense. #### Juice "Juice" is the feedback that makes an action feel good: the same hit, with and without it: | Effect | How | | --- | --- | | Screen shake | `shake = game.Shake()`, `shake.kick(8)` on a hit, `shake.update(dt)` every frame, and start the canvas with `*shake.shapes()` | | Particles | `game.Particles(...)`, `burst(x, y, ...)` on events, `update(dt)` and `shapes(glow=True)` | | Hit-stop | skip the world's update for 50–100 ms after a big hit (keep particles moving) | | Flash | draw the hit thing white for one or two frames | | Squash and stretch | `draw.scale(sx, sy)` around the feet after landing, or along the direction of motion | | Easing | tweens with `ease_out` curves instead of linear moves (see [Sprites and animation](https://albakore.github.io/pyzed/games/sprites-animation/)) | | Glow | `game.glow(x, y, r, color)` on canvas; `bloom` and `glow` on widgets | **Tip:** Add a setting to turn shake and flashes off. Some players need it, and `pz.system_reduced_motion()` says whether the system already asks for less motion. #### Organizing a game - **State in one place.** The app's attributes are the game state; `reset()` or `new_game()` sets all of them, so starting again is one call. - **Small classes for things.** `Enemy`, `Rock`, `Tile`: a few attributes each. Lists of them are the world. - **Rules as functions.** Code that decides *what happens* (`slide_line`, `can_move`, collisions) should not draw; code that draws should not change the state. Then both are easy to change and to test. - **Scenes.** One `scene` attribute (`"title"`, `"play"`, `"pause"`...) decides what `tick` runs and what `view` shows (see [Menus, HUD and settings](https://albakore.github.io/pyzed/games/menus-hud/)). - **Constants at the top.** Speeds, sizes and timings as named constants are the game's tuning knobs. - **Randomness with a seed.** `self.random = random.Random(seed)` makes a game reproducible for tests and bug reports; `seed=None` is random as usual. #### Debugging - **Draw what the code sees**: hitboxes, velocities, paths, the tile under the mouse. Toggle it with a key, as [Jump Lab](https://albakore.github.io/pyzed/games/collisions-physics/) does with H. - **Print numbers on the canvas** with `draw.text(..., font="mono")` instead of to the console, so you see them in context. - **Slow motion**: multiply `dt` by 0.25 while a key is held to watch a fast moment. #### Testing games headlessly A Pyzed game is a normal Python object: you can create it, call `tick` in a loop and check the result, with no window. The games of this section let tests set the keys they read through a `held` attribute: ```python from coin_chase import CoinChase, SPEED def test_moves_right(): app = CoinChase(seed=1) app.held = {"right"} # instead of the real keyboard x = app.x for _ in range(30): app.tick(1 / 60) # half a second, frame by frame assert app.x == pytest.approx(x + SPEED * 0.5) def test_draws(tmp_path): app = CoinChase(seed=1) app.screenshot(tmp_path / "frame.png", scale=1) # renders off-screen, no window needed ``` `game.axis`, `game.vector` and `game.is_pressed` all take `held=` for this. `App.screenshot` (or `pz.screenshot(app, path)`) renders any moment of the game to a PNG, which is also how the animations on these pages were recorded. The tests of these games are in [`tests/test_game_tutorials.py`](https://github.com/albakore/pyzed/blob/main/tests/test_game_tutorials.py). **Also read:** [Testing](https://albakore.github.io/pyzed/guides/testing/) for testing Pyzed apps in general, and [Drawing with canvas](https://albakore.github.io/pyzed/guides/canvas/) for every shape and transform. - [Back to the games overview](https://albakore.github.io/pyzed/games/): The learning path and the complete games. ### Tutorial: Snake > Build the classic Snake with a fixed-rate timer, a key queue, a canvas board and a glass game-over card. Source: https://albakore.github.io/pyzed/games/snake/ Snake moves on a grid, one cell at a time, so it doesn't need a frame loop: a timer that fires a few times a second is enough. You will learn how to keep the game state in the app, step it with `pz.every`, steer with `on_key`, draw the board on a canvas and put a menu on top with `pz.stack`. | Goal | Time | Level | You need | | --- | --- | --- | --- | | Classic Snake: a grid, a timer, a key queue and a game-over card | 30 minutes | Intermediate | The [To-do list](https://albakore.github.io/pyzed/tutorials/basic/todo-app/) tutorial, or a few Pyzed apps of your own | Play it first: 1. **The state.** The snake is a list of cells, head first. The direction is a `(dx, dy)` pair, and `turns` queues key presses until the next step (two quick presses between steps would otherwise lose the first one): ```python CELL = 22 COLS, ROWS = 24, 18 class Snake(pz.App): title = "Snake" size = (COLS * CELL + 32, ROWS * CELL + 96) def __init__(self, seed=None): self.random = random.Random(seed) # a seed makes games reproducible in tests self.best = 0 self.reset() def reset(self): middle = (COLS // 2, ROWS // 2) self.body = [middle, (middle[0] - 1, middle[1]), (middle[0] - 2, middle[1])] self.direction = (1, 0) self.turns = [] self.food = self.free_cell() self.score = 0 self.paused = self.over = False ``` 2. **One step.** Move the head; if it lands on the food the snake grows (the tail stays), otherwise the tail moves too. Walls wrap around with `%`, and biting yourself ends the game: ```python def step(self): if self.turns: self.direction = self.turns.pop(0) dx, dy = self.direction x, y = self.body[0] head = ((x + dx) % COLS, (y + dy) % ROWS) if head in self.body[:-1]: self.over = True self.best = max(self.best, self.score) return self.body.insert(0, head) if head == self.food: self.score += 1 self.food = self.free_cell() else: self.body.pop() ``` 3. **The timer.** `subscriptions()` is re-read after every update, so the game decides when the clock runs. Putting the speed in the `key` restarts the timer when the snake speeds up, and returning nothing pauses it: ```python @property def speed(self): return min(8 + self.score // 3, 20) # steps per second def subscriptions(self): if self.over or self.paused: return [] return [pz.every(1 / self.speed, self.step, key=f"snake-{self.speed}")] ``` 4. **Steering.** `on_key` receives every key press as a string. Turning back on yourself (or pressing the same direction twice) is ignored: ```python DIRECTIONS = {"up": (0, -1), "w": (0, -1), "down": (0, 1), "s": (0, 1), "left": (-1, 0), "a": (-1, 0), "right": (1, 0), "d": (1, 0)} def on_key(self, combo): if combo in DIRECTIONS: last = self.turns[-1] if self.turns else self.direction new = DIRECTIONS[combo] if new != last and (new[0] + last[0], new[1] + last[1]) != (0, 0): self.turns.append(new) elif combo == "space" and not self.over: self.paused = not self.paused elif combo == "enter" and self.over: self.reset() ``` 5. **The board.** A canvas is a list of shapes from `pyzed.draw`. The checkerboard is a single `draw.rects` shape (fast even with hundreds of cells), the food gets a soft halo from a translucent circle and the body is rounded rectangles: ```python def board(self, t): shapes = [draw.background(t.editor)] shapes.append(draw.rects(((x * CELL, y * CELL, CELL, CELL) for x in range(COLS) for y in range(ROWS) if (x + y) % 2), fill=t.surface)) fx, fy = self.food shapes.append(draw.circle(fx * CELL + CELL / 2, fy * CELL + CELL / 2, CELL / 2 - 3, fill="#d07277")) for i, (x, y) in enumerate(self.body): shapes.append(draw.rect(x * CELL + 2, y * CELL + 2, CELL - 4, CELL - 4, radius=6, fill=t.accent if i == 0 else "#a1c181")) return pz.canvas(shapes, width=COLS * CELL, height=ROWS * CELL) ``` 6. **HUD and game over.** The score is a row of `ui.badge`s. The game-over card is a normal container with `glass=True` (a blurred, tinted panel), stacked over the board: ```python def overlay(self, t): if not self.over: return None card = pz.container(pz.column([ ui.heading("Game over", level=2), ui.muted(f"Score {self.score} · Best {self.best}"), ui.button("Play again", self.reset, variant="solid", icon="rotate-ccw"), ], spacing=10, align="center"), padding=24).style(glass=True, radius=14) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def view(self): game = pz.stack([self.board(t), self.overlay(t)], width=COLS * CELL, height=ROWS * CELL) ``` (Image: Game over, on a glass card) **Tip:** Ideas to keep going: walls that kill instead of wrapping, golden apples worth three points that disappear after a few seconds (a second `pz.every`), or a two-player mode with WASD and the arrows. #### Full code `examples/games/snake.py`: ```py """Snake: the classic, in about 150 lines of Pyzed. python examples/games/snake.py Arrows or WASD steer, Space pauses, Enter starts again. Shows a fixed-rate game loop with ``pz.every``, steering from ``on_key`` (with a queue so quick turns are not lost), a ``pz.canvas`` board and a HUD made of ordinary widgets stacked on top of it. Tutorial: https://albakore.github.io/pyzed/games/snake/ """ import random import pyzed as pz from pyzed import draw, ui CELL = 22 COLS, ROWS = 24, 18 DIRECTIONS = { "up": (0, -1), "w": (0, -1), "down": (0, 1), "s": (0, 1), "left": (-1, 0), "a": (-1, 0), "right": (1, 0), "d": (1, 0), } class Snake(pz.App): title = "Snake" size = (COLS * CELL + 32, ROWS * CELL + 96) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) self.best = 0 self.reset() # -- game state ----------------------------------------------------------- def reset(self): middle = (COLS // 2, ROWS // 2) self.body = [middle, (middle[0] - 1, middle[1]), (middle[0] - 2, middle[1])] self.direction = (1, 0) self.turns = [] # key presses waiting for the next step self.food = self.free_cell() self.score = 0 self.paused = False self.over = False def free_cell(self): while True: cell = (self.random.randrange(COLS), self.random.randrange(ROWS)) if cell not in self.body: return cell @property def speed(self): """Steps per second: starts at 8 and speeds up as the snake grows.""" return min(8 + self.score // 3, 20) def step(self): if self.turns: self.direction = self.turns.pop(0) dx, dy = self.direction x, y = self.body[0] head = ((x + dx) % COLS, (y + dy) % ROWS) # walls wrap around if head in self.body[:-1]: self.over = True self.best = max(self.best, self.score) return self.body.insert(0, head) if head == self.food: self.score += 1 self.food = self.free_cell() else: self.body.pop() # -- input and timing ----------------------------------------------------- def subscriptions(self): if self.over or self.paused: return [] # The key includes the speed so the timer restarts when it changes. return [pz.every(1 / self.speed, self.step, key=f"snake-{self.speed}")] def on_key(self, combo): if combo in DIRECTIONS: last = self.turns[-1] if self.turns else self.direction new = DIRECTIONS[combo] # Ignore turning back on itself and repeated presses. if new != last and (new[0] + last[0], new[1] + last[1]) != (0, 0) and len(self.turns) < 3: self.turns.append(new) elif combo == "space" and not self.over: self.paused = not self.paused elif combo == "enter" and self.over: self.reset() # -- drawing -------------------------------------------------------------- def board(self, t): shapes = [draw.background(t.editor)] # A subtle checkerboard makes the grid readable without lines. shapes.append(draw.rects(((x * CELL, y * CELL, CELL, CELL) for x in range(COLS) for y in range(ROWS) if (x + y) % 2), fill=t.surface)) fx, fy = self.food shapes.append(draw.circle(fx * CELL + CELL / 2, fy * CELL + CELL / 2, CELL * 0.62, fill=(0.82, 0.45, 0.47, 0.18))) shapes.append(draw.circle(fx * CELL + CELL / 2, fy * CELL + CELL / 2, CELL / 2 - 3, fill="#d07277")) for i, (x, y) in enumerate(self.body): inset = 2 if i else 1 shapes.append(draw.rect(x * CELL + inset, y * CELL + inset, CELL - 2 * inset, CELL - 2 * inset, radius=6 if i else 7, fill=t.accent if i == 0 else "#a1c181")) # Eyes looking where the snake goes. hx, hy = self.body[0] dx, dy = self.direction cx, cy = hx * CELL + CELL / 2 + dx * 4, hy * CELL + CELL / 2 + dy * 4 for side in (-1, 1): shapes.append(draw.circle(cx + dy * 5 * side, cy + dx * 5 * side, 2.2, fill=t.editor)) return pz.canvas(shapes, width=COLS * CELL, height=ROWS * CELL) def overlay(self, t): if self.over: title, hint = "Game over", "Press Enter to play again" elif self.paused: title, hint = "Paused", "Press Space to continue" else: return None card = pz.container(pz.column([ ui.heading(title, level=2), ui.muted(f"Score {self.score} · Best {self.best}"), ui.button(hint.split(" to ")[1].capitalize(), self.reset if self.over else (lambda: setattr(self, "paused", False)), variant="solid", icon="rotate-ccw" if self.over else "play"), ui.muted(hint, size="sm"), ], spacing=10, align="center"), padding=24).style(glass=True, radius=14, border=(1, "border")) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def view(self): t = self.resolved_theme() hud = pz.row([ pz.icon("worm", size=18, color=t.accent), ui.label("Snake", weight="semibold"), pz.hspace(), ui.badge(f"Score {self.score}", icon="apple"), ui.badge(f"Best {self.best}", tint="warning", icon="trophy"), ui.badge(f"{self.speed}/s", tint="text_muted", icon="gauge"), ], spacing=8, align="center") game = pz.stack([self.board(t), self.overlay(t)], width=COLS * CELL, height=ROWS * CELL) return pz.container( pz.column([hud, pz.container(game, radius=10, clip=True), ui.muted( "Arrows / WASD steer · Space pauses · Enter restarts", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(Snake) ``` ### Tutorial: Breakout > A 60 fps Breakout with pz.frames, held keys and the mouse, ball physics, particles, combos and screen shake. Source: https://albakore.github.io/pyzed/games/breakout/ Breakout needs smooth movement, so instead of a timer it uses `pz.frames`: a callback that runs once per displayed frame with the time since the previous one. You will learn the frame loop, continuous input with `pz.pressed()`, collisions with `pyzed.game`, and the small touches that make a game feel good: particles, combos and screen shake. | Goal | Time | Level | You need | | --- | --- | --- | --- | | Breakout at 60 fps: paddle, ball physics, bricks, particles and shake | 45 minutes | Intermediate · game loop | [Snake](https://albakore.github.io/pyzed/games/snake/), or [A game loop](https://albakore.github.io/pyzed/tutorials/advanced/game-loop/) | Play it first: 1. **Bricks as boxes.** `game.Rect` is a box with collision helpers. Each brick keeps its rectangle, color, points and how many hits it takes: ```python from pyzed import draw, game, ui def build_level(self): self.bricks = [] for row in range(BRICK_ROWS): for col in range(BRICK_COLS): rect = game.Rect(left + col * (BRICK_W + BRICK_GAP), BRICK_TOP + row * (BRICK_H + BRICK_GAP), BRICK_W, BRICK_H) hits = 2 if self.level > 1 and row < self.level - 1 else 1 self.bricks.append({"rect": rect, "color": ROW_COLORS[row], "hits": hits, "points": (BRICK_ROWS - row) * 10}) ``` 2. **The frame loop.** Return `pz.frames(self.tick)` and Pyzed calls `tick(dt)` before every frame. Everything moves by *speed × dt*, so the game runs at the same speed on a 60 Hz laptop and a 144 Hz monitor: ```python def subscriptions(self): return [pz.frames(self.tick)] def tick(self, dt): self.sparks.update(dt) self.shake.update(dt) if self.paused or self.over: return move = game.axis(("left", "a"), ("right", "d")) # -1, 0 or 1 from held keys self.paddle_x = game.clamp(self.paddle_x + move * 520 * dt, PADDLE_W / 2, W - PADDLE_W / 2) ... ``` `game.axis` reads `pz.pressed()`, the set of keys held right now. That is what makes movement continuous: `on_key` would only fire on presses (and on the keyboard's repeat). 3. **The mouse too.** The canvas reports the pointer in its own coordinates, so the paddle can follow it and a click can launch the ball: ```python pz.canvas(shapes, width=W, height=H, on_move=self.mouse, on_press=lambda x, y: self.launch(), cursor="crosshair") def mouse(self, x, y): self.paddle_x = game.clamp(x, PADDLE_W / 2, W - PADDLE_W / 2) ``` 4. **Moving the ball without tunneling.** A fast ball could jump over a thin brick in one frame. Splitting the frame into sub-steps no longer than the ball's radius fixes it: ```python steps = max(1, math.ceil(self.speed * dt / BALL_R)) for _ in range(steps): if self.advance(dt / steps): # True when the ball was lost break ``` 5. **Bouncing.** `game.circle_rect` says whether a circle touches a box and from which side, as a direction `(nx, ny)`. Bouncing is then one line per axis. On the paddle, the point of contact chooses the angle, which is what lets the player aim: ```python for brick in self.bricks: normal = game.circle_rect(self.ball_x, self.ball_y, BALL_R, brick["rect"]) if normal: nx, ny = normal if nx: self.vx = abs(self.vx) * nx if ny: self.vy = abs(self.vy) * ny ... if self.vy > 0 and game.circle_rect(self.ball_x, self.ball_y, BALL_R, self.paddle): offset = (self.ball_x - self.paddle_x) / (PADDLE_W / 2) # -1 … 1 angle = math.radians(-90 + 60 * game.clamp(offset, -1, 1)) self.vx, self.vy = math.cos(angle) * self.speed, math.sin(angle) * self.speed ``` 6. **Juice: particles, combos and shake.** `game.Particles` is a tiny particle system: `burst` when something happens, `update(dt)` every frame, `shapes()` to draw. A combo multiplies the points of bricks broken without touching the paddle, and `game.Shake` moves the whole scene a few pixels on every hit: ```python self.sparks = game.Particles(gravity=420, drag=1.5) self.shake = game.Shake() # when a brick breaks: self.combo += 1 self.score += brick["points"] * self.combo self.sparks.burst(cx, cy, count=22, color=[brick["color"], "#ffffff"]) self.shake.kick(2 + min(self.combo, 6)) # in the scene, the translate goes first so everything after it shakes: shapes = [draw.background("#101216"), *self.shake.shapes()] ... shapes += self.sparks.shapes(glow=True) shapes += game.glow(self.ball_x, self.ball_y, BALL_R, "#ffffff", layers=3) ``` 7. **HUD.** Lives are three `heart` icons, the combo is a badge that only appears from ×2, and the "Level 1 · Space to launch" banner is a `glass` card stacked over the canvas: ```python hearts = pz.row([pz.icon("heart", size=15, color="#d07277" if i < self.lives else t.border) for i in range(3)], spacing=3) board = pz.stack([self.scene(t), self.banner()], width=W, height=H) ``` **Tip:** Logic that only depends on `dt` is easy to test: create the app, call `tick(1 / 60)` a few hundred times and check the score, lives or bricks left. No window is needed. #### Full code `examples/games/breakout.py`: ```py """Breakout: a paddle, a ball and a wall of bricks, running at the display's frame rate. python examples/games/breakout.py Move with the mouse or the arrows/A-D, Space launches the ball, P pauses, Enter starts again. Shows ``pz.frames`` (a callback per frame with the elapsed time), ``pz.pressed`` for keys held down, ball-against-box collisions with ``game.circle_rect``, particles and screen shake from ``pyzed.game``. Tutorial: https://albakore.github.io/pyzed/games/breakout/ """ import math import random import pyzed as pz from pyzed import draw, game, ui W, H = 640, 480 PADDLE_W, PADDLE_H, PADDLE_Y = 96, 12, H - 40 BALL_R = 7 BRICK_COLS, BRICK_ROWS = 10, 6 BRICK_W, BRICK_H, BRICK_GAP, BRICK_TOP = 58, 20, 4, 56 ROW_COLORS = ["#d07277", "#dfa067", "#e5c07b", "#a1c181", "#6eb4bf", "#74ade8"] class Breakout(pz.App): title = "Breakout" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) self.sparks = game.Particles(gravity=420, drag=1.5, seed=seed) self.shake = game.Shake(seed=seed) self.best = 0 self.new_game() # -- state ---------------------------------------------------------------- def new_game(self): self.level, self.score, self.lives = 1, 0, 3 self.over = self.won = self.paused = False self.build_level() def build_level(self): left = (W - (BRICK_COLS * (BRICK_W + BRICK_GAP) - BRICK_GAP)) / 2 self.bricks = [] for row in range(BRICK_ROWS): for col in range(BRICK_COLS): rect = game.Rect(left + col * (BRICK_W + BRICK_GAP), BRICK_TOP + row * (BRICK_H + BRICK_GAP), BRICK_W, BRICK_H) # Top rows are worth more and, from level 2, take two hits. hits = 2 if self.level > 1 and row < self.level - 1 else 1 self.bricks.append({"rect": rect, "color": ROW_COLORS[row], "hits": hits, "points": (BRICK_ROWS - row) * 10}) self.paddle_x = W / 2 self.reset_ball() def reset_ball(self): self.stuck = True # the ball rides on the paddle until Space self.ball_x, self.ball_y = self.paddle_x, PADDLE_Y - BALL_R - 1 self.vx = self.vy = 0.0 self.speed = 330 + 30 * self.level self.combo = 0 def launch(self): if self.stuck and not (self.over or self.won): angle = math.radians(self.random.uniform(-120, -60)) self.vx, self.vy = math.cos(angle) * self.speed, math.sin(angle) * self.speed self.stuck = False @property def paddle(self): return game.Rect(self.paddle_x - PADDLE_W / 2, PADDLE_Y, PADDLE_W, PADDLE_H) # -- the loop ------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] # particles keep moving even when paused def tick(self, dt): self.sparks.update(dt) self.shake.update(dt) if self.paused or self.over or self.won: return move = game.axis(("left", "a"), ("right", "d")) self.paddle_x = game.clamp(self.paddle_x + move * 520 * dt, PADDLE_W / 2, W - PADDLE_W / 2) if self.stuck: self.ball_x = self.paddle_x return # Small sub-steps so a fast ball can't jump through a brick in one frame. steps = max(1, math.ceil(self.speed * dt / BALL_R)) for _ in range(steps): if self.advance(dt / steps): break def advance(self, dt): """Moves the ball once; returns True when it was lost.""" self.ball_x += self.vx * dt self.ball_y += self.vy * dt if self.ball_x < BALL_R or self.ball_x > W - BALL_R: self.vx = -self.vx self.ball_x = game.clamp(self.ball_x, BALL_R, W - BALL_R) if self.ball_y < BALL_R: self.vy = abs(self.vy) if self.ball_y > H + BALL_R: self.lose_life() return True if self.vy > 0 and game.circle_rect(self.ball_x, self.ball_y, BALL_R, self.paddle): # Where it hits the paddle decides the angle: the edges send it sideways. offset = (self.ball_x - self.paddle_x) / (PADDLE_W / 2) angle = math.radians(-90 + 60 * game.clamp(offset, -1, 1)) self.vx, self.vy = math.cos(angle) * self.speed, math.sin(angle) * self.speed self.combo = 0 self.sparks.burst(self.ball_x, PADDLE_Y, count=6, color="#dce0e5", speed=(40, 120), angle=-90, spread=120, life=(0.15, 0.3)) for brick in self.bricks: normal = game.circle_rect(self.ball_x, self.ball_y, BALL_R, brick["rect"]) if normal: self.hit(brick, normal) break return False def hit(self, brick, normal): nx, ny = normal if nx: self.vx = abs(self.vx) * nx if ny: self.vy = abs(self.vy) * ny brick["hits"] -= 1 cx, cy = brick["rect"].center if brick["hits"] > 0: self.sparks.burst(cx, cy, count=8, color="#ffffff", speed=(40, 120), life=(0.15, 0.35)) return self.bricks.remove(brick) self.combo += 1 self.score += brick["points"] * self.combo self.sparks.burst(cx, cy, count=22, color=[brick["color"], "#ffffff"], life=(0.4, 0.9)) self.shake.kick(2 + min(self.combo, 6)) self.speed = min(self.speed + 4, 640) if not self.bricks: self.level += 1 if self.level > 3: self.won = True self.best = max(self.best, self.score) else: self.build_level() def lose_life(self): self.lives -= 1 self.shake.kick(14) self.sparks.burst(self.ball_x, H - 4, count=40, color=["#d07277", "#dfa067"], angle=-90, spread=90, speed=(120, 380)) if self.lives <= 0: self.over = True self.best = max(self.best, self.score) self.reset_ball() # -- input ---------------------------------------------------------------- def on_key(self, combo): if combo == "space": self.launch() elif combo == "p" and not (self.over or self.won): self.paused = not self.paused elif combo == "enter" and (self.over or self.won): self.new_game() def mouse(self, x, y): if not self.paused: self.paddle_x = game.clamp(x, PADDLE_W / 2, W - PADDLE_W / 2) # -- drawing -------------------------------------------------------------- def scene(self, t): shapes = [draw.background("#101216"), *self.shake.shapes()] for brick in self.bricks: r = brick["rect"] shapes.append(r.shape(fill=brick["color"], radius=4)) if brick["hits"] > 1: # armored: a lighter inner plate shapes.append(draw.rect(r.x + 4, r.y + 4, r.w - 8, r.h - 8, radius=3, fill="#ffffff55")) shapes.append(draw.rect(r.x, r.y, r.w, 4, radius=2, fill="#ffffff33")) # highlight p = self.paddle shapes += game.glow(p.center[0], p.center[1], 10, t.accent, layers=3, strength=0.35, core=False) shapes.append(p.shape(fill=t.accent, radius=PADDLE_H / 2)) shapes += self.sparks.shapes(glow=True) shapes += game.glow(self.ball_x, self.ball_y, BALL_R, "#ffffff", layers=3, strength=0.6) return pz.canvas(shapes, width=W, height=H, on_move=self.mouse, on_press=lambda x, y: self.launch(), cursor="crosshair") def banner(self): if self.over: text, hint = "Game over", "Enter plays again" elif self.won: text, hint = "You cleared every level!", "Enter plays again" elif self.paused: text, hint = "Paused", "P continues" elif self.stuck: text, hint = f"Level {self.level}", "Space or click to launch" else: return None card = pz.container(pz.column([ui.heading(text, level=2), ui.muted(hint)], spacing=6, align="center"), padding=(16, 28)).style(glass=True, radius=12, border=(1, "border")) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center", padding=(220, 0, 0, 0)) def view(self): t = self.resolved_theme() hearts = pz.row([pz.icon("heart", size=15, color="#d07277" if i < self.lives else t.border) for i in range(3)], spacing=3) hud = pz.row([ ui.label("Breakout", weight="semibold"), ui.badge(f"Level {self.level}", tint="text_muted"), pz.hspace(), ui.badge(f"x{self.combo} combo", tint="warning", icon="zap") if self.combo > 1 else None, ui.badge(f"{self.score:,}", icon="star"), ui.badge(f"Best {self.best:,}", tint="text_muted", icon="trophy"), hearts, ], spacing=8, align="center") board = pz.stack([self.scene(t), self.banner()], width=W, height=H) return pz.container(pz.column([hud, pz.container(board, radius=10, clip=True), ui.muted("Mouse or ←/→ moves · Space launches · P pauses", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(Breakout) ``` ### Tutorial: Neon Invaders > A complete space shooter with pixel-art sprites, waves, power-ups, neon glow, particles and a glass HUD with bloom. Source: https://albakore.github.io/pyzed/games/neon-invaders/ The last demo puts everything together in a shooter that looks like a modern indie game: a parallax starfield, pixel-art invaders that march and dive, auto-fire, a triple-shot power-up, neon lights everywhere, explosions, screen shake and a HUD of glass panels and glowing text drawn by the widget library. | Goal | Time | Level | You need | | --- | --- | --- | --- | | A shoot 'em up with pixel-art sprites, waves, power-ups, neon glow and a glass HUD | 1 hour | Advanced | [Breakout](https://albakore.github.io/pyzed/games/breakout/) | Play it first: 1. **Pixel art in plain text.** Sprites are lists of strings; `game.sprite` maps each character to a color and draws one shape per color, so a 9×7 ship costs two shapes: ```python SHIP = ["....#....", "...###...", "...#@#...", "..#####..", ".##.#.##.", "###.#.###", "#.......#"] game.sprite(SHIP, {"#": NEON["cyan"], "@": "#ffffff"}, self.x - 13.5, self.y - 10.5, scale=3) ``` Prefer image files? `draw.image("ship.png", x, y, w, h, rotation=angle)` draws them, decoded once and cached. 2. **Flying and auto-fire.** `game.vector()` turns arrows and WASD into a direction (diagonals normalized), and `pz.is_pressed("space")` fires while the key is held, with a cooldown: ```python def fly(self, dt): dx, dy = game.vector() self.x = game.clamp(self.x + dx * PLAYER_SPEED * dt, 20, W - 20) self.y = game.clamp(self.y + dy * PLAYER_SPEED * dt, H * 0.55, H - 30) self.cooldown -= dt if game.is_pressed("space") and self.cooldown <= 0: self.fire() def fire(self): self.cooldown = FIRE_DELAY for a in ((-12, 0, 12) if self.triple else (0,)): # the power-up fans out r = math.radians(a - 90) self.shots.append([self.x, self.y - 14, math.cos(r) * 640, math.sin(r) * 640]) ``` 3. **A formation that marches and dives.** The formation slides side to side and drops a little at each edge; now and then an invader leaves it and dives towards the ship, steering a bit every frame: ```python self.march += self.direction * speed * dt if abs(self.march) > 60: self.direction = -self.direction self.drop = min(self.drop + 10, 140) for inv in self.invaders: if inv.diving: inv.vx += (self.x - inv.x) * 1.4 * dt # home in on the ship inv.x += inv.vx * dt inv.y += inv.vy * dt elif self.random.random() < dt * (0.02 + 0.01 * self.wave): inv.diving, inv.vx, inv.vy = True, 0.0, 170 + 20 * self.wave ``` Using `random.random() < rate * dt` keeps events at the same average rate whatever the frame rate. 4. **Neon lights.** A canvas has no blur, but light is easy to fake: `game.glow` draws a few translucent circles of growing size behind a bright core. Lasers are two lines, a wide transparent one under a thin white one: ```python shapes += game.glow(cx, cy, 10, color, layers=2, strength=0.25, core=False) # invader halo shapes += game.glow(x, y, 3.5, NEON["pink"], layers=2, strength=0.6) # enemy bullet shapes.append(draw.line(x, y, x, y + 12, stroke=game.rgba(NEON["cyan"], 0.3), width=7, cap="round")) shapes.append(draw.line(x, y, x, y + 12, stroke="#e0fbff", width=2.5, cap="round")) ``` The starfield is three layers of dots that fall at different speeds (parallax), and a perspective grid at the bottom is a handful of `draw.line`s. 5. **Explosions and feedback.** Every kill bursts particles in the invader's color with a glow, and big hits shake the screen. After being hit the ship blinks and can't be hurt for a moment: ```python def explode(self, inv, big=False): cx, cy = inv.rect.center color = INVADER_COLORS[inv.kind] self.sparks.burst(cx, cy, count=40 if big else 26, color=[color, "#ffffff", color], speed=(60, 300)) self.shake.kick(10 if big else 3) ``` 6. **A HUD with real UI effects.** This is where a UI toolkit shines. The score is text with `bloom` (light from its bright pixels), the bar is a `glass` panel that blurs the stars behind it, and the title and "WAVE 2" banners use entrance animations: ```python def neon(widget, color, intensity=1.0): """Bloom on the bright parts only, without the lit box edge.""" return pz.container(widget, padding=(4, 12)).style( bloom=(color, intensity), border_width=0, shadow=("#00000000", (0, 0), 0)) score = neon(pz.text(f"{self.score:06d}", size=22, font="mono", color="#ffffff"), NEON["cyan"], 0.8) bar = pz.row([score, pz.hspace(), ui.badge(f"Wave {self.wave}", icon="crosshair"), shield]) hud = pz.container(bar, padding=(8, 14)).style(glass=True, radius=12, border=(1, "#ffffff22")) banner = neon(pz.text(f"WAVE {self.wave}", size=30, weight="bold"), NEON["lime"]).animate("zoom_in") board = pz.stack([self.scene(), banner, hud], width=W, height=H) ``` (Image: The title screen: a glass card with a pulsing neon title) **Tip:** Everything on screen is rebuilt every frame from the game state, like any Pyzed view. That keeps the code simple: there are no sprite objects to create, move or delete, just lists that `view()` turns into shapes. #### Full code `examples/games/neon_invaders.py`: ```py """Neon Invaders: a space shooter with a glowing HUD, particles, bloom and screen shake. python examples/games/neon_invaders.py Arrows/WASD fly, hold Space to fire, P pauses, Enter starts. Waves of pixel-art invaders march and dive; shoot the purple capsules for a triple shot. Shows a complete game built from Pyzed parts: ``pz.frames`` for the loop, ``pz.pressed`` for flying and auto-fire, ``game.sprite`` pixel art, ``game.glow`` neon lights, ``game.Particles`` explosions, ``game.Shake``, and a HUD of regular widgets with ``glass`` panels, ``bloom`` and animations stacked over the canvas. Tutorial: https://albakore.github.io/pyzed/games/neon-invaders/ """ import math import random import pyzed as pz from pyzed import draw, game, ui W, H = 760, 560 PLAYER_SPEED = 330 FIRE_DELAY = 0.16 NEON = {"cyan": "#22d3ee", "pink": "#f472b6", "lime": "#a3e635", "amber": "#fbbf24", "violet": "#a78bfa"} SHIP = [ "....#....", "...###...", "...#@#...", "..#####..", ".##.#.##.", "###.#.###", "#.......#", ] INVADERS = [ ["..#.....#..", "...#...#...", "..#######..", ".##.###.##.", "###########", "#.#######.#", "#.#.....#.#", "...##.##..."], ["...####...", ".########.", "##########", "###..##..#", "##########", "..##..##..", ".##.##.##.", "##......##"], ["...##...", "..####..", ".######.", "##.##.##", "########", "..#..#..", ".#.##.#.", "#.#..#.#"], ] INVADER_COLORS = [NEON["lime"], NEON["pink"], NEON["amber"]] PIXEL = 3 def neon(widget, color, intensity=1.0): """Bloom on the bright parts only: no lit edge or box shadow, so text glows by itself.""" return pz.container(widget, padding=(4, 12)).style(bloom=(color, intensity), border_width=0, shadow=("#00000000", (0, 0), 0)) class Invader: def __init__(self, kind, col, row): self.kind, self.col, self.row = kind, col, row self.hp = 1 + (kind == 0) self.diving = False self.flash = 0.0 # seconds left of the white flash after a hit self.x = self.y = 0.0 self.vx = self.vy = 0.0 @property def w(self): return len(INVADERS[self.kind][0]) * PIXEL @property def rect(self): return game.Rect(self.x, self.y, self.w, 8 * PIXEL) class NeonInvaders(pz.App): title = "Neon Invaders" size = (W + 32, H + 32) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) self.sparks = game.Particles(drag=2.2, seed=seed) self.shake = game.Shake(seed=seed) self.stars = [(self.random.uniform(0, W), self.random.uniform(0, H), layer) for layer in (1, 2, 3) for _ in range(40 // layer)] self.best = 0 self.started = False # the title screen until Enter self.time = 0.0 self.new_game() # -- state ---------------------------------------------------------------- def new_game(self): self.x, self.y = W / 2, H - 60 self.shield, self.score, self.wave = 3, 0, 0 self.shots, self.enemy_shots, self.powerups = [], [], [] self.cooldown = 0.0 self.triple = 0.0 # seconds of triple shot left self.hurt = 0.0 # invulnerability after a hit self.over = self.paused = False self.next_wave() def next_wave(self): self.wave += 1 self.invaders = [Invader(min(row // 2, 2), col, row) for row in range(4) for col in range(9)] self.march, self.direction, self.drop = 0.0, 1, 0.0 self.banner = 2.0 # "Wave N" shown for two seconds self.place_formation() def place_formation(self): left = 70 + self.march for inv in self.invaders: if not inv.diving: inv.x = left + inv.col * 64 + (33 - inv.w) / 2 inv.y = 70 + inv.row * 44 + self.drop # -- the loop ------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] def tick(self, dt): self.time += dt self.sparks.update(dt) self.shake.update(dt) self.stars = [(x, (y + 20 * layer * dt) % H, layer) for x, y, layer in self.stars] if not self.started or self.paused or self.over: return self.banner = max(0.0, self.banner - dt) self.hurt = max(0.0, self.hurt - dt) self.triple = max(0.0, self.triple - dt) self.fly(dt) self.move_invaders(dt) self.move_shots(dt) if not self.invaders: self.next_wave() def fly(self, dt): dx, dy = game.vector() self.x = game.clamp(self.x + dx * PLAYER_SPEED * dt, 20, W - 20) self.y = game.clamp(self.y + dy * PLAYER_SPEED * dt, H * 0.55, H - 30) self.cooldown -= dt if game.is_pressed("space") and self.cooldown <= 0: self.fire() def fire(self): self.cooldown = FIRE_DELAY angles = (-12, 0, 12) if self.triple else (0,) for a in angles: r = math.radians(a - 90) self.shots.append([self.x, self.y - 14, math.cos(r) * 640, math.sin(r) * 640]) def move_invaders(self, dt): speed = 40 + 10 * self.wave + (36 - len(self.invaders)) * 3 self.march += self.direction * speed * dt if abs(self.march) > 60: self.direction = -self.direction self.march = game.clamp(self.march, -60, 60) self.drop = min(self.drop + 10, 140) self.place_formation() for inv in self.invaders: inv.flash = max(0.0, inv.flash - dt) if inv.diving: inv.vx += (self.x - inv.x) * 1.4 * dt # home in on the ship inv.x += inv.vx * dt inv.y += inv.vy * dt if inv.y > H + 30: inv.diving = False # back to the formation from the top elif self.random.random() < dt * (0.02 + 0.01 * self.wave): inv.diving, inv.vx, inv.vy = True, 0.0, 170 + 20 * self.wave if self.random.random() < dt * (0.06 + 0.02 * self.wave): cx, cy = inv.rect.center self.enemy_shots.append([cx, cy + 12, 0, 230 + 15 * self.wave]) if self.hurt <= 0 and inv.rect.overlaps(self.ship_rect): self.invaders.remove(inv) self.explode(inv, big=True) self.damage() break def move_shots(self, dt): for shot in self.shots[:]: shot[0] += shot[2] * dt shot[1] += shot[3] * dt if shot[1] < -10: self.shots.remove(shot) continue for inv in self.invaders: if inv.rect.contains(shot[0], shot[1]): self.shots.remove(shot) inv.hp -= 1 if inv.hp <= 0: self.invaders.remove(inv) self.score += (30 - 10 * inv.kind) * (2 if inv.diving else 1) self.explode(inv) if self.random.random() < 0.06: self.powerups.append(list(inv.rect.center)) else: inv.flash = 0.08 self.sparks.burst(shot[0], shot[1], count=6, color="#ffffff", speed=(40, 140)) break for shot in self.enemy_shots[:]: shot[1] += shot[3] * dt if shot[1] > H + 10: self.enemy_shots.remove(shot) elif self.hurt <= 0 and self.ship_rect.contains(shot[0], shot[1]): self.enemy_shots.remove(shot) self.damage() for p in self.powerups[:]: p[1] += 90 * dt if self.ship_rect.contains(*p) or self.ship_rect.moved(-8, -8).contains(*p): self.powerups.remove(p) self.triple = 8.0 self.sparks.burst(p[0], p[1], count=30, color=NEON["violet"], speed=(60, 200)) elif p[1] > H + 10: self.powerups.remove(p) @property def ship_rect(self): return game.Rect(self.x - 13, self.y - 10, 27, 21) def explode(self, inv, big=False): cx, cy = inv.rect.center color = INVADER_COLORS[inv.kind] self.sparks.burst(cx, cy, count=40 if big else 26, color=[color, "#ffffff", color], speed=(60, 300), life=(0.3, 0.8), size=(1.5, 3.2)) self.shake.kick(10 if big else 3) def damage(self): self.shield -= 1 self.hurt = 1.5 self.shake.kick(16) self.sparks.burst(self.x, self.y, count=50, color=[NEON["cyan"], "#ffffff"], speed=(80, 320)) if self.shield <= 0: self.over = True self.best = max(self.best, self.score) # -- input ---------------------------------------------------------------- def on_key(self, combo): if combo == "enter" and (not self.started or self.over): self.started = True self.new_game() elif combo == "p" and self.started and not self.over: self.paused = not self.paused # -- drawing -------------------------------------------------------------- def scene(self): shapes = [draw.background("#05060d"), *self.shake.shapes()] for x, y, layer in self.stars: shapes.append(draw.circle(x, y, 0.5 * layer, fill=game.rgba("#c7d2fe", 0.25 * layer))) # A neon horizon grid at the bottom. for i in range(9): y = H - 120 + i * i * 1.6 shapes.append(draw.line(0, y, W, y, stroke=game.rgba(NEON["violet"], 0.05 + i * 0.012))) for i in range(-8, 9): shapes.append(draw.line(W / 2 + i * 30, H - 120, W / 2 + i * 120, H, stroke=game.rgba(NEON["violet"], 0.08))) bob = math.sin(self.time * 6) for inv in self.invaders: color = "#ffffff" if inv.flash > 0 else INVADER_COLORS[inv.kind] cx, cy = inv.rect.center shapes += game.glow(cx, cy, 10, color, layers=2, strength=0.25, core=False) shapes += game.sprite(INVADERS[inv.kind], {"#": color}, inv.x, inv.y + bob * (0 if inv.diving else 1.5), scale=PIXEL) for x, y in self.powerups: shapes += game.glow(x, y, 7, NEON["violet"], layers=3, strength=0.7) shapes.append(draw.text(x, y, "3", color="#05060d", size=10, weight="bold", align_x="center", align_y="center")) for x, y, *_ in self.shots: shapes.append(draw.line(x, y, x, y + 12, stroke=game.rgba(NEON["cyan"], 0.3), width=7, cap="round")) shapes.append(draw.line(x, y, x, y + 12, stroke="#e0fbff", width=2.5, cap="round")) for x, y, *_ in self.enemy_shots: shapes += game.glow(x, y, 3.5, NEON["pink"], layers=2, strength=0.6) if self.started and not self.over and (self.hurt <= 0 or int(self.time * 12) % 2): flame = 6 + 3 * math.sin(self.time * 40) shapes += game.glow(self.x, self.y + 14, 4, NEON["amber"], layers=2, strength=0.8) shapes.append(draw.polygon([(self.x - 4, self.y + 11), (self.x + 4, self.y + 11), (self.x, self.y + 11 + flame)], fill=NEON["amber"])) shapes += game.glow(self.x, self.y, 16, NEON["cyan"], layers=3, strength=0.22, core=False) shapes += game.sprite(SHIP, {"#": NEON["cyan"], "@": "#ffffff"}, self.x - 13.5, self.y - 10.5, scale=PIXEL) shapes += self.sparks.shapes(glow=True) return pz.canvas(shapes, width=W, height=H) def hud(self, t): shield = pz.row([ pz.icon("shield", size=14, color=NEON["cyan"]), pz.container(ui.progress(self.shield, max=3, tint=NEON["cyan"], height=6), width=90), ], spacing=6, align="center") score = neon(pz.text(f"{self.score:06d}", size=22, font="mono", color="#ffffff"), NEON["cyan"], 0.8) chips = [ ui.badge(f"Wave {self.wave}", tint=NEON["pink"], icon="crosshair"), ui.badge(f"Triple {self.triple:.0f}s", tint=NEON["violet"], icon="sparkles") if self.triple else None, ui.badge(f"Best {self.best:,}", tint="text_muted", icon="trophy"), ] bar = pz.row([score, pz.hspace(), *chips, shield], spacing=10, align="center") return pz.container(bar, padding=(8, 14)).style(glass=True, radius=12, border=(1, "#ffffff22")) def center_card(self, title, lines, color): heading = neon(pz.text(title, size=40, weight="bold", color="#ffffff"), color).animate("pulse") body = [ui.muted(line) for line in lines] card = pz.container(pz.column([heading, *body], spacing=10, align="center"), padding=(28, 40)).style(glass=True, radius=16, border=(1, "#ffffff26")) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def overlay(self): if not self.started: return self.center_card("NEON INVADERS", ["Arrows / WASD fly · hold Space to fire", "Press Enter to start"], NEON["pink"]) if self.over: return self.center_card("GAME OVER", [f"Score {self.score:,} · wave {self.wave}", "Press Enter to play again"], NEON["pink"]) if self.paused: return self.center_card("PAUSED", ["Press P to continue"], NEON["cyan"]) if self.banner: text = neon(pz.text(f"WAVE {self.wave}", size=30, weight="bold", color="#ffffff"), NEON["lime"]).animate("zoom_in") return pz.container(text, width="fill", height="fill", align_x="center", align_y="center") return None def view(self): t = self.resolved_theme() hud = pz.container(self.hud(t), padding=12, width="fill") if self.started else None board = pz.stack([self.scene(), self.overlay(), hud], width=W, height=H) return pz.container(pz.container(board, radius=12, clip=True), padding=16, width="fill", height="fill", bg="#0b0d14", align_x="center", align_y="center") if __name__ == "__main__": pz.run(NeonInvaders) ``` ## Audio ### Audio > Make music apps in Python with Pyzed — a modular audio engine, FL Studio-style knobs, faders and pads, waveforms, spectrum analyzers, a webcam view and a VCV Rack-style rack with animated cables. Source: https://albakore.github.io/pyzed/audio/ (Image: A modular rack: modules on rails, cables being plugged, a knob turning the filter) Pyzed has everything to build a synthesizer, a drum machine, a MIDI controller or a music visualizer in Python. It comes in two layers that you can use together or apart: - **The engine** (`pyzed.audio`) makes the sound. You add modules (oscillators, filters, envelopes, effects, sequencers, drums), join them with cables and it plays them on your sound card with low latency. It needs no window: a script, a test or a server can use it alone. - **The interface** (`pyzed.ui.audio`, `pyzed.ui.rack`, `pyzed.ui.camera`) shows and controls it: knobs, faders, pads, a piano, meters, waveforms, spectrum analyzers and a rack where you patch modules with cables you drag, like VCV Rack or Reaktor. **In plain words:** The engine is the instrument; the interface is its front panel. You can play the instrument with code only, or give it a panel the user can touch. ```python import pyzed as pz from pyzed import audio, ui engine = audio.Engine() osc = engine.add("oscillator", wave="saw", freq=110) lpf = engine.add("filter", cutoff=800, resonance=0.5) engine.chain(osc, lpf, engine.add("audio_out")) class Synth(pz.App): def on_start(self): engine.start() def subscriptions(self): return [pz.frames(lambda dt: None)] # redraw every frame: the meters move def view(self): return pz.row([ ui.audio.Knob(value=engine.get(lpf, "cutoff"), min=20, max=20000, curve="log", unit="Hz", caption="Cutoff", variant="fl", on_change=lambda e: engine.set(lpf, "cutoff", e.value)), ui.audio.Spectrum(magnitudes=engine.spectrum(), width=360, height=120), ui.audio.VuMeter(level=engine.level()), ], spacing=16, align="center") pz.run(Synth) ``` #### Where to start - [The engine](https://albakore.github.io/pyzed/audio/engine/getting-started/): Modules, cables, MIDI and latency: making sound from Python. - [Components](https://albakore.github.io/pyzed/audio/components/knob/): Knobs, faders, pads, piano, meters, waveforms and analyzers. - [The rack](https://albakore.github.io/pyzed/audio/rack/rack/): A VCV Rack-style rack with modules you patch with cables. - [Demos](https://albakore.github.io/pyzed/audio/demos/synth/): A synthesizer, a visualizer, a MIDI controller, a drum machine and more. #### The components Every component is in the designer's toolbox, in the **Audio** group (the webcam is under **Media**), and has its own page with an animation: | Component | What it is | | --- | --- | | [`Knob`](https://albakore.github.io/pyzed/audio/components/knob/) | A rotary control: five looks, logarithmic ranges, stepped selectors. | | [`Fader`](https://albakore.github.io/pyzed/audio/components/fader/) | A slider like a mixing desk's, with an optional level meter. | | [`Pad`, `LedButton`](https://albakore.github.io/pyzed/audio/components/pads-and-buttons/) | Velocity pads and buttons with a light. | | [`StepSequencer`](https://albakore.github.io/pyzed/audio/components/step-sequencer/) | A grid of steps like FL Studio's channel rack. | | [`Piano`](https://albakore.github.io/pyzed/audio/components/piano/) | A keyboard you play with the mouse or the computer keys. | | [`VuMeter`](https://albakore.github.io/pyzed/audio/components/vu-meter/) | LED, bar and analog needle meters with peak hold. | | [`Waveform`](https://albakore.github.io/pyzed/audio/components/waveform/) | A recording's shape, with a playhead and a selection. | | [`Scope`](https://albakore.github.io/pyzed/audio/components/scope/) | An oscilloscope with trigger, XY mode and persistence. | | [`Spectrum`](https://albakore.github.io/pyzed/audio/components/spectrum/) | A spectrum analyzer: bars, blocks, lines, mirror, radial. | | [`Spectrogram`](https://albakore.github.io/pyzed/audio/components/spectrogram/) | The spectrum over time, as a scrolling picture. | | [`Webcam`](https://albakore.github.io/pyzed/audio/components/webcam/) | The live picture of a camera. | | [`Rack`](https://albakore.github.io/pyzed/audio/rack/rack/) | Modules on rails joined with [animated cables](https://albakore.github.io/pyzed/audio/rack/cables/). | Colors and palettes are explained in [Colors and palettes](https://albakore.github.io/pyzed/audio/components/colors/). ### Knob > A rotary knob for Python audio apps — drag, scroll or double-click to reset, five looks from FL Studio to Eurorack, logarithmic ranges and stepped selectors. Source: https://albakore.github.io/pyzed/audio/components/knob/ (Image: Six knobs turning: arc, FL Studio, vintage, rack, minimal and a bipolar one) **In plain words:** A knob is the round control of every synthesizer and mixer. Drag it up to turn it right and down to turn it left; hold Shift for fine adjustments; double-click to put it back. ```python import pyzed as pz from pyzed import ui class Filter(pz.App): cutoff = 1200.0 def view(self): return ui.audio.Knob( value=self.cutoff, min=20, max=20000, curve="log", unit="Hz", caption="Cutoff", on_change=lambda e: setattr(self, "cutoff", e.value), ) pz.run(Filter) ``` #### How it works - **Drag** up or down: `travel` pixels (200 by default) cover the whole range. With Shift held, the same drag moves it ten times less. - **Scroll** the mouse wheel over it: 2 % of the range per notch, or one `step`. - **Double-click** to go back to `default` (or `min` when there is none). - `change` fires on every movement with the new value in `event.value`. The knob keeps the value the user sets even if your app does not store it. When your app passes a different `value` (a preset was loaded, a MIDI controller moved), that one wins. #### Looks `variant` picks one of five drawings; all of them are vector shapes drawn on a canvas, so they stay sharp at any `diameter`. | `variant` | Looks like | | --- | --- | | `"arc"` | A ring that fills with `color` around a small dial. The default, matches the theme. | | `"fl"` | FL Studio: a shaded dark knob with a glowing ring. | | `"vintage"` | A cream knob with tick marks, like old hardware. | | `"rack"` | A small Eurorack knob with a colored pointer (the rack uses it). | | `"minimal"` | Only the ring, with the value written in the middle. | `bipolar=True` draws the ring from the top center instead of the left end: right for positive values, left for negative ones. Use it for pan, detune or a filter's envelope amount. #### Ranges, units and choices - `curve="log"` turns the knob by ratios instead of steps: the middle of 20 Hz–20 kHz is about 630 Hz, which is how we hear pitch. Use it for frequencies and times. - `unit` formats the value: `"Hz"` (switches to kHz), `"ms"` (switches to seconds), `"dB"`, `"%"` (0..1 shown as a percentage) or any text. `decimals` fixes the decimals. - `step` snaps the value (`step=1` for whole numbers). - `options=["Sine", "Triangle", "Saw", "Square"]` makes a **stepped selector**: the value is the index of the option and the label shows its name. ```python ui.audio.Knob(value=self.wave, options=["Sine", "Tri", "Saw", "Square"], caption="Wave", variant="fl", on_change=lambda e: engine.set(osc, "wave", e.value)) ``` #### Properties | Property | Default | What it does | | --- | --- | --- | | `value` | `0.5` | The value shown. | | `min`, `max` | `0`, `1` | The range. | | `default` | `None` | Where a double-click takes it (`min` when `None`). | | `step` | `None` | Snaps the value to multiples of `step`. | | `curve` | `"lin"` | `"log"` for frequencies and times. | | `caption` | `""` | Text under the knob. | | `unit`, `decimals` | `""`, `None` | How the value is written. | | `options` | `None` | Names of choices: turns it into a stepped selector. | | `diameter` | `48` | Size in pixels. | | `variant` | `"arc"` | `"arc"`, `"fl"`, `"vintage"`, `"rack"` or `"minimal"`. | | `color` | `"accent"` | A theme token or a hex color. | | `bipolar` | `False` | Draws the ring from the center. | | `show_value` | `True` | Writes the value under the caption. | | `travel` | `200` | Pixels of dragging for the whole range. | | `disabled` | `False` | Grays it out and ignores the pointer. | Event: `on_change` (`event.value` is the new value). In Pyzed Design the knob is in the **Audio** group of the toolbox. **Tip:** Connecting a knob to the audio engine is one line: `on_change=lambda e: engine.set(node, "cutoff", e.value)`. `engine.set` only hands a number to the audio thread, so calling it on every movement is fine. ### Synthesizer > A playable synthesizer with effects in Python — two oscillators, a resonant filter, an envelope, distortion, chorus, delay and reverb, with FL Studio-style knobs and a piano. Source: https://albakore.github.io/pyzed/audio/demos/synth/ (Image: A synthesizer panel: knobs in four sections, an oscilloscope, a spectrum, a volume fader and a piano being played) A complete synthesizer in about 200 lines: [`examples/audio/synth.py`](https://github.com/albakore/pyzed/blob/main/examples/audio/synth.py). ```bash python examples/audio/synth.py ``` Play it with the mouse on the piano or with the computer keys: A W S E D… are the white and black keys of one octave, and Z and X move the octave down and up. The three buttons at the top load presets (Bass, Lead, Pad). **In plain words:** The sound is made by the engine; this file is only the panel. Every knob sets one value of a module, and the displays read what comes out. #### How it is built The patch is a chain of engine modules: ``` MIDI in → 2 oscillators → mixer → filter → amplifier → distortion → chorus → delay → reverb → speakers ↑ ↑ └── envelope ┘ ``` `midi_in` turns `engine.note_on()` into a pitch and a gate; the envelope opens the filter and the amplifier on every note. The panel keeps a dictionary of values and one function, `put`, sends each one to the module it controls: ```python def put(self, key, value): self.values[key] = value if key in ("cutoff", "resonance"): self.engine.set(self.vcf, key, value) elif key in ("attack", "decay", "sustain", "release"): self.engine.set(self.env, key, value) ... ``` The view is made of [knobs](https://albakore.github.io/pyzed/audio/components/knob/) grouped in sections, a [scope](https://albakore.github.io/pyzed/audio/components/scope/) and a [spectrum](https://albakore.github.io/pyzed/audio/components/spectrum/) reading `engine.scope()` and `engine.spectrum()`, a [VU meter](https://albakore.github.io/pyzed/audio/components/vu-meter/), a [fader](https://albakore.github.io/pyzed/audio/components/fader/) for the volume and a [piano](https://albakore.github.io/pyzed/audio/components/piano/) whose `note_on`/`note_off` events go to the engine. `pz.frames()` redraws the window every frame so the displays move. ### The audio engine > Build synthesizers, effects, drum machines and sequencers in Python with pyzed.audio — modules joined by cables like VCV Rack, computed in Rust with low latency, with or without a window. Source: https://albakore.github.io/pyzed/audio/engine/getting-started/ `pyzed.audio` is a modular synthesizer you program from Python. You add **modules** (oscillators, filters, envelopes, effects, sequencers, drums, samplers), plug their **ports** together with **cables**, turn their **knobs**, and the engine plays the result on your sound card with low latency. It works on its own, with no window at all: a script, a server or a test can make sound. The interface side (knobs, faders, waveforms and the rack with animated cables) is built on top of it.
Six modules joined by glowing cables: MIDI into an oscillator and an envelope, through a filter and an amplifier to the output
A synth voice: the dots are the signal travelling along the cables, the bars each module's level.
#### Your first sound ```python from pyzed import audio engine = audio.Engine() # the default sound card, low-latency settings osc = engine.add("oscillator", freq=220, wave="saw") lpf = engine.add("filter", cutoff=800, resonance=0.5) out = engine.add("audio_out") engine.chain(osc, lpf, out) # osc → filter → speakers with engine: # plays while inside the block input("Playing. Press Enter to stop.") ``` **Building from source on Linux:** The wheels from PyPI need nothing else. A build of Pyzed from source on Linux leaves audio devices and MIDI out unless you ask for them, so it never needs extra packages: `Engine("auto")` then plays on the silent clock and `engine.info["error"]` says why. For sound, install ALSA's headers (`sudo apt install libasound2-dev pkg-config`; Fedora: `sudo dnf install alsa-lib-devel`) and build with `maturin develop --release --features alsa` (with uv, keep `MATURIN_PEP517_ARGS="--features alsa"` exported and run `uv sync --reinstall-package pyzed-ui`). Every line of the patch is plain Python, so you can change it while it plays: ```python lpf["cutoff"] = 2000 # turn a knob (clamped to its range) osc["wave"] = "square" # a choice takes the name of an option... osc.set(wave=1, fine=0.1) # ...or its index; set() turns several knobs at once ``` #### How it works 1. **Python describes the patch.** `engine.add`, `engine.connect` and `node["knob"] = value` only record what you asked for and send it to the audio thread through a lock-free queue. They never wait for the sound, and the sound never waits for them. 2. **Rust computes the sound.** A real-time thread runs the modules in order, 64 frames at a time, with no allocation and no locks. Python's garbage collector or a busy interface cannot make it miss its deadline. 3. **The system plays it** through its own low-latency API: WASAPI (or ASIO) on Windows, CoreAudio on macOS, PulseAudio/PipeWire or ALSA on Linux. See [Devices and latency](https://albakore.github.io/pyzed/audio/engine/latency/). 4. **The engine reports back**: every module publishes its recent signal, level and state, so an interface can draw scopes, spectra, meters and step lights at 60 fps without touching the audio thread. #### Signals Like in a hardware modular synth (and in VCV Rack), every cable carries **one signal at audio rate**, and any output can go into any input. The port's type only says how to read it: | Type | What it carries | Range | | --- | --- | --- | | `audio` | Sound | -1 … 1 | | `cv` | A control voltage: pitch in **V/oct** or an amount | pitch: 0 = C4, +1 = one octave up | | `gate` | On or off: a key held, a step that plays | 0 or 1 | Pitch in volts per octave means adding 1 to the `pitch` input of an oscillator plays it an octave higher, and 1/12 a semitone higher. `audio.volts("A4")` gives `0.75`, so a constant can tune an oscillator to any note: ```python pitch = engine.add("constant", value=audio.volts("E2")) engine.connect(pitch, "out", osc, "pitch") ```
The sine, triangle, saw and square waves of the oscillator scrolling as they play
The four outputs of the oscillator, read live from the engine with node.scope().
#### Modules, ports and cables `engine.add(kind, id=None, **params)` adds a module and returns a `Node`. Its `id` (`"oscillator1"` unless you name it) is how cables and saved patches refer to it. The [module catalog](https://albakore.github.io/pyzed/audio/engine/modules/) lists every kind with its ports and knobs. ```python keys = engine.add("midi_in", id="keys") vco = engine.add("oscillator", id="vco", wave="saw") vcf = engine.add("filter", id="vcf", cutoff=600, resonance=0.6) env = engine.add("adsr", id="env", attack=0.01, decay=0.3, sustain=0.4, release=0.5) vca = engine.add("vca", id="vca") engine.connect(keys, "pitch", vco, "pitch") # the keyboard plays the oscillator engine.connect(keys, "gate", env, "gate") # each key opens the envelope engine.connect(env, "out", vca, "cv") # the envelope shapes the volume... engine.connect(env, "out", vcf, "cutoff") # ...and opens the filter engine.chain(vco, vcf, vca, engine.add("audio_out")) ``` - **An input takes any number of cables**; their signals add up. - **`engine.chain(a, b, c)`** plugs each module's first output into the next one's first input (both sides of a stereo pair when both modules have `left` and `right`). - **Loops are allowed.** A cable that goes back to an earlier module (feedback through a delay, an oscillator modulating itself) reads the previous block, as in a hardware rack. - `engine.disconnect(...)` takes the same arguments as `connect`; `engine.cables` lists them all; `node.remove()` takes a module out with its cables.
An ADSR envelope drawn while a key is held and released: attack, decay, sustain and release
The adsr module while a key is held and let go: attack, decay, sustain, release.
#### Playing notes The `midi_in` module turns notes into `pitch`, `gate` and `velocity`. They come from a MIDI keyboard or from your code: ```python engine.note_on("C4", velocity=100) # a MIDI number (60) or a name engine.note_off("C4") engine.note("E4", duration=0.5) # the release happens on a timer engine.cc(1, 64) # the mod wheel engine.pitch_bend(0.5) # half the bend range up engine.open_midi_input() # the first MIDI keyboard; or open_midi_input("Arturia") engine.open_midi_input(virtual="Pyzed") # a port your DAW can play into (Linux and macOS) ``` `audio.midi_inputs()` and `audio.midi_outputs()` list the ports. The `midi_out` module plays a gate and a pitch on the output opened with `engine.open_midi_output(...)`, so a Pyzed sequencer can drive a hardware synth or a DAW. #### Sequencing A `clock` sets the tempo; a `sequencer` steps through up to 16 pitches and gates on each clock tick (or on its own `bpm` when nothing is plugged into `clock`): ```python clock = engine.add("clock", bpm=124, division="1/16", swing=0.1) seq = engine.add("sequencer", steps=8, pitch_1=0, pitch_2=7, pitch_3=3, pitch_4=12, gate_3=0) engine.connect(clock, "clock", seq, "clock") engine.connect(seq, "pitch", vco, "pitch") engine.connect(seq, "gate", env, "gate") ```
Eight sequencer steps lighting up in turn, with the gate signal they send drawn below
The current step comes from seq.state["step"]; the gate below is seq.probe("gate").
Drums are modules too: `drum` synthesizes a kick, snare, hi-hat, clap or tom on each rising edge of its `trigger`. The [drum machine example](https://github.com/albakore/pyzed/blob/main/examples/audio/engine_drum_machine.py) builds a whole beat with a bass line ducked by the kick through a compressor's `sidechain`. #### Samples The `sampler` plays a WAV file or a list of samples, once, while its gate is held or in a loop: ```python smp = engine.add("sampler", mode="one-shot") smp.load("snare.wav") # or smp.load([0.0, 0.3, ...], sample_rate=48000) engine.connect(seq, "trigger", smp, "gate") smp.trigger() # or play it from code ``` #### Watching the sound Every module records its signal for the interface: read it as often as you like, from any thread. | Call | Returns | | --- | --- | | `node.scope(n=1024)` | the last `n` samples, oldest first (a waveform view) | | `node.spectrum(bins=256, size=2048, log=True)` | `bins` bands in dB (-120 … 0), spread over octaves from 20 Hz | | `node.level()` | `(rms, peak)` from 0 to 1 (a VU meter) | | `node.probe("out")` | the latest value of an output (a cable can glow with it) | | `node.state` | what the module reports: the step of a sequencer, the stage of an envelope… | | `engine.scope()`, `engine.spectrum()`, `engine.level()` | the same for what goes to the speakers |
A waveform and a bar spectrum moving with a beat and a bass line
A beat read at 20 fps with engine.scope() and engine.spectrum(bins=48).
The spectrum of a saw wave before and after a low-pass filter whose cutoff sweeps up and down
vco.spectrum() (grey) and vcf.spectrum() (blue) while the filter's cutoff sweeps.
In a `pz.App`, read them on each frame with `pz.frames` or `pz.every` and draw them with the audio components of `pz.ui`. #### Rendering without a sound card `audio.Engine("offline")` never opens a device: `engine.render(seconds)` computes the patch as fast as the processor allows and returns `(left, right)`. It is how the tests and these pictures are made, and how you bounce a patch to a file: ```python engine = audio.Engine("offline") ... # build the patch engine.note_on("A3") engine.render(1.0) # one second with the key held engine.note_off("A3") engine.render_wav("pad.wav", 3.0) # three more seconds into a WAV file ``` `audio.write_wav(path, left, right, sample_rate)` saves any list of samples. `backend="null"` runs the patch in real time with no sound, for machines without a sound card. #### Saving patches ```python import json json.dump(engine.to_dict(), open("patch.json", "w")) engine.load(json.load(open("patch.json"))) copy = audio.Engine.from_dict(data) ``` The data holds the modules with their knobs, the cables and the sample files. Anything you put in `engine.ui` or `node.ui` (positions in a rack, colors) travels with it untouched. #### The catalog `audio.modules()` describes every kind of module, the same data as the [catalog page](https://albakore.github.io/pyzed/audio/engine/modules/): its `inputs` and `outputs` (`Port` with `name`, `label` and `type`), its `params` and its `state`. A `Param` knows its range, unit and curve, which is all an interface needs to draw a knob: ```python cutoff = audio.module("filter").param("cutoff") cutoff.to_unit(1000) # 0.57: where 1 kHz sits on the knob (logarithmic, by octaves) cutoff.from_unit(0.25) # 112.5 Hz cutoff.format(1500) # "1.50 kHz" ``` #### Examples The [`examples/audio/`](https://github.com/albakore/pyzed/tree/main/examples/audio) folder has complete scripts, each with `--render out.wav` to hear them without a device: - `engine_synth.py`: a subtractive synth with sub oscillator, filter, envelope, LFO, delay and reverb, played by your MIDI keyboard or a melody. - `engine_drum_machine.py`: drums, an acid bass line and sidechain pumping at 124 BPM. - `engine_microphone.py`: your microphone through distortion, chorus, delay and reverb, live. ### The modular rack > Build a VCV Rack-style modular synthesizer in Python with Pyzed — modules on rails, knobs and jacks laid out from each module's description, a module browser, and patches saved to JSON. Source: https://albakore.github.io/pyzed/audio/rack/rack/ (Image: A rack of modules — sequencer, oscillator, filter, envelope, delay, scope and output — patched with cables that sway and carry signal dots) **In plain words:** A modular synthesizer is a set of boxes (modules) that each do one thing — make a tone, filter it, shape its volume, echo it — joined by cables. `ui.rack.Rack` puts the audio engine's modules on screen like hardware on rails: turn their knobs and drag cables between their sockets. ```python import pyzed as pz from pyzed import audio, ui class Modular(pz.App): def __init__(self): self.engine = audio.Engine() self.layout = {} # where each module sits; saved with the patch seq = self.engine.add("sequencer", bpm=118) osc = self.engine.add("oscillator", wave="saw") vcf = self.engine.add("filter", cutoff=900, resonance=0.6) out = self.engine.add("audio_out") self.engine.connect(seq, "pitch", osc, "pitch") self.engine.chain(osc, vcf, out) self.engine.start() def subscriptions(self): return [pz.frames(lambda dt: None)] # cables sway, scopes and LEDs move def view(self): return ui.rack.Rack(engine=self.engine, layout=self.layout, rows=2) pz.run(Modular) ``` #### Two layers: logic and graphics The rack is only the **graphical layer**. The sound comes from the [audio engine](https://albakore.github.io/pyzed/audio/engine/getting-started/) (`pyzed.audio`), which works without any window: a script can build the same patch, play it or render it to a WAV file. The rack reads three things from the engine and changes them through it: | The rack reads | From | And changes it with | | --- | --- | --- | | The modules | `engine.nodes` | `engine.add(kind)`, `engine.remove(id)` | | What each module has | `audio.modules()`: ports and parameters | | | The knobs | `node.params` | `engine.set(id, param, value)` | | The cables | `engine.cables` | `engine.connect(...)`, `engine.disconnect(...)` | | Screens, LEDs, meters | `node.scope()`, `node.spectrum()`, `node.state`, `node.level()` | | So anything your code does to the engine shows up in the rack on the next frame, and anything the user does in the rack is a normal engine call. #### Panels are laid out for you You never draw a module. Each panel is built from the module's description: - **Knobs** for its parameters, in columns from the top (a parameter with `options`, like an oscillator's wave, becomes a stepped selector). Numbered parameters, like a sequencer's `pitch_1`…`pitch_16` and `gate_1`…`gate_16`, become a grid of small knobs and switches, one column per step. - **Jacks** for its inputs at the bottom, and for its outputs below them on a **darker plate**, the convention every Eurorack maker follows so outputs are easy to spot. - A **screen** for the modules that have one: a live oscilloscope on `scope`, step LEDs on the `sequencer` (the current step lights up), level meters on `audio_out`, `mixer`, `vca` and `compressor`. - A colored strip on top by category (sources orange, filters blue, effects pink...), the title, four screws and a × to remove it. Widths are whole rack units (HP, 15 px) and every module is 380 px tall, like 3U Eurorack. #### Using it 1. **Add modules** from the browser on the left (type to search); a module goes at the end of the first row with room. 2. **Patch a cable**: drag from a jack and drop it on another. An output goes to an input; the cable snaps to the closest jack within reach as you drag. 3. **Move a cable**: drag a patched input — you pick the cable up by its plug, as in VCV Rack. Drop it on another input, or anywhere else to unplug it. 4. **Unplug**: right-click a jack. 5. **Move a module**: drag its title. It snaps to the HP grid and to a row, and moves along if it lands on another module. 6. **Turn knobs**: drag up and down (Shift for fine), double-click to reset. #### Saving patches ```python ui.rack.save(self.engine, self.layout, "bass.json") # engine.to_dict() + the layout self.layout = ui.rack.load(self.engine, "bass.json") # loads both back ``` The file is the engine's own patch format with the rack layout under `"ui"`, so a patch made in the rack plays the same from a script without the rack. | Property | Default | What it does | | --- | --- | --- | | `engine` | `None` | The `audio.Engine` to show and edit. | | `layout` | `{}` | Your dict of positions: `{node id: {"row": r, "x": hp}}`. | | `catalog` | `None` | Module descriptions (`audio.modules()` by default). | | `rows` | `2` | Rows of rails. | | `browser` | `True` | The module browser on the left. | | `tension` | `0.45` | How tight cables hang (0 loose, 1 straight). | | `cable_opacity` | `0.9` | See the modules through the cables. | | `flow` | `True` | Signal dots moving along the cables. | | `light` | `False` | Light panels, like VCV Rack's classic look. | Event: `on_change` with what changed: `"add"`, `"remove"`, `"move"`, `"connect"`, `"disconnect"` or `"set"` (to mark a document as unsaved, for instance). **Tip:** `ui.rack.panel_layout(audio.module("filter"))` tells you where every knob and jack of a module goes (`knobs`, `inputs`, `outputs`, `width`), if you want to draw your own panels with the same geometry. ### Animated cables > How Pyzed's patch cables work — Bézier curves that sag with gravity, a spring that settles them when plugged, signal dots that flow, and how to draw them yourself. Source: https://albakore.github.io/pyzed/audio/rack/cables/ (Image: Cables being plugged one by one: each plug travels to its jack, then the cable drops, overshoots and settles while dots flow along it) **In plain words:** The cables of the rack behave like real ones: they hang, they bounce a little when you plug them in, and small dots run along them in the direction the signal travels. #### The shape: a hanging curve A cable between two jacks is a **cubic Bézier curve**: it starts at one jack, ends at the other, and two control points pull it down. The control points sit a little inside the two ends and lower by the **sag**, which grows with the distance between the jacks: ``` sag = (18 + 0.3 × distance) × (1.15 − tension) ``` so a short cable is almost straight and a long one droops, as gravity would make it. `tension` (0..1) tightens all of them at once. ```python from pyzed.ui import rack c0, c1 = rack.cable_path((40, 60), (300, 80), tension=0.45) shape = draw.path([("M", 40, 60), ("C", *c0, *c1, 300, 80)], stroke="#f5c542", width=5) ``` #### The drop: a damped spring When a cable is plugged, its sag does not appear at once. It follows a **damped spring**: ``` drop(t) = 1 − e^(−5.5 t) · cos(13 t) ``` At `t = 0` the cable is straight; it falls, goes a little past its resting sag (the cosine makes it overshoot) and settles in about half a second. The rack stores when each cable was plugged and multiplies its sag by `drop(now − plugged)` on every frame. Nothing is simulated step by step, so the motion is the same at any frame rate. #### The look Each cable is drawn as five layers, from the bottom up: 1. A soft **shadow**, offset down and right, so the cable floats over the panels. 2. A darker **outline**. 3. The **body** in the cable's color (`cable_opacity` lets you see the panels through it). 4. A thin white **shine** along the top edge, which makes it read as round. 5. Two **plugs**: a ring, a lighter center and a highlight. New cables take the next color of `ui.rack.CABLE_COLORS` (yellow, red, green, blue, purple...), like VCV Rack. A cable keeps its color when you pick it up and move it. #### Signal flow With `flow=True` small light dots travel from the output to the input, spaced about 38 px apart. Their position along the curve is the time multiplied by a speed, wrapped around, so they move smoothly at any frame rate and every cable shows which way its signal goes. #### While dragging A cable being dragged follows the pointer with a tighter, lighter curve and **snaps** to the nearest compatible jack within 18 px (outputs only reach inputs and the other way round), so you see where it will land before you let go. An input takes one cable: dropping on a patched input replaces its cable. **Tip:** The rack needs to redraw every frame for the drop and the flow: `pz.frames(lambda dt: None)` in `subscriptions()`. Set `flow=False` and return no subscription when nothing moves to let the app sleep. ### Fader > A mixer fader for Python — a console cap, FL Studio's style or a flat handle, vertical or as a crossfader, with an optional level meter beside the track. Source: https://albakore.github.io/pyzed/audio/components/fader/ (Image: Three vertical faders with level meters and a horizontal crossfader moving) **In plain words:** A fader is the slider of a mixing desk: one per channel, usually for the volume. Drag the cap, click anywhere on the track to jump there, or scroll over it. ```python ui.audio.Fader(value=self.volume, caption="Master", level=engine.level()[1], on_change=lambda e: engine.set(out, "volume", e.value)) ``` #### How it works - **Drag the cap** to move it; Shift makes the movement ten times finer. - **Click on the track** (not on the cap) to jump to that point. - **Scroll** to nudge it, **double-click** to reset it to `default`. - `level` (0..1) draws a small meter beside the track: pass the channel's peak every frame and the fader doubles as a meter, like on a real console. #### Looks | `variant` | Looks like | | --- | --- | | `"console"` | A grooved metal cap, like an analog mixing desk. | | `"fl"` | FL Studio's dark cap with a colored line, the track filled up to it in `color`. | | `"flat"` | A round handle on a filled track, matching the rest of the theme. | `vertical=False` lays it out horizontally (a crossfader). `ticks` sets how many marks are drawn along the track (`0` for none) and `length` its size in pixels. #### Properties | Property | Default | What it does | | --- | --- | --- | | `value`, `min`, `max` | `0.75`, `0`, `1` | The value and its range. | | `default`, `step`, `curve` | `None`, `None`, `"lin"` | As on the [knob](https://albakore.github.io/pyzed/audio/components/knob/). | | `caption`, `unit`, `decimals` | | Text under it and how the value is written. | | `length` | `160` | Length of the track in pixels. | | `vertical` | `True` | `False` for a horizontal fader. | | `variant` | `"console"` | `"console"`, `"fl"` or `"flat"`. | | `ticks` | `11` | Marks along the track. | | `level` | `None` | A 0..1 level drawn as a meter next to the track. | | `color` | `"accent"` | The fill and the line on the cap. | | `show_value`, `disabled` | `True`, `False` | | Event: `on_change` with the new value. **Tip:** A mixer channel is a column: a [knob](https://albakore.github.io/pyzed/audio/components/knob/) for pan, two [LED buttons](https://albakore.github.io/pyzed/audio/components/pads-and-buttons/) for mute and solo and a fader with its `level`. The [MIDI controller demo](https://albakore.github.io/pyzed/audio/demos/midi-controller/) puts eight of them side by side. ### Module catalog > Every module of the Pyzed audio engine: oscillators, filters, envelopes, LFOs, effects, sequencers, drums, samplers and MIDI, with their ports, ranges and defaults. Source: https://albakore.github.io/pyzed/audio/engine/modules/ Every module the engine has, grouped as in a rack. Add one with `engine.add(kind, **params)` and plug its ports with `engine.connect(src, output, dst, input)`. The same data is in Python as [`audio.modules()`](https://albakore.github.io/pyzed/audio/engine/getting-started/#the-catalog), which an interface can use to draw any module. Signals follow the [conventions of modular synthesizers](https://albakore.github.io/pyzed/audio/engine/getting-started/#signals): audio swings between -1 and 1, pitch is in V/oct (0 = C4) and gates are 0 or 1. #### Sources ##### Oscillator · `oscillator` A band-limited VCO: sine, triangle, saw and square (with pulse width), tuned by `freq` plus the V/oct `pitch` input. **Inputs** | Port | | Type | | --- | --- | --- | | `pitch` | Pitch (V/oct) | cv | | `fm` | FM | cv | | `pwm` | PWM | cv | | `sync` | Sync | gate | **Outputs** | Port | | Type | | --- | --- | --- | | `out` | Out | audio | | `sine` | Sine | audio | | `triangle` | Triangle | audio | | `saw` | Saw | audio | | `square` | Square | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `freq` | Frequency | 8 … 20000 Hz (logarithmic) | 261.63 | | `wave` | Wave | options: `sine`, `triangle`, `saw`, `square` | `saw` | | `fine` | Fine | -1 … 1 st | 0 | | `pw` | Pulse width | 0.05 … 0.95 | 0.5 | | `fm_amount` | FM amount | 0 … 1 | 0 | | `level` | Level | 0 … 1 | 0.8 | ##### Noise · `noise` White, pink or brown noise. **Outputs** | Port | | Type | | --- | --- | --- | | `out` | Out | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `color` | Color | options: `white`, `pink`, `brown` | `white` | | `level` | Level | 0 … 1 | 0.5 | ##### Sampler · `sampler` Plays a WAV file or a list of samples (`node.load`) when its gate opens: once, while the gate is held or in a loop. **Inputs** | Port | | Type | | --- | --- | --- | | `gate` | Gate | gate | | `pitch` | Pitch (V/oct) | cv | **Outputs** | Port | | Type | | --- | --- | --- | | `left` | Left | audio | | `right` | Right | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `mode` | Mode | options: `one-shot`, `gate`, `loop` | `one-shot` | | `tune` | Tune | -24 … 24 st | 0 | | `start` | Start | 0 … 1 | 0 | | `end` | End | 0 … 1 | 1 | | `level` | Level | 0 … 1 | 0.8 | **State** (`node.state`): `position`, `playing` ##### Drum · `drum` A synthesized drum voice (kick, snare, hi-hat, clap or tom) struck by each rising edge of `trigger`. **Inputs** | Port | | Type | | --- | --- | --- | | `trigger` | Trigger | gate | | `accent` | Accent | cv | **Outputs** | Port | | Type | | --- | --- | --- | | `out` | Out | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `sound` | Sound | options: `kick`, `snare`, `hihat`, `clap`, `tom` | `kick` | | `tune` | Tune | -12 … 12 st | 0 | | `decay` | Decay | 0.02 … 2 s (logarithmic) | 0.4 | | `tone` | Tone | 0 … 1 | 0.5 | | `level` | Level | 0 … 1 | 0.8 | **State** (`node.state`): `hit` #### Modulation ##### LFO · `lfo` A slow oscillator to move other parameters: sine, triangle, saw, square or random steps (sample and hold). **Inputs** | Port | | Type | | --- | --- | --- | | `rate` | Rate | cv | | `reset` | Reset | gate | **Outputs** | Port | | Type | | --- | --- | --- | | `out` | Out | cv | | `unipolar` | Unipolar | cv | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `rate` | Rate | 0.01 … 50 Hz (logarithmic) | 1 | | `wave` | Wave | options: `sine`, `triangle`, `saw`, `square`, `random` | `sine` | | `amount` | Amount | 0 … 1 | 1 | | `offset` | Offset | -1 … 1 | 0 | **State** (`node.state`): `phase` ##### VCA · `vca` An amplifier whose gain follows `cv` (an envelope or an LFO); with nothing on `cv` it is a plain volume. **Inputs** | Port | | Type | | --- | --- | --- | | `in` | In | audio | | `cv` | CV | cv | **Outputs** | Port | | Type | | --- | --- | --- | | `out` | Out | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `gain` | Gain | 0 … 2 | 1 | | `response` | Response | options: `linear`, `exponential` | `linear` | ##### Envelope · `adsr` An ADSR envelope: rises while the gate opens, settles at sustain and fades out when it closes. **Inputs** | Port | | Type | | --- | --- | --- | | `gate` | Gate | gate | | `retrig` | Retrigger | gate | **Outputs** | Port | | Type | | --- | --- | --- | | `out` | Out | cv | | `inverted` | Inverted | cv | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `attack` | Attack | 0.001 … 10 s (logarithmic) | 0.01 | | `decay` | Decay | 0.001 … 10 s (logarithmic) | 0.2 | | `sustain` | Sustain | 0 … 1 | 0.7 | | `release` | Release | 0.001 … 10 s (logarithmic) | 0.3 | **State** (`node.state`): `stage`, `level` #### Filters ##### Filter · `filter` A resonant state-variable filter (VCF) with low-pass, high-pass, band-pass and notch; `cutoff` takes V/oct, so an envelope or LFO sweeps it. **Inputs** | Port | | Type | | --- | --- | --- | | `in` | In | audio | | `cutoff` | Cutoff | cv | | `resonance` | Resonance | cv | **Outputs** | Port | | Type | | --- | --- | --- | | `out` | Out | audio | | `lowpass` | Low-pass | audio | | `bandpass` | Band-pass | audio | | `highpass` | High-pass | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `mode` | Mode | options: `lowpass`, `highpass`, `bandpass`, `notch` | `lowpass` | | `cutoff` | Cutoff | 20 … 20000 Hz (logarithmic) | 1200 | | `resonance` | Resonance | 0 … 1 | 0.2 | | `cv_amount` | CV amount | -1 … 1 | 1 | | `drive` | Drive | 0 … 1 | 0 | #### Mixing ##### Mixer · `mixer` Four channels with level and pan into a stereo pair. **Inputs** | Port | | Type | | --- | --- | --- | | `in_1` | In 1 | audio | | `in_2` | In 2 | audio | | `in_3` | In 3 | audio | | `in_4` | In 4 | audio | **Outputs** | Port | | Type | | --- | --- | --- | | `left` | Left | audio | | `right` | Right | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `level_1` | Level 1 | 0 … 1.5 | 0.8 | | `level_2` | Level 2 | 0 … 1.5 | 0.8 | | `level_3` | Level 3 | 0 … 1.5 | 0.8 | | `level_4` | Level 4 | 0 … 1.5 | 0.8 | | `pan_1` | Pan 1 | -1 … 1 | 0 | | `pan_2` | Pan 2 | -1 … 1 | 0 | | `pan_3` | Pan 3 | -1 … 1 | 0 | | `pan_4` | Pan 4 | -1 … 1 | 0 | | `master` | Master | 0 … 1.5 | 1 | #### Effects ##### Delay · `delay` An echo with feedback and a tone control on the repeats. **Inputs** | Port | | Type | | --- | --- | --- | | `in` | In | audio | | `time` | Time | cv | **Outputs** | Port | | Type | | --- | --- | --- | | `out` | Out | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `time` | Time | 0.001 … 2 s (logarithmic) | 0.35 | | `feedback` | Feedback | 0 … 0.98 | 0.4 | | `tone` | Tone | 200 … 20000 Hz (logarithmic) | 8000 | | `mix` | Mix | 0 … 1 | 0.35 | ##### Reverb · `reverb` A stereo room (Freeverb): size, damping and width. **Inputs** | Port | | Type | | --- | --- | --- | | `left` | Left | audio | | `right` | Right | audio | **Outputs** | Port | | Type | | --- | --- | --- | | `left` | Left | audio | | `right` | Right | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `size` | Size | 0 … 1 | 0.6 | | `damping` | Damping | 0 … 1 | 0.4 | | `width` | Width | 0 … 1 | 1 | | `mix` | Mix | 0 … 1 | 0.3 | ##### Distortion · `distortion` Drives the signal into a soft or hard clipper, a wavefolder or a fuzz. **Inputs** | Port | | Type | | --- | --- | --- | | `in` | In | audio | **Outputs** | Port | | Type | | --- | --- | --- | | `out` | Out | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `type` | Type | options: `soft`, `hard`, `fold`, `fuzz` | `soft` | | `drive` | Drive | 0 … 1 | 0.4 | | `tone` | Tone | 200 … 20000 Hz (logarithmic) | 6000 | | `mix` | Mix | 0 … 1 | 1 | | `level` | Level | 0 … 1 | 0.7 | ##### Chorus · `chorus` A stereo chorus; with feedback and a short delay it becomes a flanger. **Inputs** | Port | | Type | | --- | --- | --- | | `in` | In | audio | **Outputs** | Port | | Type | | --- | --- | --- | | `left` | Left | audio | | `right` | Right | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `rate` | Rate | 0.05 … 5 Hz (logarithmic) | 0.8 | | `depth` | Depth | 0 … 1 | 0.5 | | `delay` | Delay | 1 … 30 ms (logarithmic) | 12 | | `feedback` | Feedback | 0 … 0.9 | 0 | | `mix` | Mix | 0 … 1 | 0.5 | ##### Bitcrusher · `bitcrusher` Lo-fi: fewer bits and a lower sample rate. **Inputs** | Port | | Type | | --- | --- | --- | | `in` | In | audio | **Outputs** | Port | | Type | | --- | --- | --- | | `out` | Out | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `bits` | Bits | 1 … 16 bit | 8 | | `downsample` | Downsample | 1 … 32 x | 4 | | `mix` | Mix | 0 … 1 | 1 | ##### Compressor · `compressor` Evens out the level; a signal on `sidechain` ducks the input instead (the pumping of dance music). **Inputs** | Port | | Type | | --- | --- | --- | | `in` | In | audio | | `sidechain` | Sidechain | audio | **Outputs** | Port | | Type | | --- | --- | --- | | `out` | Out | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `threshold` | Threshold | -60 … 0 dB | -18 | | `ratio` | Ratio | 1 … 20 :1 | 4 | | `attack` | Attack | 0.1 … 100 ms (logarithmic) | 10 | | `release` | Release | 10 … 1000 ms (logarithmic) | 120 | | `makeup` | Makeup | 0 … 24 dB | 0 | **State** (`node.state`): `reduction` #### Sequencing ##### Clock · `clock` The tempo of a patch: gates at the chosen division of the beat, on each beat and on each bar. **Inputs** | Port | | Type | | --- | --- | --- | | `reset` | Reset | gate | **Outputs** | Port | | Type | | --- | --- | --- | | `clock` | Clock | gate | | `beat` | Beat | gate | | `bar` | Bar | gate | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `bpm` | Tempo | 20 … 300 BPM | 120 | | `division` | Division | options: `1/4`, `1/8`, `1/16`, `1/32` | `1/16` | | `swing` | Swing | 0 … 0.5 | 0 | | `run` | Run | options: `off`, `on` | `on` | **State** (`node.state`): `tick`, `beat` ##### Sequencer · `sequencer` Up to 16 steps of pitch and gate, advanced by `clock` (or by its own `bpm` in sixteenth notes when nothing is on `clock`). **Inputs** | Port | | Type | | --- | --- | --- | | `clock` | Clock | gate | | `reset` | Reset | gate | **Outputs** | Port | | Type | | --- | --- | --- | | `pitch` | Pitch (V/oct) | cv | | `gate` | Gate | gate | | `trigger` | Trigger | gate | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `steps` | Steps | 1 … 16 | 8 | | `gate_length` | Gate length | 0.05 … 1 | 0.5 | | `bpm` | Tempo | 0 … 300 BPM | 120 | | `pitch_1` … `pitch_16` | Pitch | -24 … 24 st | 0 | | `gate_1` … `gate_16` | Gate | options: `off`, `on` | `on` | **State** (`node.state`): `step` #### Utilities ##### Constant · `constant` A fixed voltage, to drive an input from a knob. **Outputs** | Port | | Type | | --- | --- | --- | | `out` | Out | cv | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `value` | Value | -10 … 10 V | 1 | ##### Scope · `scope` Passes its input through untouched, to watch a signal anywhere in a patch. **Inputs** | Port | | Type | | --- | --- | --- | | `in` | In | audio | **Outputs** | Port | | Type | | --- | --- | --- | | `out` | Out | audio | #### I/O ##### Audio input · `audio_in` The sound of the input device (a microphone or a line in), opened with `Engine(input_device=...)` or `input=True`. **Outputs** | Port | | Type | | --- | --- | --- | | `left` | Left | audio | | `right` | Right | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `gain` | Gain | 0 … 4 | 1 | ##### MIDI to CV · `midi_in` Turns the notes of a MIDI keyboard (or `engine.note_on`) into pitch, gate and velocity, playing the last note held. **Outputs** | Port | | Type | | --- | --- | --- | | `pitch` | Pitch (V/oct) | cv | | `gate` | Gate | gate | | `velocity` | Velocity | cv | | `mod` | Mod wheel | cv | | `retrigger` | Retrigger | gate | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `channel` | Channel | 0 … 16 | 0 | | `glide` | Glide | 0.0001 … 2 s (logarithmic) | 0.0001 | | `bend_range` | Bend range | 0 … 24 st | 2 | **State** (`node.state`): `note`, `gate` ##### Audio output · `audio_out` The speakers. With nothing on `right`, `left` plays on both sides. **Inputs** | Port | | Type | | --- | --- | --- | | `left` | Left | audio | | `right` | Right | audio | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `volume` | Volume | 0 … 1 | 0.8 | ##### CV to MIDI · `midi_out` Plays the notes of a gate and a pitch on a MIDI output (`engine.open_midi_output`). **Inputs** | Port | | Type | | --- | --- | --- | | `pitch` | Pitch (V/oct) | cv | | `gate` | Gate | gate | | `velocity` | Velocity | cv | **Parameters** | Parameter | | Range | Default | | --- | --- | --- | --- | | `channel` | Channel | 1 … 16 | 1 | **State** (`node.state`): `note` ### Music visualizer > A music visualizer in Python — spectrum analyzers, a radial spectrum, a spectrogram, an oscilloscope and VU meters whose look changes live, fed by a song or the microphone. Source: https://albakore.github.io/pyzed/audio/demos/visualizer/ (Image: A visualizer cycling through looks: blocks, bars, filled, mirror and line spectrums, a radial one, a spectrogram, a scope and an analog VU meter) Every analyzer of Pyzed in one window: [`examples/audio/visualizer.py`](https://github.com/albakore/pyzed/blob/main/examples/audio/visualizer.py). ```bash python examples/audio/visualizer.py ``` It plays a short groove made with the engine (a sequenced bass, kick, snare, hats and chords). Switch the source to **Microphone** to see your own voice. The panel on the left changes the main spectrum live: its look (`variant`), its colors (`colors`), how many bands it has, how fast the bars fall and whether the peaks stay. #### How it is built - `demo_song(engine)` builds the groove: a `clock` drives a `sequencer` for the bass and another one for the snare, and its beat output strikes the kick. Everything goes through a `scope` module before the speakers, so it can be analyzed. - Each frame, the view asks that module for its analysis, computed in Rust: ```python mags = tap.spectrum(512) # 512 bands, 20 Hz to 24 kHz, in dB rms, peak = tap.level() wave = tap.scope(2048) # the last 2048 samples ``` - The same `mags` feed a [`Spectrum`](https://albakore.github.io/pyzed/audio/components/spectrum/) in two looks and a [`Spectrogram`](https://albakore.github.io/pyzed/audio/components/spectrogram/); `wave` feeds a [`Scope`](https://albakore.github.io/pyzed/audio/components/scope/); the levels feed two [`VuMeter`](https://albakore.github.io/pyzed/audio/components/vu-meter/)s. When the source is the microphone, the song's output is muted so the speakers do not feed back into the microphone. ### Devices and latency > How pyzed.audio picks the sound card, the audio API, the buffer and the sample rate on Windows, macOS and Linux, how to measure the latency and how to bring it down. Source: https://albakore.github.io/pyzed/audio/engine/latency/ **Latency** is the time between something happening (a key pressed, a knob turned, your voice in the microphone) and hearing it. The engine computes 64 frames at a time, but the sound card asks for a whole **buffer** at once: that buffer, plus what the system adds, is the latency. Smaller buffers mean less delay but less time to compute each one; too small and the sound crackles.
Blocks of 64 frames filling four slots of a 256-frame buffer that then goes to the speakers
A buffer of 256 frames is 5.3 ms at 48 kHz: four blocks of the engine.
#### What the engine picks With no settings, `audio.Engine()` uses the system's best audio API, its default device at the device's own sample rate, and the smallest buffer that plays without dropouts on a typical machine: | System | Audio API (host) | Buffer | Latency at 48 kHz | | --- | --- | --- | --- | | Windows | WASAPI (ASIO when built with it) | 256 frames (WASAPI in shared mode may choose its own) | ~5–10 ms | | macOS | CoreAudio | 128 frames | ~3 ms | | Linux | PulseAudio, which also reaches PipeWire; ALSA when there is no sound server | 256 frames | ~5 ms | `audio.recommended_settings()` returns these choices for the current machine. #### Choosing yourself ```python from pyzed import audio audio.hosts() # ['PulseAudio', 'ALSA']: the APIs available here audio.default_host() # 'PulseAudio' for d in audio.devices(): # the devices of the default host (or devices("ALSA")) print(d.name, d.outputs, d.inputs, d.sample_rate, d.buffer_range) engine = audio.Engine( host="ALSA", # an API from audio.hosts() device="USB Audio", # a device's name or id (None: the default one) sample_rate=48000, # None: the device's own rate buffer_size=128, # frames; None: the recommended size input=True, # also open the default input (for the audio_in module) input_device=None, # or name another one ) engine.start() print(engine.info) ``` `engine.start(**settings)` also takes these settings, so you can stop and start again with a smaller buffer or another device; the patch stays as it is. #### Measuring `engine.info` says what opened and how it runs: | Key | Meaning | | --- | --- | | `backend` | `"device"`, `"null"` (no sound card: a clock that keeps time) or `"offline"` | | `host`, `device`, `input_device` | the API and the devices in use | | `sample_rate`, `buffer_size`, `channels` | the stream's format (`buffer_size` is `None` when the system chose) | | `latency_ms` | from the moment a buffer is computed to the moment it leaves the speakers, as the system reports it | | `load` | the share of each buffer's time spent computing it: above 1.0 the sound breaks up | | `underruns` | buffers that came out silent | | `error` | the last problem (a device unplugged, or why there is no sound) | A `load` that stays below 0.5 leaves room for a smaller buffer. #### Each system **Windows:** - **WASAPI** is the default. In shared mode Windows mixes every program and decides the buffer (usually 10 ms); the engine asks for 256 frames and accepts what Windows gives. - **ASIO** gives the lowest latency with audio interfaces that ship an ASIO driver. It needs Steinberg's ASIO SDK at build time, so it is not in the PyPI wheels: build Pyzed with `maturin develop --release --features asio` (with `CPAL_ASIO_DIR` pointing at the SDK), then use `audio.Engine(host="ASIO")`. - For virtual MIDI ports, install [loopMIDI](https://www.tobias-erichsen.de/software/loopmidi.html). **macOS:** - **CoreAudio** runs the audio thread at real-time priority by itself and handles small buffers well: 128 frames is the default, 64 works on most Macs. - An aggregate device (Audio MIDI Setup) joins an input and an output that are different devices. - `engine.open_midi_input(virtual="Pyzed")` creates a MIDI port other apps can play into. **Linux:** - **PulseAudio** is the default when a sound server runs. On current desktops that server is **PipeWire**, which answers PulseAudio's protocol; the engine asks it for a 256-frame latency. - **ALSA** (`host="ALSA"`) talks to the card directly when no server runs, or to PipeWire through its ALSA plugin. `device="hw:0,0"` style names bypass any mixing for the lowest latency, but take the card for Pyzed alone. - **JACK** is available when you build Pyzed with `--features jack`. - The audio thread asks for real-time priority. If your user is not allowed it, the engine still runs; add yourself to the `audio` group (with an `rtprio` limit, as most distributions set it) for steadier small buffers. #### Without a sound card | Backend | Use it for | | --- | --- | | `audio.Engine("auto")` | The default: the sound card, or the `null` clock when there is none (`info["error"]` says why) | | `audio.Engine("device")` | The sound card or an error | | `audio.Engine("null")` | Real time with no sound: servers, CI, an interface that only shows the signals | | `audio.Engine("offline")` | No clock at all: `engine.render(seconds)` computes as fast as possible, for tests and files | **Headphones for the microphone:** With an `audio_in` module playing through the speakers, the microphone hears them and the sound feeds back. Use headphones, or keep the input's `gain` low. ### MIDI controller > A MIDI controller in Python like FL Studio's — channel strips with pan knobs, faders, mute and solo, velocity pads, pitch and mod wheels, and a virtual MIDI port for your DAW. Source: https://albakore.github.io/pyzed/audio/demos/midi-controller/ (Image: A MIDI controller playing a beat: eight channel strips with faders moving, pads lighting up and MIDI messages in a log) A controller you can use with any music program: [`examples/audio/midi_controller.py`](https://github.com/albakore/pyzed/blob/main/examples/audio/midi_controller.py). ```bash python examples/audio/midi_controller.py ``` It has eight channel strips (a pan [knob](https://albakore.github.io/pyzed/audio/components/knob/), a [fader](https://albakore.github.io/pyzed/audio/components/fader/) with a level meter, mute and solo), eight velocity [pads](https://albakore.github.io/pyzed/audio/components/pads-and-buttons/), pitch and mod wheels, transport buttons and a three-octave [piano](https://albakore.github.io/pyzed/audio/components/piano/). **PLAY** runs a beat on the pads. It plays its own drum kit and synth through the engine, so you hear everything. Switch on **MIDI OUT** to also send every move to other programs: | Control | MIDI message | | --- | --- | | Fader 1–8 | Control change 70–77 | | Pan 1–8 | Control change 10–17 | | Pad 1–8 | Note 36–43 on channel 10 (drums) | | Piano | Note on / note off on channel 1 | | Pitch wheel | Pitch bend | | Mod wheel | Control change 1 | **Using it with a DAW:** On Linux and macOS the controller opens a virtual port named **Pyzed Controller**: pick it as a MIDI input in your DAW. On Windows it uses the first MIDI output found (install a loopback driver such as loopMIDI to reach a DAW). #### How it is built ```python def set_level(self, i, value): self.levels[i] = value self.apply_mix() # the engine's mixer channel self.send(0xB0, 70 + i, int(value * 127)) # and a MIDI control change def toggle_midi_out(self, on): if on: self.engine.open_midi_output(virtual="Pyzed Controller") self.midi_out = on ``` `send` writes the bytes to the log on the right and, when MIDI OUT is on, calls `engine.send_midi(message)`. ### Pads and LED buttons > Velocity-sensitive drum pads and LED buttons (mute, solo, record) for Python music apps, styled after FL Studio and drum machines. Source: https://albakore.github.io/pyzed/audio/components/pads-and-buttons/ (Image: A 4×3 grid of drum pads lighting up with a beat, next to PLAY, REC, MUTE and SOLO buttons) **In plain words:** Pads are the rubber squares of a drum machine: hit one and it plays a sound. LED buttons are the small switches with a light, like mute, solo or record. #### Pad ```python ui.audio.Pad(text="KICK", color="#ff9f3d", on_press=lambda e: engine.node("drums").trigger(), on_release=lambda e: None) ``` - `press` fires when the pad is pressed, with a **velocity** from 1 to 127 in `event.value` (like MIDI): the lower on the pad you click, the harder the hit. `velocity=False` always sends 100. - `release` fires when the button is released or the pointer leaves the pad. - `lit=True` lights it from outside: a sequencer's current step, a note coming from a keyboard. The pad glows in its `color`, so a grid of them shows the rhythm at a glance. | Property | Default | What it does | | --- | --- | --- | | `text` | `""` | The label in the middle. | | `color` | `"accent"` | The color it lights up in. | | `lit` | `False` | Lit from outside. | | `velocity` | `True` | Velocity from where it is pressed. | | `width`, `height` | `72`, `72` | Size in pixels. | #### LED button ```python ui.audio.LedButton(text="MUTE", on=self.muted, color="warning", on_toggle=lambda e: setattr(self, "muted", e.value)) ``` A dark key with a small light. It **toggles** on each click (`toggle` gives `True` or `False`), or with `momentary=True` it lights only while held (`press` / `release`), like a tap-tempo key. | Property | Default | What it does | | --- | --- | --- | | `text` | `""` | The label. | | `on` | `False` | Whether it is lit. | | `color` | `"success"` | The light's color (a theme token or hex). | | `variant` | `"fl"` | `"fl"` (square key), `"round"` (bigger LED, round key) or `"pill"`. | | `momentary` | `False` | Lit only while held. | | `icon_name` | `""` | A Lucide icon next to the label (`"play"`, `"circle"`...). | Events: `on_toggle`, `on_press`, `on_release`. **Tip:** Play pads from the computer keyboard too: map keys in `on_key` and set `lit` from your app's state, so the pad lights whether it was clicked or typed. ### Drum machine > A drum machine in Python like FL Studio's channel rack — a step grid you paint, a pad per sound, tempo and swing, with sample-accurate timing from the audio engine. Source: https://albakore.github.io/pyzed/audio/demos/drum-machine/ (Image: A drum machine playing: the playhead moving over a grid of steps, a pointer painting new tom steps, pads flashing) A six-sound drum machine: [`examples/audio/drum_machine.py`](https://github.com/albakore/pyzed/blob/main/examples/audio/drum_machine.py). ```bash python examples/audio/drum_machine.py ``` Click or drag on the [step grid](https://albakore.github.io/pyzed/audio/components/step-sequencer/) to write the beat, play each sound with its [pad](https://albakore.github.io/pyzed/audio/components/pads-and-buttons/), and change the tempo and the swing with the knobs. #### Why the timing is exact The rhythm runs inside the engine, not in Python. A `clock` module ticks every sixteenth note and drives one `sequencer` per row; each sequencer's trigger strikes its `drum`: ```python clock = engine.add("clock", bpm=124, division="1/16") for sound in ("kick", "snare", "clap", "hihat", "hihat", "tom"): drum = engine.add("drum", sound=sound) seq = engine.add("sequencer", steps=16) engine.connect(clock, "clock", seq, "clock") engine.connect(seq, "trigger", drum, "trigger") ``` So the beat stays on time however busy the window is. The grid only switches the sequencers' gates (`engine.set(seq, "gate_5", 1)`), and the playhead reads the step they are on (`seq.state["step"]`). ### Step sequencer > A grid of steps like FL Studio's channel rack for Python — click or paint steps, rows with names, beat grouping and a moving playhead. Source: https://albakore.github.io/pyzed/audio/components/step-sequencer/ (Image: A five-row drum pattern with the playhead running across sixteen steps) **In plain words:** Each row is an instrument and each square a moment in the bar. Lit squares play, dark ones don't. Click a square to switch it, or drag across a row to paint several at once. ```python class Beat(pz.App): pattern = ["x...x...x...x...", "....x.......x...", "x.x.x.x.x.x.x.x."] step = 0 def view(self): return ui.audio.StepSequencer( pattern=self.pattern, labels=["Kick", "Snare", "Hat"], current=self.step, on_change=lambda e: setattr(self, "pattern", e.value), ) ``` #### Patterns `pattern` is a list of rows. Each row is either a list (`True`/`False`, or a velocity from 0 to 1 that sets how bright the step is) or text where `x` (or `1`) is a step that plays and any other character is a rest: `"x...x...x...x..."` is a kick on every beat. `change` gives the whole new pattern as a list of lists, ready to store. A drag starting on an empty step paints steps on; one starting on a lit step erases. #### Playhead `current` is the step being played: the sequencer frames that column and outlines its steps. Drive it from a timer (`pz.every`) or from the engine's sequencer (`node.state["step"]`). Steps are shaded in groups of `group` (4 by default), so beats are easy to count. | Property | Default | What it does | | --- | --- | --- | | `pattern` | `()` | Rows of steps. | | `labels` | `()` | A name per row (shown on the left). | | `current` | `-1` | The step being played (`-1` for none). | | `color` | `"accent"` | Color of the steps that play. | | `group` | `4` | Steps per shaded group. | | `cell` | `22` | Size of a step in pixels. | Event: `on_change` with the new pattern. **Tip:** The [drum machine demo](https://albakore.github.io/pyzed/audio/demos/drum-machine/) joins a step sequencer, a row of pads and the engine's `drum` module: the pads light with `lit=` as the steps play. ### Modular synthesizer > A modular synthesizer in Python like VCV Rack — modules on rails, cables you drag between jacks, a module browser, and patches saved to JSON. Source: https://albakore.github.io/pyzed/audio/demos/modular/ (Image: A modular rack: a cable dragged from the delay to the scope, then the filter's cutoff knob turned) VCV Rack in about 90 lines: [`examples/audio/modular.py`](https://github.com/albakore/pyzed/blob/main/examples/audio/modular.py). ```bash python examples/audio/modular.py # a starter patch python examples/audio/modular.py patch.json # a saved one ``` It opens with an acid bass line: a sequencer plays an oscillator through a filter, an envelope, an amplifier and a delay, with a scope and the output. Then: - Add modules from the browser on the left (search or pick a category). - Drag from one jack to another to plug a cable; outputs are on the dark plates. - Right-click a jack to unplug it; drag a plugged input to move its cable. - Drag a module by its title to move it; the × removes it. - Ctrl+S saves the patch, Ctrl+O opens it again. #### How it is built The whole window is one [`Rack`](https://albakore.github.io/pyzed/audio/rack/rack/) component on top of the engine: ```python engine = audio.Engine() starter_patch(engine) # engine.add(...) and engine.connect(...) class Modular(pz.App): layout = {} def view(self): return ui.rack.Rack(engine=self.engine, layout=self.layout, rows=2) ``` The rack reads the modules and cables from the engine, draws a panel for each module from its description in the catalog, and writes back every change you make. `ui.rack.save()` stores the patch and the position of each module in one JSON file. See [Cables](https://albakore.github.io/pyzed/audio/rack/cables/) for how the cables are drawn and animated. ### Piano keyboard > A clickable piano keyboard for Python synthesizers — glissando, velocity, lit notes from MIDI or a sequencer, and computer-keyboard playing. Source: https://albakore.github.io/pyzed/audio/components/piano/ (Image: A four-octave keyboard lighting up a chord progression and an arpeggio) **In plain words:** A keyboard you can play with the mouse: click a key to play a note, drag across the keys to slide from one to the next. Notes played from elsewhere (a MIDI keyboard, a sequencer) light up. ```python from pyzed import audio, ui engine = audio.Engine() synth = engine.add("midi_in") # notes from note_on/note_off reach the midi_in modules ... ui.audio.Piano(start=48, octaves=3, pressed=sorted(self.held), on_note_on=lambda e: engine.note_on(e.value.note, e.value.velocity), on_note_off=lambda e: engine.note_off(e.value)) ``` #### Notes and velocity Notes are MIDI numbers: 60 is middle C (C4), 69 is the A at 440 Hz. `note_on` gives a `Note(note, velocity)`; the lower on a key you click, the louder (velocity 30 to 127). `note_off` gives the note number. Dragging across keys releases one and presses the next. `ui.audio.note_name(61)` gives `"C#4"`, handy for labels. #### Playing from the computer keyboard `ui.audio.KEYBOARD` maps keys to semitones like most music apps: the row `a s d f g h j k` is the white keys and `w e t y u o p` the black ones. ```python def on_key(self, combo): if combo in ui.audio.KEYBOARD and combo not in self.keys: self.keys.add(combo) note = self.octave * 12 + ui.audio.KEYBOARD[combo] engine.note_on(note) def on_key_up(self, key): if key in self.keys: self.keys.discard(key) engine.note_off(self.octave * 12 + ui.audio.KEYBOARD[key]) ``` | Property | Default | What it does | | --- | --- | --- | | `start` | `48` | The first note (rounded down to a C). | | `octaves` | `2` | How many octaves (plus the last C). | | `pressed` | `()` | Notes to light, played from elsewhere. | | `color` | `"accent"` | Color of pressed keys. | | `key_width`, `height` | `24`, `110` | Size of a white key. | | `labels` | `True` | Writes the name of every C. | Events: `on_note_on` (`Note(note, velocity)`), `on_note_off` (the note number). ### VU meter > Level meters for Python audio apps — LED segments, a smooth gradient bar or an analog needle VU with ballistics, in decibels, with peak hold and any colors. Source: https://albakore.github.io/pyzed/audio/components/vu-meter/ (Image: LED, gradient and four-channel meters bouncing with the beat next to an analog needle VU meter) **In plain words:** A meter shows how loud a sound is right now. Green is fine, yellow is loud, red is about to distort. The little line that stays at the top for a moment is the latest peak. ```python rms, peak = engine.level() # the master output, 0..1 ui.audio.VuMeter(level=[rms, peak]) # one bar per value ``` #### Levels and scale `level` is one number or a list (one bar per channel), as linear amplitude from 0 to 1, which is what `node.level()` and `engine.level()` return. With `scale="db"` (the default) the meter shows it in decibels from `floor` (−60 dB) to 0 dB, as mixers do: quiet sounds still move it. `scale="linear"` shows the raw number (good for control signals). Each channel holds its peak for `hold` seconds, then lets it fall. #### Looks | `variant` | Looks like | | --- | --- | | `"led"` | Segments that light up (`segments` of them), dim when off. | | `"bar"` | A smooth bar with the colors as a gradient. | | `"analog"` | A needle VU meter (−20 to +3 VU) that moves with the ~300 ms ballistics of a real one. | `colors` is a palette name (`"vu"`, `"fl"`, `"neon"`, `"fire"`... see [Colors](https://albakore.github.io/pyzed/audio/components/colors/)) or a list of colors from quiet to loud. `vertical=False` lays it out horizontally. | Property | Default | What it does | | --- | --- | --- | | `level` | `0` | A level or a list of levels (0..1). | | `variant` | `"led"` | `"led"`, `"bar"` or `"analog"`. | | `vertical` | `True` | Direction of the bars. | | `length`, `thickness` | `160`, `10` | Size of each bar in pixels. | | `segments` | `24` | LEDs per bar. | | `scale`, `floor` | `"db"`, `-60` | How a level maps to the meter. | | `hold` | `1.2` | Seconds a peak is held. | | `colors` | `"vu"` | A palette name or a list of colors. | **Tip:** Meters need to be redrawn every frame. Add `pz.frames(lambda dt: None)` to your app's `subscriptions()` while the engine plays and read the levels in `view()`. ### Webcam viewer > A webcam viewer in Python — the live picture, a snapshot button that saves PNG files and the measured frame rate, with OpenCV, ffmpeg or a test pattern. Source: https://albakore.github.io/pyzed/audio/demos/webcam/ (Image: The live picture of a camera with a frame rate badge) A camera app in 50 lines: [`examples/audio/webcam.py`](https://github.com/albakore/pyzed/blob/main/examples/audio/webcam.py). ```bash python examples/audio/webcam.py # the default camera python examples/audio/webcam.py test # moving color bars, without a camera ``` **Snapshot** saves the current picture as a PNG next to the script, **Pause** freezes it. ```python from pyzed import camera, ui cam = camera.Camera(0, width=1280, height=720, mirror=True) cam.start() def view(self): return pz.column([ ui.camera.Webcam(camera=cam, width=720, height=405, radius=10), ui.button("Snapshot", lambda: cam.snapshot("photo.png")), ]) ``` The camera is read on its own thread; each new picture asks the window to redraw, so the app needs no timer. It uses OpenCV when it is installed (`pip install opencv-python`), else the `ffmpeg` program, else the test pattern. See [Webcam](https://albakore.github.io/pyzed/audio/components/webcam/) for every option. ### Waveform > Draw audio waveforms in Python — a music-app bar view with a played/unplayed split, an envelope, a glowing line, a playhead, selections and seeking. Source: https://albakore.github.io/pyzed/audio/components/waveform/ (Image: Three waveforms: rounded bars with a playhead, a mirrored envelope with a selection and a live glowing line) **In plain words:** A waveform is the picture of a sound over time: tall where it is loud, flat where it is quiet. Music players use it to show where you are in a song and let you click to jump. ```python left, right = engine.render(5.0) # five seconds rendered offline ui.audio.Waveform(samples=left, variant="bars", playhead=self.position, on_seek=lambda e: self.seek(e.value)) ``` #### Samples `samples` are numbers from −1 to 1: a whole file or the last buffer of a live signal (`node.scope(1024)`). A long list is reduced to one minimum/maximum pair per pixel column, so a minute of audio draws as fast as a short buffer. `gain` scales it up when the sound is quiet. #### Looks | `variant` | Looks like | | --- | --- | | `"mirror"` | The envelope, symmetric around the center line (the default). | | `"filled"` | The real shape, filled between its lowest and highest points. | | `"line"` | A line through every sample; `glow=True` adds a soft halo. Best for short buffers. | | `"bars"` | Rounded bars like a music app: the played part in `color`, the rest dimmed. | | `"dots"` | One dot per sample. | #### Playhead, selection and seeking - `playhead` (0..1) draws the playing position (with `"bars"`, it splits played and unplayed). - `selection=(0.2, 0.4)` highlights a range: a loop, a region to cut. - With `on_seek`, clicking or dragging on it reports the position (0..1) in `event.value`. | Property | Default | What it does | | --- | --- | --- | | `samples` | `()` | Values from −1 to 1. | | `width`, `height` | `360`, `96` | Size in pixels. | | `variant` | `"mirror"` | See the table above. | | `color`, `background` | `"accent"`, `"#0f1012"` | Colors. | | `gain`, `thickness`, `glow` | `1`, `1.5`, `False` | Scale, line width and halo. | | `grid` | `True` | Faint vertical lines and a center line. | | `playhead`, `selection` | `None` | Position (0..1) and range `(start, end)`. | | `bar` | `3` | Width of a bar with `variant="bars"`. | Event: `on_seek` (0..1). ### Oscilloscope > An oscilloscope for Python — several traces over a grid, a trigger that holds periodic waves still, XY Lissajous mode, phosphor persistence and glow. Source: https://albakore.github.io/pyzed/audio/components/scope/ (Image: A two-trace oscilloscope with fading persistence next to an XY scope drawing a rotating Lissajous figure) **In plain words:** An oscilloscope draws a signal the way a lab instrument does: a few milliseconds of it on a green grid. It is how you see the shape of a wave — sine, saw, square — while you patch. ```python ui.audio.Scope(traces=[osc.scope(1024), lfo.scope(1024)], width=320, height=180) ``` #### Traces and trigger `traces` is a list of signals (or a single list of numbers), each drawn in the next color of `colors`. With `trigger=True` each trace starts at the first point where the wave rises through zero, so a steady tone stands still instead of drifting. `window` is how many samples are shown. #### XY mode `xy=True` draws the first trace against the second: the first moves the dot left and right, the second up and down. Two sines make **Lissajous figures**; a stereo pair (left, right) shows how wide the stereo image is — a vertical line is mono, a cloud is wide. #### Phosphor `persistence=3` leaves fading copies of the last three frames, like the glow of an old CRT screen; `glow=True` adds a soft halo around the newest trace. | Property | Default | What it does | | --- | --- | --- | | `traces` | `()` | Signals to draw (−1..1). | | `width`, `height` | `320`, `180` | Size in pixels. | | `colors` | `"scope"` | A palette name or a list (one color per trace). | | `background` | `"#0b120e"` | The screen. | | `columns`, `rows` | `8`, `6` | Divisions of the grid. | | `gain` | `1` | Vertical zoom. | | `trigger`, `window` | `True`, `512` | Holds periodic waves still; samples shown. | | `xy` | `False` | Lissajous mode. | | `persistence`, `glow` | `0`, `True` | Fading copies and halo. | ### Spectrum analyzer > A real-time spectrum analyzer for Python — bars, LED blocks, lines, mirrored or radial, log-spaced bands, falloff, peak caps and color palettes. Source: https://albakore.github.io/pyzed/audio/components/spectrum/ (Image: Six spectrum analyzers dancing to the same song: bars, blocks, line, filled, mirror and radial) **In plain words:** A spectrum analyzer splits a sound into its pitches: bass on the left, treble on the right, and the taller the bar, the louder that pitch. It is the classic music visualizer. ```python class Visualizer(pz.App): def subscriptions(self): return [pz.frames(lambda dt: None)] # redraw every frame def view(self): return ui.audio.Spectrum(magnitudes=engine.spectrum(512), variant="blocks", colors="fl") ``` #### From FFT bins to bands `magnitudes` are decibels (−120 to 0), one per frequency bin — exactly what `node.spectrum()` and `engine.spectrum()` return. The analyzer groups them into `bands` bands spaced **logarithmically** (each band a little wider than the one before), the way the ear hears: an octave takes the same width at 100 Hz as at 5 kHz. Each band shows its loudest bin. `spacing` says how your bins are spaced: - `"log"` (the default): already log-spaced from 20 Hz to half the sample rate, like the engine's spectrum. They are only regrouped into `bands`. - `"linear"`: evenly spaced from 0 Hz, like the output of a plain FFT (`numpy.fft.rfft`). The analyzer works out which bins belong to each band from `sample_rate`. `min_db` and `max_db` are the bottom and top of the display. #### Motion Bars jump up at once and fall at `falloff` decibels per second (0 makes them follow the input exactly), so the display looks smooth even when the input flickers. With `peaks=True` a small cap stays on top of each bar and drops more slowly. The speed is measured in real time, so it is the same at 30 or 144 frames per second. #### Looks and colors | `variant` | Looks like | | --- | --- | | `"bars"` | Solid bars. | | `"blocks"` | LED segments, like a hi-fi. | | `"line"` | A glowing line through the band tops. | | `"filled"` | The same line with the area below filled. | | `"mirror"` | Bars centered vertically, for a symmetric look. | | `"radial"` | Bars around a circle, like a music visualizer. | `colors` is a palette (`"fl"`, `"neon"`, `"fire"`, `"ice"`, `"magma"`, `"viridis"`...) or a list of colors. `color_by="level"` paints each bar from quiet (bottom) to loud (top); `color_by="frequency"` gives each band its own color from bass to treble. `labels=True` marks 100 Hz, 1 kHz and 10 kHz. | Property | Default | What it does | | --- | --- | --- | | `magnitudes` | `()` | dB per frequency bin. | | `width`, `height` | `360`, `140` | Size in pixels. | | `variant` | `"bars"` | See the table above. | | `bands` | `48` | Number of bands. | | `spacing` | `"log"` | How `magnitudes` are spaced: `"log"` (the engine) or `"linear"` (a plain FFT). | | `min_db`, `max_db` | `-90`, `0` | Range shown. | | `colors`, `color_by` | `"fl"`, `"level"` | Palette and what it follows. | | `falloff`, `peaks` | `48`, `True` | Fall speed (dB/s) and peak caps. | | `sample_rate` | `48000` | For the labels and the band edges. | | `labels`, `gap`, `background` | `False`, `2`, `"#0f1012"` | | **Tip:** `ui.audio.bands(magnitudes, 16)` gives you the same log-spaced bands as numbers, to drive anything else with the music: the size of a logo, the color of the window, a particle system. ### Spectrogram > A scrolling spectrogram (waterfall) for Python — frequency over time in color, with magma, viridis or any palette, drawn fast enough for live audio. Source: https://albakore.github.io/pyzed/audio/components/spectrogram/ (Image: Two spectrograms scrolling left as a song plays: kicks as columns at the bottom, chords as bright lines) **In plain words:** A spectrogram is a spectrum analyzer with memory: time runs from left to right, pitch from bottom to top, and the color says how loud each pitch was. You can see a song's rhythm and chords in it. ```python ui.audio.Spectrogram(magnitudes=engine.spectrum(512), history=160, colors="magma") ``` Every render that receives a **new** `magnitudes` list adds a column on the right; redrawing for any other reason (a hover, another widget) does not, so the speed follows your updates. It keeps the last `history` columns. Loudness is quantized into `levels` colors and every cell of one color is drawn in a single shape, so even 160 columns × 64 bands stay cheap to draw. | Property | Default | What it does | | --- | --- | --- | | `magnitudes` | `()` | dB per frequency bin (like `node.spectrum()`). | | `width`, `height` | `360`, `140` | Size in pixels. | | `history` | `120` | Columns kept. | | `bands` | `48` | Rows (log-spaced bands). | | `min_db`, `max_db` | `-90`, `0` | Range of the colors. | | `colors`, `levels` | `"magma"`, `24` | Palette and how many color steps. | | `sample_rate`, `spacing` | `48000`, `"log"` | Like [`Spectrum`](https://albakore.github.io/pyzed/audio/components/spectrum/): how the bins are spaced. | ### Colors and behavior > Customize Pyzed's audio components — color palettes, theme tokens, variants, sizes and how meters move — and build visuals of your own on the same helpers. Source: https://albakore.github.io/pyzed/audio/components/colors/ (Image: One spectrum switching between every built-in palette) **In plain words:** Every audio component can change its colors and its look without touching its code: pick a palette by name, pass your own colors, or let it follow the app's theme. #### Colors Any `color=` takes a **theme token** (`"accent"`, `"success"`, `"warning"`, `"error"`...) or a hex color (`"#ff9f3d"`). Tokens follow the theme: switch to a light theme and the knobs follow. Meters and analyzers take `colors=`: a palette name or a list of colors, from quiet to loud (or from bass to treble with `color_by="frequency"`). | Palette | Colors | | --- | --- | | `"fl"` | Green → yellow → orange → red, like FL Studio's meters. | | `"vu"` | Mostly green, yellow near the top, red at the very top. | | `"neon"` | Cyan → violet → pink. | | `"fire"` | Dark purple → red → orange → yellow → white. | | `"ice"` | Navy → blue → light cyan. | | `"magma"`, `"viridis"` | The scientific color maps, best for spectrograms. | | `"mono"` | Gray to white. | | `"scope"` | The oscilloscope's trace colors. | Add your own to `ui.audio.PALETTES` and use it by name everywhere: ```python ui.audio.PALETTES["sunset"] = ["#2d1b69", "#f72585", "#ffbe0b"] ui.audio.Spectrum(magnitudes=mags, colors="sunset") ``` #### Behavior | What | Where | | --- | --- | | Look | `variant=` on every component ([knob](https://albakore.github.io/pyzed/audio/components/knob/), [fader](https://albakore.github.io/pyzed/audio/components/fader/), [meter](https://albakore.github.io/pyzed/audio/components/vu-meter/), [spectrum](https://albakore.github.io/pyzed/audio/components/spectrum/)...). | | How far a drag turns a control | `travel=` on knobs, `length=` on faders; Shift is always ten times finer. | | How a value maps to the control | `curve="log"`, `step=`, `options=`, `bipolar=True`. | | How meters move | `hold=` (peak hold), `falloff=` and `peaks=` (analyzers), `variant="analog"` ballistics. | | How a scope reads | `trigger=`, `window=`, `persistence=`, `xy=`. | All of them are `pz.Component`s, so `style=`, `class_=`, stylesheets and `animate=` work as on any other component (see [Components](https://albakore.github.io/pyzed/guides/components/)). #### Building your own The helpers are public: `ui.audio.bands()` (FFT bins to log bands), `ui.audio.format_value()` (`1.2 kHz`, `-6.0 dB`), `ui.audio.note_name()` and `ui.audio.PALETTES`. Every component also has a `shapes()` method returning its drawing as `pyzed.draw` shapes, so you can put a knob or a meter inside your own `pz.canvas`. **Tip:** Audio displays move every frame. Return `pz.frames(lambda dt: None)` from `subscriptions()` while something plays, and nothing at all when it stops, so the app sleeps when idle. ### Webcam > Show a live webcam in a Python desktop app with Pyzed — OpenCV or ffmpeg on Windows, macOS and Linux, snapshots, raw RGBA frames and a test pattern. Source: https://albakore.github.io/pyzed/audio/components/webcam/ (Image: A webcam view with a recording badge showing the moving test pattern) **In plain words:** Put a camera's live picture in your window with one line. It opens the camera when it appears and closes it when it goes away. ```python ui.camera.Webcam(device=0, width=480, height=360, mirror=True) ``` #### How frames arrive The camera is read in a background thread. Every new frame asks the window to redraw, so the view stays live without a timer and without slowing the rest of the app. Two `Webcam`s of the same device share one capture. The capture uses whichever of these is available, in this order (`backend="auto"`): | Backend | Needs | Notes | | --- | --- | --- | | `"opencv"` | `pip install opencv-python` | The most compatible on every system. | | `"ffmpeg"` | the `ffmpeg` program | Video4Linux on Linux, AVFoundation on macOS, DirectShow on Windows. | | `"test"` | nothing | Moving color bars: for screenshots, tests and machines without a camera. | #### Using the frames `pyzed.camera.Camera` gives you the frames to process (detect faces, measure brightness, record): ```python from pyzed import camera cam = camera.Camera(0, width=640, height=480, mirror=True) cam.start() frame = cam.read() # waits for the next Frame(width, height, pixels, index, time) cam.snapshot("photo.png") # saved without Pillow cam.on_frame.append(lambda f: print(f.index)) # runs in the camera thread cam.stop() ``` `frame.pixels` are RGBA bytes (`width * height * 4`): `numpy.frombuffer(frame.pixels, "uint8")` turns them into an array, and `pz.image(frame.pixels, size=(frame.width, frame.height))` draws them. `camera.devices()` lists the cameras found. | Property | Default | What it does | | --- | --- | --- | | `device` | `0` | Camera index, a path (`"/dev/video2"`) or, on Windows with ffmpeg, its name. | | `camera` | `None` | A `Camera` of yours to show instead. | | `width`, `height` | `320`, `240` | Size of the view. | | `capture_width`, `capture_height`, `fps` | `640`, `480`, `30` | What to ask the camera for. | | `mirror` | `True` | Flip it like a mirror. | | `fit` | `"cover"` | `"cover"` crops to fill, `"contain"` shows it all. | | `radius` | `8` | Rounded corners. | | `overlay` | `True` | A badge with a recording dot and the frame rate. | | `paused` | `False` | Freezes the last frame. | | `backend` | `"auto"` | See above. | **Caution:** On macOS the first run asks for permission to use the camera; the program that runs Python (Terminal, your editor) is the one that gets it. ## Integrations ### Integrations overview > Use Pyzed as the desktop frontend for FastAPI, Django, SQLAlchemy, SQLModel, httpx, Pydantic and aiosqlite, with demos that follow each library's official tutorial. Source: https://albakore.github.io/pyzed/integrations/overview/ :::note[Runnable demos] Every integration has a complete demo in `examples/integrations` that runs with uv, pip or plain python. More libraries will be added over time. ::: Pyzed is only the interface: everything else in your app is ordinary Python. That means the libraries you already use for web backends, databases and HTTP work unchanged in a desktop app. This section shows how to wire the most common ones. Each demo starts from the library's own official tutorial (FastAPI's SQL tutorial, Django's polls app, SQLAlchemy's Quick Start, SQLModel's heroes) and adds a Pyzed window to it. When a backend has to run alongside the window, [Architectures](https://albakore.github.io/pyzed/integrations/architectures/) explains the four ways to combine them, with a diagram for each: same process, client and server, embedded server and in-process API. | Demo | Libraries | What it shows | Tutorial | | --- | --- | --- | --- | | [`fastapi_heroes`](https://github.com/albakore/pyzed/tree/main/examples/integrations/fastapi_heroes) | FastAPI, SQLModel, httpx | FastAPI's SQL tutorial API and a desktop client for it, run three ways: separate server, embedded server and in-process. | [FastAPI](https://albakore.github.io/pyzed/integrations/fastapi/) | | [`django_polls`](https://github.com/albakore/pyzed/tree/main/examples/integrations/django_polls) | Django, httpx | Django's polls tutorial, used from a window through the ORM in the same process or over HTTP while runserver runs. | [Django](https://albakore.github.io/pyzed/integrations/django/) | | [`sqlalchemy_quickstart`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlalchemy_quickstart) | SQLAlchemy 2.0, aiosqlite | The ORM Quick Start's users and addresses, with a sync Session in threads or an AsyncSession on the loop. | [SQLAlchemy](https://albakore.github.io/pyzed/integrations/sqlalchemy/) | | [`sqlalchemy_contacts`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlalchemy_contacts) | SQLAlchemy 2.0 | 20,000 rows in a virtual table, searched and sorted in SQL from a worker thread. | [SQLAlchemy](https://albakore.github.io/pyzed/integrations/sqlalchemy/#many-rows-the-contacts-demo) | | [`sqlmodel_heroes`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlmodel_heroes) | SQLModel | SQLModel's tutorial: teams, heroes, where() filters, relationships and a validated form. | [SQLModel](https://albakore.github.io/pyzed/integrations/sqlmodel/) | | [`sqlmodel_inventory`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlmodel_inventory) | SQLModel, Pydantic | One model validates the form and defines the table. | [SQLModel](https://albakore.github.io/pyzed/integrations/sqlmodel/#one-model-for-the-form-and-the-table-the-inventory-demo) | | [`httpx_pypi`](https://github.com/albakore/pyzed/tree/main/examples/integrations/httpx_pypi) | httpx, Pydantic | Concurrent requests streamed into the window and parsed into models. | [httpx](https://albakore.github.io/pyzed/integrations/httpx/) | | [`httpx_download`](https://github.com/albakore/pyzed/tree/main/examples/integrations/httpx_download) | httpx | Streaming downloads with progress bars and cancel. | [httpx](https://albakore.github.io/pyzed/integrations/httpx/#streaming-downloads-with-progress) | | [`pydantic_signup`](https://github.com/albakore/pyzed/tree/main/examples/integrations/pydantic_signup) | Pydantic | A sign-up form validated by a model, saved and loaded as JSON. | [Pydantic](https://albakore.github.io/pyzed/integrations/pydantic/) | | [`aiosqlite_journal`](https://github.com/albakore/pyzed/tree/main/examples/integrations/aiosqlite_journal) | aiosqlite | An async SQLite connection on Pyzed's event loop. | [Other libraries](https://albakore.github.io/pyzed/integrations/other-libraries/#aiosqlite) | #### Running a demo Each demo is a small [uv](https://docs.astral.sh/uv/) project. From its folder: `uv run` creates the virtual environment and installs the dependencies listed in the demo's `pyproject.toml` the first time. Inside the repository the demos use your checkout of Pyzed (the `[tool.uv.sources]` table); to start your own app from one, copy the folder and delete those two lines so `pyzed-ui` comes from PyPI. #### Where your code runs A Pyzed app has three places where code runs. Knowing which one a library call lands in is all it takes to integrate anything: ```mermaid flowchart LR UI["UI thread
view() · update() · callbacks"] Loop["asyncio loop (one thread)
coroutines · async generators"] Threads["worker threads
Command.perform(function)"] UI -- "return a coroutine" --> Loop UI -- "Command.perform(fn, then)" --> Threads Loop -- "result → update()" --> UI Threads -- "then(result)" --> UI ``` | The library is… | Call it with | Examples | | --- | --- | --- | | **async** (`await ...`) | Return a coroutine or an async generator from `update`, a callback or `on_start`. Its result goes to `update`. | httpx `AsyncClient`, aiosqlite, Django's async ORM, SQLAlchemy `AsyncSession`, websockets, redis.asyncio | | **blocking** | `Command.perform(function, then)`: `function` runs in a thread and `then(result)` on the UI thread. | SQLAlchemy and SQLModel sessions, requests, pandas, file I/O | | **a server** | Run it apart (`fastapi dev`, `manage.py runserver`) or in a daemon thread before `pz.run`. See [Architectures](https://albakore.github.io/pyzed/integrations/architectures/). | FastAPI, Django, Starlette | | **a stream** | `pz.listen(async_generator)` in `subscriptions()`. | websockets, server-sent events, Redis pub/sub | Three rules keep it safe: 1. **Change `self` on the UI thread.** Coroutines and threads *return* results; `update` or `then` store them. Reading `self` from a coroutine is fine. 2. **Never block the UI thread.** A query that takes 50 ms freezes the window for 50 ms. Use `Command.perform` or an async library. 3. **Ignore stale answers.** When the user types a search, results can come back out of order. Send the query along with the request and drop answers for an older one (every demo does this in its `loaded` handler). All coroutines share **one** asyncio loop (`pz.asyncio_loop()`), so objects tied to a loop, such as an `httpx.AsyncClient` or an `aiosqlite` connection, can be created once and reused. #### Validation errors on each field Most backends report invalid input as a list of field errors. `pz.validation_errors(error)` turns any of them into `{field: message}`, and `ui.field(..., error=)` shows the message under the field title, in the theme's error color: ```python from pydantic import ValidationError def save(self): try: item = ItemIn.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) return ... def view(self): return ui.field("Price", ui.input(self.draft["price"], self.set_price), error=self.errors.get("price")) ``` It understands: - Pydantic and SQLModel `ValidationError` (custom `ValueError` messages lose their `Value error,` prefix), - the JSON of a FastAPI **422** response, `{"detail": [...]}`, without the `body`/`query` part of each location, and a plain `{"detail": "Not found"}`, - Django's `ValidationError` from `full_clean()` (`__all__` errors use the key `""`). Nested fields are joined with dots (`"address.city"`), and errors that belong to no field use the key `""`, which the demos show in a `ui.callout`. #### Selects with objects `ui.select` accepts any objects as options and shows them with `str()`: a Django model, an enum, a dataclass. `on_select` receives the object back, not its label: ```python ui.select(self.authors, self.draft["author"], lambda author: self.draft.update(author=author)) ``` ### Architectures > The ways a Pyzed desktop app can live with FastAPI, Django, SQLAlchemy, SQLModel and other backends, with a diagram and a runnable demo for each. Source: https://albakore.github.io/pyzed/integrations/architectures/ A Pyzed window is a Python program, so a backend library can sit next to it in four different ways. The choice depends on one question: **who else needs the data?** | Architecture | Who uses the data | Network | Demos | | --- | --- | --- | --- | | [1. Same process](#1-same-process) | Only this desktop app | None | SQLAlchemy, SQLModel, Django ORM, aiosqlite, Pydantic | | [2. Client and server](#2-client-and-server) | A website, a phone app, other users | HTTP to a server | FastAPI `client.py`, Django `client.py`, httpx | | [3. Embedded server](#3-embedded-server) | This app and other programs on the same computer | Local port | FastAPI `embedded.py` | | [4. In-process API](#4-in-process-api) | Only this app, but the code is an API | None | FastAPI `in_process.py` | All four can share the same code: the models and the API don't know whether a window, a browser or a test calls them. That is what lets an app start with architecture 1 and move to 2 later. #### 1. Same process The window imports the library and calls it directly. There is no server and nothing listens on the network. This is the default for a desktop app with its own database. ```mermaid flowchart LR subgraph app["Your program (uv run main.py)"] W["Pyzed window
view · update"] T["Worker threads
Command.perform"] L["asyncio loop
coroutines"] M["Models and queries
SQLAlchemy · SQLModel · Django ORM"] W --> T --> M W --> L --> M end M --> DB[("SQLite / PostgreSQL")] ``` - **Blocking libraries** (a SQLAlchemy `Session`, SQLModel, the Django ORM) run in a worker thread with `Command.perform(function, then)`. - **Async libraries** (`AsyncSession`, aiosqlite) run as coroutines on Pyzed's loop. - The database can still be shared: the [Django demo](https://albakore.github.io/pyzed/integrations/django/)'s `desktop.py` and `manage.py runserver` use the same `db.sqlite3` at the same time. Demos: [`sqlalchemy_quickstart`](https://albakore.github.io/pyzed/integrations/sqlalchemy/) (both backends), [`sqlmodel_heroes`](https://albakore.github.io/pyzed/integrations/sqlmodel/), [`django_polls`](https://albakore.github.io/pyzed/integrations/django/) `desktop.py`, [`aiosqlite_journal`](https://albakore.github.io/pyzed/integrations/other-libraries/#aiosqlite), [`pydantic_signup`](https://albakore.github.io/pyzed/integrations/pydantic/). #### 2. Client and server The backend is a separate program, started with its own tool (`fastapi dev`, `manage.py runserver`, a deployment on a server). The window is one more client and only speaks HTTP, usually with httpx. ```mermaid flowchart LR subgraph server["Server process (fastapi dev main.py · manage.py runserver)"] API["FastAPI / Django
routes and views"] --> DB[("Database")] end subgraph desktop["Desktop process (uv run client.py)"] W["Pyzed window"] --> H["httpx.AsyncClient"] end H -- "HTTP / JSON" --> API B["Browser · phone app"] -- "HTTP" --> API ``` - Use it when the data must be shared between users or devices, or the API already exists. - The window needs neither the database driver nor the backend framework installed. - Errors arrive as HTTP statuses: a FastAPI 422 goes straight into `pz.validation_errors`. Demos: [`fastapi_heroes`](https://albakore.github.io/pyzed/integrations/fastapi/) `client.py`, [`django_polls`](https://albakore.github.io/pyzed/integrations/django/) `client.py`, [`httpx_pypi`](https://albakore.github.io/pyzed/integrations/httpx/) (a public API). #### 3. Embedded server One program starts the API in a background thread with Uvicorn and then opens the window. The API is available on a local port while the window is open, for a browser, a script or another app. ```mermaid flowchart LR subgraph app["Your program (uv run embedded.py)"] direction LR W["Pyzed window
main thread"] -- "httpx" --> U U["Uvicorn in a daemon thread
FastAPI app"] --> DB[("Database")] end B["Browser at /docs
other local programs"] -- "http://127.0.0.1:8000" --> U ``` - Use it for a tool that also exposes an API or a web page, such as a local dashboard or a device that receives webhooks. - The server stops with the window, because its thread is a daemon. - Bind to `127.0.0.1` unless other computers must connect. Demo: [`fastapi_heroes`](https://albakore.github.io/pyzed/integrations/fastapi/#3-embedded-server) `embedded.py`. #### 4. In-process API httpx can send requests straight to an ASGI app with `httpx.ASGITransport`, without a server or a port. The code is written as an API and a client, but runs as one offline program. ```mermaid flowchart LR subgraph app["Your program (uv run in_process.py)"] W["Pyzed window"] --> H["httpx.AsyncClient
transport=ASGITransport(app)"] H -- "function call, no network" --> API["FastAPI app"] API --> DB[("Database")] end ``` - Use it to ship the same API as a desktop app that works offline, or to test the client. - `ASGITransport` doesn't run startup or lifespan events: call your setup code yourself. Demo: [`fastapi_heroes`](https://albakore.github.io/pyzed/integrations/fastapi/#4-in-process) `in_process.py`. #### Which one fits each library | Library | Same process | Client and server | Embedded server | In-process API | | --- | --- | --- | --- | --- | | FastAPI | Import its models only | ✅ `fastapi dev` / `fastapi run` | ✅ Uvicorn in a thread | ✅ `ASGITransport` | | Django | ✅ ORM after `django.setup()` | ✅ `manage.py runserver` | Not recommended: run it apart | Possible through its ASGI app | | SQLAlchemy | ✅ `Session` or `AsyncSession` | Behind your API | — | — | | SQLModel | ✅ `Session` | Behind FastAPI | — | — | | httpx | — | ✅ the client | ✅ the client | ✅ the transport | | Pydantic | ✅ validates the form | ✅ parses the responses | ✅ | ✅ | | aiosqlite | ✅ on Pyzed's loop | — | — | — | #### Structure of a project Keep the backend free of Pyzed and the window free of SQL. The demos all follow this layout, which lets the same backend serve a window, an API and tests: ```text myapp/ ├── pyproject.toml ├── models.py # SQLModel / SQLAlchemy / Django models, Pydantic schemas ├── db.py # the queries, one function per action, no Pyzed imports ├── main.py # FastAPI app or Django project (when there is a server) └── window.py # the Pyzed App: calls db.py or the API ``` ### FastAPI > Build FastAPI's SQL tutorial API as written in its documentation, then use it from a Pyzed desktop window as a separate server, an embedded server or in-process. Source: https://albakore.github.io/pyzed/integrations/fastapi/ The API in this demo is FastAPI's own [SQL (Relational) Databases](https://fastapi.tiangolo.com/tutorial/sql-databases/) tutorial, copied without changes: heroes stored with SQLModel, with separate models for creating, reading and updating. The Pyzed window is a desktop client for it, and the same window runs in the three [architectures](https://albakore.github.io/pyzed/integrations/architectures/) that involve an API. (Image: A desktop window listing heroes from the FastAPI API, with an edit form) ```text fastapi_heroes/ ├── main.py # the API, exactly as in FastAPI's tutorial ├── heroes_window.py # the Pyzed window, which only speaks HTTP (httpx) ├── client.py # 2. client and server ├── embedded.py # 3. embedded server └── in_process.py # 4. in process ``` #### 1. The API, as FastAPI teaches it 1. **Install FastAPI with its standard extras**, which bring the `fastapi` CLI and Uvicorn. The demo's `pyproject.toml` already lists them: 2. **Write the models.** `HeroBase` holds the shared fields, `Hero` is the table, `HeroPublic` is what the API returns (never the `secret_name`), and `HeroCreate` and `HeroUpdate` describe the request bodies: ```python class HeroBase(SQLModel): name: str = Field(index=True) age: int | None = Field(default=None, index=True) class Hero(HeroBase, table=True): id: int | None = Field(default=None, primary_key=True) secret_name: str class HeroPublic(HeroBase): id: int class HeroCreate(HeroBase): secret_name: str class HeroUpdate(HeroBase): name: str | None = None age: int | None = None secret_name: str | None = None ``` 3. **Declare a session dependency** so every request gets its own `Session`: ```python def get_session(): with Session(engine) as session: yield session SessionDep = Annotated[Session, Depends(get_session)] ``` 4. **Write the path operations.** `response_model` filters the output and FastAPI validates the input, returning **422** with one entry per invalid field: ```python @app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate, session: SessionDep): db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero @app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate, session: SessionDep): hero_db = session.get(Hero, hero_id) if not hero_db: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) hero_db.sqlmodel_update(hero_data) ... ``` 5. **Run it with the FastAPI CLI** and open the interactive docs at [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs): `fastapi dev` reloads on every change; `fastapi run main.py` is the production command. #### The desktop client `heroes_window.py` is a `pz.App` that talks to the API with an `httpx.AsyncClient`. It receives a function that creates the client, so the same window works with any of the architectures below. 1. **Send requests from coroutines.** Returning a coroutine from a callback runs it on Pyzed's asyncio loop; its return value reaches `update`. The client is created on first use, so it belongs to that loop: ```python async def api(self, method, url, **kwargs): if self.client is None: self.client = self.make_client() try: return await self.client.request(method, url, **kwargs) except httpx.HTTPError as error: return f"Can't reach the API: {error}" async def load(self): response = await self.api("GET", "/heroes/", params={"limit": 100}) if isinstance(response, str): return ("offline", response) return ("loaded", response.json()) ``` 2. **Create with POST and edit with PATCH.** Only the fields the user filled in are sent, which is what `HeroUpdate` and `exclude_unset=True` expect: ```python async def save(self): body = {key: value for key, value in self.draft.items() if value != ""} if self.selected is None: response = await self.api("POST", "/heroes/", json=body) else: response = await self.api("PATCH", f"/heroes/{self.selected['id']}", json=body) if response.status_code == 422: return ("invalid", response.json()) ... ``` 3. **Show FastAPI's 422 on each field.** `pz.validation_errors` reads `{"detail": [...]}` and returns `{"age": "Input should be a valid integer, ..."}`: ```python def update(self, message): kind, value = message if kind == "invalid": self.errors = pz.validation_errors(value) ``` #### 2. Client and server The API and the window are two programs. This is how a deployed API is used, and how you work on both at once with reload: `client.py`: ```py """Architecture 1, client and server: the API runs on its own and the window talks to it over HTTP. uv run fastapi dev main.py # terminal 1: the API (FastAPI's own CLI) uv run client.py # terminal 2: the desktop client uv run client.py https://api.example.com # or any other deployment of main.py The usual setup when the API also serves a website or a mobile app, or runs on a server. """ import sys import httpx import pyzed as pz from heroes_window import HeroesWindow API = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8000" if __name__ == "__main__": pz.run(HeroesWindow(lambda: httpx.AsyncClient(base_url=API, timeout=5), mode=f"server at {API}", docs_url=f"{API}/docs")) ``` #### 3. Embedded server One command starts the API in a background thread with Uvicorn, the server `fastapi run` uses, and opens the window. While the window is open, [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) works in a browser too. `embedded.py`: ```py """Architecture 2, embedded server: one program starts the API and opens the window. uv run embedded.py The API runs with Uvicorn (the server `fastapi run` and `fastapi dev` use) in a background thread, so other programs on this computer, a browser at /docs for instance, can use it while the window is open. It stops when the window closes. """ import threading import httpx import uvicorn import pyzed as pz from heroes_window import HeroesWindow from main import app HOST, PORT = "127.0.0.1", 8000 def start_api() -> None: # What `fastapi run main.py` does, but in a thread of this process. server = uvicorn.Server(uvicorn.Config(app, host=HOST, port=PORT, log_level="warning")) threading.Thread(target=server.run, daemon=True).start() if __name__ == "__main__": start_api() url = f"http://{HOST}:{PORT}" pz.run(HeroesWindow(lambda: httpx.AsyncClient(base_url=url, timeout=5), mode=f"embedded at {url}", docs_url=f"{url}/docs")) ``` #### 4. In process No server and no port: `httpx.ASGITransport` hands each request to the FastAPI app as a function call. The program works offline, and the API code stays the same. `in_process.py`: ```py """Architecture 3, in process: the window calls the FastAPI app directly, with no server or port. uv run in_process.py httpx can send requests straight to an ASGI app (httpx.ASGITransport), so the same API code and the same client code run inside one desktop program that works offline. Nothing listens on the network. ASGITransport doesn't send startup events, so the tables are created here. """ import httpx import pyzed as pz from heroes_window import HeroesWindow from main import app, create_db_and_tables if __name__ == "__main__": create_db_and_tables() transport = httpx.ASGITransport(app=app) pz.run(HeroesWindow(lambda: httpx.AsyncClient(transport=transport, base_url="http://heroes"), mode="in process")) ``` **Caution:** `ASGITransport` doesn't send startup or lifespan events, so code in `@app.on_event("startup")` or a `lifespan` function doesn't run. `in_process.py` calls `create_db_and_tables()` itself. #### Full code **main.py (API):** ```py """The FastAPI app, exactly as FastAPI's "SQL (Relational) Databases" tutorial writes it: https://fastapi.tiangolo.com/tutorial/sql-databases/ uv run fastapi dev main.py # http://127.0.0.1:8000, interactive docs at /docs Nothing in this file knows about Pyzed. The three desktop entry points (client.py, embedded.py, in_process.py) reuse it unchanged. """ from typing import Annotated from fastapi import Depends, FastAPI, HTTPException, Query from sqlmodel import Field, Session, SQLModel, create_engine, select class HeroBase(SQLModel): name: str = Field(index=True) age: int | None = Field(default=None, index=True) class Hero(HeroBase, table=True): id: int | None = Field(default=None, primary_key=True) secret_name: str class HeroPublic(HeroBase): id: int class HeroCreate(HeroBase): secret_name: str class HeroUpdate(HeroBase): name: str | None = None age: int | None = None secret_name: str | None = None sqlite_file_name = "database.db" sqlite_url = f"sqlite:///{sqlite_file_name}" connect_args = {"check_same_thread": False} engine = create_engine(sqlite_url, connect_args=connect_args) def create_db_and_tables(): SQLModel.metadata.create_all(engine) def get_session(): with Session(engine) as session: yield session SessionDep = Annotated[Session, Depends(get_session)] app = FastAPI() @app.on_event("startup") def on_startup(): create_db_and_tables() @app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate, session: SessionDep): db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero @app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( session: SessionDep, offset: int = 0, limit: Annotated[int, Query(le=100)] = 100, ): heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes @app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int, session: SessionDep): hero = session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") return hero @app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate, session: SessionDep): hero_db = session.get(Hero, hero_id) if not hero_db: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) hero_db.sqlmodel_update(hero_data) session.add(hero_db) session.commit() session.refresh(hero_db) return hero_db @app.delete("/heroes/{hero_id}") def delete_hero(hero_id: int, session: SessionDep): hero = session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") session.delete(hero) session.commit() return {"ok": True} ``` **heroes_window.py:** ```py """The Pyzed window: a desktop client for the heroes API in main.py, written with httpx. It only knows the API's URLs and JSON, so it works the same whether the API runs in another process, in a thread of this one or in-process without a network (see client.py, embedded.py and in_process.py). """ from __future__ import annotations import webbrowser from typing import Callable import httpx import pyzed as pz from pyzed import ui EMPTY = {"name": "", "secret_name": "", "age": ""} class HeroesWindow(pz.App): title = "Heroes · FastAPI" size = (980, 620) def __init__(self, make_client: Callable[[], httpx.AsyncClient], mode: str, docs_url: str | None = None): self.make_client = make_client # called once, inside a coroutine, so the client lives on Pyzed's loop self.client: httpx.AsyncClient | None = None self.mode, self.docs_url = mode, docs_url self.heroes: list[dict] = [] self.selected: dict | None = None # the hero being edited, or None for a new one self.draft = dict(EMPTY) self.errors: dict[str, str] = {} self.status = "Connecting…" # -- the API, with httpx -------------------------------------------------------------- async def api(self, method: str, url: str, **kwargs) -> httpx.Response | str: """Sends a request; returns the response, or the error text when the API can't be reached.""" if self.client is None: self.client = self.make_client() try: return await self.client.request(method, url, **kwargs) except httpx.HTTPError as error: return f"Can't reach the API: {error or type(error).__name__}" async def load(self): response = await self.api("GET", "/heroes/", params={"limit": 100}) # GET /heroes/?offset=0&limit=100 if isinstance(response, str): return ("offline", response) return ("loaded", response.json()) async def save(self): body = {key: value for key, value in self.draft.items() if value != ""} # leave empty fields out if self.selected is None: response = await self.api("POST", "/heroes/", json=body) else: response = await self.api("PATCH", f"/heroes/{self.selected['id']}", json=body) if isinstance(response, str): return ("offline", response) if response.status_code == 422: # FastAPI's validation error, one entry per field return ("invalid", response.json()) response.raise_for_status() return ("saved", response.json()) async def delete(self): response = await self.api("DELETE", f"/heroes/{self.selected['id']}") if isinstance(response, str): return ("offline", response) return ("deleted", self.selected["id"]) # -- state ----------------------------------------------------------------------------- def on_start(self): return self.load() def pick(self, hero: dict | None): self.selected, self.errors = hero, {} self.draft = dict(EMPTY) if hero is None else { "name": hero["name"], "secret_name": "", "age": "" if hero["age"] is None else str(hero["age"])} def edit(self, key: str): return lambda value: self.draft.__setitem__(key, value) def update(self, message): kind, value = message if kind == "loaded": self.heroes, self.status = value, f"{len(value)} heroes · {self.mode}" elif kind == "offline": self.status = value elif kind == "invalid": self.errors = pz.validation_errors(value) elif kind == "saved": self.pick(value) return self.load() elif kind == "deleted": self.pick(None) return self.load() # -- view -------------------------------------------------------------------------------- def form(self): e = self.errors.get new = self.selected is None return pz.column([ ui.heading("New hero" if new else f"Hero #{self.selected['id']}", level=3), ui.field("Name", ui.input(self.draft["name"], self.edit("name"), placeholder="Deadpond", width=220), error=e("name")), ui.field("Secret name", ui.input(self.draft["secret_name"], self.edit("secret_name"), placeholder="Dive Wilson" if new else "unchanged", width=220), description="Never sent back by the API", error=e("secret_name")), ui.field("Age", ui.input(self.draft["age"], self.edit("age"), placeholder="optional", width=220), error=e("age")), pz.row([ None if new else ui.button("Delete", self.delete, icon="trash-2", variant="ghost", tint="error"), pz.hspace(), ui.button("New", lambda: self.pick(None), variant="ghost") if not new else None, ui.button("Create" if new else "Save", self.save, icon="check", variant="solid"), ], spacing=8), ], spacing=14, padding=16, width=380) def view(self): t = self.resolved_theme() columns = [ui.Column("ID", "id", width=50), ui.Column("Name", "name"), ui.Column("Age", lambda h: "—" if h["age"] is None else h["age"], width=70, align="end")] table = ui.table(columns, self.heroes, on_row_press=self.pick, selected=self.selected, height="fill") offline = self.status.startswith("Can't") status = ui.status_bar( [ui.status_item(self.status, icon="cloud-off" if offline else "server", icon_color="error" if offline else "success")], [ui.status_item("API docs", lambda: webbrowser.open(self.docs_url), icon="book-open")] if self.docs_url else [], ) body = pz.row([pz.container(table, padding=16, width="fill", height="fill"), pz.container(self.form(), height="fill", bg=t.panel)]) return pz.container(pz.column([body, status]), width="fill", height="fill", bg=t.editor) ``` ### Django > Follow Django's polls tutorial, then use the project from a Pyzed desktop window through the ORM in the same process or as an HTTP client of runserver. Source: https://albakore.github.io/pyzed/integrations/django/ The demo is the project from [Django's tutorial](https://docs.djangoproject.com/en/stable/intro/tutorial01/), `mysite` with its `polls` app, built step by step as the tutorial does: models, migrations, the admin, generic views and templates. A Pyzed window then votes on the polls in two ways, which correspond to two [architectures](https://albakore.github.io/pyzed/integrations/architectures/): - **`desktop.py`**: the window imports the models and uses the ORM directly (same process). - **`client.py`**: the site runs with `runserver` and the window calls its JSON views (client and server). (Image: A desktop window with Django polls, results as bars and vote buttons) #### The Django project, as the tutorial builds it 1. **Create the project and the app** (already done in the demo folder): and add `"polls.apps.PollsConfig"` to `INSTALLED_APPS` in `mysite/settings.py`. 2. **Write the models** in `polls/models.py` (tutorial part 2): ```python class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) ``` 3. **Create the tables and an admin user:** 4. **Register the admin** (part 7), with the choices inline, so you can add polls at [http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/). 5. **Write the views and URLs** (parts 3 and 4): `IndexView`, `DetailView`, `ResultsView` and `vote`, which uses an `F()` expression so concurrent votes don't overwrite each other. The demo adds two small JSON views for the desktop client, `api/questions/` and `api/questions//vote/`. 6. **Run the site:** The polls are at [http://127.0.0.1:8000/polls/](http://127.0.0.1:8000/polls/). #### Way 1: the ORM in the same process `desktop.py` sets up Django the way `manage.py` does and then imports the models. No web server is needed; the window reads and writes `db.sqlite3` through the ORM. ```mermaid flowchart LR subgraph p["uv run desktop.py"] W["PollsWindow"] -- "Command.perform" --> F["load · vote · create
worker thread"] F --> ORM["Django ORM
polls.models"] end ORM --> DB[("db.sqlite3")] S["manage.py runserver
(optional, at the same time)"] --> DB ``` 1. **Call `django.setup()` before importing models:** ```python os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") django.setup() from polls.models import Question ``` 2. **Run ORM queries in worker threads.** They block, so `Command.perform` runs them off the UI thread. Django opens one database connection per thread; closing them afterwards keeps the thread pool from holding connections: ```python def in_thread(function): @functools.wraps(function) def wrapper(*args, **kwargs): try: return function(*args, **kwargs) finally: connections.close_all() return wrapper @in_thread def vote(question_id, choice_id): choice = Question.objects.get(pk=question_id).choice_set.get(pk=choice_id) choice.votes = F("votes") + 1 choice.save() ``` 3. **Validate with the model's own rules.** `full_clean()` checks `max_length`, required fields and custom validators; `pz.validation_errors` turns Django's `ValidationError` into `{field: message}` for `ui.field`: ```python question = Question(question_text=draft["question_text"], pub_date=timezone.now()) try: question.full_clean() except ValidationError as error: return pz.validation_errors(error) with transaction.atomic(): question.save() for text in draft["choices"]: question.choice_set.create(choice_text=text, votes=0) ``` **Tip:** `runserver` can run at the same time on the same database. A vote cast in the window appears on `/polls/` and in the admin, and a question added in the admin appears in the window after a reload. #### Way 2: an HTTP client of the site `client.py` doesn't import Django at all. It talks to the site's JSON views with httpx, so the site can run on this computer or on a server: ```mermaid flowchart LR W["PollsWindow
uv run client.py"] -- "httpx · JSON" --> V["polls/views.py
api_questions · api_vote"] B["Browser
/polls/ · /admin/"] --> V2["Tutorial views
and admin"] subgraph s["uv run manage.py runserver"] V --> DB[("db.sqlite3")] V2 --> DB end ``` ```python async def vote(question_id, choice_id): async with httpx.AsyncClient(base_url=SITE, timeout=5) as client: response = await client.post(f"/polls/api/questions/{question_id}/vote/", json={"choice": choice_id}) response.raise_for_status() return response.json() ``` **Caution:** The JSON vote view is marked `@csrf_exempt` because a desktop app has no CSRF cookie. For a real API, authenticate the client with tokens, for example with [Django REST framework](https://www.django-rest-framework.org/). #### One window, two backends `PollsWindow` receives `load`, `vote` and optionally `create` as plain functions. `desktop.py` passes regular functions (run in threads) and `client.py` passes coroutines (run on the asyncio loop); `Command.perform` accepts both, so the window code is the same. #### Full code **desktop.py:** ```py """Desktop app on Django's ORM: the models, validation and database of the site, with no web server. uv run manage.py migrate # once, as in Django's tutorial uv run desktop.py The window imports the polls app and queries it directly (architecture "same process"). `uv run manage.py runserver` can run at the same time on the same database: votes cast in the window show up on /polls/ and in /admin/, and the other way round. """ import functools import os import django os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") django.setup() # what manage.py does before anything touches the models from django.core.exceptions import ValidationError # noqa: E402 from django.db import connections, transaction # noqa: E402 from django.db.models import F # noqa: E402 from django.utils import timezone # noqa: E402 import pyzed as pz # noqa: E402 from polls.models import Question # noqa: E402 from polls.views import question_json # noqa: E402 from polls_window import PollsWindow # noqa: E402 def in_thread(function): """Pyzed runs these in a worker thread; Django opens one connection per thread, so close it.""" @functools.wraps(function) def wrapper(*args, **kwargs): try: return function(*args, **kwargs) finally: connections.close_all() return wrapper @in_thread def load(): questions = Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date").prefetch_related("choice_set") return [question_json(q) for q in questions] @in_thread def vote(question_id, choice_id): choice = Question.objects.get(pk=question_id).choice_set.get(pk=choice_id) choice.votes = F("votes") + 1 # the database adds one, so two voters never overwrite each other choice.save() @in_thread def create(draft): question = Question(question_text=draft["question_text"], pub_date=timezone.now()) try: question.full_clean() # the model's own rules: required, max_length=200 if len(draft["choices"]) < 2: raise ValidationError("A poll needs at least two choices.") except ValidationError as error: return pz.validation_errors(error) with transaction.atomic(): question.save() for text in draft["choices"]: question.choice_set.create(choice_text=text, votes=0) return question_json(question) if __name__ == "__main__": pz.run(PollsWindow(load, vote, create, mode="Django ORM, same process")) ``` **client.py:** ```py """Desktop client for the Django site over HTTP: the site runs on its own, the window uses its JSON API. uv run manage.py runserver # terminal 1: the site (http://127.0.0.1:8000/polls/) uv run client.py # terminal 2: the window uv run client.py https://polls.example.com Use this when the site is deployed on a server and the desktop app is one more client. The window needs no Django and no database, only httpx. """ import sys import httpx import pyzed as pz from polls_window import PollsWindow SITE = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8000" async def load(): try: async with httpx.AsyncClient(base_url=SITE, timeout=5) as client: response = await client.get("/polls/api/questions/") response.raise_for_status() except httpx.HTTPError as error: return f"Can't reach {SITE}: {error or type(error).__name__}" return response.json()["questions"] async def vote(question_id, choice_id): try: async with httpx.AsyncClient(base_url=SITE, timeout=5) as client: response = await client.post(f"/polls/api/questions/{question_id}/vote/", json={"choice": choice_id}) response.raise_for_status() except httpx.HTTPError as error: return f"The vote failed: {error or type(error).__name__}" return response.json() if __name__ == "__main__": pz.run(PollsWindow(load, vote, mode=f"HTTP client of {SITE}")) ``` **polls_window.py:** ```py """The Pyzed window shared by desktop.py (Django ORM in this process) and client.py (HTTP to the site). A backend is three functions returning plain dicts like ``polls.views.question_json``. They may be regular functions (Pyzed runs them in a thread) or ``async def`` (run on Pyzed's asyncio loop): ``Command.perform`` accepts both. """ from __future__ import annotations from datetime import datetime, timedelta, timezone from typing import Callable, Optional import pyzed as pz from pyzed import Command, ui class PollsWindow(pz.App): title = "Polls · Django" size = (960, 600) def __init__(self, load: Callable, vote: Callable, create: Optional[Callable] = None, mode: str = ""): self.backend_load, self.backend_vote, self.backend_create = load, vote, create self.mode = mode self.questions: list[dict] = [] self.selected_id: int | None = None self.message = "" self.creating = False self.draft = {"question_text": "", "choices": ["", "", ""]} self.errors: dict[str, str] = {} # -- backend calls ---------------------------------------------------------------------- def on_start(self): return self.reload() def reload(self): return Command.perform(self.backend_load, self.loaded) def loaded(self, result): if isinstance(result, str): # the backend couldn't answer self.message = result return self.questions, self.message = result, "" if self.questions and self.selected_id not in {q["id"] for q in self.questions}: self.selected_id = self.questions[0]["id"] def vote(self, question_id: int, choice_id: int): return Command.perform(lambda: self.backend_vote(question_id, choice_id), self.voted) def voted(self, result): if isinstance(result, str): self.message = result return None return self.reload() def create(self): draft = {"question_text": self.draft["question_text"], "choices": [c for c in self.draft["choices"] if c.strip()]} return Command.perform(lambda: self.backend_create(draft), self.created) def created(self, result): if "id" not in result: # {field: message} self.errors = result return None self.creating, self.selected_id = False, result["id"] self.draft = {"question_text": "", "choices": ["", "", ""]} return self.reload() # -- view -------------------------------------------------------------------------------- @staticmethod def recent(question: dict) -> bool: # Question.was_published_recently, on the JSON published = datetime.fromisoformat(question["pub_date"]) now = datetime.now(timezone.utc) return now - timedelta(days=1) <= published <= now def sidebar(self, t): rows = [ui.list_item(q["question_text"], lambda q=q: setattr(self, "selected_id", q["id"]), icon="vote", end=ui.badge("new", tint="success") if self.recent(q) else None, selected=q["id"] == self.selected_id, inset=True, height=30) for q in self.questions] new = ui.button("New question", lambda: setattr(self, "creating", True), icon="plus", full_width=True) \ if self.backend_create else ui.muted("Add questions in the Django admin.", size="sm") return pz.container(pz.column([ui.muted("QUESTIONS", size="xs", weight="semibold"), *rows, pz.vspace(), new], spacing=2), padding=8, width=300, height="fill", bg=t.panel) def results(self): question = next((q for q in self.questions if q["id"] == self.selected_id), None) if question is None: return ui.empty_state("No polls are available.", icon="vote") total = sum(c["votes"] for c in question["choices"]) or 1 rows = [pz.row([ pz.container(ui.label(c["choice_text"]), width=200), pz.container(ui.progress(c["votes"] / total, height=8), width="fill"), pz.container(ui.muted(f"{c['votes']} vote{'s' if c['votes'] != 1 else ''}", size="sm"), width=70, align_x="end"), ui.button("Vote", lambda c=c: self.vote(question["id"], c["id"]), size="sm"), ], spacing=12, align="center") for c in question["choices"]] return pz.column([ui.heading(question["question_text"], level=2), ui.muted(f"Published {question['pub_date'][:16].replace('T', ' ')}", size="sm"), *rows], spacing=14, padding=24) def dialog(self): if not self.creating: return None choices = self.draft["choices"] def set_choice(i): return lambda value: choices.__setitem__(i, value) form = pz.column([ ui.field("Question", ui.input(self.draft["question_text"], lambda v: self.draft.__setitem__("question_text", v), placeholder="What's new?", width=260), error=self.errors.get("question_text")), *[ui.field(f"Choice {i + 1}", ui.input(text, set_choice(i), width=260)) for i, text in enumerate(choices)], ui.callout(self.errors[""], tint="error") if self.errors.get("") else None, ], spacing=12) close = lambda: setattr(self, "creating", False) # noqa: E731 return ui.modal(form, title="New question", on_close=close, width=520, footer=[ui.button("Cancel", close, variant="ghost"), ui.button("Publish", self.create, icon="check", variant="solid")]) def view(self): t = self.resolved_theme() status = ui.status_bar([ui.status_item(self.message or self.mode, icon="circle-alert" if self.message else "database", icon_color="error" if self.message else "success")]) page = pz.column([pz.row([self.sidebar(t), pz.container(self.results(), width="fill", height="fill", bg=t.editor)]), status]) return ui.layer(page, self.dialog(), on_dismiss=lambda: setattr(self, "creating", False), backdrop="#00000066") ``` **polls/views.py:** ```py """The views from Django's tutorial (parts 3 and 4), plus a small JSON API for the desktop client. Tutorial: https://docs.djangoproject.com/en/stable/intro/tutorial04/ """ import json from django.db.models import F from django.http import HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.utils import timezone from django.views import generic from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_GET, require_POST from .models import Choice, Question class IndexView(generic.ListView): template_name = "polls/index.html" context_object_name = "latest_question_list" def get_queryset(self): """Return the last five published questions (not including those set to be published in the future).""" return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[:5] class DetailView(generic.DetailView): model = Question template_name = "polls/detail.html" def get_queryset(self): """Excludes any questions that aren't published yet.""" return Question.objects.filter(pub_date__lte=timezone.now()) class ResultsView(generic.DetailView): model = Question template_name = "polls/results.html" def vote(request, question_id): question = get_object_or_404(Question, pk=question_id) try: selected_choice = question.choice_set.get(pk=request.POST["choice"]) except (KeyError, Choice.DoesNotExist): # Redisplay the question voting form. return render(request, "polls/detail.html", {"question": question, "error_message": "You didn't select a choice."}) else: selected_choice.votes = F("votes") + 1 selected_choice.save() # Always return an HttpResponseRedirect after successfully dealing with POST data. return HttpResponseRedirect(reverse("polls:results", args=(question.id,))) # -- JSON API for desktop clients (client.py) --------------------------------------------------- def question_json(question): return { "id": question.id, "question_text": question.question_text, "pub_date": question.pub_date.isoformat(), "choices": [{"id": c.id, "choice_text": c.choice_text, "votes": c.votes} for c in question.choice_set.all()], } @require_GET def api_questions(request): questions = Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date").prefetch_related("choice_set") return JsonResponse({"questions": [question_json(q) for q in questions]}) @csrf_exempt # a desktop app has no CSRF cookie; protect a real API with tokens instead (e.g. Django REST framework) @require_POST def api_vote(request, question_id): question = get_object_or_404(Question, pk=question_id) try: choice_id = json.loads(request.body)["choice"] selected_choice = question.choice_set.get(pk=choice_id) except (KeyError, ValueError, Choice.DoesNotExist): return JsonResponse({"detail": "You didn't select a choice."}, status=400) selected_choice.votes = F("votes") + 1 selected_choice.save() return JsonResponse(question_json(question)) ``` ### SQLAlchemy > SQLAlchemy's ORM Quick Start in a Pyzed desktop app, with a sync Session in worker threads or an AsyncSession on Pyzed's asyncio loop, plus a 20,000-row table. Source: https://albakore.github.io/pyzed/integrations/sqlalchemy/ SQLAlchemy runs in the same process as the window (architecture 1 in [Architectures](https://albakore.github.io/pyzed/integrations/architectures/)). There are two demos: - **`sqlalchemy_quickstart`**: the `User` and `Address` models of SQLAlchemy's [ORM Quick Start](https://docs.sqlalchemy.org/en/20/orm/quickstart.html), with the same window on a sync `Session` or on the [asyncio extension](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html). - **`sqlalchemy_contacts`**: 20,000 rows in a virtual table, with search and sorting in SQL. #### The Quick Start in a window (Image: Users and their email addresses from SQLAlchemy's Quick Start) 1. **Declare the models** exactly as the Quick Start does, with `DeclarativeBase`, `Mapped` and `relationship`. The cascade deletes a user's addresses with the user: ```python class Base(DeclarativeBase): pass class User(Base): __tablename__ = "user_account" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(30)) fullname: Mapped[Optional[str]] addresses: Mapped[List["Address"]] = relationship(back_populates="user", cascade="all, delete-orphan") class Address(Base): __tablename__ = "address" id: Mapped[int] = mapped_column(primary_key=True) email_address: Mapped[str] user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) user: Mapped["User"] = relationship(back_populates="addresses") ``` 2. **Create an Engine and the tables:** ```python engine = create_engine("sqlite:///quickstart.db") Base.metadata.create_all(engine) ``` 3. **Open a Session per unit of work.** Each backend function opens its own `Session`, which makes it safe to run in any worker thread. `selectinload` loads the addresses before the session closes, so the window can read `user.addresses` later: ```python def users() -> list[User]: stmt = select(User).options(selectinload(User.addresses)).order_by(User.name) with Session(engine) as session: return list(session.scalars(stmt)) def add_address(user_id: int, email_address: str) -> None: with Session(engine) as session: user = session.get(User, user_id) user.addresses.append(Address(email_address=email_address)) session.commit() ``` 4. **Call it from the window with `Command.perform`.** The function runs in a thread and `then` receives the result on the UI thread: ```python def on_start(self): return Command.perform(backend.setup, lambda _: self.reload()) def reload(self): return Command.perform(backend.users, self.loaded) def add_address(self, *_): user_id, email = self.selected_id, self.new_email.strip() return Command.perform(lambda: backend.add_address(user_id, email), lambda _: self.reload()) ``` ##### The asyncio version `async_backend.py` has the same functions written with `create_async_engine`, an `async_sessionmaker` and the aiosqlite driver. `Command.perform` sees that they are coroutine functions and runs them on Pyzed's asyncio loop instead of a thread, so `main.py` doesn't change: ```python engine = create_async_engine("sqlite+aiosqlite:///quickstart.db") async_session = async_sessionmaker(engine, expire_on_commit=False) async def setup() -> None: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async def users() -> list[User]: stmt = select(User).options(selectinload(User.addresses)).order_by(User.name) async with async_session() as session: return list(await session.scalars(stmt)) ``` **Note:** With `AsyncSession`, lazy loading would need an `await`, so relationships are loaded eagerly with `selectinload`, and `expire_on_commit=False` keeps the objects readable after `commit()`. Install `sqlalchemy[asyncio]` and `aiosqlite`; the extra brings `greenlet`, which the extension needs. | | `Session` | `AsyncSession` | | --- | --- | --- | | Where it runs | A worker thread per call | Pyzed's asyncio loop | | Pyzed call | `Command.perform(sync_function, then)` | `Command.perform(async_function, then)` or return the coroutine | | Use it when | Most desktop apps; any driver | You already use asyncio drivers (asyncpg, aiosqlite) | **main.py:** ```py """SQLAlchemy's ORM Quick Start in a Pyzed window: users and their email addresses. uv run main.py # sync Session, run in worker threads (sync_backend.py) uv run main.py --async # AsyncSession on Pyzed's asyncio loop (async_backend.py) Both backends have the same functions and the window doesn't care which one it gets: Command.perform runs a regular function in a thread and a coroutine on the loop. Walkthrough: https://albakore.github.io/pyzed/integrations/sqlalchemy/ """ from __future__ import annotations import sys import pyzed as pz from pyzed import Command, ui if "--async" in sys.argv: import async_backend as backend else: import sync_backend as backend class Quickstart(pz.App): title = "Users · SQLAlchemy" size = (900, 560) def __init__(self): self.users = [] self.selected_id: int | None = None self.new_user = {"name": "", "fullname": ""} self.new_email = "" self.editing: tuple[int, str] | None = None # (address id, text being typed) self.errors: dict[str, str] = {} # -- database ---------------------------------------------------------------------------- def on_start(self): return Command.perform(backend.setup, lambda _: self.reload()) def reload(self): return Command.perform(backend.users, self.loaded) def loaded(self, users): self.users = users if self.selected_id not in {u.id for u in users}: self.selected_id = users[0].id if users else None def run(self, work): """Runs a backend call, then reloads. ``work`` is a lambda, so its arguments are read now.""" return Command.perform(work, lambda _: self.reload()) # -- actions ------------------------------------------------------------------------------- def add_user(self, *_): name, fullname = self.new_user["name"].strip(), self.new_user["fullname"].strip() self.errors = {"name": "Required"} if not name else {"name": "30 characters at most"} if len(name) > 30 else {} if self.errors: return None self.new_user = {"name": "", "fullname": ""} return self.run(lambda: backend.add_user(name, fullname)) def add_address(self, *_): email, self.new_email = self.new_email.strip(), "" user_id = self.selected_id return self.run(lambda: backend.add_address(user_id, email)) if "@" in email else None def save_address(self, *_): address_id, email = self.editing self.editing = None return self.run(lambda: backend.rename_address(address_id, email.strip())) def delete_user(self): user_id = self.selected_id return self.run(lambda: backend.delete_user(user_id)) # -- view ---------------------------------------------------------------------------------- def sidebar(self, t): rows = [ui.list_item(u.name, lambda u=u: setattr(self, "selected_id", u.id), icon="user", end=ui.count_badge(len(u.addresses), tint="text_muted") if u.addresses else None, selected=u.id == self.selected_id, inset=True, height=30) for u in self.users] form = pz.column([ ui.input(self.new_user["name"], lambda v: self.new_user.__setitem__("name", v), placeholder="name", on_submit=self.add_user), ui.label(self.errors["name"], size="sm", color="error") if self.errors.get("name") else None, ui.input(self.new_user["fullname"], lambda v: self.new_user.__setitem__("fullname", v), placeholder="Full name (optional)", on_submit=self.add_user), ui.button("Add user", self.add_user, icon="user-plus", full_width=True), ], spacing=6) return pz.container(pz.column([ui.muted("user_account", size="xs", mono=True), *rows, pz.vspace(), form], spacing=2), padding=8, width=260, height="fill", bg=t.panel) def address_row(self, address): if self.editing and self.editing[0] == address.id: return pz.row([ ui.input(self.editing[1], lambda v: setattr(self, "editing", (address.id, v)), on_submit=self.save_address), ui.icon_button("check", self.save_address, tooltip="Save"), ], spacing=6, align="center") return ui.list_item(address.email_address, lambda: setattr(self, "editing", (address.id, address.email_address)), icon="mail", end=ui.muted("click to edit", size="sm"), inset=True, height=30) def detail(self): user = next((u for u in self.users if u.id == self.selected_id), None) if user is None: return ui.empty_state("No users", "Add one on the left.", icon="users") return pz.column([ pz.row([ui.heading(user.fullname or user.name, level=2), pz.hspace(), ui.button("Delete user", self.delete_user, icon="trash-2", variant="ghost", tint="error")], align="center"), ui.muted(repr(user), mono=True, size="sm"), ui.label("Addresses", weight="semibold"), *[self.address_row(a) for a in user.addresses], pz.row([ui.input(self.new_email, lambda v: setattr(self, "new_email", v), placeholder="new@sqlalchemy.org", icon="mail-plus", on_submit=self.add_address), ui.button("Add", self.add_address)], spacing=6), ], spacing=10, padding=24) def view(self): t = self.resolved_theme() mode = "AsyncSession · aiosqlite" if backend.__name__ == "async_backend" else "Session · worker threads" return pz.column([ pz.row([self.sidebar(t), pz.container(self.detail(), width="fill", height="fill", bg=t.editor)]), ui.status_bar([ui.status_item(mode, icon="database", icon_color="success")]), ]) if __name__ == "__main__": pz.run(Quickstart) ``` **models.py:** ```py """The models of SQLAlchemy's ORM Quick Start: https://docs.sqlalchemy.org/en/20/orm/quickstart.html""" from typing import List, Optional from sqlalchemy import ForeignKey, String from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship class Base(DeclarativeBase): pass class User(Base): __tablename__ = "user_account" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(30)) fullname: Mapped[Optional[str]] addresses: Mapped[List["Address"]] = relationship(back_populates="user", cascade="all, delete-orphan") def __repr__(self) -> str: return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})" class Address(Base): __tablename__ = "address" id: Mapped[int] = mapped_column(primary_key=True) email_address: Mapped[str] user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) user: Mapped["User"] = relationship(back_populates="addresses") def __repr__(self) -> str: return f"Address(id={self.id!r}, email_address={self.email_address!r})" def sample_users() -> list[User]: """The users the Quick Start creates.""" return [ User(name="spongebob", fullname="Spongebob Squarepants", addresses=[Address(email_address="spongebob@sqlalchemy.org")]), User(name="sandy", fullname="Sandy Cheeks", addresses=[Address(email_address="sandy@sqlalchemy.org"), Address(email_address="sandy@squirrelpower.org")]), User(name="patrick", fullname="Patrick Star"), ] ``` **sync_backend.py:** ```py """The database code, as the Quick Start writes it: an Engine, a Session per unit of work, select(). Each function opens its own Session, so Pyzed can run it in a worker thread (Command.perform). """ from sqlalchemy import create_engine, select from sqlalchemy.orm import Session, selectinload from models import Address, Base, User, sample_users engine = create_engine("sqlite:///quickstart.db") def setup() -> None: Base.metadata.create_all(engine) with Session(engine) as session: if session.scalars(select(User)).first() is None: session.add_all(sample_users()) session.commit() def users() -> list[User]: # selectinload fetches the addresses now, so the window can read them after the session closes. stmt = select(User).options(selectinload(User.addresses)).order_by(User.name) with Session(engine) as session: return list(session.scalars(stmt)) def add_user(name: str, fullname: str) -> None: with Session(engine) as session: session.add(User(name=name, fullname=fullname or None)) session.commit() def add_address(user_id: int, email_address: str) -> None: with Session(engine) as session: user = session.get(User, user_id) user.addresses.append(Address(email_address=email_address)) session.commit() def rename_address(address_id: int, email_address: str) -> None: with Session(engine) as session: session.get(Address, address_id).email_address = email_address session.commit() def delete_user(user_id: int) -> None: with Session(engine) as session: session.delete(session.get(User, user_id)) # cascade="all, delete-orphan" removes the addresses session.commit() ``` **async_backend.py:** ```py """The same database code with SQLAlchemy's asyncio extension and the aiosqlite driver. https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html The functions are coroutines: Pyzed runs them on its asyncio loop, where the engine lives. """ from sqlalchemy import select from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.orm import selectinload from models import Address, Base, User, sample_users engine = create_async_engine("sqlite+aiosqlite:///quickstart.db") async_session = async_sessionmaker(engine, expire_on_commit=False) async def setup() -> None: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async with async_session() as session: if (await session.scalars(select(User))).first() is None: session.add_all(sample_users()) await session.commit() async def users() -> list[User]: # Lazy loading needs an await, so load relationships eagerly with selectinload. stmt = select(User).options(selectinload(User.addresses)).order_by(User.name) async with async_session() as session: return list(await session.scalars(stmt)) async def add_user(name: str, fullname: str) -> None: async with async_session() as session: session.add(User(name=name, fullname=fullname or None)) await session.commit() async def add_address(user_id: int, email_address: str) -> None: async with async_session() as session: session.add(Address(user_id=user_id, email_address=email_address)) await session.commit() async def rename_address(address_id: int, email_address: str) -> None: async with async_session() as session: address = await session.get(Address, address_id) address.email_address = email_address await session.commit() async def delete_user(user_id: int) -> None: async with async_session() as session: user = await session.get(User, user_id, options=[selectinload(User.addresses)]) await session.delete(user) await session.commit() ``` #### Many rows: the contacts demo A contacts manager on SQLAlchemy 2.0 and SQLite. The table holds 20,000 rows and scrolls smoothly because `ui.table` only builds the rows in view; search and sorting run in SQL, in a worker thread, so the window never waits for the database. (Image: A contacts table with 20,000 rows, statistics and an edit panel) The demo keeps the database code in `db.py`, with no Pyzed in it, and the window in `main.py`. That split is worth copying: the same `db.py` could serve a FastAPI app or a CLI. 1. **Models are plain SQLAlchemy 2.0.** ```python class Contact(Base): __tablename__ = "contacts" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(80), index=True) email: Mapped[str] = mapped_column(String(120), unique=True) company: Mapped[str] = mapped_column(String(80), default="") city: Mapped[str] = mapped_column(String(60), default="") ``` 2. **Each query is a function with its own session.** SQLAlchemy sessions are not shared between threads, so every function opens one and closes it. The loaded objects keep their values after the session closes, which is all the view needs: ```python def search(text, sort, descending): column = COLUMNS[sort] query = select(Contact).order_by(column.desc() if descending else column, Contact.id) if text: pattern = f"%{text}%" query = query.where(or_(Contact.name.ilike(pattern), Contact.email.ilike(pattern), Contact.company.ilike(pattern))) with Session(engine) as session: return list(session.scalars(query)) ``` 3. **Run it with `Command.perform`.** The function runs in a thread and `then` gets the result on the UI thread. Returning a list of commands runs them together: ```python def reload(self): query, sort, descending = self.query, self.sort, self.descending return [ Command.perform(lambda: db.search(query, sort, descending), lambda rows: self.loaded(query, rows)), Command.perform(db.stats, lambda stats: setattr(self, "stats", stats)), ] def loaded(self, query, rows): if query == self.query: # a newer search is on its way: keep waiting for it self.rows, self.loading = rows, False ``` Copy the state you need into locals first (`query, sort, ...`): the lambda runs later, in another thread, and should not read `self` while the user keeps typing. 4. **Hand all the rows to `ui.table`.** It is virtualized, so 20,000 rows cost the same as 20. Clicking a header calls `on_sort`, which here re-runs the query with a new `ORDER BY`: ```python ui.table(columns, self.rows, sort_by=self.sort, descending=self.descending, on_sort=self.sort_by, on_row_press=self.open, selected=self.editing, striped=True, height="fill") ``` 5. **Turn database errors into field errors.** `save` returns either the contact or `{field: message}`. A unique constraint becomes a message under the Email field: ```python try: session.commit() except IntegrityError: return {"email": "Another contact already uses this email"} ``` ```python ui.field("Email", ui.input(self.draft["email"], ...), error=self.errors.get("email")) ``` **Tip:** Prefer `asyncio`? SQLAlchemy's `AsyncSession` with `aiosqlite` or `asyncpg` works too: return the coroutine instead of using `Command.perform`. See [Other libraries](https://albakore.github.io/pyzed/integrations/other-libraries/#sqlalchemy-asyncio). **main.py:** ```py """A contacts manager on SQLAlchemy 2.0: 20,000 rows in a virtual table, searched and sorted in SQL. uv run main.py The database (contacts.db) is created next to this file on the first run. Walkthrough: https://albakore.github.io/pyzed/integrations/sqlalchemy/ """ from __future__ import annotations import pyzed as pz from pyzed import Command, ui import db FIELDS = [("name", "Name", "Ada Lovelace"), ("email", "Email", "ada@example.com"), ("company", "Company", "Analytical Engines"), ("city", "City", "London")] class Contacts(pz.App): title = "Contacts · SQLAlchemy" size = (1180, 720) def __init__(self): self.rows: list[db.Contact] = [] self.stats = {"total": 0, "companies": 0, "cities": []} self.query = "" self.sort, self.descending = "Name", False self.loading = True self.editing = None # a Contact, "new" or None self.draft: dict[str, str] = {} self.errors: dict[str, str] = {} # -- database work runs in a thread with Command.perform ------------------------------ def on_start(self): return Command.perform(db.setup, lambda _: self.reload()) def reload(self): self.loading = True query, sort, descending = self.query, self.sort, self.descending return [ Command.perform(lambda: db.search(query, sort, descending), lambda rows: self.loaded(query, rows)), Command.perform(db.stats, lambda stats: setattr(self, "stats", stats)), ] def loaded(self, query: str, rows: list[db.Contact]): if query == self.query: # a newer search is on its way: keep waiting for it self.rows, self.loading = rows, False def search(self, text: str): self.query = text return self.reload() def sort_by(self, title: str): self.descending = not self.descending if title == self.sort else False self.sort = title return self.reload() # -- the edit panel ------------------------------------------------------------------ def open(self, contact): self.editing, self.errors = contact, {} self.draft = {key: "" if contact == "new" else getattr(contact, key) for key, _, _ in FIELDS} def save(self): contact_id = None if self.editing == "new" else self.editing.id values = dict(self.draft) return Command.perform(lambda: db.save(contact_id, values), self.saved) def saved(self, result): if isinstance(result, dict): self.errors = result return None self.editing = None return self.reload() def delete(self): contact_id = self.editing.id self.editing = None return Command.perform(lambda: db.delete(contact_id), lambda _: self.reload()) def on_key(self, combo): if combo == "escape": self.editing = None elif combo == "ctrl-n": self.open("new") # -- view ---------------------------------------------------------------------------- def header(self): cities = self.stats["cities"] return pz.row([ ui.stat("Contacts", f"{self.stats['total']:,}"), ui.stat("Companies", self.stats["companies"]), ui.card(pz.column([ui.muted("Top cities", size="sm"), ui.bar_chart([count for _, count in cities], labels=[c.split()[0] for c, _ in cities], width=360, height=90)], spacing=6)), ], spacing=12) def panel(self, t): if self.editing is None: return None rows = [ui.field(title, ui.input(self.draft[key], lambda v, k=key: self.draft.__setitem__(k, v), placeholder=hint, width=220), error=self.errors.get(key)) for key, title, hint in FIELDS] new = self.editing == "new" return pz.container( pz.column([ pz.row([ui.heading("New contact" if new else "Edit contact", level=3), pz.hspace(), ui.icon_button("x", lambda: setattr(self, "editing", None), tooltip="Close (Esc)")], align="center"), *rows, pz.row([None if new else ui.button("Delete", self.delete, icon="trash-2", variant="ghost", tint="error"), pz.hspace(), ui.button("Save", self.save, icon="check", variant="solid")], spacing=8), ], spacing=14), padding=16, width=420, height="fill", bg=t.panel, ) def view(self): t = self.resolved_theme() columns = [ ui.Column("Name", "name", width="fill:3"), ui.Column("Email", "email", width="fill:3"), ui.Column("Company", "company", width="fill:2"), ui.Column("City", "city", width="fill:2"), ] toolbar = pz.row([ ui.search_input(self.query, self.search, placeholder="Search by name, email or company…", width=360), ui.spinner() if self.loading else ui.muted(f"{len(self.rows):,} found", size="sm"), pz.hspace(), ui.button("New contact", lambda: self.open("new"), icon="user-plus", keys="ctrl-n"), ], spacing=10, align="center") # Rows come sorted from SQL; sort_by only draws the arrow (re-sorting a sorted list is cheap). table = ui.table(columns, self.rows, sort_by=self.sort, descending=self.descending, on_sort=self.sort_by, on_row_press=self.open, selected=self.editing, striped=True, height="fill") main = pz.column([self.header(), toolbar, table], spacing=14, padding=16) return pz.container(pz.row([main, self.panel(t)]), width="fill", height="fill", bg=t.editor) if __name__ == "__main__": pz.run(Contacts) ``` **db.py:** ```py """The database: SQLAlchemy 2.0 models on SQLite, plus the queries the window runs.""" from __future__ import annotations import random from datetime import datetime from pathlib import Path from sqlalchemy import String, create_engine, func, or_, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column DATABASE = Path(__file__).with_name("contacts.db") engine = create_engine(f"sqlite:///{DATABASE}") class Base(DeclarativeBase): pass class Contact(Base): __tablename__ = "contacts" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(80), index=True) email: Mapped[str] = mapped_column(String(120), unique=True) company: Mapped[str] = mapped_column(String(80), default="") city: Mapped[str] = mapped_column(String(60), default="") created: Mapped[datetime] = mapped_column(default=datetime.now) # Sorting by a column title of the table is an ORDER BY on this column. COLUMNS = {"Name": Contact.name, "Email": Contact.email, "Company": Contact.company, "City": Contact.city} def setup(rows: int = 20_000) -> None: """Creates the table and fills it with made-up contacts the first time.""" Base.metadata.create_all(engine) with Session(engine) as session: if session.scalar(select(func.count()).select_from(Contact)): return rng = random.Random(7) first = ["Ada", "Alan", "Grace", "Linus", "Guido", "Margaret", "Ken", "Barbara", "Dennis", "Radia", "Tim", "Frances", "Bjarne", "Anita", "John", "Hedy", "Yukihiro", "Sophie", "Brendan", "Katherine"] last = ["Lovelace", "Turing", "Hopper", "Torvalds", "van Rossum", "Hamilton", "Thompson", "Liskov", "Ritchie", "Perlman", "Berners-Lee", "Allen", "Stroustrup", "Borg", "McCarthy", "Lamarr", "Matsumoto", "Wilson", "Eich", "Johnson"] companies = ["Acme", "Globex", "Initech", "Umbrella", "Hooli", "Stark", "Wayne", "Tyrell", "Cyberdyne", "Aperture"] cities = ["Buenos Aires", "Madrid", "Mexico City", "Bogotá", "Lima", "Santiago", "Montevideo", "Barcelona", "Berlin", "Tokyo", "Toronto", "Lisbon"] weights = [12, 10, 9, 7, 6, 5, 4, 4, 3, 3, 2, 2] session.add_all( Contact(name=f"{rng.choice(first)} {rng.choice(last)}", email=f"user{i}@example.com", company=rng.choice(companies), city=rng.choices(cities, weights)[0]) for i in range(rows) ) session.commit() def search(text: str, sort: str, descending: bool) -> list[Contact]: """Contacts whose name, email or company contains ``text``, sorted in SQL.""" column = COLUMNS[sort] query = select(Contact).order_by(column.desc() if descending else column, Contact.id) if text: pattern = f"%{text}%" query = query.where(or_(Contact.name.ilike(pattern), Contact.email.ilike(pattern), Contact.company.ilike(pattern))) with Session(engine) as session: return list(session.scalars(query)) # plain objects keep their loaded values after the session closes def stats() -> dict: """Totals and the five cities with more contacts, for the header.""" with Session(engine) as session: total = session.scalar(select(func.count()).select_from(Contact)) companies = session.scalar(select(func.count(func.distinct(Contact.company)))) cities = session.execute(select(Contact.city, func.count()).group_by(Contact.city) .order_by(func.count().desc()).limit(5)).all() return {"total": total, "companies": companies, "cities": [(city, count) for city, count in cities]} def save(contact_id: int | None, values: dict) -> Contact | dict: """Inserts or updates a contact. Returns it, or ``{field: message}`` when it can't be saved.""" errors = {} if not values["name"].strip(): errors["name"] = "A contact needs a name" if "@" not in values["email"]: errors["email"] = "Not an email address" if errors: return errors with Session(engine, expire_on_commit=False) as session: contact = session.get(Contact, contact_id) if contact_id else Contact() for key, value in values.items(): setattr(contact, key, value.strip()) session.add(contact) try: session.commit() except IntegrityError: return {"email": "Another contact already uses this email"} return contact def delete(contact_id: int) -> None: with Session(engine) as session: contact = session.get(Contact, contact_id) if contact is not None: session.delete(contact) session.commit() ``` ### SQLModel > SQLModel's tutorial (teams, heroes, where filters, relationships) in a Pyzed desktop app, plus an inventory whose form and table share one model. Source: https://albakore.github.io/pyzed/integrations/sqlmodel/ SQLModel runs in the same process as the window (architecture 1 in [Architectures](https://albakore.github.io/pyzed/integrations/architectures/)); when the same models must be shared over HTTP, put them behind FastAPI as in the [FastAPI demo](https://albakore.github.io/pyzed/integrations/fastapi/). There are two demos: - **`sqlmodel_heroes`**: the `Team` and `Hero` tables of [SQLModel's tutorial](https://sqlmodel.tiangolo.com/tutorial/), with its sample data, `where()` filters, relationships and a data model for the form. - **`sqlmodel_inventory`**: an inventory whose validation rules and table come from one class. #### The tutorial's heroes in a window (Image: Heroes grouped by team, filtered by age, with an edit panel) 1. **Define the tables** as in the tutorial's [Relationship Attributes](https://sqlmodel.tiangolo.com/tutorial/relationship-attributes/) chapter: ```python class Team(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) headquarters: str heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str age: int | None = Field(default=None, index=True) team_id: int | None = Field(default=None, foreign_key="team.id") team: Team | None = Relationship(back_populates="heroes") ``` 2. **Create the engine and the tables**, and add the tutorial's heroes the first time: ```python sqlite_file_name = "database.db" sqlite_url = f"sqlite:///{sqlite_file_name}" engine = create_engine(sqlite_url) def create_db_and_tables(): SQLModel.metadata.create_all(engine) ``` 3. **Query with `select()` and `where()`.** Filters are added one by one; `col()` gives the type checker a column for the comparison, as the tutorial recommends: ```python def heroes(team_id, min_age): statement = select(Hero).options(selectinload(Hero.team)).order_by(Hero.name) if team_id != "all": statement = statement.where(Hero.team_id == team_id) if min_age is not None: statement = statement.where(col(Hero.age) >= min_age) with Session(engine) as session: return list(session.exec(statement)) ``` `selectinload` loads each hero's team before the session closes, so the table can show `hero.team.name`. 4. **Validate the form with a data model.** `table=True` models don't validate (SQLAlchemy creates them), so the tutorial's [Multiple Models](https://sqlmodel.tiangolo.com/tutorial/fastapi/multiple-models/) approach applies: a `HeroCreate` without `table=True` checks what the form typed. ```python class HeroCreate(SQLModel): name: str = Field(min_length=1) secret_name: str = Field(min_length=1) age: int | None = Field(default=None, ge=0) team_id: int | None = None def save(self): try: data = HeroCreate.model_validate({**self.draft, "age": self.draft["age"] or None, ...}) except ValidationError as error: self.errors = pz.validation_errors(error) return None return Command.perform(lambda: db.save(hero_id, data), lambda _: self.reload()) ``` 5. **Create or update with `sqlmodel_update`:** ```python def save(hero_id, data: HeroCreate): with Session(engine) as session: hero = session.get(Hero, hero_id) if hero_id else Hero.model_validate(data) hero.sqlmodel_update(data.model_dump()) session.add(hero) session.commit() ``` **main.py:** ```py """SQLModel's tutorial in a Pyzed window: teams, heroes, where() filters and a validated form. uv run main.py The database (database.db) is created and filled with the tutorial's heroes on the first run. Walkthrough: https://albakore.github.io/pyzed/integrations/sqlmodel/ """ from __future__ import annotations from pydantic import ValidationError import pyzed as pz from pyzed import Command, ui import db from models import Hero, HeroCreate NO_TEAM = "No team" class Heroes(pz.App): title = "Heroes · SQLModel" size = (1060, 620) def __init__(self): self.teams = [] self.heroes: list[Hero] = [] self.team_id: int | None | str = "all" # a team id, None (heroes without a team) or "all" self.older = False # only heroes aged 35 or more self.editing: Hero | None | str = None # a Hero, "new" or None self.draft: dict = {} self.errors: dict[str, str] = {} # -- database, in worker threads ------------------------------------------------------------ def on_start(self): return Command.perform(db.setup, lambda _: self.reload()) def reload(self): team_id, min_age = self.team_id, 35 if self.older else None return [Command.perform(db.teams, lambda teams: setattr(self, "teams", teams)), Command.perform(lambda: db.heroes(team_id, min_age), lambda heroes: setattr(self, "heroes", heroes))] def show_team(self, team_id): self.team_id = team_id return self.reload() def toggle_older(self, value: str): self.older = value != "All ages" return self.reload() # -- the form --------------------------------------------------------------------------------- def open(self, hero): self.editing, self.errors = hero, {} if hero == "new": self.draft = {"name": "", "secret_name": "", "age": "", "team": NO_TEAM} else: self.draft = {"name": hero.name, "secret_name": hero.secret_name, "age": "" if hero.age is None else str(hero.age), "team": hero.team.name if hero.team else NO_TEAM} def save(self): team = next((t for t in self.teams if t.name == self.draft["team"]), None) try: data = HeroCreate.model_validate({**self.draft, "age": self.draft["age"] or None, "team_id": team.id if team else None}) except ValidationError as error: self.errors = pz.validation_errors(error) return None hero_id = None if self.editing == "new" else self.editing.id self.editing, self.errors = None, {} return Command.perform(lambda: db.save(hero_id, data), lambda _: self.reload()) def delete(self): hero_id, self.editing = self.editing.id, None return Command.perform(lambda: db.delete(hero_id), lambda _: self.reload()) def edit(self, key): return lambda value: self.draft.__setitem__(key, value) # -- view --------------------------------------------------------------------------------------- def sidebar(self, t): def item(label, team_id, count, icon): return ui.list_item(label, lambda: self.show_team(team_id), icon=icon, end=ui.muted(str(count), size="sm"), selected=self.team_id == team_id, inset=True, height=30) teams = [item(team.name, team.id, len(team.heroes), "shield") for team in self.teams] return pz.container(pz.column([ ui.muted("TEAMS", size="xs", weight="semibold"), item("All heroes", "all", sum(len(t.heroes) for t in self.teams), "users"), *teams, item(NO_TEAM, None, "", "user-x"), ], spacing=2), padding=8, width=230, height="fill", bg=t.panel) def panel(self, t): if self.editing is None: return None e = self.errors.get new = self.editing == "new" return pz.container(pz.column([ ui.heading("New hero" if new else self.editing.name, level=3), ui.field("Name", ui.input(self.draft["name"], self.edit("name"), width=180), error=e("name")), ui.field("Secret name", ui.input(self.draft["secret_name"], self.edit("secret_name"), width=180), error=e("secret_name")), ui.field("Age", ui.input(self.draft["age"], self.edit("age"), placeholder="unknown", width=180), error=e("age")), ui.field("Team", ui.select([NO_TEAM, *[team.name for team in self.teams]], self.draft["team"], self.edit("team"), width=180)), pz.row([None if new else ui.button("Delete", self.delete, icon="trash-2", variant="ghost", tint="error"), pz.hspace(), ui.button("Cancel", lambda: setattr(self, "editing", None), variant="ghost"), ui.button("Save", self.save, icon="check", variant="solid")], spacing=6), ], spacing=12), padding=16, width=380, height="fill", bg=t.panel) def view(self): t = self.resolved_theme() columns = [ui.Column("Name", "name", width="fill:2"), ui.Column("Secret name", "secret_name", width="fill:2"), ui.Column("Age", lambda h: "—" if h.age is None else h.age, width=60, align="end"), ui.Column("Team", lambda h: h.team.name if h.team else "—")] main = pz.column([ pz.row([ui.segmented(["All ages", "Age ≥ 35"], "Age ≥ 35" if self.older else "All ages", self.toggle_older), pz.hspace(), ui.button("New hero", lambda: self.open("new"), icon="plus", variant="solid")], align="center"), ui.table(columns, self.heroes, on_row_press=self.open, selected=self.editing, height="fill"), ], spacing=12, padding=16) return pz.row([self.sidebar(t), pz.container(main, width="fill", height="fill", bg=t.editor), self.panel(t)]) if __name__ == "__main__": pz.run(Heroes) ``` **models.py:** ```py """The tables of SQLModel's tutorial ("Relationship Attributes"): https://sqlmodel.tiangolo.com/tutorial/""" from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select class Team(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) headquarters: str heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str age: int | None = Field(default=None, index=True) team_id: int | None = Field(default=None, foreign_key="team.id") team: Team | None = Relationship(back_populates="heroes") class HeroCreate(SQLModel): """A data model (no table=True) that validates what the form typed, as in the "Multiple Models" chapter.""" name: str = Field(min_length=1) secret_name: str = Field(min_length=1) age: int | None = Field(default=None, ge=0) team_id: int | None = None sqlite_file_name = "database.db" sqlite_url = f"sqlite:///{sqlite_file_name}" engine = create_engine(sqlite_url) def create_db_and_tables(): SQLModel.metadata.create_all(engine) def create_heroes(): """The tutorial's heroes and teams, added the first time.""" with Session(engine) as session: if session.exec(select(Hero)).first(): return team_preventers = Team(name="Preventers", headquarters="Sharp Tower") team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add_all([ Hero(name="Deadpond", secret_name="Dive Wilson", team=team_z_force), Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers), Hero(name="Spider-Boy", secret_name="Pedro Parqueador", team=team_preventers), Team(name="Wakaland", headquarters="Wakaland Capital City", heroes=[ Hero(name="Black Lion", secret_name="Trevor Challa", age=35), Hero(name="Princess Sure-E", secret_name="Sure-E"), ]), Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32, team=team_preventers), Hero(name="Dr. Weird", secret_name="Steve Weird", age=36, team=team_preventers), Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93, team=team_preventers), ]) session.commit() ``` **db.py:** ```py """Queries the window runs (in worker threads): select(), where(), relationships, add, delete.""" from sqlalchemy.orm import selectinload from sqlmodel import Session, col, select from models import Hero, HeroCreate, Team, create_db_and_tables, create_heroes, engine def setup() -> None: create_db_and_tables() create_heroes() def teams() -> list[Team]: with Session(engine) as session: # Load each team's heroes now: relationships can't be read after the session closes. return list(session.exec(select(Team).options(selectinload(Team.heroes)).order_by(Team.name))) def heroes(team_id: int | None | str, min_age: int | None) -> list[Hero]: """team_id is a team id, None for heroes without a team, or "all".""" statement = select(Hero).options(selectinload(Hero.team)).order_by(Hero.name) if team_id != "all": statement = statement.where(Hero.team_id == team_id) if min_age is not None: statement = statement.where(col(Hero.age) >= min_age) # the tutorial's "where" chapter with Session(engine) as session: return list(session.exec(statement)) def save(hero_id: int | None, data: HeroCreate) -> None: with Session(engine) as session: hero = session.get(Hero, hero_id) if hero_id else Hero.model_validate(data) hero.sqlmodel_update(data.model_dump()) session.add(hero) session.commit() def delete(hero_id: int) -> None: with Session(engine) as session: session.delete(session.get(Hero, hero_id)) session.commit() ``` #### One model for the form and the table: the inventory demo SQLModel joins Pydantic and SQLAlchemy: one class describes the fields, their rules and the table. In a desktop app that means the form and the database agree by construction. This demo is an inventory with stock levels, a donut chart and a dialog whose fields show Pydantic's errors. (Image: An inventory table with stock badges and an edit dialog showing validation errors) 1. **Put the rules in a base class.** The table and the form's validator both inherit from it: ```python class ProductBase(SQLModel): name: str = Field(min_length=2, max_length=60) sku: str category: str = "Keyboards" price: float = Field(gt=0) stock: int = Field(ge=0) @field_validator("sku") @classmethod def sku_format(cls, sku): sku = sku.strip().upper() if not re.fullmatch(r"[A-Z]{3}-\d{4}", sku): raise ValueError("Expected something like KEY-0001") return sku class Product(ProductBase, table=True): id: int | None = Field(default=None, primary_key=True) sku: str = Field(unique=True, index=True) class ProductIn(ProductBase): """Validates what the form typed.""" ``` 2. **Validate with `ProductIn`, not `Product`.** SQLModel skips validation on `table=True` models (SQLAlchemy builds them), so a separate class without `table=True` does it. The form keeps everything as text and Pydantic coerces it: `"12.5"` becomes `12.5`, `"abc"` becomes an error. ```python def save(self): try: data = ProductIn.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) return None product_id = None if self.editing == "new" else self.editing.id return Command.perform(lambda: models.save(product_id, data), self.saved) ``` 3. **Store it in a thread.** `models.save` copies the validated data onto the row with `sqlmodel_update` and returns `{field: message}` when something only the database knows is wrong, like a duplicated SKU: ```python def save(product_id, data): with Session(engine, expire_on_commit=False) as session: clash = session.exec(select(Product).where(Product.sku == data.sku, Product.id != product_id)).first() if clash: return {"sku": f"Already used by “{clash.name}”"} product = session.get(Product, product_id) if product_id else Product.model_validate(data) product.sqlmodel_update(data.model_dump()) session.add(product) session.commit() return product ``` 4. **Show the errors in a dialog.** `ui.layer` puts a `ui.modal` over the page; each `ui.field` takes its message from `self.errors`: ```python ui.field("Price", ui.input(self.draft["price"], self.edit("price")), error=self.errors.get("price")) ``` 5. **Render cells with widgets.** A `ui.Column` can draw anything; the stock column shows buttons and a badge that turns amber or red when stock runs low: ```python ui.Column("Stock", lambda p: p, width=180, align="center", render=self.stock_cell, sortable=False) ``` **Note:** The same `ProductIn` can be the body of a FastAPI endpoint. If your app grows a server, the desktop client and the API validate with one model. See [FastAPI](https://albakore.github.io/pyzed/integrations/fastapi/). **main.py:** ```py """An inventory app on SQLModel: the form is validated by the same model that defines the table. uv run main.py The database (inventory.db) is created next to this file on the first run. Walkthrough: https://albakore.github.io/pyzed/integrations/sqlmodel/ """ from __future__ import annotations from pydantic import ValidationError import pyzed as pz from pyzed import Command, ui import models from models import CATEGORIES, Product, ProductIn LOW_STOCK = 5 class Inventory(pz.App): title = "Inventory · SQLModel" size = (1100, 680) def __init__(self): self.products: list[Product] = [] self.category = "All" self.editing = None # a Product, "new" or None self.draft: dict[str, str] = {} self.errors: dict[str, str] = {} def on_start(self): return Command.perform(lambda: (models.setup(), models.products())[1], self.loaded) def loaded(self, products: list[Product]): self.products = products def reload(self): return Command.perform(models.products, self.loaded) # -- the form ---------------------------------------------------------------------- def open(self, product): self.editing, self.errors = product, {} if product == "new": self.draft = {"name": "", "sku": "", "category": CATEGORIES[0], "price": "", "stock": "0"} else: self.draft = {key: str(getattr(product, key)) for key in ("name", "sku", "category", "price", "stock")} def edit(self, key: str): return lambda value: self.draft.__setitem__(key, value) def save(self): try: data = ProductIn.model_validate(self.draft) # strings from the inputs are coerced or rejected here except ValidationError as error: self.errors = pz.validation_errors(error) return None product_id = None if self.editing == "new" else self.editing.id return Command.perform(lambda: models.save(product_id, data), self.saved) def saved(self, result): if isinstance(result, dict): self.errors = result return None self.editing = None return self.reload() def restock(self, product: Product, amount: int): return Command.perform(lambda: models.restock(product.id, amount), lambda _: self.reload()) def on_key(self, combo): if combo == "escape": self.editing = None elif combo == "ctrl-n": self.open("new") # -- view ---------------------------------------------------------------------------- def stock_cell(self, product: Product): if product.stock == 0: badge = ui.badge("out of stock", tint="error") elif product.stock < LOW_STOCK: badge = ui.badge(f"{product.stock} left", tint="warning") else: badge = ui.label(str(product.stock), mono=True) return pz.row([ ui.icon_button("minus", lambda: self.restock(product, -1), size="sm", tooltip="Sell one"), pz.container(badge, width=96, align_x="center"), ui.icon_button("plus", lambda: self.restock(product, 1), size="sm", tooltip="Receive one"), ], spacing=4, align="center") def summary(self, t): value = sum(p.price * p.stock for p in self.products) by_category = [sum(p.price * p.stock for p in self.products if p.category == c) for c in CATEGORIES] palette = [t.accent, t.success, t.warning, t.error, t.players[3]] legend = pz.column([pz.row([ui.indicator(color), ui.muted(c, size="sm")], spacing=6, align="center") for c, color in zip(CATEGORIES, palette)], spacing=2) return pz.row([ ui.stat("Stock value", f"${value:,.0f}"), ui.stat("Low stock", sum(0 < p.stock < LOW_STOCK for p in self.products)), ui.stat("Out of stock", sum(p.stock == 0 for p in self.products)), ui.card(pz.row([ui.donut(by_category, colors=palette, size=84, thickness=12), legend], spacing=14, align="center"), padding=10), ], spacing=12, align="center") def dialog(self): if self.editing is None: return None e = self.errors.get fields = pz.column([ ui.field("Name", ui.input(self.draft["name"], self.edit("name"), width=240), error=e("name")), ui.field("SKU", ui.input(self.draft["sku"], self.edit("sku"), placeholder="KEY-0003", mono=True, width=240), description="Three letters, a dash and four digits", error=e("sku")), ui.field("Category", ui.select(CATEGORIES, self.draft["category"], self.edit("category"), width=240)), ui.field("Price", ui.input(self.draft["price"], self.edit("price"), placeholder="0.00", width=240), error=e("price")), ui.field("Stock", ui.input(self.draft["stock"], self.edit("stock"), width=240), error=e("stock")), ], spacing=12) close = lambda: setattr(self, "editing", None) # noqa: E731 return ui.modal(fields, title="New product" if self.editing == "new" else "Edit product", on_close=close, width=520, footer=[ui.button("Cancel", close, variant="ghost"), ui.button("Save", self.save, icon="check", variant="solid")]) def view(self): t = self.resolved_theme() shown = [p for p in self.products if self.category in ("All", p.category)] columns = [ ui.Column("Product", "name", width="fill:3"), ui.Column("SKU", "sku", render=lambda sku: ui.label(sku, mono=True, color="text_muted")), ui.Column("Category", "category"), ui.Column("Price", "price", align="end", render=lambda price: ui.label(f"${price:,.2f}", mono=True)), ui.Column("Stock", lambda p: p, width=180, align="center", render=self.stock_cell, sortable=False), ] toolbar = pz.row([ ui.segmented(["All", *CATEGORIES], self.category, lambda c: setattr(self, "category", c)), pz.hspace(), ui.button("New product", lambda: self.open("new"), icon="package-plus", variant="solid", keys="ctrl-n"), ], align="center") page = pz.container( pz.column([self.summary(t), toolbar, ui.table(columns, shown, on_row_press=self.open, row_height=40, height="fill")], spacing=14), padding=16, width="fill", height="fill", bg=t.editor, ) return ui.layer(page, self.dialog(), on_dismiss=lambda: setattr(self, "editing", None), backdrop="#00000066") if __name__ == "__main__": pz.run(Inventory) ``` **models.py:** ```py """SQLModel models: one class is both the Pydantic schema and the database table.""" from __future__ import annotations import re from pathlib import Path from pydantic import field_validator from sqlmodel import Field, Session, SQLModel, create_engine, select CATEGORIES = ["Keyboards", "Mice", "Monitors", "Audio", "Cables"] engine = create_engine(f"sqlite:///{Path(__file__).with_name('inventory.db')}") class ProductBase(SQLModel): """The fields and their rules, shared by the form and the table.""" name: str = Field(min_length=2, max_length=60) sku: str category: str = "Keyboards" price: float = Field(gt=0) stock: int = Field(ge=0) @field_validator("sku") @classmethod def sku_format(cls, sku: str) -> str: sku = sku.strip().upper() if not re.fullmatch(r"[A-Z]{3}-\d{4}", sku): raise ValueError("Expected something like KEY-0001") return sku class Product(ProductBase, table=True): # Table models skip validation on purpose (SQLAlchemy builds them); validate with ProductIn first. id: int | None = Field(default=None, primary_key=True) sku: str = Field(unique=True, index=True) class ProductIn(ProductBase): """Validates what the form typed: "12.5" becomes 12.5, "abc" becomes an error on that field.""" def setup() -> None: SQLModel.metadata.create_all(engine) with Session(engine) as session: if session.exec(select(Product)).first(): return session.add_all([ Product(name="Mechanical keyboard", sku="KEY-0001", category="Keyboards", price=129.0, stock=14), Product(name="Low-profile keyboard", sku="KEY-0002", category="Keyboards", price=89.5, stock=3), Product(name="Wireless mouse", sku="MOU-0001", category="Mice", price=49.9, stock=40), Product(name="Vertical mouse", sku="MOU-0002", category="Mice", price=59.0, stock=0), Product(name='27" 4K monitor', sku="MON-0001", category="Monitors", price=399.0, stock=6), Product(name='34" ultrawide', sku="MON-0002", category="Monitors", price=649.0, stock=2), Product(name="Studio headphones", sku="AUD-0001", category="Audio", price=149.0, stock=11), Product(name="USB microphone", sku="AUD-0002", category="Audio", price=99.0, stock=7), Product(name="USB-C cable 2 m", sku="CAB-0001", category="Cables", price=12.0, stock=120), Product(name="HDMI 2.1 cable", sku="CAB-0002", category="Cables", price=18.0, stock=4), ]) session.commit() def products() -> list[Product]: with Session(engine) as session: return list(session.exec(select(Product).order_by(Product.category, Product.name))) def save(product_id: int | None, data: ProductIn) -> Product | dict: """Stores validated data; returns ``{field: message}`` when the SKU is taken.""" with Session(engine, expire_on_commit=False) as session: clash = session.exec(select(Product).where(Product.sku == data.sku, Product.id != product_id)).first() if clash: return {"sku": f"Already used by “{clash.name}”"} product = session.get(Product, product_id) if product_id else Product.model_validate(data) product.sqlmodel_update(data.model_dump()) session.add(product) session.commit() return product def restock(product_id: int, amount: int) -> None: with Session(engine) as session: product = session.get(Product, product_id) product.stock = max(0, product.stock + amount) session.add(product) session.commit() ``` ### httpx > Call web APIs from a Pyzed app with httpx, following its quickstart: requests, an AsyncClient, concurrent calls, errors and streaming downloads with a progress bar. Source: https://albakore.github.io/pyzed/integrations/httpx/ [httpx](https://www.python-httpx.org/) is the HTTP client a Pyzed window uses to talk to any API: a public one, your own [FastAPI](https://albakore.github.io/pyzed/integrations/fastapi/) or [Django](https://albakore.github.io/pyzed/integrations/django/) server (the client and server [architecture](https://albakore.github.io/pyzed/integrations/architectures/)). Two demos: - **`httpx_pypi`**: concurrent requests to PyPI's JSON API, shown as they arrive. - **`httpx_download`**: a download manager with streaming responses and a progress bar. #### The basics, from httpx's quickstart httpx has the same API in sync and async form. In a Pyzed app use either: **AsyncClient (recommended):** ```python import httpx class App(pz.App): async def load(self): # A coroutine returned from a callback runs on Pyzed's asyncio loop. async with httpx.AsyncClient() as client: response = await client.get("https://pypi.org/pypi/httpx/json") response.raise_for_status() return ("loaded", response.json()) def update(self, message): kind, data = message self.info = data["info"] ``` **Sync API in a thread:** ```python import httpx def fetch(): response = httpx.get("https://pypi.org/pypi/httpx/json") response.raise_for_status() return response.json() class App(pz.App): def load(self): # A blocking call runs in a worker thread; then() receives the result. return Command.perform(fetch, lambda data: setattr(self, "info", data["info"])) ``` The rest of the quickstart applies unchanged: | Task | httpx | | --- | --- | | Query parameters | `client.get(url, params={"q": "pyzed", "page": 2})` | | Send JSON | `client.post(url, json={"name": "Deadpond"})` | | Send a form or files | `client.post(url, data={...}, files={"upload": open("a.png", "rb")})` | | Headers and auth | `httpx.AsyncClient(headers={"Authorization": f"Bearer {token}"})`, `auth=("user", "pass")` | | A base URL | `httpx.AsyncClient(base_url="https://api.example.com")` then `client.get("/items/")` | | Timeouts | `httpx.AsyncClient(timeout=10)` | | Errors | `response.raise_for_status()`; catch `httpx.HTTPStatusError` and `httpx.RequestError` | **Tip:** Create one `AsyncClient` and reuse it, as [httpx recommends](https://www.python-httpx.org/advanced/clients/), to keep connections open. All of Pyzed's coroutines share one asyncio loop, so a client created inside a coroutine can be stored on `self` and used by later ones (the FastAPI demo does this). #### Concurrent requests: the PyPI explorer (Image: A PyPI explorer listing packages with versions, and the details of FastAPI) 1. **Return a value for every outcome.** The model, or a short text for the list: ```python async def fetch(client, name): try: response = await client.get(f"https://pypi.org/pypi/{name}/json") except httpx.HTTPError as error: return f"Network error: {error}" if response.status_code == 404: return "Not on PyPI" return Package.model_validate_json(response.content) # see the Pydantic page ``` 2. **Send the requests at once and yield each answer.** An async generator returned from `on_start` or a callback sends every `yield` to `update`, so the list fills in as answers arrive: ```python async def fetch_all(names): async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client: async def one(name): return name, await fetch(client, name) for done in asyncio.as_completed([one(n) for n in names]): yield await done class Explorer(pz.App): def on_start(self): return fetch_all(self.names) def update(self, message): name, result = message self.packages[name] = result ``` **main.py:** ```py """A PyPI explorer: concurrent httpx requests streamed into the window, responses validated by Pydantic. uv run main.py uv run main.py rich textual polars # start with other packages Walkthrough: https://albakore.github.io/pyzed/integrations/httpx/ """ from __future__ import annotations import sys import webbrowser from datetime import datetime, timezone import pyzed as pz from pyzed import ui from pypi import Package, fetch_all DEFAULT = ["pyzed-ui", "fastapi", "django", "sqlalchemy", "sqlmodel", "httpx", "pydantic", "requests"] def ago(when: datetime) -> str: days = (datetime.now(timezone.utc) - when).days if days < 1: return "today" if days < 60: return f"{days} days ago" return f"{days // 30} months ago" if days < 730 else f"{days // 365} years ago" class Explorer(pz.App): title = "PyPI explorer · httpx + Pydantic" size = (1100, 680) def __init__(self, names: list[str]): self.names = names self.packages: dict[str, Package | str | None] = {name: None for name in names} # None while loading self.selected = names[0] if names else None self.new_name = "" def on_start(self): return fetch_all(self.names) # an async generator: each (name, result) it yields reaches update() def update(self, message): name, result = message self.packages[name] = result def add(self, *_): name = self.new_name.strip().lower() self.new_name = "" if not name: return None self.selected = name if name in self.packages and isinstance(self.packages[name], Package): return None self.packages[name] = None return fetch_all([name]) # -- view ---------------------------------------------------------------------------- def row(self, name: str): package = self.packages[name] if package is None: end = ui.spinner(size=12) elif isinstance(package, str): end = pz.icon("circle-alert", size=14, color=self.resolved_theme().error) else: end = ui.muted(package.info.version, size="sm", mono=True) return ui.list_item(name, lambda: setattr(self, "selected", name), icon="package", end=end, selected=name == self.selected, inset=True, height=30) def details(self, t): package = self.packages.get(self.selected) if package is None: return pz.center(ui.spinner(size=24)) if isinstance(package, str): return pz.center(ui.empty_state(self.selected, package, icon="circle-alert")) info, history = package.info, package.history() years = package.per_year() links = [ui.button(label, lambda url=url: webbrowser.open(url), icon="external-link", size="sm", variant="ghost") for label, url in list((info.project_urls or {}).items())[:4]] versions = ui.virtual_list(history, lambda item: pz.row([ ui.label(item[0], mono=True, width="fill"), ui.muted(item[1].strftime("%Y-%m-%d"), size="sm")], padding=(0, 8), align="center", height=26), item_height=26) return pz.column([ pz.row([ui.heading(info.name, level=2), ui.badge(info.version, tint="success")], spacing=10, align="center"), ui.muted(info.summary or "No summary.", size="lg"), pz.row(links, spacing=4) if links else None, pz.row([ ui.stat("Releases", len(history)), ui.stat("Latest", ago(history[0][1]) if history else "—"), ui.stat("Python", info.requires_python or "any"), ui.stat("License", (info.license_expression or info.license or "—")[:18]), ], spacing=12), pz.row([ ui.card(ui.bar_chart([count for _, count in years], labels=[str(y)[2:] for y, _ in years], width=340, height=150), title="Releases per year", width=374), ui.card(pz.container(versions, height=150), title="Versions", width="fill"), ], spacing=12), ], spacing=14, padding=20) def view(self): t = self.resolved_theme() loading = sum(p is None for p in self.packages.values()) sidebar = pz.container(pz.column([ ui.input(self.new_name, lambda v: setattr(self, "new_name", v), placeholder="Add a package…", icon="plus", on_submit=self.add), ui.virtual_list(list(self.packages), self.row, item_height=30), ui.muted(f"Loading {loading}…" if loading else f"{len(self.packages)} packages", size="sm"), ], spacing=8), padding=8, width=260, height="fill", bg=t.panel) return pz.row([sidebar, pz.container(self.details(t), width="fill", height="fill", bg=t.editor)]) if __name__ == "__main__": pz.run(Explorer(sys.argv[1:] or DEFAULT)) ``` **pypi.py:** ```py """PyPI's JSON API, described with Pydantic models and fetched with httpx.""" from __future__ import annotations import asyncio from collections import Counter from datetime import datetime from typing import AsyncIterator import httpx from pydantic import BaseModel, ValidationError class Info(BaseModel): name: str version: str summary: str | None = None license: str | None = None license_expression: str | None = None # newer packages declare an SPDX expression instead requires_python: str | None = None project_urls: dict[str, str] | None = None class File(BaseModel): upload_time_iso_8601: datetime yanked: bool = False class Package(BaseModel): """Only the fields the app shows; Pydantic ignores the rest of the response.""" info: Info releases: dict[str, list[File]] def history(self) -> list[tuple[str, datetime]]: """(version, first upload) for every release with files, newest first.""" dated = [(version, min(f.upload_time_iso_8601 for f in files)) for version, files in self.releases.items() if files] return sorted(dated, key=lambda item: item[1], reverse=True) def per_year(self, years: int = 8) -> list[tuple[int, int]]: counts = Counter(date.year for _, date in self.history()) last = datetime.now().year return [(year, counts.get(year, 0)) for year in range(last - years + 1, last + 1)] async def fetch(client: httpx.AsyncClient, name: str) -> Package | str: """The package, or a short message saying why it couldn't be loaded.""" try: response = await client.get(f"https://pypi.org/pypi/{name}/json") except httpx.HTTPError as error: return f"Network error: {error or type(error).__name__}" if response.status_code == 404: return "Not on PyPI" if response.is_error: return f"PyPI answered {response.status_code}" try: return Package.model_validate_json(response.content) except ValidationError as error: return f"Unexpected response: {error.error_count()} problems" async def fetch_all(names: list[str]) -> AsyncIterator[tuple[str, Package | str]]: """Requests every package at once and yields each one as soon as it arrives.""" async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client: async def one(name): return name, await fetch(client, name) for done in asyncio.as_completed([one(n) for n in names]): yield await done ``` #### Streaming downloads with progress (Image: A download manager with progress bars) httpx's [streaming responses](https://www.python-httpx.org/quickstart/#streaming-responses) read the body in chunks instead of loading it into memory, and its [download progress](https://www.python-httpx.org/advanced/clients/#monitoring-download-progress) recipe uses `response.num_bytes_downloaded`. In Pyzed the loop becomes an async generator that yields after every chunk, and the window redraws the bar each time: ```python async def download(item): async with httpx.AsyncClient(follow_redirects=True) as client: async with client.stream("GET", item.url) as response: response.raise_for_status() item.total = int(response.headers["Content-Length"]) with open(FOLDER / item.name, "wb") as file: async for chunk in response.aiter_bytes(): if item.cancel: # set by the Cancel button break file.write(chunk) item.done = response.num_bytes_downloaded yield item # reaches update(), which redraws class Downloads(pz.App): def start(self, *_): item = Download(self.url) self.items.insert(0, item) return download(item) def view(self): ... ui.progress(item.done / item.total) ``` Several downloads can run at the same time: each one is its own generator on the same loop. `examples/integrations/httpx_download/main.py`: ```py """A download manager built on httpx's streaming responses, with a live progress bar. uv run main.py It follows httpx's "Monitoring download progress" recipe (https://www.python-httpx.org/advanced/clients/#monitoring-download-progress), written with ``AsyncClient.stream`` and ``aiter_bytes``. The download is an async generator: every progress report it yields reaches ``update``, so the window redraws while the bytes arrive. Files are saved in a ``downloads`` folder next to this file. Walkthrough: https://albakore.github.io/pyzed/integrations/httpx/ """ from __future__ import annotations import time from pathlib import Path from urllib.parse import urlsplit import httpx import pyzed as pz from pyzed import ui FOLDER = Path(__file__).with_name("downloads") EXAMPLE = ("https://files.pythonhosted.org/packages/54/a4/f8188c4f3e07f7737683588210c073478abcb542048cf4ab6fedad0b458a/" "numpy-2.1.0.tar.gz") def size(num_bytes: float) -> str: for unit in ("B", "KB", "MB", "GB"): if num_bytes < 1024 or unit == "GB": return f"{num_bytes:.0f} {unit}" if unit == "B" else f"{num_bytes:.1f} {unit}" num_bytes /= 1024 class Download: """One row of the list. ``total`` is None when the server sends no Content-Length.""" def __init__(self, url: str): self.url = url self.name = Path(urlsplit(url).path).name or "download" self.done, self.total = 0, None self.speed = 0.0 self.state = "waiting" # waiting, downloading, finished, cancelled or the error text self.cancel = False # set by the Cancel button, read by the loop below async def download(item: Download): """Streams ``item.url`` to disk, yielding ``item`` after every chunk.""" FOLDER.mkdir(exist_ok=True) started = time.monotonic() try: async with httpx.AsyncClient(follow_redirects=True, timeout=30) as client: async with client.stream("GET", item.url) as response: response.raise_for_status() length = response.headers.get("Content-Length") item.total, item.state = (int(length) if length else None), "downloading" with open(FOLDER / item.name, "wb") as file: async for chunk in response.aiter_bytes(): if item.cancel: item.state = "cancelled" break file.write(chunk) item.done = response.num_bytes_downloaded item.speed = item.done / max(time.monotonic() - started, 1e-6) yield item if item.state == "downloading": item.state = "finished" except httpx.HTTPStatusError as error: item.state = f"The server answered {error.response.status_code}" except httpx.HTTPError as error: item.state = f"{type(error).__name__}: {error}" yield item class Downloads(pz.App): title = "Downloads · httpx" size = (760, 480) def __init__(self): self.url = EXAMPLE self.items: list[Download] = [] def start(self, *_): url = self.url.strip() if not url.startswith(("http://", "https://")): return None item = Download(url) self.items.insert(0, item) return download(item) # Pyzed runs the generator on its loop and sends each yield to update() def update(self, item: Download): pass # the Download object was changed in place; returning from update redraws the window def row(self, item: Download): t = self.resolved_theme() if item.state == "downloading": detail = f"{size(item.done)} of {size(item.total) if item.total else '?'} · {size(item.speed)}/s" action = ui.icon_button("x", lambda: setattr(item, "cancel", True), tooltip="Cancel") elif item.state == "finished": detail, action = f"{size(item.done)} · saved in downloads/", None else: detail, action = item.state.capitalize(), None failed = item.state not in ("waiting", "downloading", "finished") value = item.done / item.total if item.total else (1.0 if item.state == "finished" else 0.0) return ui.card(pz.column([ pz.row([pz.icon("file-down", size=16, color=t.icon_muted), ui.label(item.name, width="fill"), action], spacing=8, align="center"), ui.progress(value, tint="error" if failed else "success" if item.state == "finished" else "accent"), ui.muted(detail, size="sm"), ], spacing=8), padding=12) def view(self): t = self.resolved_theme() body = pz.scrollable(pz.column([self.row(i) for i in self.items], spacing=8), height="fill") if self.items \ else ui.empty_state("No downloads", "Paste a URL and press Download.", icon="download") return pz.container(pz.column([ pz.row([ui.input(self.url, lambda v: setattr(self, "url", v), icon="link", on_submit=self.start), ui.button("Download", self.start, icon="download", variant="solid")], spacing=8), body, ], spacing=14), padding=20, width="fill", height="fill", bg=t.editor) if __name__ == "__main__": pz.run(Downloads) ``` ### Pydantic > Validate Pyzed forms with Pydantic models, show the errors on each field, and parse or save JSON with model_validate_json and model_dump_json. Source: https://albakore.github.io/pyzed/integrations/pydantic/ [Pydantic](https://docs.pydantic.dev/) checks data against type hints. In a desktop app it does two jobs: it validates what the user typed in a form, and it parses JSON from files and APIs. It runs in the same process as the window, with no thread or loop needed, because validation is fast. (Image: A sign-up form with Pydantic errors and the saved accounts as JSON) 1. **Write the rules as a model.** [Fields](https://docs.pydantic.dev/latest/concepts/fields/) carry the constraints, special types like `EmailStr` (from `pydantic[email]`) and `SecretStr` do the rest, and [validators](https://docs.pydantic.dev/latest/concepts/validators/) add custom checks. An `after` model validator compares two fields: ```python class SignUp(BaseModel): username: str = Field(min_length=3, max_length=20, pattern=r"^[a-z0-9_]+$") email: EmailStr birthday: date plan: Literal["free", "pro", "team"] = "free" password: SecretStr password_repeat: SecretStr newsletter: bool = False @field_validator("username", mode="before") @classmethod def lowercase(cls, value: str) -> str: return value.strip().lower() @model_validator(mode="after") def passwords_match(self) -> "SignUp": if self.password.get_secret_value() != self.password_repeat.get_secret_value(): raise ValueError("The passwords don't match") return self ``` 2. **Keep the form as plain values and validate on submit.** Inputs give strings; Pydantic converts `"1990-05-02"` to a `date` and reports every problem at once: ```python def submit(self, *_): try: signup = SignUp.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) return ... ``` 3. **Show each error under its field.** `pz.validation_errors` returns `{field: message}`; errors from a model validator have the key `""`: ```python ui.field("Email", ui.input(d["email"], self.edit("email")), error=self.errors.get("email")) ui.callout(self.errors[""], tint="error") if self.errors.get("") else None ``` 4. **Save and load JSON.** [Serialization](https://docs.pydantic.dev/latest/concepts/serialization/) with `model_dump` and `model_dump_json`, and a `TypeAdapter` for a whole list: ```python account = Account.model_validate(signup.model_dump(exclude={"password", "password_repeat"})) Accounts = TypeAdapter(list[Account]) FILE.write_bytes(Accounts.dump_json(self.accounts, indent=2)) accounts = Accounts.validate_json(FILE.read_bytes()) ``` #### Parsing API responses The same models parse JSON from the network. Declare only the fields you use; Pydantic ignores the rest, converts dates and fails clearly if the API changes shape. From the [PyPI explorer](https://albakore.github.io/pyzed/integrations/httpx/#concurrent-requests-the-pypi-explorer): ```python class Info(BaseModel): name: str version: str summary: str | None = None class File(BaseModel): upload_time_iso_8601: datetime class Package(BaseModel): info: Info releases: dict[str, list[File]] package = Package.model_validate_json(response.content) ``` **Tip:** Pydantic models are what FastAPI uses for request bodies and what SQLModel builds on. A model written for the desktop form can become the body of an API endpoint later; see [FastAPI](https://albakore.github.io/pyzed/integrations/fastapi/) and [SQLModel](https://albakore.github.io/pyzed/integrations/sqlmodel/). #### Full code **main.py:** ```py """A sign-up form whose rules live in a Pydantic model; accounts are saved to accounts.json. uv run main.py The window keeps what the user types as plain strings and hands them to ``SignUp.model_validate``: Pydantic converts them (a date, an email, a bool), checks every rule and reports the errors per field, which ``pz.validation_errors`` turns into ``{field: message}``. Walkthrough: https://albakore.github.io/pyzed/integrations/pydantic/ """ from __future__ import annotations from pathlib import Path from pydantic import ValidationError import pyzed as pz from pyzed import ui from models import Account, Accounts, SignUp FILE = Path(__file__).with_name("accounts.json") EMPTY = {"username": "", "email": "", "birthday": "", "plan": "free", "password": "", "password_repeat": "", "newsletter": False} def load() -> list[Account]: return Accounts.validate_json(FILE.read_bytes()) if FILE.exists() else [] class SignUpForm(pz.App): title = "Sign up · Pydantic" size = (860, 600) def __init__(self): self.draft = dict(EMPTY) self.errors: dict[str, str] = {} self.accounts = load() self.preview = "" # the JSON Pydantic produced for the last account def edit(self, key): return lambda value: self.draft.__setitem__(key, value) def submit(self, *_): try: signup = SignUp.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) # the model validator's error has the key "" return account = Account.model_validate(signup.model_dump(exclude={"password", "password_repeat"})) if any(a.username == account.username for a in self.accounts): self.errors = {"username": "That username is taken"} return self.accounts.append(account) FILE.write_bytes(Accounts.dump_json(self.accounts, indent=2)) self.preview = account.model_dump_json(indent=2) self.draft, self.errors = dict(EMPTY), {} def form(self): e, d = self.errors.get, self.draft def text(key, title, placeholder="", **kwargs): return ui.field(title, ui.input(d[key], self.edit(key), placeholder=placeholder, on_submit=self.submit, **kwargs), error=e(key)) return pz.column([ ui.heading("Create an account", level=2), text("username", "Username", "lowercase letters, digits and _"), text("email", "Email", "you@example.com"), text("birthday", "Birthday", "YYYY-MM-DD"), ui.field("Plan", ui.segmented(["free", "pro", "team"], d["plan"], self.edit("plan"))), text("password", "Password", "8 characters or more", secure=True), text("password_repeat", "Repeat the password", secure=True), ui.checkbox(d["newsletter"], self.edit("newsletter"), label="Send me the newsletter"), ui.callout(e(""), tint="error") if e("") else None, ui.button("Sign up", self.submit, icon="user-plus", variant="solid", full_width=True), ], spacing=12, padding=24, width=420) def side(self, t): rows = [ui.list_item(a.username, None, icon="user", end=ui.badge(a.plan), inset=True, height=30) for a in reversed(self.accounts)] return pz.container(pz.column([ ui.muted("accounts.json", size="xs", mono=True), *rows, ui.card(ui.label(self.preview, mono=True, size="sm"), title="model_dump_json()") if self.preview else None, ], spacing=6), padding=16, width="fill", height="fill", bg=t.panel) def view(self): t = self.resolved_theme() return pz.row([pz.container(pz.scrollable(self.form()), height="fill", bg=t.editor), self.side(t)]) if __name__ == "__main__": pz.run(SignUpForm) ``` **models.py:** ```py """The form's rules, written as Pydantic models (https://docs.pydantic.dev/latest/concepts/models/).""" from __future__ import annotations from datetime import date from typing import Literal from pydantic import BaseModel, EmailStr, Field, SecretStr, TypeAdapter, field_validator, model_validator class SignUp(BaseModel): username: str = Field(min_length=3, max_length=20, pattern=r"^[a-z0-9_]+$") email: EmailStr birthday: date plan: Literal["free", "pro", "team"] = "free" password: SecretStr # repr() and str() show ******** password_repeat: SecretStr newsletter: bool = False @field_validator("username", mode="before") @classmethod def lowercase(cls, value: str) -> str: return value.strip().lower() @field_validator("birthday") @classmethod def old_enough(cls, value: date) -> date: today = date.today() age = today.year - value.year - ((today.month, today.day) < (value.month, value.day)) if age < 13: raise ValueError("You must be at least 13 years old") return value @field_validator("password") @classmethod def long_enough(cls, value: SecretStr) -> SecretStr: if len(value.get_secret_value()) < 8: raise ValueError("Use 8 characters or more") return value @model_validator(mode="after") def passwords_match(self) -> SignUp: if self.password.get_secret_value() != self.password_repeat.get_secret_value(): raise ValueError("The passwords don't match") return self class Account(BaseModel): """What gets saved: the sign-up without the passwords.""" username: str email: EmailStr birthday: date plan: Literal["free", "pro", "team"] newsletter: bool Accounts = TypeAdapter(list[Account]) # validates and serializes a whole list at once ``` ### Other libraries > Recipes for aiosqlite, pandas, Polars, requests, Redis and Matplotlib in a Pyzed desktop app. Source: https://albakore.github.io/pyzed/integrations/other-libraries/ Every library falls into one of the cases in the [overview](https://albakore.github.io/pyzed/integrations/overview/#where-your-code-runs): async, blocking, a server or a stream. These recipes cover the ones people ask about most. #### aiosqlite [aiosqlite](https://aiosqlite.omnilib.dev/) gives SQLite an async API. Because Pyzed runs every coroutine on one loop, a single connection opened in `on_start` can serve the whole app: (Image: A journal app storing entries with aiosqlite) ```python class Journal(pz.App): async def on_start(self): self.db = await aiosqlite.connect("journal.db") self.db.row_factory = aiosqlite.Row await self.db.execute("CREATE TABLE IF NOT EXISTS entries (...)") return await self.load() async def load(self): async with self.db.execute("SELECT * FROM entries ORDER BY id DESC") as cursor: return ("loaded", await cursor.fetchall()) ```
Full code: examples/integrations/aiosqlite_journal/main.py ```py """A journal on aiosqlite: one async SQLite connection living on Pyzed's asyncio loop. uv run main.py The database (journal.db) is created next to this file on the first run. Walkthrough: https://albakore.github.io/pyzed/integrations/other-libraries/#aiosqlite """ from __future__ import annotations from datetime import datetime from pathlib import Path import aiosqlite import pyzed as pz from pyzed import ui DATABASE = Path(__file__).with_name("journal.db") MOODS = {"great": ("smile", "success"), "okay": ("meh", "warning"), "rough": ("frown", "error")} class Journal(pz.App): title = "Journal · aiosqlite" size = (720, 560) def __init__(self): self.db: aiosqlite.Connection | None = None # opened on Pyzed's loop, used only from coroutines self.entries: list[aiosqlite.Row] = [] self.text = "" self.mood = "great" async def on_start(self): self.db = await aiosqlite.connect(DATABASE) self.db.row_factory = aiosqlite.Row await self.db.execute("""CREATE TABLE IF NOT EXISTS entries ( id INTEGER PRIMARY KEY, written TEXT NOT NULL, mood TEXT NOT NULL, text TEXT NOT NULL)""") await self.db.commit() return await self.load() async def load(self): async with self.db.execute("SELECT * FROM entries ORDER BY id DESC LIMIT 200") as cursor: return ("loaded", await cursor.fetchall()) async def add(self, text: str, mood: str): await self.db.execute("INSERT INTO entries (written, mood, text) VALUES (?, ?, ?)", (datetime.now().isoformat(timespec="minutes"), mood, text)) await self.db.commit() return await self.load() async def remove(self, entry_id: int): await self.db.execute("DELETE FROM entries WHERE id = ?", (entry_id,)) await self.db.commit() return await self.load() def submit(self, *_): text, self.text = self.text.strip(), "" return self.add(text, self.mood) if text else None def update(self, message): kind, rows = message if kind == "loaded": self.entries = rows def entry(self, row): icon, tint = MOODS[row["mood"]] return ui.card(pz.row([ pz.icon(icon, size=18, color=self.resolved_theme().color(tint)), pz.column([ui.label(row["text"]), ui.muted(row["written"].replace("T", " · "), size="sm")], spacing=4, width="fill"), ui.icon_button("trash-2", lambda: self.remove(row["id"]), size="sm", tooltip="Delete"), ], spacing=12, align="center"), padding=12) def view(self): t = self.resolved_theme() composer = pz.row([ ui.toggle_group([(mood, icon) for mood, (icon, _) in MOODS.items()], self.mood, lambda m: setattr(self, "mood", m)), ui.input(self.text, lambda v: setattr(self, "text", v), placeholder="How was your day? (Enter to save)", on_submit=self.submit), ], spacing=8, align="center") entries = pz.scrollable(pz.column([self.entry(r) for r in self.entries], spacing=8), height="fill") \ if self.entries else ui.empty_state("Nothing written yet", "Your entries are stored in journal.db.", icon="notebook-pen") return pz.container(pz.column([ui.heading("Journal", level=2), composer, entries], spacing=14), padding=20, width="fill", height="fill", bg=t.editor) if __name__ == "__main__": pz.run(Journal) ```
#### SQLAlchemy asyncio Covered on the [SQLAlchemy page](https://albakore.github.io/pyzed/integrations/sqlalchemy/#the-asyncio-version): the `sqlalchemy_quickstart` demo runs the same window on a sync `Session` or on an `AsyncSession` with aiosqlite (`uv run main.py --async`). #### pandas and Polars `ui.table` takes a list of dicts, which is one call away from a DataFrame. Read big files in a thread so the window stays responsive: ```python import pandas as pd class Sales(pz.App): def __init__(self): self.frame = pd.DataFrame() def open(self, path): return pz.Command.perform(lambda: pd.read_csv(path), self.loaded) def loaded(self, frame): self.frame = frame self.rows = frame.to_dict("records") # Polars: frame.to_dicts() def view(self): columns = [ui.Column(name, name) for name in self.frame.columns] return ui.table(columns, self.rows, height="fill") ``` The table is virtualized, so hundreds of thousands of rows scroll smoothly. For filtering, run the query on the DataFrame (`frame[frame.city == city]`) and rebuild `self.rows`. #### requests and other blocking clients Any blocking call goes through `Command.perform`. `raise_for_status` errors are worth catching in the function so `then` always gets something to show: ```python import requests def fetch_weather(city): try: response = requests.get("https://wttr.in/" + city, params={"format": "j1"}, timeout=10) response.raise_for_status() return response.json() except requests.RequestException as error: return {"error": str(error)} class Weather(pz.App): def search(self, city): return pz.Command.perform(lambda: fetch_weather(city), self.show) ``` #### Redis pub/sub and other streams A stream of events is a subscription: `pz.listen` runs an async generator while it appears in `subscriptions()` and stops it when it no longer does: ```python import redis.asyncio as redis async def events(): client = redis.Redis() async with client.pubsub() as pubsub: await pubsub.subscribe("orders") async for message in pubsub.listen(): if message["type"] == "message": yield ("order", message["data"].decode()) class Orders(pz.App): def subscriptions(self): return [pz.listen(events)] if self.live else [] ``` WebSockets work the same way; see `examples/websocket_stream.py` and the [Async guide](https://albakore.github.io/pyzed/guides/async/#subscriptions-timers-and-streams). #### Matplotlib Pyzed draws its own charts (`ui.bar_chart`, `ui.sparkline`, `ui.donut`, `pz.canvas`), but a Matplotlib figure can be shown as an image. `pz.image` accepts PNG bytes; build the figure with `matplotlib.figure.Figure` (not `pyplot`) so it is safe to render in a thread: ```python import io from matplotlib.figure import Figure def render(values) -> bytes: figure = Figure(figsize=(6, 3), dpi=144) figure.subplots().plot(values) buffer = io.BytesIO() figure.savefig(buffer, format="png", transparent=True) return buffer.getvalue() class Plot(pz.App): def refresh(self): return pz.Command.perform(lambda: render(self.values), lambda png: setattr(self, "png", png)) def view(self): return pz.image(self.png) if self.png else ui.spinner() ``` ## AI agents ### Pyzed for AI agents > Pyzed is an AI-native Python GUI framework: an MCP server, llms.txt, editor rules, an agent inside Pyzed Design and pyzed.agents let AI agents build, check and see desktop apps. Source: https://albakore.github.io/pyzed/ai/ :::tip[Works with any model] Everything in this section works with Claude, GPT, Gemini, local models through Ollama and any MCP client. Nothing here is required to use Pyzed. ::: :::caution[Provisional] `pyzed.agents` and the MCP server (`pyzed.mcp`) are provisional in Pyzed 1.x: a minor release may still adjust details, and every change is listed under *Breaking Changes* in the [release notes](https://albakore.github.io/pyzed/changelog/). ::: Pyzed is built to be written **with** AI agents, and to put AI **inside** your apps. An agent connected to Pyzed does not guess an API from memory: it looks up the exact signature of every component, follows Pyzed's rules, checks the code it writes and renders a screenshot of the window to see the result, all without opening a window. ```mermaid flowchart LR subgraph You["Your editor or agent"] A["AI agent
(Cursor, VS Code, Zed,
Claude Desktop...)"] end subgraph Pyzed["Pyzed"] M["MCP server
pyzed mcp"] K["Knowledge
components · icons · themes · docs"] G["Generators
new_app · new_component · new_form"] V["Checks
validate_code · validate_form"] S["Off-screen renderer
screenshot"] end D["Docs as text
llms.txt · page.md"] A -- "tools over stdio" --> M M --> K & G & V & S A -. "reads" .-> D S -- "PNG" --> A A -- "writes" --> P["Your Pyzed app"] ``` #### Three ways to work with AI | | What it is | Start here | | --- | --- | --- | | **Agents that write Pyzed** | Connect any agent that speaks [MCP](https://modelcontextprotocol.io) and it gets Pyzed's components, docs, generators, checks and screenshots. Agents without MCP can read the docs as plain text. | [MCP server](https://albakore.github.io/pyzed/ai/mcp/), [llms.txt and editor rules](https://albakore.github.io/pyzed/ai/llms-txt/) | | **The agent in Pyzed Design** | The visual designer has an agent panel that places controls, wires events, writes handlers and creates forms, components and whole apps; it sees images, uses MCP servers and runs external agents, with every change undoable. | [Agent in Pyzed Design](https://albakore.github.io/pyzed/ai/agent-composer/), [API keys](https://albakore.github.io/pyzed/ai/api-keys/) | | **AI inside your app** | `pyzed.agents` talks to many model providers with tools and streaming, and Pyzed's async model streams answers into the UI without freezing it. | [Building AI apps](https://albakore.github.io/pyzed/ai/ai-apps/) | #### Why agents write good Pyzed code - **One way to build a UI.** State on the app, `view()` returns widgets, events call methods. There is no widget tree to mutate, no layout manager to pick and no `mainloop` threading rules to remember, so there are fewer ways for generated code to go wrong. - **Everything is looked up, not guessed.** `describe_component("ui.table")` returns the real signature and parameter docs of the installed version, and `validate_code` rejects parameters that don't exist (`heigth=8`), side effects in `view()` and lambdas in loops before the code ever runs. - **The agent can see.** The `screenshot` tool renders the app off-screen (no display, no GPU) and returns a PNG, so the agent can check the layout and fix it, like a person would. - **Docs made for machines too.** Every page of this site exists as Markdown (`/pyzed/.md`), the whole site as [`llms.txt`](https://albakore.github.io/pyzed/llms.txt) and [`llms-full.txt`](https://albakore.github.io/pyzed/llms-full.txt), and the package ships typed stubs (`.pyi`) that editors and agents read. - **Blocks to start from.** The [Blocks](https://albakore.github.io/pyzed/blocks/) are complete, tested screens an agent can read with `read_example("blocks/dashboard")` and adapt, instead of starting from a blank file. #### What an agent can do with Pyzed | Ask | What happens | | --- | --- | | *"Build a Pomodoro timer with a progress bar"* | Reads the best practices, looks up `ui.progress`, writes the app, validates it and looks at a screenshot. See the [worked example](https://albakore.github.io/pyzed/ai/prompting/#a-worked-example). | | *"Make a reusable rating component"* | Generates a `pz.Component` with props, state and events that also shows up in Pyzed Design's toolbox. | | *"Create a customer form with validation"* | Generates a `.pzform` and its handlers module, checks both with `validate_form`. | | *"Start from the kanban block and load the cards from SQLite"* | Reads the block, keeps the UI and swaps the sample data for a query. | | *"Why is my list not scrolling?"* | Reads the layout guide and the component docs, then fixes the code. | | *"Add a chat with a local model"* | Uses `pyzed.agents` or an OpenAI-compatible HTTP call, streaming into the UI. | #### Quick start 1. Install Pyzed with the MCP extra: `pip install "pyzed-ui[mcp]"` (or let the agent run it with `uvx`). 2. Add the server to your agent, for example in `.cursor/mcp.json` or `.vscode/mcp.json`, as shown in [MCP server](https://albakore.github.io/pyzed/ai/mcp/#connect-your-agent). 3. Drop the [`AGENTS.md` template](https://albakore.github.io/pyzed/ai/llms-txt/#agentsmd-for-a-pyzed-project) in your project, then ask for what you want. The [prompting guide](https://albakore.github.io/pyzed/ai/prompting/) has prompts that work well. ### MCP server > Connect Cursor, VS Code, Zed, Claude Desktop or any MCP client to Pyzed: components, docs, code generators, validation and off-screen screenshots for AI agents. Source: https://albakore.github.io/pyzed/ai/mcp/ `pyzed mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server that ships with Pyzed. It gives an AI agent the same knowledge a Pyzed expert has, taken from the **installed version** of the package: every component's signature and docs, the icons, the themes, this documentation, the examples and blocks, generators for apps, components and forms, two validators, an off-screen renderer and project tools to build a whole app in a folder. This page is the short version. The [full MCP reference](https://albakore.github.io/pyzed/guides/mcp/) lists every tool, resource and prompt. #### Connect your agent Most MCP clients (Claude Desktop, Cursor, Windsurf and others) read a JSON file with this shape, such as `.cursor/mcp.json` in your project: ```json { "mcpServers": { "pyzed": { "command": "uvx", "args": ["--from", "pyzed-ui[mcp]", "pyzed", "mcp"] } } } ``` VS Code reads `.vscode/mcp.json`: ```json { "servers": { "pyzed": { "type": "stdio", "command": "uvx", "args": ["--from", "pyzed-ui[mcp]", "pyzed", "mcp"] } } } ``` Zed and other editors take the same command and arguments in their own settings (Zed calls them *context servers*). :::tip[Use your project's environment] Inside a project made with `pyzed new`, point the agent at the project's own environment, so the server sees the same Pyzed version and **your own components**: `"command": "uv", "args": ["run", "pyzed", "mcp"]`. If Pyzed is already installed, `"command": "pyzed", "args": ["mcp"]` works too. ::: `uvx` runs the server without installing anything first; it needs [uv](https://docs.astral.sh/uv/). Without uv, run `pip install "pyzed-ui[mcp]"` (Python 3.10 or newer) and use `pyzed mcp` as the command. #### What the agent can do | Group | Tools | Use them to | | --- | --- | --- | | Learn | `get_guide`, `list_guides`, `list_docs`, `read_docs` | Read how Pyzed works, its rules, the cookbook of runnable recipes, topics such as games or shaders, and any page of these docs (English or Spanish). | | Look up | `list_components`, `describe_component`, `search_reference`, `read_reference`, `search_icons`, `list_themes`, `editor_keymap` | Get exact signatures, parameters, icon names, color tokens and key bindings instead of guessing. | | Reuse | `list_examples`, `read_example` | Read the examples, the [Blocks](https://albakore.github.io/pyzed/blocks/) (`read_example("blocks/settings")`), games, showcase and backend integrations. | | Generate | `new_app`, `new_component`, `new_form` | Start from correct skeletons: an app, a `pz.Component`, a visual-designer form. | | Check | `validate_code`, `validate_form` | Catch unknown names, wrong keyword arguments, side effects in `view()` and broken forms before running. | | See | `screenshot` | Render the app or a component off-screen and get a PNG back. | | Plan | `plan_app` | Find the blocks, examples, components and guides an app needs, and the steps to build it. | | Build | `project_info`, `create_project`, `list_files`, `read_file`, `write_files`, `add_dependency`, `check_project`, `run_tests`, `screenshot_project`, `project_settings`, `set_project_settings` | Create a project, write it, add libraries, check, test and look at it, set its renderer and shared settings, in the folder the server runs in and the project's own environment. | | Watch | `start_live_preview`, `live_preview_status`, `stop_live_preview` | Open the app in a window with hot reload, so you see it change as the agent writes each file. | The prompts `create_app`, `create_component`, `create_form` and `build_project` (a whole app, end to end) start a task with the right steps, and resources such as `pyzed://best-practices` and `pyzed://docs/{slug}` can be attached by clients that support them. #### Prompts that use the server well - *"Use the pyzed MCP tools. Read the best practices first, then build..."*: agents follow instructions about their tools more reliably when you name them. - *"Validate with validate_code and look at a screenshot before you answer."*: makes the agent close the loop. - *"Start from the dashboard block."*: a tested screen is a better base than a blank file. - *"Check describe_component for ui.table before using it."*: for components with many parameters. More in the [prompting guide](https://albakore.github.io/pyzed/ai/prompting/), including a full session with real tool outputs. #### Security - Over **stdio** (the default) the server only talks to the agent that started it. - `screenshot` **runs the code it is given** in a separate process with a 60 second limit and without opening a window. Treat it like running code yourself: agents normally ask before calling tools, keep that on for untrusted prompts. - The project tools read and write files only inside the folder the server was started in (or `--root PATH`), never files that may hold secrets (`.env`, keys...), and run uv, ruff, ty and pytest there. - `pyzed mcp --transport streamable-http` serves it on `127.0.0.1` without authentication; there `screenshot` and the project tools are off unless you add `--allow-code`. #### Troubleshooting | Problem | Fix | | --- | --- | | The agent lists no `pyzed` tools | Run the command from the config by hand (`uvx --from "pyzed-ui[mcp]" pyzed mcp`); it should wait silently for input. Check the client's MCP logs. | | `ModuleNotFoundError: mcp` | Install the extra: `pip install "pyzed-ui[mcp]"`. Python 3.10+ is required. | | The agent doesn't know your components | Run the server from your project's environment (`uv run pyzed mcp`), so it imports them. | | Docs can't be read offline | Pages are read from a source checkout or downloaded from GitHub; offline, point the agent at a checkout or at the [Markdown pages](https://albakore.github.io/pyzed/ai/llms-txt/). | | Screenshots time out | The code must define a `pz.App` or a top-level `preview()`; long work in `__init__` hits the 60 second limit. | #### Without MCP Every part of the server is plain Python, usable from scripts, tests or your own agent: ```python from pyzed.mcp import knowledge, scaffold, validate print(knowledge.describe_component("ui.progress")["signature"]) print(validate.check_code(open("main.py").read())["issues"]) print(scaffold.app("Notes", layout="workspace")) ``` Agents that can't use MCP at all can read the docs as text: see [llms.txt and editor rules](https://albakore.github.io/pyzed/ai/llms-txt/). ### The agent in Pyzed Design > The AI agent built into Pyzed Design at a glance: a composer that builds forms, components and whole apps, sees images, uses MCP servers and external agents, and signs in to many model providers. Source: https://albakore.github.io/pyzed/ai/agent-composer/ Pyzed Design, the visual designer, has an AI agent in its right dock. You describe a screen in words and the agent places the controls, wires the events, writes the Python handlers, creates new forms, components or a whole project, looks at the result and checks its own code. Every change it makes to the open form is an ordinary undo step, and **Reject** puts back everything a run touched. (Image: The agent added a switch and wired its event) This page is a summary. The complete manual, with every button, command and setting, is [Pyzed Design → AI agents](https://albakore.github.io/pyzed/editor/ai-agents/). #### At a glance | Feature | What it does | Details | | --- | --- | --- | | **Composer** | A message box: @ attaches the selection, the form, its code, a control or a file; / runs commands such as `/app`, `/fix`, `/tidy` and `/look`. | [The panel](https://albakore.github.io/pyzed/editor/ai-agents/#the-panel) | | **Profiles** | **Write** (the default), **Ask** (reads only) and **Minimal** (no tools), plus **Ask before writing files**. Edits to the open form never ask, since they can be undone. | [Profiles](https://albakore.github.io/pyzed/editor/ai-agents/#the-panel) | | **Many providers** | Anthropic, OpenAI, Gemini, Vertex AI, GitHub Copilot, OpenRouter, xAI, Mistral, DeepSeek, Groq, Ollama, LM Studio and any OpenAI-compatible server, with effort and fast mode on Claude. | [Providers](https://albakore.github.io/pyzed/editor/ai-agents/#providers) | | **Sign in** | Sign in with the browser (the copied key is picked up), OpenRouter OAuth, **Sign in with GitHub** (device flow) and **Sign in with Google** for Vertex AI. | [Signing in](https://albakore.github.io/pyzed/editor/ai-agents/#signing-in) | | **Claude without a key** | Workload identity federation exchanges your identity provider's token for a short-lived Claude token. | [Claude without an API key](https://albakore.github.io/pyzed/editor/ai-agents/#claude-without-an-api-key) | | **Images** | Attach or paste pictures, or a screenshot of the form; the agent also looks at the form itself with `screenshot_form`. | [Images](https://albakore.github.io/pyzed/editor/ai-agents/#images) | | **Whole apps** | `/app` creates a project, adds packages, writes forms and components, looks at them and runs the checks. | [Building an application](https://albakore.github.io/pyzed/editor/ai-agents/#building-an-application) | | **MCP servers** | Add the tools of any Model Context Protocol server (local command or URL); their secrets go to the keychain. | [MCP servers](https://albakore.github.io/pyzed/editor/ai-agents/#mcp-servers) | | **External agents** | Run Claude Code, Gemini CLI, Codex or any Agent Client Protocol agent inside the panel, sharing Pyzed's MCP server. | [External agents](https://albakore.github.io/pyzed/editor/ai-agents/#external-agents) | | **Inline assist** | Ctrl+Alt+I in the code editor rewrites a handler or the file in place. | [Inline assist](https://albakore.github.io/pyzed/editor/ai-agents/#inline-assist) | | **Credentials** | Keys and tokens live in the system keychain, never in settings or the project. | [API keys and credentials](https://albakore.github.io/pyzed/ai/api-keys/) | (Image: Adding context with @) #### A first session 1. Open the panel: the **Agent** tab in the right dock, or Ctrl+Alt+A. 2. Connect a model: click a provider and sign in, or pick a local model. See [API keys and credentials](https://albakore.github.io/pyzed/ai/api-keys/) for keys, environment variables and good practices. 3. Ask for a screen: *"A customer form: name, email, a country drop-down and Save / Cancel; Save validates the email and shows a toast"*. Then refine it: select controls and ask, attach a mock-up, or run `/look` and `/fix`. 4. Review the run with **Keep** or **Reject**, and press F5 to run the form. Tips for good prompts are in [Getting good results](https://albakore.github.io/pyzed/editor/ai-agents/#getting-good-results) and the [prompting guide](https://albakore.github.io/pyzed/ai/prompting/). #### The same agent in your own app The panel runs on `pyzed.agents`, which any Pyzed app can use: the same providers, keychain storage, images, MCP servers, external agents, tools written as Python functions and streamed events. See [Building AI apps](https://albakore.github.io/pyzed/ai/ai-apps/) and [AI agents in your app](https://albakore.github.io/pyzed/guides/agents/). ### API keys and credentials > How Pyzed Design and pyzed.agents find model provider credentials: the system keychain, environment variables, browser sign-in, Google Cloud credentials and workload identity federation, and how to keep keys safe. Source: https://albakore.github.io/pyzed/ai/api-keys/ The agent in [Pyzed Design](https://albakore.github.io/pyzed/editor/ai-agents/) and the [`pyzed.agents`](https://albakore.github.io/pyzed/guides/agents/) library need a credential for the model provider you pick: an API key, a sign-in token or, for Claude, workload identity federation. This page explains where Pyzed looks for them, how to give it one without writing it in a file, and how to keep it safe. Local models (Ollama, LM Studio) need nothing. #### Where Pyzed looks For each request, Pyzed takes the first credential it finds, in this order: 1. **The system keychain.** A key you pasted or a sign-in token, saved under the service `pyzed` in the operating system's credential store: Keychain on macOS, Credential Manager on Windows, Secret Service (GNOME Keyring, KWallet) on Linux. Sign-in tokens (OpenRouter, GitHub, Google) are kept there too and refreshed when they are about to expire. 2. **Workload identity federation** (Anthropic only), when it is turned on in the settings and no key is saved. See [Claude without an API key](#claude-without-an-api-key). 3. **The provider's environment variable**, such as `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` (table below). It is read, never copied anywhere. 4. **What the computer already has.** Google Vertex AI uses the Google Cloud credentials from `gcloud auth application-default login`. Anthropic also uses an Anthropic CLI profile (`ant auth login`) and the `ANTHROPIC_FEDERATION_RULE_ID` / `ANTHROPIC_ORGANIZATION_ID` variables. A saved key always wins: if you set an environment variable and it seems to be ignored, open the provider in the settings and click **Remove key**. Each provider in the settings has a badge that says where its credential comes from, and **Test the connection** checks that it works: | Badge | Meaning | | --- | --- | | **Key saved in the system keychain** | A key you pasted, stored in the keychain. | | **Key kept for this session only** | No keychain was found: the key lives in memory until the designer closes. | | **Signed in** | A sign-in token (OAuth or GitHub device flow) in the keychain. | | **Key from the environment** | The provider's environment variable. | | **Workload identity federation** | Claude through federation, no key. | | **Signed in with the Anthropic CLI profile** | An `ant auth login` profile on this computer. | | **Google Cloud credentials of this computer** | Vertex AI through `gcloud`'s application default credentials. | | **No key needed** | Ollama, LM Studio, or an OpenAI-compatible server without a key. | #### Environment variables per provider | Provider | Model prefix | Environment variable | Other ways in | | --- | --- | --- | --- | | Anthropic | `anthropic:` | `ANTHROPIC_API_KEY` | Sign in with the browser, federation, `ant auth login` | | OpenAI | `openai:` | `OPENAI_API_KEY` | Sign in with the browser | | Google Gemini (AI Studio) | `google:` | `GEMINI_API_KEY` | Sign in with the browser | | Google Vertex AI | `vertex:` | none | Sign in with Google, `gcloud auth application-default login` or a service account file in `GOOGLE_APPLICATION_CREDENTIALS`; the project comes from the settings or `GOOGLE_CLOUD_PROJECT` | | GitHub Copilot (GitHub Models) | `github:` | `GITHUB_TOKEN` | Sign in with GitHub (device flow), a fine-grained token with the **Models** permission | | OpenRouter | `openrouter:` | `OPENROUTER_API_KEY` | Sign in with the browser (OAuth with PKCE) | | xAI | `xai:` | `XAI_API_KEY` | Sign in with the browser | | Mistral | `mistral:` | `MISTRAL_API_KEY` | Sign in with the browser | | DeepSeek | `deepseek:` | `DEEPSEEK_API_KEY` | Sign in with the browser | | Groq | `groq:` | `GROQ_API_KEY` | Sign in with the browser | | Ollama, LM Studio | `ollama:`, `lmstudio:` | none | Nothing: they run on your computer | | OpenAI-compatible | `custom:` | `PYZED_AGENT_API_KEY` | A key in the settings, only if the server needs one | Other variables Pyzed reads: `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_IDENTITY_TOKEN_FILE` and `ANTHROPIC_IDENTITY_TOKEN` (federation), `PYZED_GITHUB_CLIENT_ID`, `PYZED_GOOGLE_CLIENT_ID` and `PYZED_GOOGLE_CLIENT_SECRET` (your own OAuth apps for signing in), `GOOGLE_APPLICATION_CREDENTIALS` (a Google Cloud service account file for Vertex AI) and `PYZED_KEYRING=0` (don't use the keychain). #### The recommended way: the keychain Keep keys out of files and shells altogether and let Pyzed Design put them in the keychain: 1. Open **Settings → AI agents → AI agents settings…** (also the gear in the Agent panel, or `/settings`). 2. Under **Providers**, click the provider to open it. 3. Either click **Sign in with the browser** (Pyzed opens the provider's key page and picks up the key when you click its **Copy** button; see [Signing in](https://albakore.github.io/pyzed/editor/ai-agents/#signing-in)), or paste the key in **API key** and click **Save**. 4. A notice says **Key saved in the system keychain**, and the provider's badge says the same. The key never goes to `agents.json`, to the threads or to your project, and the field only shows a masked preview (`sk-ant…a1b2`) afterwards. **Remove key** deletes it from the keychain. From code, the same store is one call away: ```python from pyzed import agents agents.set_key("anthropic", "sk-ant-…") # True: in the keychain; False: kept for this session only agents.get_key("anthropic") # the saved key, or the environment variable agents.delete_key("anthropic") ``` Run that once from a Python prompt, not from a file you keep, so the key is never written to disk. ##### "Key kept for this session only" If the settings show **No system keychain found** and the badge says **Key kept for this session only**, Pyzed could not reach a keychain, so a key you paste lasts until the designer closes. It is never written in plain text as a fallback. To fix it: - **Running Pyzed from pip or uv:** install the extra that brings `keyring`: - **Linux:** `keyring` needs a Secret Service running, which GNOME and KDE desktops start for you. On a minimal window manager or a headless machine, install and unlock `gnome-keyring` (or KWallet), or use an environment variable instead. - **The compiled Pyzed Design** ([download](https://albakore.github.io/pyzed/download/)) already bundles `keyring`; if it still says so, the keychain itself is locked or missing. Pyzed ignores keyring's "fail" and plain-text backends on purpose, so a key is never saved unencrypted. #### Using environment variables An environment variable is handy on a CI runner, in a container or when you already keep keys in a password manager that exports them. Pyzed only reads it. From a terminal, set it and start the designer from the same terminal: ```bash export ANTHROPIC_API_KEY="sk-ant-…" # macOS and Linux (bash, zsh) pyzed design ``` ```powershell $env:ANTHROPIC_API_KEY = "sk-ant-…" # Windows PowerShell, this window only pyzed design ``` Apps started from the Finder, the Start menu or a desktop launcher don't read your shell's `~/.zshrc` or `~/.bashrc`, so a variable set there is not seen by the compiled Pyzed Design. Set it where GUI apps get their environment, then quit and reopen Pyzed Design: - **macOS:** `launchctl setenv ANTHROPIC_API_KEY "sk-ant-…"`. It lasts until you restart the Mac; to keep it, run it from a login item or a LaunchAgent. - **Windows:** **Start → Edit environment variables for your account → New…** (user variables), or `setx ANTHROPIC_API_KEY "sk-ant-…"` in a terminal. Programs started after that see it. - **Linux (systemd desktops):** add `ANTHROPIC_API_KEY=sk-ant-…` to a file such as `~/.config/environment.d/pyzed.conf`, then log out and in again. Keep in mind that an environment variable is plain text: every program started from that environment inherits it, including the apps you run with F5. A key in the keychain is only handed to Pyzed. Prefer the keychain on your own computer, and environment variables where there is no keychain. Pyzed Design does not load `.env` files. In your own app you can load one with a library such as `python-dotenv`; keep it out of git (add it to `.gitignore`). #### Claude without an API key Workload identity federation exchanges a short-lived token from your identity provider (GitHub Actions, Google Cloud, Azure, Okta, Kubernetes…) for a short-lived Claude token, so no Anthropic key is stored at all. Set it up in the Claude Console, then fill in **Anthropic → Workload identity federation** in the settings, or set `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID` and `ANTHROPIC_IDENTITY_TOKEN_FILE`. The identity token can come from a file, a command that prints a fresh one (`gcloud auth print-identity-token`) or a variable. The fields are explained in [Claude without an API key](https://albakore.github.io/pyzed/editor/ai-agents/#claude-without-an-api-key), and the code version in [AI agents in your app](https://albakore.github.io/pyzed/guides/agents/#claude-without-an-api-key). Federation is used only when no key is saved: remove a saved Anthropic key to switch to it. #### Sign-in tokens - **OpenRouter** signs in with OAuth and PKCE; the key it issues for Pyzed goes to the keychain. - **GitHub Copilot** signs in with GitHub's device flow (you type a short code on `github.com/login/device`). It needs a GitHub App client ID in the provider's settings or `PYZED_GITHUB_CLIENT_ID`; without one, paste a fine-grained personal access token with the **Models** permission. - **Google Vertex AI** signs in with Google (OAuth with PKCE) or uses `gcloud` credentials. Signing in with your own OAuth client needs its client secret, which goes to the keychain as well. The tokens are saved in the keychain as the provider's entry, refreshed shortly before they expire, and **Sign out** deletes them. #### MCP servers and external agents [MCP servers](https://albakore.github.io/pyzed/editor/ai-agents/#mcp-servers) and [external agents](https://albakore.github.io/pyzed/editor/ai-agents/#external-agents) often need their own token (`GITHUB_TOKEN` for the GitHub MCP server, a key for an external agent). Put them in the **Secrets** field as `NAME=value` pairs separated by `;`. They are saved in the keychain, never in `agents.json`, and passed to the server as environment variables (or as HTTP headers for a remote server) when it starts. In code: ```python from pyzed.agents import mcp_client mcp_client.set_secrets("github", {"GITHUB_TOKEN": "github_pat_…"}) # in the keychain ``` #### What the agent cannot read The agent's file tools refuse to list, read, search or write files that usually hold credentials, so a prompt injection hidden in a web page or a file can't make it send them to the model: `.env`, `.env.*`, `*.pem`, `*.key`, `*.p12`, `*.pfx`, `id_rsa*`, `id_ecdsa*`, `id_ed25519*`, `.netrc`, `.pypirc`, `.npmrc` and `.git-credentials`. They are not offered in the @ menu either, and external agents get the same rule when they ask Pyzed for files. The list is `pyzed.agents.tools.SECRET_FILES`; pass your own to `file_tools(folder, hidden=...)` in your app. Keys are also never written to threads or settings, and the field only shows a masked preview. #### Good practices - **Use a dedicated key** for Pyzed, with a **spending limit** or budget in the provider's console, so a leak or a runaway loop has a ceiling. - **Never put a key in code**, in a `.pzform`, in `agents.json` or anywhere in the repository. If one is committed, deleting the commit is not enough: revoke it. - **Rotate a key you think leaked**: revoke it in the provider's console, create a new one and paste it again (**Remove key**, then **Save**). - **Prefer short-lived credentials** where you can: sign-in tokens, Google Cloud credentials or Claude federation instead of a long-lived API key, especially on shared machines and CI. - **Shipping an app that uses `pyzed.agents`?** Don't bundle your key in it: anything inside an app can be extracted. Let each user sign in or paste their own key (it goes to their keychain), or put a server you control between the app and the provider. - **Use local models** (Ollama, LM Studio) when the data must not leave the computer. ### llms.txt and editor rules > Feed the Pyzed docs to any LLM or AI IDE: llms.txt, llms-full.txt, Markdown pages, plus copy-ready AGENTS.md, Cursor rules and Copilot instructions for Pyzed projects. Source: https://albakore.github.io/pyzed/ai/llms-txt/ Not every agent speaks MCP, and sometimes you just want to paste the docs into a chat. This site publishes itself as plain text for language models, following the [llms.txt](https://llmstxt.org) convention, and this page has ready-made instruction files that teach any AI editor how to write Pyzed. #### The docs as text | URL | What it has | Use it for | | --- | --- | --- | | [`/pyzed/llms.txt`](https://albakore.github.io/pyzed/llms.txt) | An index: every page with its title, link and one-line summary. | Letting an agent pick what to read. | | [`/pyzed/llms-small.txt`](https://albakore.github.io/pyzed/llms-small.txt) | The essentials in one file: getting started, tutorials, guides, AI and FAQ. | Pasting into a chat or a model with a small context. | | [`/pyzed/llms-full.txt`](https://albakore.github.io/pyzed/llms-full.txt) | The whole documentation in one file. | Indexing in an AI IDE, long-context models. | | `/pyzed/llms/
.txt` | One section, such as [`guides`](https://albakore.github.io/pyzed/llms/guides.txt), [`library`](https://albakore.github.io/pyzed/llms/library.txt) or [`blocks`](https://albakore.github.io/pyzed/llms/blocks.txt). | Just what a task needs. | | `/pyzed/.md` | Any page as Markdown: [`/pyzed/guides/layout.md`](https://albakore.github.io/pyzed/guides/layout.md). | Linking one page from a prompt or a rule. | | [`/pyzed/es/llms.txt`](https://albakore.github.io/pyzed/es/llms.txt), [`/pyzed/es/llms-full.txt`](https://albakore.github.io/pyzed/es/llms-full.txt) | The same in Spanish. | Working in Spanish. | The full address starts with `https://albakore.github.io`, for example `https://albakore.github.io/pyzed/llms-full.txt`. All of them are generated from the same pages you are reading, so they always match the current release. #### In your AI editor - **Cursor**: *Settings → Indexing & Docs → Add doc* with `https://albakore.github.io/pyzed/llms-full.txt`, then mention it as `@Pyzed` in a chat. Add the [Cursor rule](#cursor-rules) below for every request. - **VS Code with Copilot**: add the [instructions file](#github-copilot) below; in a chat, `#fetch` a page such as `https://albakore.github.io/pyzed/guides/async.md` when you need its details. - **Zed**: `/fetch https://albakore.github.io/pyzed/llms-small.txt` in the agent panel, and add the [`AGENTS.md`](#agentsmd-for-a-pyzed-project) to the project. - **A chat in the browser**: attach or paste `llms-small.txt`, or give the model the link if it can browse. - **Any agent with MCP**: prefer the [MCP server](https://albakore.github.io/pyzed/ai/mcp/). It reads the same pages on demand and also validates code and takes screenshots. The package also ships **type stubs** (`pyzed/_core.pyi` and inline types), so editors and agents that read types see every widget's parameters even without the docs. #### AGENTS.md for a Pyzed project Many agents read an `AGENTS.md` at the root of the project. Copy this one; `pyzed new` projects are a good fit. ```markdown # AGENTS.md This is a desktop app written with Pyzed (Python UI on Iced/Rust, with ready-made themed components). Docs: https://albakore.github.io/pyzed/llms-small.txt (all of it: /pyzed/llms-full.txt). If the `pyzed` MCP server is available, use its tools instead of guessing APIs. ## Commands - Run with hot reload: `uv run pyzed dev main.py` - Check (ruff + types): `uv run pyzed check` - Tests: `uv run pytest -q` - Package: `uv run pyzed build` ## How Pyzed apps work - An app is a `pz.App` subclass. State lives on `self` (set in `__init__`), `view()` returns widgets, events call methods or send messages to `update(message)`. - `view()` runs after every event: no I/O, no sleeps, no state changes inside it. - Slow work: return a coroutine (`async def`) or an async generator from a callback or `update`; each result or yielded value goes back to `update`. Timers and streams go in `subscriptions()` (`pz.every`, `pz.listen`). - Effects (focus, scroll, clipboard, windows) are `pz.Command`s returned from callbacks. ## Rules - Prefer `pyzed.ui` components (`ui.button`, `ui.input`, `ui.table`, `ui.card`...) over raw `pz.*` widgets. - Look up signatures (MCP `describe_component`, or the docs) before using a component. Never invent parameters. - Colors: use theme tokens (`"accent"`, `"panel"`, `"text_muted"`, `"border"`) so light and dark both work. `pz.container(bg=...)` takes real colors; use `.style(background="panel")` for tokens. - Icons are Lucide names (`pyzed icons` lists them). - Lambdas in loops capture the variable: `lambda item=item: self.open(item)`. - Don't name app attributes after window options (`title`, `size`, `position`, `theme`, `style`...) unless you mean them. - Reusable pieces with their own state: `pz.Component` with typed props, `pz.state()` and `events`. - Call `pz.run(App)` once, under `if __name__ == "__main__":`. ## Before you finish - Run `pyzed check` (or the MCP `validate_code` tool) and fix every issue. - Smoke-test the UI off-screen: `App().screenshot("shot.png")` (or the MCP `screenshot` tool) and look at it. ``` #### Cursor rules Save as `.cursor/rules/pyzed.mdc`. It applies to every Python file and points Cursor at the docs and the `AGENTS.md` above: ```markdown --- description: Writing desktop UIs with Pyzed globs: ["**/*.py", "**/*.pzform"] alwaysApply: false --- - This project uses Pyzed for its UI. Follow AGENTS.md at the repository root. - Before using a Pyzed component, check its signature with the `pyzed` MCP tools (`describe_component`) or in https://albakore.github.io/pyzed/llms/library.txt. - Build screens from `pyzed.ui` components; start from a block when one fits: https://albakore.github.io/pyzed/llms/blocks.txt - After editing, run `validate_code` (MCP) or `uv run pyzed check`, and fix what it reports. ``` #### GitHub Copilot Save as `.github/copilot-instructions.md` (repository-wide), or as `.github/instructions/pyzed.instructions.md` with an `applyTo` header to scope it: ```markdown --- applyTo: "**/*.py" --- This repository's UI is written with Pyzed (https://albakore.github.io/pyzed/). Read https://albakore.github.io/pyzed/llms-small.txt for how Pyzed apps work, and follow AGENTS.md. - `pz.App` subclasses hold state on `self`; `view()` only builds widgets from that state. - Use `pyzed.ui` components and theme tokens ("accent", "panel", "text_muted"). - Long work goes in coroutines or async generators returned from callbacks; never block `view()`. - Verify component parameters in https://albakore.github.io/pyzed/llms/library.txt instead of guessing. ``` #### A system prompt for your own agent Building an agent that writes Pyzed (with `pyzed.agents` or any SDK)? Give it the guide and the tools: ```python from pyzed import agents from pyzed.mcp import knowledge system = knowledge.guide("overview") + "\n\n" + knowledge.guide("best-practices") agent = agents.Agent(tools=agents.pyzed_tools(), system=system) ``` `agents.pyzed_tools()` gives the model the same lookups, generators and checks as the MCP server. ### Prompting guide > How to prompt AI agents to build Pyzed desktop apps, with a worked example: an agent builds a Pomodoro timer using the Pyzed MCP tools, with their real outputs. Source: https://albakore.github.io/pyzed/ai/prompting/ Agents write much better Pyzed code when the prompt gives them three things: **what the screen is for**, **what it must do**, and **how to check the result**. This page has prompt patterns that work and a full session, with the real outputs of the [MCP tools](https://albakore.github.io/pyzed/ai/mcp/), of an agent building a small app. #### Anatomy of a good prompt > Build a **Pomodoro timer** as a Pyzed app. *(what)* > It has Focus (25 min) and Break (5 min) modes, a big countdown, a progress bar, Start/Pause and Reset, and counts > finished sessions. *(behavior)* > Use `pyzed.ui` components and theme tokens, keep `view()` free of side effects. *(constraints)* > Use the pyzed MCP tools: check signatures with `describe_component`, run `validate_code`, and look at a > `screenshot` before you answer. *(how to check)* | Include | Why | | --- | --- | | The purpose and the user | *"A settings screen for a note-taking app"* gets better defaults than *"a form"*. | | Behavior as a list | Each item becomes state and a method; nothing is forgotten. | | Constraints | Theme, window size, components to use or avoid, Python version. | | A starting point | *"Start from the settings block"* or *"follow examples/todo.py"*. | | The check | Validation and a screenshot close the loop. Without them the agent stops at "it should work". | #### Prompts that work **New app** > Create a Pyzed app to browse a folder of Markdown notes: a searchable list on the left, the rendered note on the > right, Ctrl+N for a new note. Read the best practices with get_guide first. Validate and screenshot it. **From a block** > Start from the dashboard block (`read_example("blocks/dashboard")`). Replace the sample data with numbers read > from `sales.csv` with the csv module, loaded in a coroutine so the window never blocks. **Component** > Make a reusable `pz.Component` called TagInput: a text input that turns comma-separated words into removable > chips. Props: `tags: list[str]`; event `change` with the new list. Show it in a `preview()` screenshot. **Designer form** > Create a designer form "invoice" with customer, date, a table of lines and a total; the total updates when a > line changes. Use new_form and validate_form. **Fixing** > This list doesn't scroll and the window freezes while it loads. Read guides/layout and guides/async with > read_docs, explain the cause, then fix it. **Styling** > Make this screen feel like a game launcher: dark theme with a purple accent, a glass panel over a gradient, > a glowing Play button. Check the effects in animation/effects. #### Things to avoid - **"Use tkinter-style code"** or pasting code from another toolkit: Pyzed has no `pack()`, `grid()` or widget objects to mutate. Describe the behavior instead and let the agent write it the Pyzed way. - **Asking for everything at once** in a large app. Build the screen, check it, then add features: each step is validated and looked at. - **Skipping the check.** An agent that never runs `validate_code` will sometimes invent a parameter; one call catches it. #### A worked example What follows is a real session with the Pyzed MCP server (`python -m pyzed.mcp`), driven by an MCP client. The outputs are exactly what the tools returned, shortened where marked. **The prompt:** *Build a Pomodoro timer with Focus and Break modes, a progress bar, Start/Pause and Reset. Use the pyzed MCP tools to check what you use, validate the code and look at a screenshot.* ##### 1. Look up the components The agent needs a progress bar, so it searches for one: ```text list_components(query="progress") ``` ```json [ {"name": "pz.progress_bar", "kind": "widget", "category": "Controls", "summary": "A progress bar."}, {"name": "ui.progress", "kind": "ui", "category": "Inputs", "summary": "A thin rounded progress bar filled to ``value`` out of ``max``, in the ``tint`` color."}, {"name": "designer:progress", "kind": "designer", "category": "Display", "summary": "Shows how far a task has got."} ] ``` It prefers the `ui` component and asks for its exact signature: ```text describe_component(name="ui.progress") ``` ```json { "name": "ui.progress", "signature": "ui.progress(value: 'float', *, max: 'float' = 1.0, tint: 'str' = 'accent', height: 'float' = 4, width: 'Any' = 'fill', theme: 'Optional[Theme]' = None) -> '_core.Widget'", "parameters": [ {"name": "value", "required": true, "description": "Current value."}, {"name": "max", "default": "1.0", "description": "Maximum value."}, {"name": "height", "default": "4", "description": "Thickness."}, "…" ], "docs": "https://albakore.github.io/pyzed/reference/ui/" } ``` And an icon name, since icons are Lucide names: ```text search_icons(query="timer", limit=8) ``` ```json {"total": 3, "icons": ["timer", "timer-off", "timer-reset"]} ``` ##### 2. A first draft, and what the validator says The first draft has three mistakes typical of generated code: a mutable class attribute, a state change inside `view()` and a misspelled keyword argument. ```python import pyzed as pz from pyzed import ui class Pomodoro(pz.App): title = "Pomodoro" size = (420, 360) tasks = [] def __init__(self): self.left = 25 * 60 self.running = False def subscriptions(self): return [pz.every(1, self.tick)] if self.running else [] def tick(self): self.left -= 1 def view(self): self.running = self.left > 0 return pz.column([ ui.heading(f"{self.left // 60:02d}:{self.left % 60:02d}"), ui.progress(1 - self.left / 1500, heigth=8), ui.button("Start", lambda: setattr(self, "running", True), icon="play"), ], spacing=16, padding=24) pz.run(Pomodoro) ``` ```text validate_code(code=…) ``` ```json { "ok": false, "issues": [ {"line": 8, "column": 5, "severity": "warning", "message": "mutable class attribute tasks on an App is shared state; initialize it in __init__"}, {"line": 21, "column": 9, "severity": "warning", "message": "view() changes self.running; view() runs after every message and must not change state"}, {"line": 24, "column": 47, "severity": "error", "message": "ui.progress() has no parameter 'heigth'; it takes: height, max, theme, tint, value, width"} ] } ``` ##### 3. The fixed app The agent fixes the three issues, adds the modes and the session counter, and validates again: ```json {"ok": true, "issues": []} ``` ```python title="examples/pomodoro.py" """A Pomodoro timer: the app built step by step in the AI prompting guide.""" import pyzed as pz from pyzed import ui WORK, BREAK = 25 * 60, 5 * 60 class Pomodoro(pz.App): title = "Pomodoro" size = (420, 380) def __init__(self): self.mode = "Focus" self.left = WORK self.running = False self.done = 0 def subscriptions(self): return [pz.every(1, self.tick, key="timer")] if self.running else [] def tick(self): self.left -= 1 if self.left <= 0: if self.mode == "Focus": self.done += 1 self.mode = "Break" if self.mode == "Focus" else "Focus" self.left = BREAK if self.mode == "Break" else WORK def switch(self, mode): self.mode, self.running = mode, False self.left = WORK if mode == "Focus" else BREAK def toggle(self): self.running = not self.running def reset(self): self.running, self.left = False, WORK if self.mode == "Focus" else BREAK def view(self): total = WORK if self.mode == "Focus" else BREAK return pz.center(pz.column([ ui.segmented(["Focus", "Break"], self.mode, self.switch), pz.text(f"{self.left // 60:02d}:{self.left % 60:02d}", size=64, weight="bold"), ui.progress(1 - self.left / total, height=8, width=280), pz.row([ ui.button("Pause" if self.running else "Start", self.toggle, icon="pause" if self.running else "play", variant="solid", size="lg"), ui.icon_button("rotate-ccw", self.reset, tooltip="Reset"), ], spacing=8, align="center"), ui.muted(f"{self.done} sessions today", size="sm"), ], spacing=18, align="center")).style(background="editor") if __name__ == "__main__": pz.run(Pomodoro) ``` ##### 4. Looking at the result ```text screenshot(code=…, width=420, height=380, scale=2) ``` The tool rendered the app off-screen and returned this PNG, which the agent checks before answering: (Image: The Pomodoro timer rendered by the screenshot tool) The whole loop (look up, write, validate, look) took five tool calls. The app is in the repository as [`examples/pomodoro.py`](https://github.com/albakore/pyzed/blob/main/examples/pomodoro.py) and runs with `python examples/pomodoro.py`. #### Reproduce the session The session above can be run with the MCP Python SDK, which is useful to test your own prompts or tools: ```python import asyncio, sys from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): server = StdioServerParameters(command=sys.executable, args=["-m", "pyzed.mcp"]) async with stdio_client(server) as (read, write), ClientSession(read, write) as session: await session.initialize() result = await session.call_tool("describe_component", {"name": "ui.progress"}) print(result.content[0].text) asyncio.run(main()) ``` ### Building AI apps > Build a streaming AI chat desktop app in Python with Pyzed: an OpenAI-compatible API over httpx, async generators that stream tokens into the UI, stop, errors and tests without network. Source: https://albakore.github.io/pyzed/ai/ai-apps/ Pyzed is a good fit for AI apps: answers stream in token by token while the window stays responsive, Markdown and code render out of the box, and the components (inputs, cards, spinners, badges) already look like a modern AI tool. This page builds a complete **streaming chat** that talks to any OpenAI-compatible server (OpenAI, Ollama, LM Studio, vLLM, llama.cpp, OpenRouter, Groq...) with [httpx](https://www.python-httpx.org), and runs without network in a demo mode. | Empty thread | Streaming | | --- | --- | | (Image: The chat with suggestions) | (Image: An answer streaming in) | (Image: The finished answer with Markdown, code and actions) #### Run it } uv run --with httpx python examples/ai_chat.py `} pip={` python examples/ai_chat.py # demo: a canned answer, no network, no key pip install httpx # for a real model {env CHAT_BASE_URL=http://localhost:11434/v1} {env CHAT_MODEL=llama3.2} python examples/ai_chat.py # Ollama {env CHAT_BASE_URL=https://api.openai.com/v1} {env CHAT_API_KEY=sk-...} {env CHAT_MODEL=} python examples/ai_chat.py `} python={` {py} examples/ai_chat.py # demo: a canned answer, no network, no key {py} -m pip install httpx # for a real model {env CHAT_BASE_URL=http://localhost:11434/v1} {env CHAT_MODEL=llama3.2} {py} examples/ai_chat.py # Ollama {env CHAT_BASE_URL=https://api.openai.com/v1} {env CHAT_API_KEY=sk-...} {env CHAT_MODEL=} {py} examples/ai_chat.py `} /> The code is [`examples/ai_chat.py`](https://github.com/albakore/pyzed/blob/main/examples/ai_chat.py), about 220 lines with the UI. The important parts follow. #### How streaming works ```mermaid sequenceDiagram participant UI as UI thread participant App as AIChat participant Loop as asyncio loop participant API as Model server UI->>App: Enter → send() App-->>Loop: return reply() (async generator) Loop->>API: POST /chat/completions (stream=true) loop every token API-->>Loop: data: {"choices":[{"delta":{"content":"…"}}]} Loop-->>App: yield ("delta", turn, text) → update() App->>UI: view() with the longer answer end Loop-->>App: yield ("done", turn, None) ``` A callback that **returns an async generator** makes Pyzed run it on its asyncio loop, and every value it yields goes to `update` on the UI thread (see [Async and subscriptions](https://albakore.github.io/pyzed/guides/async/)). No threads, queues or locks in your code. ##### 1. Read the stream from the server OpenAI-compatible servers stream [server-sent events](https://developer.mozilla.org/docs/Web/API/Server-sent_events): lines that start with `data:` and hold a JSON chunk, ending with `data: [DONE]`. ```python async def openai_stream(messages, *, model=MODEL, base_url=BASE_URL, api_key=API_KEY): """Yields the answer's text pieces from an OpenAI-compatible ``/chat/completions`` endpoint.""" import httpx # only needed when talking to a real server headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} body = {"model": model, "messages": messages, "stream": True} async with httpx.AsyncClient(timeout=httpx.Timeout(60, connect=10)) as client: async with client.stream("POST", f"{base_url.rstrip('/')}/chat/completions", json=body, headers=headers) as response: if response.status_code >= 400: detail = (await response.aread()).decode(errors="replace")[:300] raise RuntimeError(f"HTTP {response.status_code}: {detail}") async for line in response.aiter_lines(): # server-sent events: "data: {...}" if not line.startswith("data:"): continue data = line[5:].strip() if data == "[DONE]": break delta = json.loads(data)["choices"][0].get("delta", {}).get("content") if delta: yield delta ``` The demo mode swaps it for a function with the same shape, so the rest of the app doesn't know the difference: ```python async def demo_stream(messages, *, delay=0.015): """A stand-in for the model: streams a canned answer a few characters at a time.""" text = DEMO_ANSWER for i in range(0, len(text), 4): await asyncio.sleep(delay) yield text[i:i + 4] ``` ##### 2. Send, and turn the stream into messages ```python def send(self, text=None): text = (text if text is not None else self.draft).strip() if not text or self.busy: return None self.messages += [{"role": "user", "content": text}, {"role": "assistant", "content": ""}] self.draft, self.error, self.busy = "", "", True self.turn += 1 history = [{"role": "system", "content": SYSTEM}] + self.messages[:-1] return [self.reply(self.turn, history), pz.Command.scroll_to_end("thread")] async def reply(self, turn, history): """Runs on Pyzed's asyncio loop; each yield goes to update() on the UI thread.""" try: async for piece in self.stream(history): if turn != self.turn: # stopped or superseded return yield ("delta", turn, piece) except Exception as error: # network errors, bad keys... shown in the thread yield ("error", turn, str(error) or type(error).__name__) yield ("done", turn, None) ``` - `send` returns **a list**: the generator and a command that scrolls the thread down. Pyzed runs both. - Each reply is tagged with a **turn** number. **Stop** just increments `self.turn`: the running generator sees a newer turn and returns, and `update` ignores pieces of an older turn that were already on their way. - Errors become a message instead of an exception, so a wrong key or a server that's down shows a red callout in the thread and the app keeps working. ##### 3. Update the state ```python def update(self, message): kind, turn, value = message if turn != self.turn: return None if kind == "delta": self.messages[-1]["content"] += value return pz.Command.scroll_to_end("thread") if kind == "error": self.error = value elif kind == "done": self.busy = False return None ``` ##### 4. The view The assistant's answer is a `ui.markdown`, with a cursor (`▍`) appended while it streams and a `ui.spinner` before the first token. The thread is a `pz.scrollable(..., anchor_bottom=True)` so new text stays in view, and the composer swaps its send button for a **Stop** button while `busy`: ```python action = (ui.icon_button("square", self.stop, variant="solid", tooltip="Stop") if self.busy else ui.icon_button("arrow-up", self.send, variant="solid" if self.draft.strip() else "filled", tooltip="Send", disabled=not self.draft.strip())) ``` Copy, regenerate and feedback buttons appear under finished answers; `pz.Command.copy(text)` puts the answer on the clipboard. #### Test it without a network The stream is a constructor argument, so tests drive the whole app with a fake model, the same way Pyzed's own tests do in `tests/test_blocks.py`: ```python import asyncio from ai_chat import AIChat, DEMO_ANSWER, demo_stream def test_streams_the_answer(): app = AIChat(stream=lambda messages: demo_stream(messages, delay=0)) effects = app.send("hello") reply = next(e for e in effects if hasattr(e, "__aiter__")) async def run(): async for message in reply: app.update(message) asyncio.run(run()) assert app.messages[-1]["content"] == DEMO_ANSWER and not app.busy app.screenshot("chat.png") # off-screen, no window ``` The repository's test also starts a tiny local HTTP server that speaks server-sent events and checks that `openai_stream` sends the right request and joins the pieces. #### With `pyzed.agents`: providers and tools For more than one provider, keys in the system keychain and **tools** (functions the model can call), use [`pyzed.agents`](https://albakore.github.io/pyzed/guides/agents/), the engine of Pyzed Design's agent. `Agent.run` blocks, so run it in a worker with `pz.Command.perform` and forward its events to `update` with `pz.post`, which is safe from any thread: ```python import pyzed as pz from pyzed import agents, ui @agents.tool def stock(product: str) -> int: """How many units of a product are in stock.""" return {"chair": 4, "desk": 0}.get(product, 0) class Shop(pz.App): def __init__(self): self.agent = agents.Agent("ollama:qwen3:8b", tools=[stock], system="You help the shop's staff.") self.answer, self.prompt, self.busy = "", "", False def ask(self): self.answer, self.busy = "", True return pz.Command.perform(lambda: self.agent.run(self.prompt, on_event=pz.post), self.finished) def finished(self, text): self.busy = False def update(self, event): if isinstance(event, agents.Event) and event.kind == "text": self.answer += event.text def view(self): return pz.column([ ui.markdown(self.answer or "Ask about the stock."), ui.spinner() if self.busy else None, ui.input(self.prompt, lambda v: setattr(self, "prompt", v), on_submit=self.ask), ], spacing=8, padding=16) ``` #### Ideas to build on - **Several conversations**: keep a list of threads and show them in a sidebar with `ui.list_item`, like the [chat block](https://albakore.github.io/pyzed/blocks/chat/). - **Attachments**: read a file the user picks and add its text to the message. - **Tools that touch the UI**: a tool marked `ui=True` runs on the UI thread through the agent's `executor`. - **A local-first app**: Ollama or LM Studio on the same machine, no key, no data leaves the computer. - **Styling**: give the assistant's avatar a glow (`.style(glow=("#8b5cf6", 18))`) or the composer a glass panel; see [Animation and effects](https://albakore.github.io/pyzed/animation/effects/). ## Components by category ### Overview > Overview of pyzed.ui, the ready-made component set for Python desktop GUIs: workspaces, tabs, trees, editors, palettes, menus and more. Source: https://albakore.github.io/pyzed/components/overview/ :::tip[Component library] Every component now has its own page, with a live preview, examples and its full API, in the [component library](https://albakore.github.io/pyzed/library/). This page is kept as a summary. ::: `pyzed.ui` is a collection of ready-to-use components that reproduce the look of the [Zed](https://zed.dev) editor. They are **functions** that return widgets, so they combine freely with the [base widgets](https://albakore.github.io/pyzed/reference/widgets/) and with each other. (Image: Component gallery (One Dark)) ```python import pyzed as pz from pyzed import ui ui.button("Save", self.save, icon="save", variant="solid", keys="ctrl-s") ``` #### Common rules - **Theme colors.** Every component reads the current theme (`App.theme`, or the window's theme in [multi-window](https://albakore.github.io/pyzed/guides/windows/) apps). Changing the theme restyles the whole UI. All of them accept `theme=` to use a different one. - **Tokens or hex.** Color parameters (`color`, `icon_color`, `tint`…) accept a theme token such as `"accent"`, `"text_muted"` or `"error"`, or a `"#rrggbb"` color. - **Messages or callables.** `on_press`, `on_select`, `on_toggle`… accept a message for `update` or a callable. Those that produce a value (text, `bool`, id) call the callable with that value. - **Icons.** The `icon=` parameter accepts any [Lucide](https://lucide.dev) name. - **Shortcuts.** `keys="ctrl-shift-p"` displays the shortcut (with symbols on macOS). #### Groups | Group | Components | | --- | --- | | [Basics](https://albakore.github.io/pyzed/components/basics/) | `button`, `icon_button`, `label`, `muted`, `heading`, `badge`, `indicator`, `kbd`, `avatar`, `tooltip`, `divider`, `card` | | [Forms](https://albakore.github.io/pyzed/components/forms/) | `input`, `search_input`, `switch`, `checkbox`, `slider`, `progress`, `segmented`, `field` | | [Lists and tree](https://albakore.github.io/pyzed/components/lists/) | `list_item`, `list_header`, `tree` + `TreeNode`, `menu` + `MenuItem` | | [Navigation](https://albakore.github.io/pyzed/components/navigation/) | `tab_bar` + `Tab`, `breadcrumbs`, `toolbar` | | [Workspace](https://albakore.github.io/pyzed/components/workspace/) | `workspace`, `title_bar`, `title_button`, `status_bar`, `status_item`, `panel`, `panel_header` | | [Overlays](https://albakore.github.io/pyzed/components/overlays/) | `layer`, `modal`, `command_palette`, `picker`, `fuzzy_filter`, `toast`, `toasts` | | [Code](https://albakore.github.io/pyzed/components/code/) | `editor`, `terminal`, `ansi` | | [More controls](https://albakore.github.io/pyzed/components/controls/) | `select`, `radio_group`, `number_field`, `split_button`, `toggle_group`, `chip`, `disclosure` | | [Data and feedback](https://albakore.github.io/pyzed/components/data/) | `table` + `Column`, `callout`, `empty_state`, `settings_group`, `stat`, `sparkline`, `bar_chart`, `donut`, `spinner`, `count_badge`, `facepile`, `diff_stat`, `keybinding_hint` | | [Content and panels](https://albakore.github.io/pyzed/components/content/) | `markdown`, `split`, `popover`; base widgets `pz.image`, `pz.image_viewer`, `pz.rich_text`, `pz.qr_code`, `pz.grid` | | [Dialogs and tools](https://albakore.github.io/pyzed/components/dialogs/) | `dialog_layer`, `alert`, `confirm`, `prompt`, `choice`, `unsaved_changes`, `accordion`, `Log` + `console`, `shortcuts`, `about`, `ColorPicker`, `color_swatch` | To see them all in action, run the gallery: ```bash python examples/gallery.py ``` :::tip[Illustrations] The images in this section are generated with `python docs/scripts/component_shots.py`, which renders each example offscreen with `App.screenshot`. ::: ### Basics > Pyzed basics components for Python desktop GUIs: buttons, text, badges, keyboard shortcuts, avatars, tooltips and cards that follow the theme. Source: https://albakore.github.io/pyzed/components/basics/ :::tip[Component library] Every component now has its own page, with a live preview, examples and its full API, in the [component library](https://albakore.github.io/pyzed/library/). This page is kept as a summary. ::: #### Buttons (Image: Buttons) ```python pz.row([ ui.button("Filled", self.action), ui.button("Ghost", self.action, variant="ghost"), ui.button("Outline", self.action, variant="outline"), ui.button("Solid", self.action, variant="solid"), ui.button("Disabled", self.action, disabled=True), ], spacing=8) ui.button("Error", self.delete, variant="error", icon="trash-2") ui.button("Save", self.save, icon="save", keys="ctrl-s") ui.button("lg", self.action, size="lg") ui.icon_button("settings", self.settings, tooltip="Settings") ui.icon_button("bell", self.notifications, selected=True) ``` - `variant`: `"filled"` (default), `"ghost"`, `"subtle"`, `"outline"`, `"solid"`, `"tinted"` or a semantic color directly (`"accent"`, `"error"`, `"warning"`, `"success"`, `"info"`). - `size`: `"xs"`, `"sm"`, `"md"`, `"lg"`. - `icon` + `icon_position="start" | "end"`, `full_width=True`, `selected=True`. - If `on_press` is `None`, the button is drawn disabled. - Any style parameter of [`pz.button`](https://albakore.github.io/pyzed/reference/widgets/#button) (`bg`, `hover_bg`, `radius`…) overrides the theme's. #### Text (Image: Text) ```python ui.heading("Heading 1") ui.heading("Heading 2", level=2) ui.label("Normal label") ui.muted("Muted text") ui.label("Accent text", color="accent") ui.label("def mono(): ...", mono=True, size="sm") ``` `size` accepts `"xs"`, `"sm"`, `"md"`, `"lg"`, `"xl"`, `"2xl"` or a number. #### Badges, indicators, shortcuts and avatars (Image: Badges, indicators, shortcuts and avatars) ```python ui.badge("success", tint="success") ui.badge("error", tint="error", icon="circle-x") ui.badge("solid", variant="solid") # "soft" (default), "solid", "outline" ui.indicator("success") # colored dot ui.kbd("ctrl-shift-p") # ⌃⇧P on macOS, Ctrl Shift P elsewhere ui.kbd("cmd-k cmd-s") # chords ui.avatar("KV", index=1) # index picks a collaborator color from the theme ``` Shortcut style is controlled with `pyzed.ui.base.KEY_STYLE`: `"auto"` (default), `"symbols"` or `"words"`. #### Tooltip ```python ui.tooltip(ui.icon_button("save", self.save), "Save", keys="ctrl-s", position="bottom") ``` `ui.icon_button(..., tooltip="Text")` is a shortcut for the same thing. #### Card and divider (Image: Card) ```python ui.card( pz.column([ ui.muted("Pyzed draws the UI with Iced in Rust."), pz.row([pz.hspace(), ui.button("Cancel", self.close, variant="ghost"), ui.button("OK", self.accept, variant="solid")], spacing=8), ], spacing=12), title="Card", actions=[ui.icon_button("ellipsis", self.menu, size="sm")], width=380, ) ui.divider() # horizontal ui.divider(vertical=True) ``` Full reference: [Components (pyzed.ui)](https://albakore.github.io/pyzed/reference/ui/#basics). ### Forms > Text fields, switches, checkboxes, sliders, progress bars and segmented groups: form components for native Python desktop GUIs with Pyzed. Source: https://albakore.github.io/pyzed/components/forms/ :::tip[Component library] Every component now has its own page, with a live preview, examples and its full API, in the [component library](https://albakore.github.io/pyzed/library/). This page is kept as a summary. ::: (Image: Form controls) ```python class Settings(pz.App): def __init__(self): self.name = "" self.query = "" self.autosave = True self.format_on_save = False self.font = 0.6 self.mode = "Editor" def view(self): return pz.column([ ui.input(self.name, lambda t: setattr(self, "name", t), placeholder="Project name"), ui.search_input(self.query, lambda t: setattr(self, "query", t)), ui.field("Autosave", ui.switch(self.autosave, lambda v: setattr(self, "autosave", v)), description="Save when switching tabs"), ui.field("Format on save", ui.checkbox(self.format_on_save, lambda v: setattr(self, "format_on_save", v))), ui.field("Font size", pz.container(ui.slider(self.font, lambda v: setattr(self, "font", v)), width=160)), ui.segmented(["Editor", "Terminal", "Both"], self.mode, lambda m: setattr(self, "mode", m)), ui.progress(0.65), ], spacing=12, padding=20, width=420) ``` #### Text fields Fields are **controlled**: they show `value` and notify you of every change through `on_input`. Store the text in your state and pass it back in `view()`. ```python ui.input(self.email, self.set_email, placeholder="you@example.com", icon="mail", on_submit=self.send, id="email") ui.input(self.password, self.set_password, secure=True) # password ui.input(self.query, self.set_query, borderless=True, size="lg") ``` To focus a field from code, give it an `id` and return `Command.focus("email")`. #### Switches and checkboxes `on_toggle` receives the new `bool`: ```python ui.switch(self.wifi, lambda on: setattr(self, "wifi", on), label="Wi-Fi") ui.checkbox(self.accepted, lambda v: setattr(self, "accepted", v), label="I accept the terms") ``` #### Slider and progress ```python ui.slider(self.volume, self.set_volume, min=0, max=100, step=1) ui.progress(self.downloaded, max=self.total, tint="success", height=6) ``` #### Segmented group Mutually exclusive buttons, one selected at a time. `on_select` receives the chosen option: ```python ui.segmented(["Day", "Week", "Month"], self.view_mode, lambda v: setattr(self, "view_mode", v)) ``` #### Settings rows `ui.field(title, control, description=...)` aligns a title on the left and the control on the right, like a settings screen. Full reference: [Forms](https://albakore.github.io/pyzed/reference/ui/#forms). ### Lists, tree and menus > List rows, a project file tree and context menus: list and navigation components for native Python desktop apps built with Pyzed. Source: https://albakore.github.io/pyzed/components/lists/ :::tip[Component library] Every component now has its own page, with a live preview, examples and its full API, in the [component library](https://albakore.github.io/pyzed/library/). This page is kept as a summary. ::: #### List items (Image: List items) ```python pz.column([ ui.list_header("RECENT", end=ui.icon_button("plus", self.new, size="xs")), ui.list_item("main.py", self.open, icon="file-code", icon_color="#3572a5", selected=True, inset=True), ui.list_item("README.md", self.open, icon="file-text", inset=True, end=ui.badge("M", tint="warning")), ui.list_item("Cargo.toml", self.open, icon="file-cog", inset=True, end=ui.kbd("ctrl-1", size="xs")), ui.list_item("old.txt", self.open, icon="file", inset=True, disabled=True), ], width=300) ``` `list_item` has `start` and `end` slots for any widget, `indent` for nesting, `focused` for the focus border and `on_right_press` to open a context menu. To pass the item to the callback, use a default argument in the lambda: ```python [ui.list_item(f, lambda f=f: self.open(f), selected=f == self.current) for f in self.files] ``` #### File tree A project panel like the one in code editors: collapsible folders, per-file-type icons and git colors. (Image: File tree) ```python class Explorer(pz.App): def __init__(self): self.nodes = ui.TreeNode.from_paths( ["src/main.rs", "src/app.rs", "src/ui/button.rs", "src/ui/mod.rs", "tests/app.rs", "Cargo.toml", "README.md"], status={"src/app.rs": "modified", "src/ui/button.rs": "created", "README.md": "modified"}, ) self.expanded = {"src", "src/ui"} self.selected = "src/app.rs" def toggle(self, folder): self.expanded ^= {folder} def select(self, path): self.selected = path def view(self): return ui.tree(self.nodes, expanded=self.expanded, selected=self.selected, on_toggle=self.toggle, on_select=self.select) ``` - `TreeNode.from_paths(paths, status=...)` builds the tree, puts folders first and propagates git status to the folders that contain changes. - Each node's id is its path (`"src/ui/button.rs"`). - `status`: `"modified"`, `"created"`, `"deleted"`, `"conflict"` or `"ignored"`. - You can build the nodes by hand: `TreeNode("src", children=[TreeNode("main.rs", id="src/main.rs")])`. To read a real folder: ```python from pathlib import Path root = Path(".") paths = [p.relative_to(root).as_posix() for p in root.rglob("*") if p.is_file() and ".git" not in p.parts] nodes = ui.TreeNode.from_paths(paths) ``` #### Context menu (Image: Context menu) ```python ui.menu([ ui.MenuItem("New file", self.new, icon="file-plus", keys="ctrl-n"), ui.MenuItem("New folder", self.new_folder, icon="folder-plus"), ui.SEPARATOR, "EDIT", # a plain string is a header ui.MenuItem("Copy path", self.copy, keys="ctrl-alt-c"), ui.MenuItem("Soft wrap", self.toggle_wrap, checked=self.wrap), ui.SEPARATOR, ui.MenuItem("Delete", self.delete, icon="trash-2", danger=True), ]) ``` To show it floating, use it as the overlay of [`ui.layer`](https://albakore.github.io/pyzed/components/overlays/#layers): ```python layout = ui.layer(layout, self.menu_open and ui.menu(...) or None, on_dismiss=self.close_menu, align_x="start", offset=120) ``` Full reference: [Lists, tree and menus](https://albakore.github.io/pyzed/reference/ui/#lists-tree-and-menus). ### Navigation > Editor tabs, breadcrumbs and the toolbar: navigation components for building IDE-like Python desktop GUIs with Pyzed. Source: https://albakore.github.io/pyzed/components/navigation/ :::tip[Component library] Every component now has its own page, with a live preview, examples and its full API, in the [component library](https://albakore.github.io/pyzed/library/). This page is kept as a summary. ::: (Image: Tabs, breadcrumbs and toolbar) ```python pz.column([ ui.tab_bar( [ ui.Tab.for_file("src/main.rs"), ui.Tab.for_file("src/app.rs", dirty=True, status="modified"), ui.Tab.for_file("README.md", preview=True), ], active="src/main.rs", on_select=self.activate, on_close=self.close, start=[ui.icon_button("arrow-left", self.back, size="sm"), ui.icon_button("arrow-right", self.forward, size="sm")], end=[ui.icon_button("plus", self.new, size="sm"), ui.icon_button("columns-2", self.split, size="sm")], ), ui.toolbar( [ui.breadcrumbs([("file-code", "src"), "main.rs", "fn main"])], [ui.icon_button("search", self.search, size="sm")], ), ]) ``` #### Tabs - `Tab(id, label, icon=None, icon_color=None, dirty=False, preview=False, status=None)`. - `Tab.for_file(path)` uses the file name as label, the path as id and the icon for its type. - `dirty=True` shows a dot; on hover it turns into the close button. - `preview=True` draws the name in italics (preview tab). - `status` tints the name with the git color. - `on_select(id)` and `on_close(id)` receive the tab id. A common pattern for managing tabs: ```python def open(self, path): if path not in self.open_tabs: self.open_tabs.append(path) self.active = path def close(self, path): i = self.open_tabs.index(path) self.open_tabs.remove(path) if self.active == path: self.active = self.open_tabs[min(i, len(self.open_tabs) - 1)] if self.open_tabs else None ``` #### Breadcrumbs ```python ui.breadcrumbs(["src", "ui", "button.rs"]) ui.breadcrumbs([("folder", "src"), ("file-code", "main.rs")], on_press=self.go_to_level) ``` Parts can be strings or `(icon, text)` tuples. `on_press` receives the index. #### Toolbar `ui.toolbar(start, end)` is the bar below the tabs of a code editor: usually breadcrumbs on the left and buffer actions on the right. Full reference: [Navigation](https://albakore.github.io/pyzed/reference/ui/#navigation). ### Workspace > Build an editor-style window in Python: title bar, resizable docks, panels and status bar with the Pyzed workspace components. Source: https://albakore.github.io/pyzed/components/workspace/ :::tip[Component library] Every component now has its own page, with a live preview, examples and its full API, in the [component library](https://albakore.github.io/pyzed/library/). This page is kept as a summary. ::: `ui.workspace` builds the structure of an editor-style window: title bar on top, docks on the left, right and bottom, the central area and the status bar. (Image: Workspace) ```python def view(self): return ui.workspace( pz.column([ ui.tab_bar([ui.Tab.for_file("src/main.py")], "src/main.py", on_close=self.close), ui.editor("src/main.py", self.code, path="main.py"), ]), title_bar=ui.title_bar( [ui.title_button("pyzed", chevron=True), ui.title_button("main", icon="git-branch", muted=True)], [ui.icon_button("bell", self.notifications, size="sm"), ui.avatar("KV")], ), left=pz.column([ ui.panel_header("Project", [ui.icon_button("plus", self.new, size="xs")]), ui.tree(self.nodes, expanded=self.expanded, selected="src/main.py"), ]) if self.show_left else None, bottom=pz.column([ ui.panel_header("Terminal", icon="terminal"), ui.terminal("$ python main.py\n\x1b[32m✓\x1b[0m done"), ]) if self.show_bottom else None, status_bar=ui.status_bar( [ui.status_item("0", icon="circle-x"), ui.status_item("1", icon="triangle-alert")], [ui.status_item("Ln 4, Col 8"), ui.status_item("Python")], ), left_width=200, bottom_height=110, ) ``` #### Docks - Pass `None` as `left`, `right` or `bottom` to hide a dock. That is how shortcuts like Ctrl+B are implemented (`self.show_left = not self.show_left`). - `left_width`, `right_width` and `bottom_height` set their size. - Inside a dock, `ui.panel_header(title, actions, icon=...)` acts as the header. - `ui.panel(content, header=..., side=...)` is useful if you build your own layout. #### Title bar ```python ui.title_bar( start=[ui.title_button("my-project", self.projects, chevron=True)], end=[ui.icon_button("settings", self.settings, size="sm")], center=ui.muted("main.py"), ) ``` #### Status bar ```python ui.status_bar( [ui.status_item(icon="panel-left", on_press=self.toggle_left, selected=self.show_left, tooltip="Project panel", keys="ctrl-b")], [ui.status_item(f"Ln {line}, Col {col}"), ui.status_item(pz.icons.language_for(self.active))], ) ``` Full reference: [Workspace](https://albakore.github.io/pyzed/reference/ui/#workspace). ### Overlays > Layers, modals, a command palette, fuzzy-search pickers and toast notifications: overlay components for Python desktop apps built with Pyzed. Source: https://albakore.github.io/pyzed/components/overlays/ :::tip[Component library] Every component now has its own page, with a live preview, examples and its full API, in the [component library](https://albakore.github.io/pyzed/library/). This page is kept as a summary. ::: #### Layers `ui.layer(base, overlay, on_dismiss=...)` draws `overlay` on top of the UI. Clicking outside sends `on_dismiss`. If `overlay` is `None`, it returns `base` unchanged, so you can write: ```python def view(self): layout = ui.workspace(...) return ui.layer(layout, self.dialog(), on_dismiss=self.close, backdrop="#00000066") def dialog(self): if not self.confirming: return None return ui.modal(...) ``` `align_x`, `align_y` and `offset` control where the overlay appears (by default, centered at the top, like a command palette). #### Modal (Image: Modal) ```python ui.modal( ui.muted("main.py has unsaved changes. Do you want to save them before closing?"), title="Save changes?", on_close=self.close, footer=[ ui.button("Don't save", self.discard, variant="ghost"), ui.button("Cancel", self.close), ui.button("Save", self.save, variant="solid"), ], width=420, ) ``` #### Command palette (Image: Command palette) ```python class App(pz.App): def __init__(self): self.palette_open = False self.query = "" self.selected = 0 def commands(self): return [ ui.PaletteItem("editor: toggle soft wrap", self.toggle_wrap, keys="alt-z"), ui.PaletteItem("theme selector: toggle", self.toggle_theme, keys="ctrl-k ctrl-t"), ui.PaletteItem("workspace: toggle left dock", self.toggle_left, keys="ctrl-b"), ] def on_key(self, combo): if combo == "ctrl-shift-p": self.palette_open, self.query, self.selected = True, "", 0 return Command.focus("command-palette") if self.palette_open and combo == "escape": self.palette_open = False elif self.palette_open and combo in ("up", "down"): self.selected += 1 if combo == "down" else -1 def run_selected(self): matches = ui.fuzzy_filter(self.query, self.commands(), key=lambda c: c.label) if matches: self.palette_open = False command = matches[self.selected % len(matches)][0] return command.on_press() def view(self): layout = ... palette = None if self.palette_open: palette = ui.command_palette(self.query, lambda q: setattr(self, "query", q), self.commands(), selected=self.selected, on_submit=self.run_selected) return ui.layer(layout, palette, on_dismiss=lambda: setattr(self, "palette_open", False)) ``` Enter (`on_submit`) runs the highlighted command; the arrow keys change `selected`. #### Picker and fuzzy search (Image: Picker with fuzzy matches) `command_palette` is built on top of `picker` and `fuzzy_filter`. Use them for your own finders (files, symbols, branches…): ```python matches = ui.fuzzy_filter(self.query, self.files) # [(file, positions), ...] items = [(ui.PaletteItem(Path(f).name, lambda f=f: self.open(f), icon="file", detail=f), pos) for f, pos in matches[:50]] ui.picker(self.query, self.set_query, items, selected=self.sel, placeholder="Search files…") ``` `fuzzy_match(query, text)` returns `(score, positions)` or `None`, and `highlighted()` draws a text with the matches highlighted. #### Notifications (Image: Notifications) ```python ui.toast("Saved", "main.py was saved successfully.", kind="success", on_dismiss=self.dismiss_toast) ui.toast("Update available", "Pyzed 0.2 is now on PyPI.", kind="info", actions=[ui.button("What's new", self.whats_new, size="sm")]) ``` `ui.toasts(base, items)` stacks them in the bottom-right corner. To make them disappear on their own, use a thread and [`pz.post`](https://albakore.github.io/pyzed/guides/commands/#messages-from-other-threads): ```python def notify(self, title): self.toasts.append(title) threading.Timer(3, pz.post, args=(("remove_toast", title),)).start() ``` Full reference: [Overlays](https://albakore.github.io/pyzed/reference/ui/#overlays). ### Code > A code editor with syntax highlighting and git markers, and a terminal view: ready-made components for Python desktop apps built with Pyzed. Source: https://albakore.github.io/pyzed/components/code/ :::tip[Component library] Every component now has its own page, with a live preview, examples and its full API, in the [component library](https://albakore.github.io/pyzed/library/). This page is kept as a summary. ::: #### Editor (Image: Code editor) ```python ui.editor( "counter.py", # buffer id code, # initial text path="counter.py", # infers the language markers={6: "modified", 7: "created", 10: "created"}, on_change=self.changed, # receives the full text on_cursor=self.cursor, # receives (line, column), 1-based ) ``` - **Uncontrolled.** The text, cursor, selection and undo history live in Rust, tied to the `id`. `text` is only used the first time that `id` appears. To replace the content, return `Command.set_editor_text(id, text)`. - **One buffer per file.** Use the path as `id`: when switching tabs, each file keeps its cursor and history. - **Highlighting.** Over 100 languages (syntect / two-face grammars) using the theme's syntax colors. The language is inferred from `path` or passed with `language="rs"`. - **Git markers.** `markers` assigns `"created"`, `"modified"` or `"deleted"` to each line. - **Cursor.** `Command.set_editor_cursor(id, line, column)` moves the cursor, even before the first draw. ```python def save(self): Path(self.active).write_text(self.texts[self.active]) self.dirty.discard(self.active) def changed(self, text): self.texts[self.active] = text self.dirty.add(self.active) ``` - **Long lines.** With `wrap=False` (default) the editor scrolls horizontally and the line numbers stay pinned on the left. With `wrap=True` lines wrap and the number appears on the first row of each line. - **Active line.** The cursor line is highlighted and scrolling follows the cursor (also with PgUp/PgDn). ##### Editing like a modern code editor (Image: Folded blocks, code lens and the minimap) ```python ui.editor( "app.py", code, path="app.py", minimap=True, # or a width in pixels cursor_shape="block", # "bar" (default), "block", "underline", "hollow" on_zoom=self.zoom, # 1, -1 or 0 from Ctrl+= / Ctrl+- / Ctrl+0 and Ctrl+wheel font_size=self.font_size, ) def zoom(self, step): self.font_size = 14 if step == 0 else self.font_size + step ``` - **Auto-closing pairs** (`auto_close=True`, the default). Typing `(`, `[`, `{`, `"`, `'` or `` ` `` also inserts its closing pair and leaves the cursor in between. With a selection, the pair wraps it. Typing a closer that is already next steps over it, Backspace inside an empty pair deletes both, and Enter between brackets opens an indented line. Quotes are not paired inside words (`don't`), but they are after a string prefix (`f"`, `rb'`). - **Folding** (`folding=True`, the default). Blocks fold by indentation: hover the gutter to see the chevrons, and click one to fold its block. A folded line ends in `⋯` (click it to unfold). Ctrl+Shift+[ folds the innermost block around the cursor and Ctrl+Shift+] unfolds it. `Command.fold_line(id, line)` and `Command.fold_all(id)` do it from code. Line numbers, `markers`, `annotations`, `on_change`, `on_cursor` and `set_editor_cursor` keep using real lines; editing a folded line or moving the cursor into a fold unfolds it. - **Minimap.** `minimap=True` draws the code zoomed out on the right with the visible area highlighted; click to jump, drag to scroll, or use the wheel over it. - **Font zoom.** The editor reports Ctrl+=, Ctrl+-, Ctrl+0 and Ctrl+wheel to `on_zoom`; change `font_size` in response. - **Cursor.** `cursor_shape`, `cursor_blink`, `cursor_width` (of the bar) and `cursor_color` (the theme's player color by default), like Zed's `cursor_shape` setting. ##### Multiple cursors and Zed's keys (Image: Three cursors and the search results for prices) The editor uses Zed's and VS Code's default keys (Linux and Windows; Cmd instead of Ctrl on macOS): | Keys | Action | | --- | --- | | Alt+click | Add a cursor (or remove the one under the click) | | Ctrl+Alt+↑ / ↓ | Add a cursor above / below | | Ctrl+D | Select the word under the cursor, then add the next match as a cursor | | Ctrl+Shift+L | Select every match | | Alt+Shift+I | One cursor on each line of the selection | | Ctrl+U | Undo the last cursor change | | Esc | Back to one cursor, then drop the selection | | Ctrl+Z, Ctrl+Y or Ctrl+Shift+Z | Undo and redo (consecutive typing is one step) | | Alt+↑ / ↓ | Move the lines up / down | | Alt+Shift+↑ / ↓ or Ctrl+Alt+Shift+↑ / ↓ | Duplicate the lines of every cursor above / below | | Ctrl+Shift+D | Duplicate the selection of every cursor right after it (the line when there is none) | | Ctrl+Shift+K | Delete the lines | | Ctrl+L | Select the line (again: the next one too) | | Ctrl+Shift+J | Join lines | | Ctrl+Enter / Ctrl+Shift+Enter | New line below / above, indented | | Ctrl+] / Ctrl+[, Shift+Tab | Indent / outdent (Tab indents a selection of several lines) | | Ctrl+/ | Toggle line comments (`#`, `//` or `--`, from `language`) | | Ctrl+M | Jump to the matching (or enclosing) bracket | | Ctrl+Backspace / Ctrl+Delete | Delete the word before / after | | Home | First non-blank character, then the start of the line | | Ctrl+↑ / ↓ | Scroll one line without moving the cursor | | Ctrl+C / Ctrl+X | Without a selection, copy / cut the whole line; with several cursors, every cursor's text | | Ctrl+K Ctrl+0 / Ctrl+J / Ctrl+L | Fold everything / unfold everything / toggle the fold at the cursor | Typing, deleting and moving apply to every cursor. Pasting text with one line per cursor gives each cursor its line, as in Zed. The bracket next to the cursor and its match are outlined (`match_brackets`), and selecting text highlights its other occurrences (`selection_highlight`). Every action has a name (Zed's, in snake case). `on_action(name)` is called after each one runs, `Command.editor_action(id, name)` runs one from code (a menu, a command palette) and `Command.editor_actions()` lists them: ```python ui.editor("main.py", code, path="main.py", on_action=self.ran, highlights=self.found) # [(line, column, end_line, end_column)], 1-based def comment(self): return pz.Command.editor_action("main.py", "toggle_comments") def select_matches(self): # One cursor per range; the first one is the primary cursor. return pz.Command.select_editor_ranges("main.py", self.found) def replace_all(self): # One undo step; on_change gets the new text. return pz.Command.replace_editor_ranges("main.py", [(r, "total") for r in self.found]) ``` Names include `undo`, `redo`, `add_selection_above`, `add_selection_below`, `select_next`, `select_previous`, `select_all_matches`, `split_selection_into_lines`, `undo_selection`, `cancel`, `move_line_up`, `move_line_down`, `duplicate_line_up`, `duplicate_line_down`, `delete_line`, `newline_above`, `newline_below`, `indent`, `outdent`, `select_line`, `join_lines`, `toggle_comments`, `move_to_enclosing_bracket`, `convert_to_upper_case`, `convert_to_lower_case` and `sort_lines_case_sensitive`. ##### Input methods Chinese, Japanese, Korean and other languages typed through an input method (IME) work in the editor. While you compose, the IME's text is drawn in a box at the cursor and the caret is hidden; the committed text goes in at every cursor, and consecutive commits are undone in one step, like typing. The IME follows the cursor as you move it. (Image: Composing 你好 with a pinyin input method) #### Terminal (Image: Terminal) ```python output = "$ pytest -q\n\x1b[32m24 passed\x1b[0m in 1.21s\n$ " ui.terminal(output, id="term") ``` It is a **read-only** view that understands ANSI colors and stays scrolled to the bottom. Feed it the output of a process you run yourself (for example with `subprocess` in a thread and `pz.post`). It also accepts a list of lines made of `(text, color)` segments, where the color is an ANSI name (`"green"`, `"red"`, `"dim"`…) or a theme token. `ui.ansi(text)` converts to segments in case you want to draw them your own way. Full reference: [Code](https://albakore.github.io/pyzed/reference/ui/#code). ### More controls > Dropdowns, radio buttons, number fields, split buttons, chips and collapsible sections: themed input controls for Python desktop apps. Source: https://albakore.github.io/pyzed/components/controls/ :::tip[Component library] Every component now has its own page, with a live preview, examples and its full API, in the [component library](https://albakore.github.io/pyzed/library/). This page is kept as a summary. ::: (Image: More controls) ```python class Settings(pz.App): def __init__(self): self.language = "Rust" self.font = 14 self.indentation = "a" self.alignment = "c" self.advanced = True def view(self): return pz.column([ pz.row([ ui.select(["Python", "Rust", "TypeScript"], self.language, lambda v: setattr(self, "language", v), width=160), ui.number_field(self.font, lambda v: setattr(self, "font", v), min=8, max=32, suffix="px"), ui.split_button("Run", self.run, self.open_menu, icon="play"), ], spacing=10, align="center"), ui.radio_group([("a", "Spaces"), ("b", "Tabs")], self.indentation, lambda v: setattr(self, "indentation", v), horizontal=True), ui.toggle_group([("l", "align-left"), ("c", "align-center"), ("r", "align-right")], self.alignment, lambda v: setattr(self, "alignment", v)), pz.row([ui.chip("python", icon="tag", selected=True), ui.chip("rust", on_remove=self.remove_tag), ui.chip("iced", on_press=self.filter)], spacing=6), ui.disclosure("Advanced options", ui.muted("Collapsible content."), open=self.advanced, on_toggle=lambda: setattr(self, "advanced", not self.advanced)), ], spacing=14) ``` | Component | Purpose | | --- | --- | | `ui.select(options, selected, on_select)` | Themed dropdown. Calls `on_select` with the chosen option. | | `ui.radio_group(options, selected, on_select)` | Mutually exclusive options. `options` are strings or `(value, label)` pairs. `horizontal=True` lays them out in a row. | | `ui.number_field(value, on_change)` | Number with `−`/`+` buttons, `min`/`max` limits, `step` and `suffix`. | | `ui.split_button(content, on_press, on_menu)` | Main action plus a chevron for more options (usually opens a [`ui.popover`](https://albakore.github.io/pyzed/components/content/#popover)). | | `ui.toggle_group(options, selected, on_select)` | Icon-only segmented group: `options` are `(value, icon)` pairs. | | `ui.chip(content)` | Compact tag. `on_press` makes it clickable and `on_remove` adds an ✕. | | `ui.disclosure(title, content, open=)` | Collapsible section. The open/closed state is yours: change it in `on_toggle`. | For the basic controls (`input`, `switch`, `slider`…) see [Forms](https://albakore.github.io/pyzed/components/forms/). Full reference: [Controls](https://albakore.github.io/pyzed/reference/ui/#controls). ### Data and feedback > Tables, callouts, empty states, grouped settings, stats, charts and indicators: data display components for Python desktop GUIs with Pyzed. Source: https://albakore.github.io/pyzed/components/data/ :::tip[Component library] Every component now has its own page, with a live preview, examples and its full API, in the [component library](https://albakore.github.io/pyzed/library/). This page is kept as a summary. ::: #### Callouts and indicators (Image: Callouts and indicators) ```python ui.callout("A new version is available", "Pyzed 0.2 adds windows, async and canvas.", tint="info", actions=[ui.button("What's new", self.whats_new, size="sm")], on_dismiss=self.close) ui.callout("Could not save", "The file is read-only.", tint="error") pz.row([ ui.spinner(), # only spins while on screen ui.count_badge(7), ui.count_badge(120, tint="error"), # shows "99+" ui.facepile(["Ana", "Luis", "Kev", "Marta"]), # overlapping avatars ui.diff_stat(42, 9), # +42 −9 and colored blocks ui.keybinding_hint("Open palette", "ctrl-shift-p"), ], spacing=14, align="center") ``` `tint` accepts `"info"`, `"success"`, `"warning"`, `"error"` or any theme token or color. :::note `ui.spinner()` is animated by the app clock, so it needs redraws. While it is shown, return a subscription such as `pz.every(1 / 30, lambda: None)` (see [Async](https://albakore.github.io/pyzed/guides/async/)). ::: #### Tables (Image: Table) ```python ui.table( [ui.Column("File", "name"), ui.Column("Language", "lang", width=120), ui.Column("Lines", "lines", width=80, align="end")], self.rows, # dicts or objects sort_by=self.sort_column, descending=self.desc, on_sort=self.sort, on_row_press=self.select, selected=self.selection, striped=True, ) def sort(self, title): self.desc = not self.desc if self.sort_column == title else False self.sort_column = title ``` `ui.Column(title, key, width="fill", align="start", render=None, sortable=True)`: `key` is a dict key, an attribute or a `row -> value` function; `render(value)` returns a widget for the cell. The table sorts by the column whose **title** is `sort_by`; you only store the title and the direction. #### Stats and charts (Image: Stats and charts) ```python ui.stat("Users", "1,284", delta=12.5, trend=[3, 5, 4, 6, 8, 7, 9]) ui.stat("Errors", "37", delta=-8.0) ui.bar_chart([12, 19, 8, 15, 22, 17], labels=["M", "T", "W", "T", "F", "S"], width=260, height=150) ui.donut([60, 25, 15], label="60%") ui.sparkline([1, 4, 2, 6, 3, 7, 5, 8]) ``` They are drawn with [`pz.canvas`](https://albakore.github.io/pyzed/guides/canvas/) and use the theme colors (`tint`, `colors`). #### Empty states and settings (Image: Empty state and settings group) ```python ui.empty_state("No results", "Try a different search.", icon="search", actions=[ui.button("Clear filters", self.clear, size="sm")]) ui.settings_group("Editor", [ ui.field("Font size", ui.number_field(self.font, self.set_font, suffix="px")), ui.field("Soft wrap", ui.switch(self.wrap, self.set_wrap)), ], description="Editor preferences.") ``` Full reference: [Data and feedback](https://albakore.github.io/pyzed/reference/ui/#data-and-feedback). ### Content and panels > Markdown, rich text, images, QR codes, resizable panels, grids and popovers: content components for Python desktop GUIs built with Pyzed. Source: https://albakore.github.io/pyzed/components/content/ :::tip[Component library] Every component now has its own page, with a live preview, examples and its full API, in the [component library](https://albakore.github.io/pyzed/library/). This page is kept as a summary. ::: #### Markdown, rich text and QR (Image: Markdown, rich_text and QR) ```python pz.markdown("## Markdown\n\nText with **bold**, `code` and [links](https://iced.rs).", on_link=webbrowser.open) # import webbrowser pz.rich_text([ pz.span("rich_text "), pz.span("mixes ", color="#e5c07b"), pz.span("styles", weight="bold", underline=True), pz.span(" in one line", italic=True), ]) pz.qr_code("https://albakore.github.io/pyzed/", cell_size=4) ``` - `pz.markdown(source)` renders Markdown with the theme colors. `ui.markdown` is the same idea with the components' sizes and colors. `on_link(url)` receives clicked links. - `pz.rich_text(spans)` combines `pz.span(text, color=, size=, font=, weight=, italic=, bg=, underline=, strikethrough=, link=)` fragments into a single block of text. - `pz.qr_code(data)` draws a QR code; `color` and `bg` change its colors. Full example: [`markdown_viewer.py`](https://github.com/albakore/pyzed/blob/main/examples/markdown_viewer.py). #### Images (Image: Images) ```python pz.image("screenshot.png", width=240, height=140, content_fit="contain") pz.image("screenshot.png", width=160, height=140, opacity=0.5) pz.image("pixel-art.png", width=120, height=140, filter="nearest") pz.image_viewer("map.png", width="fill", height="fill") # wheel to zoom, drag to pan ``` `source` can be a path, `bytes` with the file (PNG, JPEG, GIF, WebP, BMP…) or RGBA pixels with `size=(width, height)`. Options: `content_fit` (`"contain"`, `"cover"`, `"fill"`, `"none"`, `"scale_down"`), `filter` (`"linear"` or `"nearest"`), `opacity`, `radius`, `rotation` and `scale`. For vector icons use `pz.svg`. Full example: [`image_viewer.py`](https://github.com/albakore/pyzed/blob/main/examples/image_viewer.py). #### Resizable panels (Image: Split panels) ```python ui.split( sidebar, ui.split(editor, terminal, vertical=True, ratio=self.height, on_resize=self.set_height), ratio=self.width, on_resize=self.set_width, ) ``` `ui.split(first, second)` separates two widgets with a draggable divider. The ratio is yours: store it in `on_resize(ratio)`. `vertical=True` stacks them. `pz.split` is the base version, with configurable `color`, `hover_color`, `thickness` and `handle`. #### Grids ```python pz.grid([card(p) for p in projects], columns=3, spacing=12) pz.grid(thumbnails, max_width=180, spacing=8, aspect_ratio=1) # columns based on width ``` `columns` sets the number of columns; `max_width` computes them from the available space. #### Popover (Image: Popover with a context menu) ```python def view(self): base = pz.mouse_area(content, on_move=self.track_pointer, on_right_press=self.open_menu) menu = ui.menu([ui.MenuItem("Copy", self.copy), ui.MenuItem("Paste", self.paste)]) if self.menu else None return ui.popover(base, menu, x=self.x, y=self.y, on_dismiss=self.close_menu) ``` `ui.popover(base, content, x=, y=)` shows `content` on top of `base` at that position; clicking outside calls `on_dismiss`. With `content=None` only `base` is shown. Full example: [`context_menu.py`](https://github.com/albakore/pyzed/blob/main/examples/context_menu.py). Reference: [Base widgets](https://albakore.github.io/pyzed/reference/widgets/) and [Overlays](https://albakore.github.io/pyzed/reference/ui/#overlays). ### Dialogs and tools > Ready-made dialogs, accordions, a log console, a shortcuts list, an About box and a color picker for Python desktop apps built with Pyzed. Source: https://albakore.github.io/pyzed/components/dialogs/ :::tip[Component library] Every component now has its own page, with a live preview, examples and its full API, in the [component library](https://albakore.github.io/pyzed/library/). This page is kept as a summary. ::: #### Dialogs (Image: A confirmation and a prompt dialog) Keep which dialog is open in your state and wrap the view with `ui.dialog_layer`, which dims the window, centers the dialog and calls `on_dismiss` when the backdrop is clicked. With `dialog=None` it returns the view unchanged, so it can always wrap it. ```python class Files(pz.App): def __init__(self): self.asking = False self.renaming = None def view(self): dialog = None if self.asking: dialog = ui.confirm("Delete main.py? This can't be undone.", self.delete, self.close_dialog, title="Delete file", danger=True) elif self.renaming is not None: dialog = ui.prompt("Rename", self.renaming, self.set_name, self.rename, self.close_dialog) return ui.dialog_layer(self.main_view(), dialog, on_dismiss=self.close_dialog) ``` | Dialog | What it does | | --- | --- | | `ui.alert(message, on_close, kind=)` | A message with one button; `kind` (`"info"`, `"success"`, `"warning"`, `"error"`) adds an icon. | | `ui.confirm(message, on_confirm, on_cancel, danger=)` | A yes/no question; `danger=True` paints the confirm button red. | | `ui.prompt(title, value, on_input, on_submit, on_cancel)` | Asks for a line of text; Enter or the button 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?" | Button labels are translated with [`pyzed.i18n`](https://albakore.github.io/pyzed/guides/i18n/). For anything else, `ui.modal(content, title=, footer=[...])` is the same surface with your own content. #### Accordion (Image: An accordion with two open sections) ```python ui.accordion( [("editor", "Editor", editor_settings), ("files", "Files", file_settings), ("git", "Git", git_settings)], open=self.open_sections, # a set of ids, a {id: bool} dict or True on_toggle=lambda key: self.open_sections.symmetric_difference_update({key}), icons={"editor": "file-code", "files": "folder", "git": "git-branch"}, ) ``` Sections can also be `ui.AccordionSection(key, title, content, icon=, end=)`, where `end` adds widgets to the right of the header. For a single collapsible block use [`ui.disclosure`](https://albakore.github.io/pyzed/components/controls/). #### Log console (Image: A log console with five levels) `ui.Log` collects entries (thread-safe, capped, repeated lines folded) and `ui.console` shows them like a browser's dev tools console: a filter box, one toggle per level with its count, copy and clear. ```python self.log = ui.Log(on_write=lambda: pz.post("log")) # redraw when a line arrives self.log.write("warning", "settings.json not found, using defaults") self.log.capture() # also collect print() and stderr ui.console(self.log.entries, query=self.query, on_query=self.set_query, hidden=self.hidden, on_toggle_level=self.toggle_level, on_clear=self.log.clear, on_copy=lambda: pz.Command.copy(self.log.text())) ``` Levels are `info`, `event`, `stdout`, `stderr`, `warning` and `error` (`ui.LOG_LEVELS`). #### Shortcuts and About | Keyboard shortcuts | About box | | --- | --- | | (Image: A searchable shortcuts list) | (Image: An About box) | ```python ui.shortcuts([("File", [("Save", "ctrl-s"), ("Open file", "ctrl-p")]), ("View", [("Command palette", "ctrl-shift-p")])], query=self.query, on_query=self.set_query) ui.about("Notes", "1.2.0", "A small notes app built with Pyzed.", icon="notebook-pen", details=[("Python", "3.12")], links=[("Website", lambda: webbrowser.open(URL))]) ``` Both fit well inside a `ui.modal`. #### Color picker (Image: The color picker and color swatches) ```python ui.ColorPicker(value=self.fill, on_change=lambda e: setattr(self, "fill", e.value)) ui.color_swatch(self.fill, size=24, on_press=self.open_picker) ``` `ui.ColorPicker` is a component: it picks a color with alpha, a linear gradient or a fill with an opacity mask, and `on_change` receives a `"#rrggbb[aa]"` string, a `pz.Gradient` or a `pz.Paint`, all valid as a `background`. `ui.color_swatch` shows any of them over a checkerboard. See [Styles](https://albakore.github.io/pyzed/theming/styles/#color-picker). Full reference: [Dialogs](https://albakore.github.io/pyzed/reference/ui/#dialogs), [Console, errors and help](https://albakore.github.io/pyzed/reference/ui/#console-errors-and-help) and [Color picker](https://albakore.github.io/pyzed/reference/ui/#color-picker). ## API reference ### Reference > The Pyzed API reference: every class, function and parameter of pyzed, pyzed.ui, themes, drawing, icons, games, shader graphs, the designer and the command line. Source: https://albakore.github.io/pyzed/reference/ Use these pages when you know what you are looking for and need the exact name, parameters or defaults. To learn how the pieces fit together, start with the [tutorials](https://albakore.github.io/pyzed/tutorials/) and the [guides](https://albakore.github.io/pyzed/guides/architecture/). pz.App, pz.run, windows and screenshots.', href: '/pyzed/reference/app/', icon: 'laptop' }, { title: 'Command', text: 'Side effects returned by update and callbacks.', href: '/pyzed/reference/command/', icon: 'right-arrow' }, { title: 'Core widgets', text: 'Layout, text, controls, the code editor, images and more.', href: '/pyzed/reference/widgets/', icon: 'puzzle' }, { title: 'Components (pyzed.ui)', text: 'Every component, its parameters, defaults and events.', href: '/pyzed/reference/ui/', icon: 'puzzle' }, { title: 'Themes', text: 'Built-in themes, color tokens and custom themes.', href: '/pyzed/reference/theme/', icon: 'moon' }, { title: 'Drawing (pyzed.draw)', text: 'Shapes, paths, text and transforms for the canvas.', href: '/pyzed/reference/draw/', icon: 'pencil' }, { title: 'Icons', text: 'The bundled icon set and file-type icons.', href: '/pyzed/reference/icons/', icon: 'star' }, { title: 'CLI', text: 'Create, run, build and inspect projects from the terminal.', href: '/pyzed/reference/cli/', icon: 'seti:shell' }, { title: 'Visual designer (pyzed.designer)', text: 'Open, run and build forms from Python code.', href: '/pyzed/reference/designer/', icon: 'pencil' }, { title: 'Services (pyzed.services)', text: 'Timers, file watchers, processes, workers, web requests and more.', href: '/pyzed/reference/services/', icon: 'setting' }, { title: 'Games (pyzed.game)', text: 'Held keys, movement, collisions, sprites and particles.', href: '/pyzed/reference/game/', icon: 'rocket' }, { title: 'Shader graphs (pyzed.shading)', text: 'Load, build and edit .pzshad graphs in code.', href: '/pyzed/reference/shading/', icon: 'star' }, ]} /> **Also available to AI agents:** The same reference is served by the [MCP server](https://albakore.github.io/pyzed/ai/mcp/) (`search_reference` and `read_reference`), so a coding agent reads the exact signature instead of guessing it. ### App and running > API reference for pyzed.App: the application class, run, screenshot, post, load_font and the window options of a Python desktop app. Source: https://albakore.github.io/pyzed/reference/app/ Everything you need to start an application. See also the [architecture](https://albakore.github.io/pyzed/guides/architecture/) guide. See also [multiple windows](https://albakore.github.io/pyzed/guides/windows/) and [async and subscriptions](https://albakore.github.io/pyzed/guides/async/). #### Application ##### `App` Base class for applications (Elm architecture, like Iced). It is also the **main window**, so it inherits everything from [`Window`](#window). Override `view()` and, if you use messages, `update(message)`. State lives in instance attributes. | Class attribute | Default | Description | | --- | --- | --- | | `title` | `"Pyzed"` | Window title. Can be a callable (or property) that returns the text. | | `size` | `(1280, 800)` | Initial window size. | | `font_size` | `14` | Default text size. | | `theme` | `themes.ONE_DARK` | A `Theme`, the name of a registered theme or `themes.auto(light, dark)`. Change it at any time. | | `style` | `None` | The global style: a `pz.Style` for the whole window. Its text color, size, background, border color and radius become theme tokens. See [Styles](https://albakore.github.io/pyzed/theming/styles/#the-global-style). | | `system_dark` | `True` | Whether the system is in dark mode (updated by the runtime). | | `tab_focus` | `True` | Tab and Shift+Tab move the focus between the fields of the window. See [Accessibility](https://albakore.github.io/pyzed/guides/accessibility/). | | `splash` | `None` | A [`Splash`](#splash) window shown before the main window opens. | | `rendering` | `None` | A [`Rendering`](#rendering): GPU or CPU, graphics API, GPU and vsync. `None` reads `[tool.pyzed.rendering]` of the project's `pyproject.toml`. | The remaining window options (`min_size`, `resizable`, `decorations`, `icon`, `scale`…) are on `Window`. | Method | Description | | --- | --- | | `view()` | Returns the widget tree. Called after every message. | | `update(message)` | Receives messages. Can return a `Command`, a list of them, a coroutine, a generator (regular or async) or another message. | | `on_start()` | Optional. Runs once at startup; can return a `Command` or a coroutine. | | `on_key(combo)` | Optional. Receives every key as `"ctrl-shift-p"`, `"escape"`, `"up"`… | | `on_key_up(key)` | Optional. Receives key releases by name (`"left"`, `"space"`). | | `subscriptions()` | Optional. Returns the active subscriptions (`pz.every`, `pz.listen`). Re-evaluated after every `update`. | | `resolved_theme()` | The concrete `Theme` in use (resolves names and `themes.auto`). | | `run()` | Opens the window and blocks until the app exits. | | `screenshot(path, *, size=None, scale=2.0, wait=0.1)` | Renders the current state to a PNG without a window. | If `view()` raises an exception, the window shows the traceback instead of closing. **Methods and properties** ###### `App.subscriptions` ```python App.subscriptions() ``` Timers and streams that should run right now (re-evaluated after every update). ###### `App.run` ```python App.run(*, reload=None, watch=()) ``` Opens the window and blocks until the app exits. ###### `App.screenshot` ```python App.screenshot(path, *, size=None, scale=2.0, wait=0.1) ``` Renders the current state off-screen (no GPU or display needed) into a PNG. ##### `run` ```python run(app, *, reload=None, watch=()) ``` Runs an app: opens the window and blocks until it is closed. Accepts the class or an instance. | Parameter | Description | | --- | --- | | `app` | An `App` instance (or the class, which is instantiated with no arguments). | | `reload` | Hot reload: saving a project file updates the running window and keeps its state. `None` follows `PYZED_RELOAD` (set by `pyzed dev`). | | `watch` | Extra folders or files to watch for hot reload. | ##### `screenshot` ```python screenshot(app, path, *, size=None, scale=2.0, wait=0.1) ``` Renders `app` offscreen (no window, no GPU) and saves a PNG. Useful for tests and documentation. | Parameter | Description | | --- | --- | | `app` | An `App` instance (or the class, which is instantiated with no arguments). | | `path` | Path of the output PNG. | | `size` | Logical size `(width, height)`. Defaults to `app.size`. | | `scale` | PNG scale factor (2.0 = retina). | | `wait` | Seconds to wait before capturing (for `on_start` tasks). | ```python pz.screenshot(MyApp, "docs/ui.png", size=(800, 600)) ``` ##### `post` ```python post(message) ``` Sends a message to the running app from **any thread**. Raises `RuntimeError` if no app is running. | Parameter | Description | | --- | --- | | `message` | Any Python value, or a callable. | ```python def worker(): for i in range(100): time.sleep(0.05) pz.post(("progress", i / 100)) threading.Thread(target=worker, daemon=True).start() ``` ##### `UNCHANGED` ```python UNCHANGED(...) ``` Return it from a callback that changed nothing on screen (a pointer move you only record, a focus change): the window is not rebuilt. Any other return value redraws as usual. See the [performance guide](https://albakore.github.io/pyzed/guides/performance/). ```python def pointer_moved(self, x, y): self.pointer = (x, y) return pz.UNCHANGED # nothing visible changed: no rebuild ``` ##### `load_font` ```python load_font(source) ``` Loads an extra font (path to a `.ttf`/`.otf` or its bytes) so you can use it by family name: `text("hello", font="JetBrains Mono")`. | Parameter | Description | | --- | --- | | `source` | Path to the font file, or `bytes`. | #### Windows ##### `Window` An extra window with its own `view()`, title, size and theme. Open it by returning `window.open()` (or `Command.open_window(window)`). Inside it, `self.app` is the app, so windows share state; the messages it produces go to its `update()`, which by default forwards them to `app.update()`. | Attribute | Default | Description | | --- | --- | --- | | `title` | `"Pyzed"` | Text or callable. | | `size` | `(800, 600)` | Initial size. | | `min_size`, `max_size` | `None` | Size limits. | | `position` | `None` | `"center"` or `(x, y)`. | | `resizable` | `True` | Whether it can be resized. | | `decorations` | `True` | `False` removes the system frame (custom title bar with `drag()`). | | `transparent`, `maximized`, `fullscreen` | `False` | Initial state. | | `scale` | `1.0` | UI scale. | | `theme` | `None` | Its own theme; `None` uses the app's. | | `style` | `None` | Its own global style; `None` uses the app's. | | `icon` | `None` | The window's icon (title bar, taskbar): a path to an image or its bytes. Set it on `pz.Window` to give every window the same one. See [Icons and metadata](https://albakore.github.io/pyzed/packaging/icons-metadata/#the-window-icon). | | `app_id` | `None` | Linux: the app id the desktop matches with a `.desktop` file (`com.acme.shop`). | | `app` | — | The app that owns the window (assigned when it opens). | | `window_size` | — | Current size in logical pixels (updated by the runtime). | | `window_position` | — | Where the window is on the screen, its outer top-left corner (updated by the runtime; `None` on Wayland). | | Method | Description | | --- | --- | | `view()` | Required. Returns the window's widget tree. | | `update(message)` | Receives the window's messages (by default forwards them to the app). | | `open()` / `close()` / `drag()` | Commands to open it, close it or start moving it. | | `is_open` | Whether it is open. | | `resolved_theme()` | The concrete `Theme` it uses. | | `on_open()` | When it opens. | | `on_close()` | When the user asks to close it; return `False` to prevent it. | | `on_resize(width, height)` | When it is resized. | | `on_key(combo)`, `subscriptions()` | As in `App`. | | `on_file_hover(path)`, `on_file_drop(path)`, `on_file_leave()` | Files dragged from the system over the window, dropped on it (once per file) or taken away. By default they go to the `ui.DropZone` they land on. | **Methods and properties** ###### `Window.view` ```python Window.view() ``` Returns the window's root widget; subclasses must override it. ###### `Window.update` ```python Window.update(message) ``` Handles a message produced in this window (forwards to the app by default). ###### `Window.on_close` ```python Window.on_close() ``` Called when the user asks to close the window. Return `False` to keep it open. ###### `Window.on_file_hover` ```python Window.on_file_hover(path) ``` A file dragged from the system is over the window: lights up the `ui.DropZone` it would land on. ###### `Window.on_file_drop` ```python Window.on_file_drop(path) ``` A file was dropped on the window from the system (once per file): goes to a `ui.DropZone`. ###### `Window.on_file_leave` ```python Window.on_file_leave() ``` The files dragged over the window left it without being dropped. ###### `Window.is_open` ```python Window.is_open # property ``` Whether the window is currently open. ###### `Window.open` ```python Window.open() ``` A command that opens this window: `return settings.open()`. ###### `Window.close` ```python Window.close() ``` A command that closes this window: `on_press=self.close`. ###### `Window.drag` ```python Window.drag() ``` A command that starts moving the window (for custom title bars). ###### `Window.minimize` ```python Window.minimize() ``` A command that minimizes the window. ###### `Window.toggle_maximize` ```python Window.toggle_maximize() ``` A command that maximizes the window, or restores it when it is maximized. ###### `Window.frame_menus` ```python Window.frame_menus() ``` The menus on the left of a custom frame's title bar (`frame = "custom"`): a list of `ui.Menu`. ###### `Window.resolved_theme` ```python Window.resolved_theme() ``` The concrete theme this window uses right now (with its global style's colors and sizes). ###### `Window.resolved_style` ```python Window.resolved_style() ``` The global style of this window: `pz.set_style` with its own (or the app's) `style` on top. ##### `Splash` ```python class Splash ``` A splash screen: a frameless window with an image, centered on the screen, shown while the app starts. Give it to `App.splash`; it stays up for `duration` seconds at least, then the main window opens and the splash closes. See [Splash screen](https://albakore.github.io/pyzed/packaging/build/#splash-screen). | Parameter | Description | | --- | --- | | `image` | Path to the picture (PNG, JPEG...) or its bytes. | | `duration` | Seconds it stays up, at least. | | `size` | Window size; default: the image's, scaled down to fit 900 × 600. | | `text` | Text written over the bottom of the image (for example `"Loading…"`). | | `color` | Color of the text. | | `background` | Color behind the image (for pictures with transparency). | | `title` | Window title (the taskbar shows it). | | `icon` | Window icon. | ```python class Shop(pz.App): splash = pz.Splash("assets/splash.png", duration=1.5, text="Loading…") ``` ##### `Rendering` ```python @dataclass class Rendering(renderer='auto', gpu_api='auto', power='auto', vsync='auto') ``` How the app draws its windows: on the GPU or the CPU, which graphics API, which GPU and vsync. Each field is `"auto"` by default (Pyzed decides). Give it to `App.rendering`, or set the same keys in `[tool.pyzed.rendering]` of the project's `pyproject.toml`, which `pz.run`, the designer and `pyzed build` read. See [Rendering](https://albakore.github.io/pyzed/editor/settings/#rendering). | Parameter | Description | | --- | --- | | `renderer` | `"auto"`, `"gpu"` (needed by shaders) or `"cpu"` (tiny-skia: starts faster, less memory, works everywhere). | | `gpu_api` | `"auto"`, `"vulkan"`, `"metal"`, `"dx12"` or `"gl"`. | | `power` | `"auto"`, `"high"` (the dedicated GPU) or `"low"` (the integrated one, saves battery). | | `vsync` | `"auto"`, `"on"` (no tearing) or `"off"` (draws as fast as it can). | ```python class Game(pz.App): rendering = pz.Rendering(renderer="gpu", vsync="off") ``` **Methods and properties** ###### `Rendering.from_table` ```python @classmethod Rendering.from_table(table) ``` Builds the settings from a `[tool.pyzed.rendering]` table, ignoring unknown keys and values. ###### `Rendering.to_table` ```python Rendering.to_table() ``` The settings that are not `"auto"`, as written to `pyproject.toml`. ###### `Rendering.custom` ```python Rendering.custom # property ``` Whether any setting is not `"auto"`. ###### `Rendering.env` ```python Rendering.env() ``` The environment variables that carry these settings to Iced and wgpu. ###### `Rendering.apply` ```python Rendering.apply(environ=None) ``` Sets the variables of `env()` that are not set yet (yours win); returns the ones it set. #### Async and subscriptions ##### `every` ```python every(seconds, message, *, key=None) ``` A subscription that sends `message` to `update` every `seconds` seconds (like Iced's `time::every`). If `message` is a callable, it is called. Return it from `subscriptions()`. | Parameter | Description | | --- | --- | | `seconds` | Interval in seconds (greater than 0). | | `message` | Any Python value, or a callable. | | `key` | Key that identifies the subscription (defaults to the message or the function). | ```python def subscriptions(self): return [pz.every(1, "tick")] if self.running else [] ``` ##### `listen` ```python listen(function, *, key=None) ``` A subscription that runs `function()` (a generator or async generator) while it is active and sends every value it yields to `update`. Ideal for websockets and streams. | Parameter | Description | | --- | --- | | `function` | Generator function (regular or `async def`). | | `key` | Key that identifies the subscription (defaults to the message or the function). | ```python async def feed(): async for message in websocket: yield message def subscriptions(self): return [pz.listen(feed)] ``` ##### `frames` ```python frames(callback, *, key=None, max_fps=240.0) ``` A subscription that calls `callback(dt)` once per displayed frame (in sync with the screen, usually 60 times a second), with `dt` the seconds since the previous frame (at most 0.25). Made for games and simulations: move things by `speed * dt`. See [Making games](https://albakore.github.io/pyzed/games/). | Parameter | Description | | --- | --- | | `callback` | Callable that receives `dt` (a `float`). | | `key` | Key that identifies the subscription (defaults to the message or the function). | | `max_fps` | Highest rate where the display does not wait for vsync (software rendering). Default 240. | ```python def subscriptions(self): return [] if self.paused else [pz.frames(self.tick)] def tick(self, dt): self.x += game.axis("left", "right") * 300 * dt ``` ##### `pressed` ```python pressed() ``` The keys held down right now, as a `frozenset` of names without modifiers (`"left"`, `"space"`, `"a"`…). Empty when no window is open. Poll it every frame for continuous movement; `on_key` is for one-off presses. ##### `is_pressed` ```python is_pressed(*keys, held=None) ``` Whether any of `keys` is held down. `held` replaces `pressed()` (handy in tests). | Parameter | Description | | --- | --- | | `keys` | Key names, such as `"left"`, `"a"`. | | `held` | Optional set of keys to use instead of `pressed()`. | ```python if pz.is_pressed("space") and self.cooldown <= 0: self.fire() ``` ##### `Subscription` What `every` and `listen` return. It is identified by its `key`: as long as `subscriptions()` keeps returning one with the same key, it keeps running; when it disappears, it stops. ##### `spawn` ```python spawn(awaitable) ``` Schedules a coroutine on Pyzed's asyncio loop without waiting for its result and returns a `concurrent.futures.Future`. To receive the result in the UI, return the coroutine from `update` or use `Command.perform`. | Parameter | Description | | --- | --- | | `awaitable` | The coroutine. | ##### `asyncio_loop` ```python asyncio_loop() ``` The asyncio loop (on a background thread) where Pyzed runs every coroutine. It starts the first time it is used. #### Events and components ##### `on` ```python on(widget, *, cursor=None, **handlers) ``` Attaches common event handlers to a widget; same as `widget.on(...)`. Events: `click`, `double_click`, `right_click`, `middle_click`, `mouse_down`, `mouse_up`, `mouse_enter`, `mouse_leave`, `mouse_move`, `scroll`, `drag_start`, `drag`, `drag_end`, `focus`, `blur`, `key_down`, `key_up`, `resize`. See [Events](https://albakore.github.io/pyzed/guides/events/). | Parameter | Description | | --- | --- | | `widget` | The widget (a string is wrapped in a container). | | `cursor` | Mouse cursor over the widget, such as `"pointer"` or `"grab"`. | | `handlers` | `event=handler` pairs. A handler that takes an argument gets a `pz.Event`; a non-callable value is a message for `update`; `None` removes it. | ```python pz.container(card, padding=12).on(click=self.select, drag=lambda e: self.move(e.dx, e.dy)) ``` ##### `Event` ```python class Event ``` What happened, passed to event handlers: `type`, `x`, `y` (relative to the widget), `dx`, `dy` (scroll or drag movement), `width`, `height`, `button`, `key`, `shift`, `ctrl`, `alt`, `logo`, `window_x`, `window_y`, `screen_x`, `screen_y` (the pointer in the window and on the screen), and `value`/`source` for custom events. | Parameter | Description | | --- | --- | | `type` | Event name. | | `x` | Pointer x, relative to the widget. | | `y` | Pointer y, relative to the widget. | | `dx` | Horizontal scroll (lines) or drag movement. | | `dy` | Vertical scroll (lines) or drag movement. | | `width` | Widget width. | | `height` | Widget height. | | `button` | `"left"`, `"right"`, `"middle"`... | | `key` | Key combo, such as `"ctrl-s"`. | | `shift` | Shift held. | | `ctrl` | Control held. | | `alt` | Alt held. | | `logo` | Cmd/Windows key held. | | `value` | Value of a custom event. | | `source` | Component that emitted it. | | `window_x` | Pointer x in the window (outside it when a drag is released outside). | | `window_y` | Pointer y in the window. | | `screen_x` | Pointer x on the screen, or `None` where the platform does not say where windows are (Wayland). `event.screen` gives `(screen_x, screen_y)`. | | `screen_y` | Pointer y on the screen, or `None`. | **Methods and properties** ###### `Event.position` ```python Event.position # property ``` `(x, y)`. ###### `Event.size` ```python Event.size # property ``` `(width, height)`. ###### `Event.screen` ```python Event.screen # property ``` `(screen_x, screen_y)`, or `None` when the platform does not report window positions. ##### `Dock` ```python class Dock ``` An editor-style workspace: a `center` with `pz.DockPanel`s docked left, right, bottom or top. Users resize a side by dragging its inner edge and move a panel by dragging its tab onto a drop zone (or from the tab's right-click menu). `on_layout` receives the layout as a dict to save; pass it back as `layout=`. See [Layout](https://albakore.github.io/pyzed/guides/layout/#docks-and-resizable-panels). | Parameter | Description | | --- | --- | | `args` | Props by position: `center`, `panels`, `layout`. | | `key` | Key that identifies the subscription (defaults to the message or the function). | | `kwargs` | Props and handlers by name, plus `key=`. | ```python pz.Dock(center=editor, panels=[ pz.DockPanel("project", "Project", tree, icon="folder", side="left"), pz.DockPanel("terminal", "Terminal", terminal, icon="terminal", side="bottom"), ], layout=self.layout, on_layout=lambda e: setattr(self, "layout", e.value)) ``` **Methods and properties** ###### `Dock.side_of` ```python Dock.side_of(panel) ``` Returns the side a panel is docked on: its saved position if valid, else `panel.side`. ###### `Dock.groups` ```python Dock.groups() ``` Returns the panels grouped by side (every side present, possibly with an empty list). ###### `Dock.snapshot` ```python Dock.snapshot() ``` Returns the layout as a plain dict (`positions`, `sizes`, `active`, `hidden`). ###### `Dock.changed` ```python Dock.changed() ``` Emits `layout` with the current snapshot. ###### `Dock.move` ```python Dock.move(panel_id, side) ``` Docks a panel on another side and makes it the active tab there. ###### `Dock.select` ```python Dock.select(side, panel_id) ``` Makes `panel_id` the active tab on `side` (a tab click) and emits `layout`. ###### `Dock.toggle` ```python Dock.toggle(side) ``` Shows or hides a whole side. ###### `Dock.resized` ```python Dock.resized(side, size) ``` Records the new `size` (pixels) of `side` after its edge is dragged and emits `layout`. ###### `Dock.start_drag` ```python Dock.start_drag(panel_id) ``` Starts dragging a panel's tab: the drop zones show and the tab menu closes. ###### `Dock.end_drag` ```python Dock.end_drag() ``` Ends a tab drag and hides the drop zones, remembering the panel for a late `drop`. ###### `Dock.drop` ```python Dock.drop(side) ``` Moves the dragged panel to `side` when released on that drop zone; no-op without one. ###### `Dock.tab` ```python Dock.tab(panel, side, active, t) ``` Builds a panel's tab in a side's header. ###### `Dock.move_menu` ```python Dock.move_menu(panel, side, t) ``` Builds the right-click menu that moves `panel` to any side other than `side`. ###### `Dock.side_view` ```python Dock.side_view(side, panels, t) ``` Builds one side: a header with the panels' tabs above the active panel's content. ###### `Dock.drop_zones` ```python Dock.drop_zones(t) ``` Builds the four drop zones (one per side) shown over the dock while a tab is dragged. ##### `DockPanel` ```python class DockPanel ``` A panel for `pz.Dock`. | Parameter | Description | | --- | --- | | `id` | Stable id (used in the saved layout). | | `title` | Tab title. | | `content` | The panel's widget. | | `icon` | Lucide icon for the tab. | | `side` | Starting side: `"left"`, `"right"`, `"bottom"` or `"top"`. | ##### `Component` ```python class Component ``` Base class for custom components, like React components: annotated class attributes are props, other attributes are state kept between renders, `render()` returns widgets and `events` declares custom events raised with `self.emit(name, value)`. Calling the class returns a widget. Components appear in the designer's toolbox. See [Custom components](https://albakore.github.io/pyzed/guides/components/). | Parameter | Description | | --- | --- | | `args` | Props, in declaration order (or the children, when the class has a `children` prop). | | `key` | Key that identifies the subscription (defaults to the message or the function). | | `kwargs` | Props by name, `on_=` handlers (custom, lifecycle or common events), `key=` to keep state when items move, and `style=`, `class_=`, `id=` and `animate=`. | ```python class Rating(pz.Component): value: int = 0 events = {"change": "A star was picked."} def render(self): return pz.row([pz.text("★").on(click=lambda i=i: self.emit("change", i)) for i in range(1, 6)]) Rating(value=3, on_change=lambda e: print(e.value)) ``` **Methods and properties** ###### `Component.refresh` ```python Component.refresh() ``` Draws the component again on the next `view()`, even if its props and state are the same. ###### `Component.create` ```python @classmethod Component.create(*args, **kwargs) ``` A standalone instance (not rendered, no lifecycle), handy in tests: `c.render()`. ###### `Component.setup` ```python Component.setup() ``` Called once before the first render (props are already set): initialize state here. ###### `Component.render` ```python Component.render() ``` Returns the widgets of this component (called on every render). ###### `Component.provide` ```python Component.provide() ``` Values to share with every component this one renders (read with `pz.use_context`). ###### `Component.context` ```python Component.context(name, default=None) ``` The closest shared value called `name` (see `pz.provide`). ###### `Component.on_mount` ```python Component.on_mount() ``` Called after the first render; may return a `Command` or a message. ###### `Component.on_unmount` ```python Component.on_unmount() ``` Called when the component is no longer rendered. ###### `Component.emit` ```python Component.emit(name, value=None) ``` Raises the custom event `name`: calls the handler the parent passed as `on_`. ###### `Component.handles` ```python Component.handles(name) ``` Whether the parent passed a handler for the custom event `name`. ###### `Component.describe` ```python @classmethod Component.describe() ``` The first paragraph of the docstring on one line, in Markdown (shown in the designer). ###### `Component.designer_spec` ```python @classmethod Component.designer_spec() ``` This component as an entry of the designer's toolbox (`pyzed.designer.Component`). ##### `Color` ```python class Color ``` Type hint for a component prop that holds a color (a theme token or `"#rrggbb"`): the designer shows a color picker for it. A plain `str` at run time. ```python tint: pz.Color = "accent" ``` ##### `Icon` ```python class Icon ``` Type hint for a component prop that holds a Lucide icon name: the designer shows an icon picker for it. A plain `str` at run time. ```python mark: pz.Icon = "star" ``` ##### `Multiline` ```python class Multiline ``` Type hint for a component prop that holds long text: the designer shows a text area for it. A plain `str` at run time. ```python notes: pz.Multiline = "" ``` ##### `FilePath` ```python class FilePath ``` Type hint for a component prop that holds a file path: the designer shows the path with a button that opens the system's file dialog (a `pathlib.Path` annotation does the same). A plain `str` at run time. ```python logo: pz.FilePath = "" ``` ##### `FolderPath` ```python class FolderPath ``` Type hint for a component prop that holds a folder: the designer shows the path with a button that opens the system's folder dialog. A plain `str` at run time. ```python output: pz.FolderPath = "" ``` ##### `Range` ```python @dataclass class Range(min, max, step=1) ``` Limits of a number prop, used with `typing.Annotated`: the designer shows a number box limited to them. See [Custom components](https://albakore.github.io/pyzed/guides/components/#in-the-designer). | Parameter | Description | | --- | --- | | `min` | Lowest value. | | `max` | Highest value. | | `step` | Step of the + and - buttons. | ```python level: Annotated[int, pz.Range(0, 10)] = 3 ``` ##### `provide` ```python provide(**values) ``` A context manager that shares values with every component rendered inside, like React's context. Read them with `pz.use_context(name)` or `self.context(name)`. A component's `provide()` method does the same for what it renders. See [Children, context and styles](https://albakore.github.io/pyzed/guides/components/#children-context-and-styles). | Parameter | Description | | --- | --- | | `values` | The values, by name. | ```python with pz.provide(user=self.user): page = Dashboard() ``` ##### `use_context` ```python use_context(name, default=None) ``` The closest value shared as `name` with `pz.provide` or a component's `provide()`. | Parameter | Description | | --- | --- | | `name` | The value's name. | | `default` | Returned when nothing shares it. | ##### `state` ```python state(default=None, *, doc='') ``` Declares a private state variable on a component class: the parent cannot pass it, and every instance starts with its own copy of the default (lists and dicts are safe). The designer lists it next to the props. See [Custom components](https://albakore.github.io/pyzed/guides/components/). | Parameter | Description | | --- | --- | | `default` | Initial value, copied for each instance. | | `doc` | Description shown in the designer. | ```python class Counter(pz.Component): step: int = 1 count: int = pz.state(0) ``` ##### `State` ```python class State ``` The default of a state variable declared on the class (see `state`). ##### `StateInfo` ```python class StateInfo ``` A state variable declared with `pz.state`: its name, annotation, default and description. ##### `PropInfo` ```python class PropInfo ``` A prop: its name, type annotation (may be a string) and default. **Methods and properties** ###### `PropInfo.required` ```python PropInfo.required # property ``` Whether the prop has no default and must be passed. ##### `find` ```python find(type_name) ``` The component class registered as `type_name` (see `Component.type_name`). ##### `registered` ```python registered() ``` Every `Component` subclass defined so far that should appear in the designer. ##### `render_pass` ```python render_pass(owner) ``` Tracks the components rendered while `owner` (a window) runs its `view()`. Components not rendered again are unmounted at the end. The runtime does this for every window; use it yourself only to render components outside a window. ##### `EventInfo` ```python class EventInfo ``` Documentation of one event: its group and what it reports. ##### `accepts_argument` ```python accepts_argument(handler) ``` Whether `handler` takes the event: it has a required positional parameter or `*args`. Parameters with defaults do not count, so `lambda i=i: ...` is called with nothing. The answer is cached per function. ##### `LIFECYCLE_EVENTS` Names of the lifecycle events every component raises (`mount`, `unmount`...). ##### `CATALOG` Every event name Pyzed knows, mapped to its `EventInfo` (what raises it and what `event.value` holds). ##### `COMMON` The events every widget accepts through `widget.on(...)` (click, hover, focus, keys...). #### Hot reload ##### `enable` ```python enable(app, paths=(), *, interval=0.25) ``` Starts hot reload for `app` (a running `App` instance) and returns the reloader. ##### `current` ```python current() ``` The active `Reloader`, or `None` when hot reload is off. ##### `is_reloading` ```python is_reloading() ``` True while modules are being re-executed by a reload. ##### `enabled_by_env` ```python enabled_by_env() ``` Whether `PYZED_RELOAD` asks for hot reload (`1`, `true`, `yes` or `on`). ##### `Reloader` ```python class Reloader ``` Watches the project's files and reloads changed code into a running app. `app` is the running `App` instance. `paths` are extra folders or files to watch; by default the folder of the module that defines the app's class is watched. `interval` is how often, in seconds, files are checked for changes. Most programs never create one directly: `pz.run(app, reload=True)` does it. **Methods and properties** ###### `Reloader.watch` ```python Reloader.watch(path, callback) ``` Calls `callback(path)` on the UI thread whenever `path` changes. ###### `Reloader.project_modules` ```python Reloader.project_modules() ``` The loaded modules whose files belong to the project, by file path. ###### `Reloader.scan` ```python Reloader.scan() ``` Checks every watched file once and returns the ones that changed. ###### `Reloader.start` ```python Reloader.start() ``` Starts the watcher thread and makes this the current reloader. ###### `Reloader.stop` ```python Reloader.stop() ``` Stops watching files. ###### `Reloader.reload` ```python Reloader.reload(changed) ``` Reloads the code in `changed` files now (call it on the UI thread). ##### `ReloadError` ```python class ReloadError ``` Raised by `view()` while the last reload failed; the window shows its message. ##### `Change` ```python @dataclass class Change(name, changed=[], added=[], removed=[], new_state=[], updated_state=[]) ``` What one reload changed in a class (see `Reloader.changes`). ##### `remember` ```python remember(obj) ``` Records the initial plain values of `obj` (once), so a reload can tell what the app changed. The runtime calls it for the app, each window and each component; call it for other objects created after hot reload started only if you need their defaults updated too. #### Languages ##### `t` ```python t(key, *, language=None, default=None, **values) ``` The text for `key` in the current language, formatted with `values`. Falls back from `"es_AR"` to `"es"`, then to the fallback language, then to the key. See [Languages](https://albakore.github.io/pyzed/guides/i18n/). | Parameter | Description | | --- | --- | | `key` | Dotted key, like `"menu.open"`. | | `language` | Look it up in another language. | | `default` | Returned when the key is missing (default: the key). | | `values` | Values for `{name}` placeholders; `count=` also picks the plural form. | ```python i18n.add("es", {"greeting": "¡Hola, {name}!", "files": {"one": "{count} archivo", "other": "{count} archivos"}}) i18n.set_language("es") pz.t("greeting", name="Kev"), pz.t("files", count=3) ``` ##### `add` ```python add(language, catalog) ``` Adds or overrides texts for a language. Nested tables become dotted keys; `{"one": ..., "other": ...}` tables are plural forms. | Parameter | Description | | --- | --- | | `language` | Language code, like `"es"` or `"pt_BR"`. | | `catalog` | Dict of texts. | ##### `load` ```python load(folder) ``` Loads every `.json` and `.toml` in `folder`; returns the languages found. | Parameter | Description | | --- | --- | | `folder` | Folder with the catalog files. | ##### `set_language` ```python set_language(language) ``` Switches the language. The app shows it on its next render, so call it from a handler. | Parameter | Description | | --- | --- | | `language` | Language code, like `"es"`. | ##### `language` ```python language() ``` The current language, normalized (`"es_ar"`). ##### `languages` ```python languages() ``` The languages with a catalog. ##### `set_fallback` ```python set_fallback(language) ``` The language used when a text is missing (default `"en"`). | Parameter | Description | | --- | --- | | `language` | Language code. | ##### `on_change` ```python on_change(listener) ``` Calls `listener(language)` after every switch; returns a function that removes it. | Parameter | Description | | --- | --- | | `listener` | Callable that receives the new language. | ##### `system_language` ```python system_language(default='en') ``` The operating system's language (`LANG`, `LC_ALL` or the locale). | Parameter | Description | | --- | --- | | `default` | Returned when the system reports none. | ##### `has` ```python has(key, *, language=None) ``` Whether `key` has a text in the language or its fallbacks. | Parameter | Description | | --- | --- | | `key` | Dotted key. | | `language` | Language to check (default: the current one). | ##### `normalize` ```python normalize(language) ``` `"es-AR"`, `"es_AR.UTF-8"` -> `"es_ar"`. #### Built-in reference ##### `Browser` ```python class Browser ``` What a reference viewer shows: the search text, the page open and the back/forward history. `ui.reference_browser(browser)` draws it; its buttons and links call the methods below. **Methods and properties** ###### `Browser.open` ```python Browser.open(id) ``` Shows the page `id` (returns `False` when there is no such page). ###### `Browser.open_symbol` ```python Browser.open_symbol(symbol) ``` Shows the page about a name written in code; returns whether one was found. ###### `Browser.back` ```python Browser.back() ``` Goes to the previous page. ###### `Browser.forward` ```python Browser.forward() ``` Goes to the next page after going back. ###### `Browser.set_query` ```python Browser.set_query(query) ``` Changes the search text. ###### `Browser.toggle` ```python Browser.toggle(section) ``` Folds or unfolds a section of the class list. ###### `Browser.results` ```python Browser.results() ``` The entries matching the search text. ###### `Browser.page` ```python Browser.page() ``` The page shown (`None` before one is opened). ###### `Browser.follow` ```python Browser.follow(url) ``` Handles a link of a page: internal links open their page; returns the URL to open otherwise. ##### `Entry` ```python @dataclass class Entry(id, title, section, group='', summary='', icon='book-open', kind='function') ``` One page of the reference, as search results and the class list show it. ##### `Page` ```python @dataclass class Page(entry, body, url=None) ``` A full page: the entry it belongs to and its body in Markdown. **Methods and properties** ###### `Page.id` ```python Page.id # property ``` The entry's id. ###### `Page.title` ```python Page.title # property ``` The entry's title. ##### `Provider` ```python @dataclass class Provider(name, entries, page, lookup=None) ``` A section of the reference that comes from somewhere else than the library's API pages. `entries(lang)` lists its pages, `page(id, lang)` builds one (`None` when the id is not its own) and `lookup(symbol)` turns a name written in code into one of its ids. ##### `register` ```python register(provider) ``` Adds a section to the reference (it replaces one with the same `name`). ##### `entries` ```python entries(lang=None) ``` Every page of the reference, in class-list order (designer controls first, then the library). ##### `sections` ```python sections(lang=None) ``` The class list: `[(section, [(group, [entries]), ...]), ...]` in order. ##### `search` ```python search(query, lang=None, limit=60) ``` Entries matching `query`: exact names first, then names that start with it, then the rest. Every word must appear in the title, group or summary (`"tab bar"` finds `ui.tab_bar`). ##### `page` ```python page(id, lang=None) ``` The page with `id` (`KeyError` when there is none). ##### `lookup` ```python lookup(symbol) ``` The id of the page about a name written in code (`"pz.text"`, `"ui.button"`, `"Theme"`...). ##### `languages` ```python languages() ``` The languages the API descriptions are available in. ##### `tables_as_lists` ```python tables_as_lists(markdown) ``` Markdown tables turned into bullet lists (`- first cell: the other cells`). For viewers whose tables don't wrap long cells, such as `ui.reference_browser` in a narrow tab. Code blocks are left alone. ##### `display_name` ```python display_name(name) ``` `"pyzed.text"` -> `"pz.text"`, `"pyzed.ui.button"` -> `"ui.button"`. ##### `summary` ```python summary(markdown) ``` The first sentence of a Markdown text, without formatting (for lists and search results). #### Forms and validation ##### `validation_errors` ```python validation_errors(error) ``` Turns a validation error into `{field: message}`, ready for `ui.field(..., error=)`. Accepts a Pydantic or SQLModel `ValidationError`, a Django `ValidationError`, the JSON of a FastAPI 422 response (`{"detail": [...]}`) or its `detail` list. Nested fields are joined with dots and errors that belong to no field use the key `""`. Only the first message of each field is kept. See [Integrations](https://albakore.github.io/pyzed/integrations/overview/#validation-errors-on-each-field). | Parameter | Description | | --- | --- | | `error` | The error, or the JSON of the response. | ```python try: item = ItemIn.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) ui.field("Price", ui.input(self.draft["price"], self.set_price), error=self.errors.get("price")) ``` ### Command > API reference for Pyzed commands: the side effects returned by update and callbacks, such as focusing fields, closing and background tasks. Source: https://albakore.github.io/pyzed/reference/command/ A `Command` is an effect the runtime performs for you. Return it from `update`, `on_start`, `on_key` or any callback. You can also return a list or a tuple of commands. See the [commands and tasks](https://albakore.github.io/pyzed/guides/commands/) guide. ```python from pyzed import Command def on_key(self, combo): if combo == "ctrl-p": self.finder_open = True return Command.focus("finder") ``` #### Static methods ##### `Command` The command class. Don't instantiate it directly: use its static methods. ##### `exit` ```python exit() ``` Closes the application. ##### `focus` ```python focus(id) ``` Focuses the `text_input` (or editor) with that `id`. | Parameter | Description | | --- | --- | | `id` | The `id` you passed to the widget. | ##### `focus_next` ```python focus_next() ``` Moves the keyboard focus to the next field of the window, wrapping around (what Tab does). ##### `focus_previous` ```python focus_previous() ``` Moves the keyboard focus to the previous field of the window (what Shift+Tab does). ##### `select_all` ```python select_all(id) ``` Focuses the `text_input` with that `id` and selects all its text (typing replaces it). | Parameter | Description | | --- | --- | | `id` | The input's `id`. | ##### `set_editor_text` ```python set_editor_text(id, text) ``` Replaces the content of the `code_editor` with that `id`. Editors are uncontrolled: `text` is only the initial value. | Parameter | Description | | --- | --- | | `id` | The editor's `id`. | | `text` | The new text. | ##### `set_editor_cursor` ```python set_editor_cursor(id, line, column) ``` Moves the editor cursor (line and column start at 1). Works even if the editor hasn't been drawn yet. | Parameter | Description | | --- | --- | | `id` | The editor's `id`. | | `line` | Line (from 1). | | `column` | Column (from 1). | ##### `fold_all` ```python fold_all(id, fold=True) ``` Folds every block of the editor (`fold=False` unfolds them all). Works even if the editor hasn't been drawn yet. | Parameter | Description | | --- | --- | | `id` | The editor's `id`. | | `fold` | `True` to fold, `False` to unfold. | ##### `fold_line` ```python fold_line(id, line, fold=True) ``` Folds (or unfolds) the block that starts at `line`, by indentation. Works even if the editor hasn't been drawn yet. | Parameter | Description | | --- | --- | | `id` | The editor's `id`. | | `line` | Line (from 1) where the block starts. | | `fold` | `True` to fold, `False` to unfold. | ##### `editor_action` ```python editor_action(id, name) ``` Runs a named editor action in the editor with that `id`, as if its key was pressed: Zed's action name in snake case (`"undo"`, `"select_next"`, `"move_line_up"`, `"toggle_comments"`…). Raises `ValueError` for an unknown name. | Parameter | Description | | --- | --- | | `id` | The editor's `id`. | | `name` | The action's name (`Command.editor_actions()` lists them; `"editor::SelectNext"` works too). | ##### `editor_actions` ```python editor_actions() ``` The names `Command.editor_action` accepts. ##### `select_editor_ranges` ```python select_editor_ranges(id, ranges) ``` Selects ranges of the editor, one cursor each; the first one is the primary cursor. Unfolds what hides them and works even if the editor hasn't been drawn yet. | Parameter | Description | | --- | --- | | `id` | The editor's `id`. | | `ranges` | `[(line, column, end_line, end_column)]`, from 1, with exclusive ends (an empty range is a plain cursor). | ##### `replace_editor_ranges` ```python replace_editor_ranges(id, edits) ``` Replaces ranges of the editor with texts as one undo step, and calls the editor's `on_change` (search and replace). | Parameter | Description | | --- | --- | | `id` | The editor's `id`. | | `edits` | `[((line, column, end_line, end_column), text)]`, from 1, with exclusive ends. | ##### `scroll_to_end` ```python scroll_to_end(id) ``` Scrolls the `scrollable` with that `id` to the end. | Parameter | Description | | --- | --- | | `id` | The scrollable's `id`. | ##### `batch` ```python batch(commands) ``` Runs several commands. | Parameter | Description | | --- | --- | | `commands` | List of `Command`. | ##### `message` ```python message(message) ``` Sends `message` to `update` again on the next cycle. | Parameter | Description | | --- | --- | | `message` | Any Python value, or a callable. | ##### `perform` ```python perform(function, then=None) ``` Runs `function()` in the background and delivers the result to `then(result)` on the UI thread. `function` can be regular (runs on a thread) or `async def` (runs on Pyzed's asyncio loop). If it returns a generator (regular or async), `then` is called with each value. It also accepts an already created coroutine or generator (`fetch(url)` instead of `fetch`). Without `then`, results go to `update`. If `function` raises, the error is printed. | Parameter | Description | | --- | --- | | `function` | Function with no arguments that does the slow work. | | `then` | Callable that receives the result (or `None` to send it to `update`). | ```python def load(self): self.loading = True return Command.perform(lambda: requests.get(URL).json(), self.received) def received(self, data): self.loading, self.data = False, data ``` ##### `scroll_to` ```python scroll_to(id, x=0.0, y=0.0) ``` Scrolls the `scrollable` with that `id` to an absolute position in pixels. | Parameter | Description | | --- | --- | | `id` | The scrollable's `id`. | | `x` | Horizontal offset. | | `y` | Vertical offset. | ##### `copy` ```python copy(text) ``` Copies `text` to the system clipboard. | Parameter | Description | | --- | --- | | `text` | The text. | ##### `paste` ```python paste(then) ``` Reads the clipboard and calls `then(text)` (empty string if it is empty). | Parameter | Description | | --- | --- | | `then` | Callable that receives the text. | ##### `paste_image` ```python paste_image(then) ``` Reads an image from the clipboard (a screenshot, a copied picture) and calls `then(png)` with its PNG bytes, or `then(None)` when the clipboard holds no image. | Parameter | Description | | --- | --- | | `then` | Callable that receives the PNG bytes or `None`. | ##### `pick_files` ```python pick_files( then, *, title=None, filters=None, directory=None, multiple=True, folder=False, ) ``` Opens the system's file dialog and calls `then(paths)` with the chosen paths (an empty list when it is cancelled). | Parameter | Description | | --- | --- | | `then` | Callable that receives the list of paths. | | `title` | The dialog's title. | | `filters` | A list of `(name, extensions)` pairs, like `[("Images", ["png", "jpg"])]`. | | `directory` | The folder it opens in. | | `multiple` | `False` picks a single file. | | `folder` | `True` picks folders instead of files. | ##### `save_file` ```python save_file(then, *, title=None, filters=None, directory=None, file_name=Ellipsis) ``` Opens the system's "Save as" dialog and calls `then(paths)` with the chosen path in a list (an empty list when it is cancelled). | Parameter | Description | | --- | --- | | `then` | Callable that receives the list with the path. | | `title` | The dialog's title. | | `filters` | A list of `(name, extensions)` pairs, like `[("Images", ["png", "jpg"])]`. | | `directory` | The folder it opens in. | | `file_name` | The name it suggests. | #### Windows and clipboard ##### `open_window` ```python open_window(window) ``` Opens a new window for that `pz.Window` instance. | Parameter | Description | | --- | --- | | `window` | The window. | ##### `close_window` ```python close_window(window=None) ``` Closes the window. | Parameter | Description | | --- | --- | | `window` | The window (`pz.Window`). Without it, the one that produced the message. | ##### `drag_window` ```python drag_window(window=None) ``` Starts moving the window with the mouse (use it when a custom title bar is pressed). | Parameter | Description | | --- | --- | | `window` | The window (`pz.Window`). Without it, the one that produced the message. | ##### `minimize_window` ```python minimize_window(window=None) ``` Minimizes the window. | Parameter | Description | | --- | --- | | `window` | The window (`pz.Window`). Without it, the one that produced the message. | ##### `toggle_maximize_window` ```python toggle_maximize_window(window=None) ``` Maximizes the window, or restores it if it already is. | Parameter | Description | | --- | --- | | `window` | The window (`pz.Window`). Without it, the one that produced the message. | ##### `maximize_window` ```python maximize_window(maximized=True, window=None) ``` Maximizes the window, or restores it with `maximized=False`. | Parameter | Description | | --- | --- | | `maximized` | `False` restores it. | | `window` | The window (`pz.Window`). Without it, the one that produced the message. | ##### `drag_resize_window` ```python drag_resize_window(direction, window=None) ``` Starts resizing the window with the mouse from an edge or corner (for windows without system decorations; `ui.window_frame` and `pz.window_edges` already do it). | Parameter | Description | | --- | --- | | `direction` | `"n"`, `"s"`, `"e"`, `"w"`, `"ne"`, `"nw"`, `"se"` or `"sw"`. | | `window` | The window (`pz.Window`). Without it, the one that produced the message. | ##### `show_window_menu` ```python show_window_menu(window=None) ``` Shows the system's window menu (move, size, close…) under the pointer. Only Windows has one. | Parameter | Description | | --- | --- | | `window` | The window (`pz.Window`). Without it, the one that produced the message. | ##### `set_fullscreen` ```python set_fullscreen(fullscreen=True, window=None) ``` Enters or leaves fullscreen. | Parameter | Description | | --- | --- | | `fullscreen` | `True` for fullscreen. | | `window` | The window (`pz.Window`). Without it, the one that produced the message. | ##### `resize_window` ```python resize_window(width, height, window=None) ``` Resizes the window (logical pixels). | Parameter | Description | | --- | --- | | `width` | Width. | | `height` | Height. | | `window` | The window (`pz.Window`). Without it, the one that produced the message. | ##### `move_window` ```python move_window(x, y, window=None) ``` Moves the window to a position on the screen. | Parameter | Description | | --- | --- | | `x` | Horizontal position. | | `y` | Vertical position. | | `window` | The window (`pz.Window`). Without it, the one that produced the message. | ##### `focus_window` ```python focus_window(window=None) ``` Brings the window to the front and focuses it. | Parameter | Description | | --- | --- | | `window` | The window (`pz.Window`). Without it, the one that produced the message. | ### Core widgets > API reference for Pyzed's native widgets: layout, text, controls, the code editor, SVG and icons, all rendered in Rust by Iced. Source: https://albakore.github.io/pyzed/reference/widgets/ The core widgets are implemented in Rust and imported from `pyzed`. They are the building blocks of the [components](https://albakore.github.io/pyzed/components/overview/). See the [layout](https://albakore.github.io/pyzed/guides/layout/) guide for sizes, padding and colors. #### Widget ##### `Widget` An immutable node of the UI tree, implemented in Rust. Returned by every widget and component function. Wherever a child is expected you can also pass a string or a number (converted to `text`) or `None` (ignored). Every widget has `widget.on(...)` (see [Events](https://albakore.github.io/pyzed/guides/events/)) and `widget.style(*styles, **props)`, which styles it like CSS: background, borders, radius, shadow, padding, margin, size and the typography of every text inside. `widget.style_props()` returns what was set. `widget.classes(*names, id=None)` applies the matching rules of `pz.stylesheet`, like HTML's `class` and `id`. See [Styles](https://albakore.github.io/pyzed/theming/styles/). `widget.animate(*animations, **options)` plays presets or keyframe animations (on mount, hover, press, focus, click or while `active`) and `widget.transition(**props)` animates property changes like a CSS transition. See [Animation](https://albakore.github.io/pyzed/animation/). #### Layout ##### `column` ```python column( children=None, *, spacing=0.0, padding=None, width=None, height=None, align=None, max_width=None, clip=False, ) ``` Stacks its children vertically. | Parameter | Description | | --- | --- | | `children` | List of children. Nested lists are flattened and `None` and `False` are ignored, so you can write `cond and widget`. | | `spacing` | Space between children, in pixels. | | `padding` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `align` | Alignment of the children on the cross axis: `"start"`, `"center"` or `"end"`. | | `max_width` | Maximum width in pixels. | | `clip` | Clips content that overflows the bounds. | ```python pz.column([pz.text("One"), pz.text("Two")], spacing=8, padding=16) ``` ##### `row` ```python row( children=None, *, spacing=0.0, padding=None, width=None, height=None, align=None, clip=False, wrap=False, line_spacing=None, ) ``` Lays out its children horizontally. | Parameter | Description | | --- | --- | | `children` | List of children. Nested lists are flattened and `None` and `False` are ignored, so you can write `cond and widget`. | | `spacing` | Space between children, in pixels. | | `padding` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `align` | Alignment of the children on the cross axis: `"start"`, `"center"` or `"end"`. | | `clip` | Clips content that overflows the bounds. | | `wrap` | Line wrapping / moves children to a new row when they don't fit. | | `line_spacing` | Space between rows when `wrap=True`. | ```python pz.row([ui.label("Name"), pz.hspace(), ui.button("OK", "ok")], align="center") ``` ##### `container` ```python container( content=None, *, padding=None, width=None, height=None, max_width=None, max_height=None, align_x=None, align_y=None, center=False, bg=None, color=None, border_color=None, border_width=0.0, radius=None, shadow=None, clip=False, gradient=None, ) ``` Wraps a child to give it size, alignment, background, border, radius and shadow. | Parameter | Description | | --- | --- | | `content` | The child (or `None` for an empty box, handy as a colored rectangle). | | `padding` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `max_width` | Maximum width in pixels. | | `max_height` | Maximum height in pixels. | | `align_x` | Horizontal alignment: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `align_y` | Vertical alignment: `"start"`/`"top"`, `"center"`, `"end"`/`"bottom"`. | | `center` | Centers the content on both axes and fills all the space. | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `color` | Text or icon color. | | `border_color` | Border color. | | `border_width` | Border width in pixels. | | `radius` | Corner radius: a number or `(top_left, top_right, bottom_right, bottom_left)`. | | `shadow` | Shadow: `(color, (dx, dy), blur)`. | | `clip` | Clips content that overflows the bounds. | | `gradient` | Linear background gradient: `(angle, [colors])` or `(angle, [(position, color), ...])`. | ##### `center` ```python center(content, **kwargs) ``` Centers `content` in all the available space. Equivalent to `container(content, center=True)`. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `kwargs` | Any `container` parameter. | ##### `scrollable` ```python scrollable( content, *, direction='vertical', width=None, height=None, padding=None, id=None, anchor_bottom=False, rail=None, scroller=None, scroller_hover=None, scrollbar_width=8.0, scroller_width=None, margin=0.0, on_scroll=None, ) ``` Makes its content scrollable. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `direction` | `"vertical"`, `"horizontal"` or `"both"`. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `padding` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `anchor_bottom` | Keeps the scroll pinned to the end (logs, terminals, chats). | | `rail` | Scrollbar rail color. | | `scroller` | Scroller color. | | `scroller_hover` | Scroller color on hover. | | `scrollbar_width` | Scrollbar width. | | `scroller_width` | Scroller width (defaults to the scrollbar's). | | `margin` | Margin between the scrollbar and the content. | | `on_scroll` | Callable that receives a dict with `x`, `y`, `relative_x`, `relative_y`, `width`, `height`, `content_width` and `content_height`. | ##### `stack` ```python stack(children=None, *, width=None, height=None) ``` Layers its children on top of each other, the last one on top. The basis for layers, modals and notifications. | Parameter | Description | | --- | --- | | `children` | List of children. Nested lists are flattened and `None` and `False` are ignored, so you can write `cond and widget`. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | ##### `space` ```python space(width=None, height=None) ``` Empty space of a given size. | Parameter | Description | | --- | --- | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | ##### `hspace` ```python hspace(width='fill') ``` Flexible horizontal gap: pushes the siblings in a row towards the edges. | Parameter | Description | | --- | --- | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | ##### `vspace` ```python vspace(height='fill') ``` Flexible vertical gap: pushes the siblings in a column towards the edges. | Parameter | Description | | --- | --- | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | ##### `rule` ```python rule(*, vertical=False, thickness=1.0, color=None) ``` A horizontal (or vertical) divider line. | Parameter | Description | | --- | --- | | `vertical` | Vertical line instead of horizontal. | | `thickness` | Thickness in pixels. | | `color` | Text or icon color. | A horizontal rule fills the full width. Inside a column that should shrink, set `width="shrink"` on the column. ##### `grid` ```python grid( children=None, *, columns=None, rows=None, max_width=None, spacing=0.0, row_spacing=None, height=None, aspect_ratio=None, width=None, padding=None, align_x=None, align_y=None, ) ``` Lays out its children in a grid. With `columns=3` the columns are equal (or `max_width` fits as many as possible). With track lists (`columns=[200, "1fr", "auto"]`, `rows=...`) it works like a CSS grid: numbers are pixels, `"auto"` fits the content and `"fr"` shares the space left. Wrap children in `pz.cell` to place them and make them span; the rest fill the free cells row by row. See [Layout](https://albakore.github.io/pyzed/guides/layout/#grids). | Parameter | Description | | --- | --- | | `children` | List of children. Nested lists are flattened and `None` and `False` are ignored, so you can write `cond and widget`. | | `columns` | Number of equal columns, or tracks: a list (`[200, "1fr", "auto"]`) or a string (`"200 1fr auto"`). | | `rows` | Row tracks, like `columns`. Rows beyond the list are `"auto"`. | | `max_width` | Fits as many columns of at most this width as possible (`columns=` only). | | `spacing` | Space between cells. | | `row_spacing` | Space between rows, if different. | | `height` | With `columns=`: height of each row. With tracks: the grid's height. | | `aspect_ratio` | Keeps cells at this width/height ratio (`columns=` only). | | `width` | With `columns=`: pixels. With tracks: the grid's width (default `"fill"`). | | `padding` | Space around the cells. | | `align_x` | Horizontal alignment of items in their cells: `"start"`, `"center"`, `"end"`. | | `align_y` | Vertical alignment of items in their cells. | ```python pz.grid([pz.cell(header, column=0, row=0, column_span=2), sidebar, content], columns=[200, "1fr"], rows=[48, "1fr"], spacing=8, height="fill") ``` ##### `cell` ```python cell( content, *, column=None, row=None, column_span=1, row_span=1, align_x=None, align_y=None, ) ``` Places a child in a track grid: its column and row (from 0) and how many it spans. Children without a cell fill the free cells row by row. Outside a grid it is just the child. | Parameter | Description | | --- | --- | | `content` | The widget. | | `column` | Column index (from 0). | | `row` | Row index (from 0). | | `column_span` | Columns it covers. | | `row_span` | Rows it covers. | | `align_x` | Horizontal alignment in the cell. | | `align_y` | Vertical alignment in the cell. | ```python pz.cell(ui.button("Save"), column=2, row=3, align_x="end") ``` ##### `box` ```python box( *children, display='block', direction='row', gap=0, wrap=False, justify='start', align=None, columns=None, rows=None, overflow='visible', class_=None, id=None, style=None, **props, ) ``` A container for anything, like an HTML `div`: it lays out its children (`display` `"block"`, `"flex"`, `"grid"` or `"stack"`) and takes every style property, stylesheet classes and an id. Children styled with `position="absolute"` are placed by `top`/`right`/`bottom`/`left` above the rest. See [Boxes](https://albakore.github.io/pyzed/guides/layout/#boxes). | Parameter | Description | | --- | --- | | `children` | The widgets, one by one or in lists. `None` and `False` are skipped. | | `display` | `"block"` (one under another, as wide as the box), `"flex"`, `"grid"` or `"stack"`. | | `direction` | With `display="flex"`: `"row"` or `"column"`. | | `gap` | Space between children. | | `wrap` | With `"flex"` rows: wraps onto new lines. | | `justify` | With `"flex"`: `"start"`, `"center"`, `"end"`, `"between"`, `"around"` or `"evenly"`. | | `align` | `"start"`, `"center"`, `"end"` or `"stretch"` (the default for `"block"`). | | `columns` | With `"grid"`: a count of equal columns or tracks (`"200 1fr auto"`). | | `rows` | With `"grid"`: row tracks. | | `overflow` | `"visible"`, `"hidden"`, `"scroll"`, `"scroll_x"` or `"scroll_y"`. | | `class_` | Stylesheet classes (`"card selected"` or a list). | | `id` | The id for `#id` rules. | | `style` | A `pz.Style`, dict or style name. | | `props` | Style properties, on top of the rest. | ```python pz.box(title, pz.box(cancel, save, display="flex", justify="end", gap=8), class_="card", gap=12) ``` ##### `flex` ```python flex( children=(), *, direction='row', justify='start', align='start', gap=0, wrap=False, padding=None, width=None, height=None, ) ``` Lays children out along one axis, like CSS flexbox, with `justify` on the main axis and `align` on the cross axis. Use `pz.flex_item` to make a child grow or give it a fixed size. | Parameter | Description | | --- | --- | | `children` | The widgets, or `pz.flex_item(...)` for children with their own sizing. | | `direction` | `"row"` or `"column"`. | | `justify` | `"start"`, `"center"`, `"end"`, `"between"`, `"around"` or `"evenly"`. No effect when a child grows. | | `align` | Cross-axis alignment: `"start"`, `"center"` or `"end"`. | | `gap` | Space between children. | | `wrap` | Wraps onto new lines (rows with `justify="start"` only). | | `padding` | Space around the children. | | `width` | Width. | | `height` | Height. | ```python pz.flex([logo, title, pz.flex_item(search, grow=1), avatar], justify="between", align="center", gap=8) ``` ##### `flex_item` ```python flex_item(content, *, grow=1, basis=None, align=None) ``` Sizes a child of `pz.flex`. | Parameter | Description | | --- | --- | | `content` | The widget. | | `grow` | Share of the free space on the main axis (`0` keeps its natural size). | | `basis` | A fixed main size in pixels, instead of growing. | | `align` | Cross-axis alignment for this child. | ##### `anchor` ```python anchor( content, *, x=0, y=0, width=100, height=28, anchor='top_left', min_width=0, min_height=0, max_width=0, max_height=0, pivot=(0.5, 0.5), rotate=0, ) ``` Places a child of `pz.anchor_layout` at `x`, `y` with `width` and `height` (in the layout's design size) and says how it follows a resize. | Parameter | Description | | --- | --- | | `content` | The widget. | | `x` | Left edge in the design size. | | `y` | Top edge in the design size. | | `width` | Width in the design size. | | `height` | Height in the design size. | | `anchor` | A preset of `pyzed.anchors.PRESETS` (`"top_right"`, `"bottom_wide"`, `"center"`, `"full"`...), words (`"left right bottom"`) or a tuple of edges. | | `min_width` | Smallest width when it stretches (`0`: no limit). | | `min_height` | Smallest height when it stretches. | | `max_width` | Largest width when it stretches (`0`: no limit). | | `max_height` | Largest height when it stretches. | | `pivot` | `(x, y)` from `0` to `1`: the point that stays put when a limit stops it, and that `rotate` turns around. | | `rotate` | Degrees clockwise. | ```python pz.anchor(ui.button("Close", on_close), x=284, y=252, width=100, height=32, anchor="bottom_right") ``` ##### `anchor_layout` ```python anchor_layout(children, *, design=None, width='fill', height='fill') ``` Places children made with `pz.anchor` freely, like a designed form: when it is resized, each child keeps its distance to the edges it is anchored to, stretches between opposite ones and stays centered otherwise. Later children are drawn on top. See [Anchored layouts](https://albakore.github.io/pyzed/guides/layout/#anchored-layouts). | Parameter | Description | | --- | --- | | `children` | Children made with `pz.anchor(...)` (`None` is skipped). | | `design` | `(width, height)` the children were laid out in; by default the smallest size that holds them. | | `width` | Width. | | `height` | Height. | ```python pz.anchor_layout([ pz.anchor(ui.heading("Report"), x=16, y=12, width=368, height=28, anchor="top_wide"), pz.anchor(ui.button("Close", on_close), x=284, y=252, width=100, height=32, anchor="bottom_right"), ], design=(400, 300)) ``` ##### `place` ```python place( rect, anchor, design, actual, *, min_size=(0, 0), max_size=(0, 0), pivot=(0.5, 0.5), ) ``` Where a box designed in a parent of size `design` goes in a parent of size `actual`, following its anchor, with size limits and a pivot. The math behind `pz.anchor_layout` and designed forms. | Parameter | Description | | --- | --- | | `rect` | `(x, y, width, height)` in the design size. | | `anchor` | A preset, words or edges. | | `design` | The parent's design size. | | `actual` | The parent's current size. | | `min_size` | `(width, height)` lower limits (`0`: none). | | `max_size` | `(width, height)` upper limits (`0`: none). | | `pivot` | The point that stays put when a limit stops the box from stretching. | ```python anchors.place((20, 20, 100, 30), "top_right", design=(400, 300), actual=(600, 300)) # (220, 20, 100, 30) ``` ##### `PRESETS` The 16 anchor presets by name (`top_left`, `top_center`, `top_right`, `top_wide`, `center_left`, `center`, `center_right`, `center_wide`, `bottom_left`, `bottom_center`, `bottom_right`, `bottom_wide`, `left_tall`, `center_tall`, `right_tall`, `full`), each the tuple of edges it anchors to. ##### `edges` ```python edges(anchor) ``` The edges of an anchor given as a preset name, words or edges. Raises `ValueError` for anything else. | Parameter | Description | | --- | --- | | `anchor` | What to read. | ##### `preset_of` ```python preset_of(edges) ``` The name of the preset a set of edges matches (every set is one of the 16). | Parameter | Description | | --- | --- | | `edges` | Edges. | ##### `preset` ```python preset(horizontal, vertical) ``` The preset placed `horizontal` (`left`, `center`, `right`, `wide`) and `vertical` (`top`, `center`, `bottom`, `tall`). | Parameter | Description | | --- | --- | | `horizontal` | Placement on the x axis. | | `vertical` | Placement on the y axis. | ##### `axes` ```python axes(edges) ``` The `(horizontal, vertical)` placement of a set of edges, such as `("right", "top")`. | Parameter | Description | | --- | --- | | `edges` | Edges. | ##### `snap_to` ```python snap_to(anchor, rect, parent, margin=0.0) ``` A box moved (and sized, on stretched axes) to sit where an anchor points: against the anchored edges, centered or filling the parent, `margin` pixels in. | Parameter | Description | | --- | --- | | `anchor` | A preset, words or edges. | | `rect` | The box. | | `parent` | The parent's size. | | `margin` | Distance to the edges. | ##### `offsets` ```python offsets(rect, parent) ``` The distances `(left, top, right, bottom)` from each side of the parent to a box. | Parameter | Description | | --- | --- | | `rect` | The box. | | `parent` | The parent's size. | ##### `pivot_offset` ```python pivot_offset(size, pivot, rotate=0.0, scale=1.0) ``` The translation that makes a rotation and a scale (which turn around a box's center) turn around `pivot` instead. | Parameter | Description | | --- | --- | | `size` | The box's size. | | `pivot` | `(x, y)` from `0` to `1`. | | `rotate` | Degrees clockwise. | | `scale` | Scale factor. | ##### `focusable` ```python focusable( content, on_key=None, *, keys=None, id=None, ring_color=None, ring_radius=4.0, ring_offset=2.0, ) ``` Lets any widget take the keyboard focus like a text field: a click or Tab focuses it (`Command.focus(id)` too) and, while it has it, the combos in `keys` call `on_key(combo)` instead of reaching the app's `on_key`. A ring is drawn around it once the keyboard is used on it. The library's switches, check boxes, sliders, segmented controls and radio groups use it. | Parameter | Description | | --- | --- | | `content` | The control. | | `on_key` | Called with the combo (`"space"`, `"shift-right"`…) of each key in `keys` pressed while it has the focus. `None` leaves `content` unfocusable. | | `keys` | The combos it takes (by default Space, Enter, the arrows, Home and End); `"*"` takes every key. Tab is left to move the focus. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `ring_color` | Color of the focus ring (none by default). | | `ring_radius` | Corner radius of the ring. | | `ring_offset` | Pixels between the control and the ring. | ```python pz.focusable(stars, lambda combo: self.rate(+1 if combo == "right" else -1), keys=["left", "right"], ring_color="#528bff") ``` ##### `window_edges` ```python window_edges(content, *, border=6.0, enabled=True) ``` Wraps the whole view of a window without system decorations so that pressing near its edges resizes the window (the corners take twice as much room). `ui.window_frame` already uses it. | Parameter | Description | | --- | --- | | `content` | The window's whole view. | | `border` | Width in pixels of the area that resizes. | | `enabled` | `False` turns it off (for a maximized window). | ```python pz.window_edges(view, enabled=not self.window_maximized) ``` ##### `resizable` ```python resizable( content, *, edge='right', size=240.0, min_size=80.0, max_size=None, on_resize=None, color=None, hover_color=None, thickness=1.0, handle=8.0, width=None, height=None, ) ``` A panel whose `edge` can be dragged to resize it. It keeps the size the user drags it to; `on_resize(size)` is called when a drag ends so you can save it. Changing `size` later resets it. | Parameter | Description | | --- | --- | | `content` | The panel's content. | | `edge` | The edge with the handle: `"right"` (a left sidebar), `"left"`, `"top"` (a bottom panel) or `"bottom"`. | | `size` | Starting width (height for top/bottom edges). | | `min_size` | Smallest size. | | `max_size` | Largest size. | | `on_resize` | Called with the new size when a drag ends. | | `color` | Line color. | | `hover_color` | Line color while hovered or dragged. | | `thickness` | Line thickness. | | `handle` | Width of the area that can be grabbed. | | `width` | Width for top/bottom edges. | | `height` | Height for left/right edges. | ```python pz.row([pz.resizable(sidebar, edge="right", size=240, on_resize=self.save_width), editor]) ``` ##### `split` ```python split( first, second, *, ratio=0.5, on_resize=None, vertical=False, min_size=80.0, min_size_second=None, color=None, hover_color=None, thickness=1.0, handle=8.0, width=None, height=None, ) ``` Two panes with a draggable divider. For the theme style use `ui.split`. | Parameter | Description | | --- | --- | | `first` | First pane. | | `second` | Second pane. | | `ratio` | Proportion of the first pane (0–1). The split keeps the ratio the user drags to until this changes. | | `on_resize` | Callable that receives the new ratio while dragging. | | `vertical` | Stacks the panes top/bottom. | | `min_size` | Minimum size of the first pane. | | `min_size_second` | Minimum size of the second pane. | | `color` | Divider color. | | `hover_color` | Divider color on hover or while dragging. | | `thickness` | Line thickness. | | `handle` | Width of the draggable area. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | ##### `pin` ```python pin(content, *, x=0.0, y=0.0, width=None, height=None) ``` Places `content` at `(x, y)` inside a box. Use it in a `stack` to position popovers and context menus. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `x` | Horizontal position. | | `y` | Vertical position. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | ##### `responsive` ```python responsive(function) ``` Builds its content with `function(width, height)` using the space actually available, so the layout can adapt (for example, hiding a sidebar when the window is narrow). | Parameter | Description | | --- | --- | | `function` | Function that receives `(width, height)` and returns a widget. | ##### `FlexItem` ```python class FlexItem ``` A child of `pz.flex` with its own sizing; made with `pz.flex_item`. #### Text and icons ##### `text` ```python text( content, *, size=None, color=None, font=None, weight=None, italic=False, line_height=None, align_x=None, align_y=None, width=None, height=None, wrap=True, selectable=None, selection_color=None, ) ``` A piece of text. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `size` | Font size. | | `color` | Text or icon color. | | `font` | `"sans"` (IBM Plex Sans), `"mono"`, `"icons"`, `"serif"` or the name of a family loaded with `load_font`. | | `weight` | `"thin"`, `"light"`, `"normal"`, `"medium"`, `"semibold"`, `"bold"`, `"black"` or a number (100–900). | | `italic` | Italic. | | `line_height` | Line height relative to the size (1.0, 1.5…). | | `align_x` | Horizontal alignment: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `align_y` | Vertical alignment: `"start"`/`"top"`, `"center"`, `"end"`/`"bottom"`. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `wrap` | Line wrapping. With `False` the text doesn't wrap. | | `selectable` | `True` lets the mouse select the text (drag, double-click a word, triple-click a block) and Ctrl+C copy it. A string names a selection group: the texts and Markdown of one group form a single selection, in the order they are drawn (the lines of a log, the messages of a chat). | | `selection_color` | Color behind the selected text. | ##### `icon` ```python icon(name, *, size=16, color=None) ``` A [Lucide](https://lucide.dev) icon. `color` defaults to the surrounding text color. | Parameter | Description | | --- | --- | | `name` | Icon name (`pyzed icons` lists them). | | `size` | Size in pixels. | | `color` | Color. | ```python pz.icon("folder", size=16, color="#74ade8") ``` ##### `svg` ```python svg( source, *, width=None, height=None, color=None, content_fit=None, opacity=1.0, rotation=0.0, ) ``` An SVG image. | Parameter | Description | | --- | --- | | `source` | Path to an `.svg`, the SVG code as a string (starting with `<`) or `bytes`. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `color` | If given, tints the whole SVG with that color (useful for monochrome icons). | | `content_fit` | `"contain"`, `"cover"`, `"fill"`, `"none"` or `"scale-down"`. | | `opacity` | Opacity from 0 to 1. | | `rotation` | Rotation in degrees. | #### Images and content ##### `image` ```python image( source, *, width=None, height=None, content_fit=None, filter='linear', opacity=1.0, radius=0.0, rotation=0.0, scale=1.0, size=None, ) ``` A bitmap image (PNG, JPEG, GIF, BMP, WebP…) from a path or `bytes`, or raw RGBA pixels. | Parameter | Description | | --- | --- | | `source` | File path, `bytes` of the encoded file, or raw RGBA pixels with `size=(width, height)`. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `content_fit` | `"contain"`, `"cover"`, `"fill"`, `"none"` or `"scale-down"`. | | `filter` | `"linear"` (smooth) or `"nearest"` (crisp pixels, for pixel art). | | `opacity` | Opacity from 0 to 1. | | `radius` | Corner radius. | | `rotation` | Rotation in degrees. | | `scale` | Scale. | | `size` | `(width, height)` when `source` is raw RGBA pixels. | ```python pz.image("photo.jpg", width=320, content_fit="cover", radius=8) ``` ##### `image_viewer` ```python image_viewer( source, *, width=None, height=None, min_scale=0.25, max_scale=10.0, scale_step=0.1, filter='linear', size=None, ) ``` An image you can pan (by dragging) and zoom (mouse wheel), like Iced's viewer. | Parameter | Description | | --- | --- | | `source` | File path, `bytes` of the encoded file, or raw RGBA pixels with `size=(width, height)`. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `min_scale` | Minimum zoom. | | `max_scale` | Maximum zoom. | | `scale_step` | How much the zoom changes per wheel step. | | `filter` | `"linear"` (smooth) or `"nearest"` (crisp pixels, for pixel art). | | `size` | `(width, height)` for raw RGBA pixels. | ##### `markdown` ```python markdown( source, *, size=14.0, color=None, link_color=None, code_color=None, code_bg=None, on_link=None, width=None, selectable=None, selection_color=None, ) ``` Renders Markdown (headings, lists, code blocks, quotes, tables, links…). For the theme colors use `ui.markdown`. | Parameter | Description | | --- | --- | | `source` | The Markdown text. | | `size` | Size. | | `color` | Text or icon color. | | `link_color` | Link color. | | `code_color` | Code color. | | `code_bg` | Code background. | | `on_link` | Callable that receives the URL when a link is clicked. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `selectable` | `True` lets the mouse select the text (drag, double-click a word, triple-click a block) and Ctrl+C copy it. A string names a selection group: the texts and Markdown of one group form a single selection, in the order they are drawn (the lines of a log, the messages of a chat). | | `selection_color` | Color behind the selected text. | ##### `rich_text` ```python rich_text( spans, *, size=None, color=None, font=None, line_height=None, width=None, align_x=None, wrap=True, ) ``` Text made of spans with different styles. Plain strings are unstyled spans. | Parameter | Description | | --- | --- | | `spans` | List of `span(...)` or strings. | | `size` | Size. | | `color` | Text or icon color. | | `font` | Font of the span or text (family name, e.g. `"IBM Plex Mono"`). | | `line_height` | Line height. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `align_x` | Horizontal alignment: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `wrap` | Line wrapping / moves children to a new row when they don't fit. | ```python pz.rich_text(["Click ", pz.span("here", color="#74ade8", underline=True, link=self.open), " to open."]) ``` ##### `span` ```python span( text, *, color=None, size=None, font=None, weight=None, italic=False, bg=None, radius=0.0, padding=None, underline=False, strikethrough=False, link=None, ) ``` A styled span for `rich_text`. With `link`, clicking it sends (or calls) that value. | Parameter | Description | | --- | --- | | `text` | The text. | | `color` | Text or icon color. | | `size` | Size. | | `font` | Font of the span or text (family name, e.g. `"IBM Plex Mono"`). | | `weight` | Font weight: `"normal"`, `"medium"`, `"semibold"` or `"bold"`. | | `italic` | If `True`, the span is drawn in italics. | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `radius` | Background radius. | | `padding` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `underline` | Underline. | | `strikethrough` | Strikethrough. | | `link` | Message or callable on click. | ##### `Span` The type of the spans created by `pz.span`. ##### `qr_code` ```python qr_code(data, *, cell_size=4.0, color=None, bg=None) ``` A QR code that encodes `data`. | Parameter | Description | | --- | --- | | `data` | The text or URL to encode. | | `cell_size` | Size of each module in pixels. | | `color` | Text or icon color. | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | ##### `shader` ```python shader( code, *, uniforms=None, textures=None, width=None, height=None, animate=True, time=None, fallback=None, ) ``` A widget painted on the GPU by your WGSL `fn shade(uv, px, size, time) -> vec4`, which returns each pixel's color, or by a shader graph made of nodes (a `.pzshad` path or a [`pyzed.shading`](https://albakore.github.io/pyzed/reference/shading/) `Graph` or `Shader`). Each entry of `uniforms` becomes a function of the same name (`{"speed": 2}` -> `speed()`); `mouse()` gives the pointer. Without a GPU the widget shows `fallback`. Raises `ValueError` with the compiler's message for invalid code. See the [shaders](https://albakore.github.io/pyzed/shading/shaders/) guide. | Parameter | Description | | --- | --- | | `code` | WGSL source that defines `shade` (and any helper functions), or a `.pzshad` path, `Graph` or `Shader`. | | `uniforms` | Up to 32 named values: a number, 2 to 4 numbers or a color. With a `.pzshad`, the shader's mutable variables by name. | | `textures` | Up to 8 images, `{name: path}`: each becomes `name(uv)`, the image's color at `uv`. | | `width` | Width (`"fill"` by default). | | `height` | Height (`"fill"` by default). | | `animate` | Redraw every frame so `time` moves (`True`). | | `time` | A fixed `time` in seconds, for a still frame. | | `fallback` | Color or gradient shown without a GPU. | ##### `canvas` ```python canvas( shapes=None, *, width=None, height=None, id=None, on_press=None, on_release=None, on_right_press=None, on_move=None, on_scroll=None, cursor=None, ) ``` A 2D drawing surface. `shapes` is a list created with [`pyzed.draw`](https://albakore.github.io/pyzed/reference/draw/). Mouse callbacks receive coordinates relative to the canvas. With an `id`, the geometry is cached between frames and only recomputed when the shapes change. See the [drawing with canvas](https://albakore.github.io/pyzed/guides/canvas/) guide. | Parameter | Description | | --- | --- | | `shapes` | List of `pyzed.draw` shapes (nested lists are accepted). | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `id` | Enables the geometry cache. | | `on_press` | Callable that receives `(x, y)` on press. | | `on_release` | Callable that receives `(x, y)` on release. | | `on_right_press` | Callable that receives `(x, y)` on right click. | | `on_move` | Called with `(x, y)` when the mouse moves. | | `on_scroll` | Callable that receives `(dx, dy)` in lines when the wheel is used. | | `cursor` | Mouse cursor (`"crosshair"`, `"pointer"`…). | #### Interaction ##### `button` ```python button( content, on_press=None, *, padding=None, width=None, height=None, bg=None, color=None, border_color=None, border_width=0.0, radius=None, shadow=None, hover_bg=None, hover_color=None, hover_border_color=None, pressed_bg=None, disabled_color=None, clip=False, ) ``` A button with no predefined style. For the themed button use `ui.button`. | Parameter | Description | | --- | --- | | `content` | Button content. | | `on_press` | Message or callable sent on press. `None` disables the control. | | `padding` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `color` | Text or icon color. | | `border_color` | Border color. | | `border_width` | Border width in pixels. | | `radius` | Corner radius: a number or `(top_left, top_right, bottom_right, bottom_left)`. | | `shadow` | Shadow: `(color, (dx, dy), blur)`. | | `hover_bg` | Background on hover. | | `hover_color` | Text color on hover. | | `hover_border_color` | Border color on hover. | | `pressed_bg` | Background while pressed. | | `disabled_color` | Text color when disabled. | | `clip` | Clips content that overflows the bounds. | ##### `text_input` ```python text_input( value, *, placeholder='', on_input=None, on_submit=None, id=None, size=None, font=None, padding=None, width=None, secure=False, icon=None, icon_size=None, bg=None, border_color=None, hover_border_color=None, focus_border_color=None, border_width=None, radius=None, color=None, placeholder_color=None, selection_color=None, icon_color=None, align_x=None, ) ``` A controlled text field: it shows `value` and notifies you of changes through `on_input`. For the themed field use `ui.input`. | Parameter | Description | | --- | --- | | `value` | Current text. | | `placeholder` | Hint text shown when empty. | | `on_input` | Callable that receives the new text on every keystroke. | | `on_submit` | Message or callable sent when Enter is pressed. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `size` | Font size. | | `font` | Font family. | | `padding` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `secure` | Hides the text (passwords). | | `icon` | Icon character (use `pyzed.icons.glyph(name)`). | | `icon_size` | Icon size. | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `border_color` | Border color. | | `hover_border_color` | Border color on hover. | | `focus_border_color` | Border color when focused. | | `border_width` | Border width in pixels. | | `radius` | Corner radius: a number or `(top_left, top_right, bottom_right, bottom_left)`. | | `color` | Text or icon color. | | `placeholder_color` | Placeholder color. | | `selection_color` | Selection color. | | `icon_color` | Icon color. | | `align_x` | Horizontal alignment: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | ##### `mouse_area` ```python mouse_area( content, *, on_press=None, on_release=None, on_right_press=None, on_double_click=None, on_enter=None, on_exit=None, on_move=None, on_scroll=None, on_middle_press=None, on_middle_release=None, cursor=None, ) ``` Detects mouse events over its content. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `on_press` | Left click (on press). | | `on_release` | On releasing the left button. | | `on_right_press` | Right click (context menus). | | `on_double_click` | Double click. | | `on_enter` | The mouse enters. | | `on_exit` | The mouse leaves. | | `on_move` | Called with `(x, y)` when the mouse moves. | | `on_scroll` | Callable that receives `(dx, dy)` in lines when the wheel is used. | | `on_middle_press` | Middle-button click. | | `on_middle_release` | Middle button released (pan gestures, for example). | | `cursor` | `"pointer"`, `"text"`, `"grab"`, `"grabbing"`, `"crosshair"`, `"not-allowed"`, `"resize-horizontal"`, `"resize-vertical"`, `"move"` or `"default"`. | ##### `hover` ```python hover(base, top) ``` Shows `top` over `base` only while the mouse is over it (for example, a tab's close button). | Parameter | Description | | --- | --- | | `base` | Widget underneath. | | `top` | Widget that appears on hover. | ##### `opaque` ```python opaque(content) ``` Makes a widget block the mouse for whatever is below it in a `stack`. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | ##### `tooltip` ```python tooltip( content, tip, *, position='bottom', gap=4.0, bg=None, color=None, border_color=None, border_width=0.0, radius=None, shadow=None, ) ``` Shows `tip` next to `content` on hover. `ui.tooltip` adds the theme's style. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `tip` | Tooltip content. | | `position` | `"top"`, `"bottom"`, `"left"`, `"right"` or `"cursor"`. | | `gap` | Distance from the content. | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `color` | Text or icon color. | | `border_color` | Border color. | | `border_width` | Border width in pixels. | | `radius` | Corner radius: a number or `(top_left, top_right, bottom_right, bottom_left)`. | | `shadow` | Shadow: `(color, (dx, dy), blur)`. | ##### `popup` ```python popup(base, content=None, *, on_dismiss=None, side='bottom', align='start', gap=4.0) ``` Shows `content` over everything else next to `base`, like the menu of a drop-down: it is not cut off by the panels `base` sits in, flips to the other side when it does not fit and stays inside the window. A press outside it (and outside `base`) or Escape calls `on_dismiss`. With `content=None` only `base` is shown. The date pickers open their calendars with it. | Parameter | Description | | --- | --- | | `base` | The widget it belongs to (a field, a button). | | `content` | What opens (a panel, a menu), or `None` while closed. | | `on_dismiss` | Message or callable sent on a press outside or Escape. | | `side` | `"bottom"`, `"top"`, `"right"` or `"left"` of `base`. | | `align` | `"start"`, `"center"` or `"end"`: how it lines up with `base`. | | `gap` | Space between `base` and the content. | ```python pz.popup(field, panel if self.open else None, on_dismiss=self.close) ``` #### Controls ##### `checkbox` ```python checkbox( checked, label=None, *, on_toggle=None, size=None, text_size=None, accent=None, bg=None, border_color=None, check_color=None, color=None, ) ``` A checkbox. | Parameter | Description | | --- | --- | | `checked` | Current state. | | `label` | Label text. | | `on_toggle` | Callable that receives the new `bool` (or a message to send). | | `size` | Box size. | | `text_size` | Text size. | | `accent` | Accent color (active part). | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `border_color` | Border color. | | `check_color` | Check mark color. | | `color` | Text or icon color. | ##### `toggler` ```python toggler( checked, label=None, *, on_toggle=None, size=None, text_size=None, accent=None, bg=None, border_color=None, knob_color=None, color=None, ) ``` An on/off switch. | Parameter | Description | | --- | --- | | `checked` | Current state. | | `label` | Label text. | | `on_toggle` | Callable that receives the new `bool` (or a message to send). | | `size` | Switch height. | | `text_size` | Text size. | | `accent` | Accent color (active part). | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `border_color` | Border color. | | `knob_color` | Knob color. | | `color` | Text or icon color. | ##### `slider` ```python slider( min, max, value, *, step=1.0, on_change=None, on_release=None, width=None, height=None, vertical=False, accent=None, bg=None, knob_color=None, border_color=None, ) ``` A slider between `min` and `max`. | Parameter | Description | | --- | --- | | `min` | Minimum. | | `max` | Maximum. | | `value` | Current value. | | `step` | Step. | | `on_change` | Callable that receives the new value. | | `on_release` | Sent when the knob is released. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `vertical` | Vertical orientation. | | `accent` | Accent color (active part). | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `knob_color` | Knob color. | | `border_color` | Border color. | ##### `progress_bar` ```python progress_bar( value, *, min=0.0, max=1.0, width=None, height=None, accent=None, bg=None, radius=2.0, ) ``` A progress bar. | Parameter | Description | | --- | --- | | `value` | Current value. | | `min` | Minimum. | | `max` | Maximum. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `accent` | Accent color (active part). | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `radius` | Corner radius. | ##### `vertical_slider` ```python vertical_slider(min, max, value, **kwargs) ``` A vertical slider (`slider(..., vertical=True)`). | Parameter | Description | | --- | --- | | `min` | Minimum. | | `max` | Maximum. | | `value` | Current value. | | `kwargs` | Same as `slider`. | ##### `pick_list` ```python pick_list( options, selected=None, on_select=None, *, on_open=None, on_close=None, placeholder=None, width=None, padding=None, text_size=None, font=None, handle=None, handle_size=12.0, menu_height=None, bg=None, color=None, placeholder_color=None, handle_color=None, border_color=None, hover_border_color=None, radius=None, menu_bg=None, menu_color=None, menu_border_color=None, selected_bg=None, selected_color=None, focus_border_color=None, id=None, ) ``` A dropdown to choose one of `options`. `on_select(option)` receives the chosen one. For the themed dropdown use `ui.select`. It takes the keyboard focus like a text field (a click, Tab or `Command.focus(id)`) and keeps it after a choice: ↑/↓ then pick the previous or next option without opening it, Home/End the first and last and typing jumps to an option; Enter, Space, Alt+↓ or F4 open the menu, where ↑/↓ move, Enter picks and Esc closes. | Parameter | Description | | --- | --- | | `options` | List of options. | | `selected` | Current option. | | `on_select` | Callable that receives the chosen item. | | `on_open` | Sent when the menu opens. | | `on_close` | Sent when the menu closes. | | `placeholder` | Hint text shown when empty. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `padding` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `text_size` | Text size. | | `font` | Font family. | | `handle` | Arrow icon character. | | `handle_size` | Arrow size. | | `menu_height` | Maximum menu height. | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `color` | Text or icon color. | | `placeholder_color` | Placeholder color. | | `handle_color` | Arrow color. | | `border_color` | Border color. | | `hover_border_color` | Border color on hover. | | `radius` | Corner radius: a number or `(top_left, top_right, bottom_right, bottom_left)`. | | `menu_bg` | Menu background. | | `menu_color` | Menu text. | | `menu_border_color` | Menu border. | | `selected_bg` | Background of the highlighted option. | | `selected_color` | Text of the highlighted option. | | `focus_border_color` | Border color while it has the keyboard focus. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | ##### `radio` ```python radio( label, selected, on_select=None, *, size=None, text_size=None, spacing=None, accent=None, bg=None, border_color=None, color=None, ) ``` A radio button. Group several yourself (or use `ui.radio_group`); `on_select` is sent when it is clicked. | Parameter | Description | | --- | --- | | `label` | Label text. | | `selected` | Whether it is checked. | | `on_select` | Message or callable on click. | | `size` | Size. | | `text_size` | Text size. | | `spacing` | Space between the circle and the text. | | `accent` | Accent color (active part). | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `border_color` | Border color. | | `color` | Text or icon color. | #### Code editor ##### `code_editor` ```python code_editor( id, text='', *, language='txt', size=14.0, line_height=None, font=None, width=None, height=None, padding=None, syntax=None, bg=None, color=None, selection_color=None, placeholder=None, placeholder_color=None, gutter=True, gutter_color=None, gutter_active_color=None, gutter_width=None, markers=None, wrap=False, on_change=None, on_cursor=None, active_line=None, scrollbar_color=None, annotations=None, whitespace=None, tab_size=None, capture_keys=None, on_scroll=None, auto_close=False, folding=False, minimap=None, minimap_thumb=None, cursor_shape='bar', cursor_width=2.0, cursor_blink=True, cursor_color=None, on_zoom=None, on_action=None, highlights=None, highlight_color=None, match_brackets=True, selection_highlight=True, keymap=None, scrollbar=True, readonly=False, hard_tabs=False, backspace_unindent=True, repeat_delay=None, repeat_rate=None, on_hover=None, hover_delay=500.0, sticky_scroll=0, ) ``` A code editor with syntax highlighting, line numbers and git markers, based on Iced's `text_editor`. It is **uncontrolled**: `text` is only the initial value for that `id`; the text, cursor and history live in Rust. Use `Command.set_editor_text` to replace it. For the themed editor use [`ui.editor`](https://albakore.github.io/pyzed/components/code/). | Parameter | Description | | --- | --- | | `id` | Identifies the buffer. Reuse it to keep state across renders. | | `text` | Initial text. | | `language` | Language extension or name (`"py"`, `"rs"`, `"json"`…). | | `size` | Font size. | | `line_height` | Line height in pixels. | | `font` | Family (monospace by default). | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `padding` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `syntax` | Map of TextMate scopes to colors (provided by `Theme.syntax_scopes()`). | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `color` | Text or icon color. | | `selection_color` | Selection color. | | `placeholder` | Hint text shown when empty. | | `placeholder_color` | Placeholder color. | | `gutter` | Shows line numbers. | | `gutter_color` | Line number color. | | `gutter_active_color` | Color of the cursor line's number. | | `gutter_width` | Gutter width (automatic by default). | | `markers` | `{line: color}` for git markers in the gutter (lines from 1). | | `wrap` | Line wrapping. | | `on_change` | Callable that receives the full text after each edit. | | `on_cursor` | Callable that receives `(line, column)`, from 1. | | `active_line` | Background color of the cursor line. | | `scrollbar_color` | Scrollbar color. | | `annotations` | `{line: "text"}` or `{line: ("text", color)}`: virtual text at the end of the line (code lens, diagnostics). | | `whitespace` | Draws spaces as `·` and tabs as `→` in this color. | | `tab_size` | Tab inserts spaces up to the next multiple; Enter keeps the indentation; Backspace removes a level. | | `capture_keys` | Keys the editor leaves to the app's `on_key` (`"up"`, `"enter"`…), e.g. while a completion list is open. | | `on_scroll` | Called on scroll. | | `auto_close` | Typing a bracket or quote also inserts its closing pair. | | `folding` | Fold chevrons in the gutter and Ctrl+Shift+[ / ]. | | `minimap` | Width of the minimap in pixels (`None`: no minimap). | | `minimap_thumb` | Color of the minimap's visible-area highlight. | | `cursor_shape` | `"bar"`, `"block"`, `"underline"` or `"hollow"`. | | `cursor_width` | Width of the bar cursor. | | `cursor_blink` | Whether the cursor blinks. | | `cursor_color` | Cursor color. | | `on_zoom` | Callable that receives `1`, `-1` or `0` for Ctrl+= / Ctrl+- / Ctrl+0 and Ctrl+wheel. | | `on_action` | Callable that receives the name of each editor action that runs (`"undo"`, `"select_next"`…). | | `highlights` | Ranges painted behind the text (search results): `[(line, column, end_line, end_column)]`, from 1, with exclusive ends. | | `highlight_color` | Color of `highlights`. | | `match_brackets` | Outlines the bracket that matches the one next to the cursor. | | `selection_highlight` | Highlights the other occurrences of the selected text. | | `keymap` | Zed-style `{binding: action}` dict; `None` as the action removes a default key (`pyzed.ui.editor.keymap_for` builds one from per-action overrides). | | `scrollbar` | `False` hides the scrollbars. | | `readonly` | `True` lets the text be selected and copied but not changed (a log, a traceback). | | `hard_tabs` | With `tab_size`: Tab, Enter and indent (Ctrl+]) use tab characters instead of spaces. | | `backspace_unindent` | With `tab_size`: Backspace in leading spaces removes a whole level (`False`: one space). | | `repeat_delay` | Milliseconds Backspace and Delete wait, held down, before repeating (`None`: the system's). | | `repeat_rate` | Deletions per second at most while Backspace or Delete is held down (`None`: the system's pace). | | `on_hover` | Callable that receives `{line, column, start, end, x, y, height}` (the word under the mouse, from 1, and its place in the editor) after the mouse rests on it, and `None` when it leaves (hover cards). | | `hover_delay` | Milliseconds the mouse rests on a word before `on_hover` is called. | | `sticky_scroll` | How many lines of the enclosing blocks (`class`, `def`, `if`…) stay pinned at the top while scrolling (`0`: none). | #### Styles ##### `Style` ```python class Style ``` A reusable set of style properties for `widget.style(...)`, a component's `style=` or `App.style`. Combine styles with `|` (the right one wins). See [Styles](https://albakore.github.io/pyzed/theming/styles/). | Parameter | Description | | --- | --- | | `styles` | Styles, dicts or style names to start from. | | `props` | Properties: `background`, `color`, `font`, `weight`, `italic`, `size`, `line_height`, `border_color`, `border_width`, `border` (`(width, color)`), `radius`, `shadow`, `padding`, `margin`, `width`, `height`, `max_width`, `max_height`, `min_width`, `min_height`, `align_x`, `align_y`, `clip`. Effects: `opacity`, `translate` (`(x, y)`, pixels or `"50%"`), `scale`, `glow` (a color, a size or `(color, size)`), `bloom` (a color or `(color, intensity)`) and `glass` (`True`, a tint or `{"tint", "strength"}`). States: `hover`, `pressed` (or `active`) and `focus`, each with the properties to show then. `transition`: milliseconds, `True` or `{"duration", "easing", "delay"}`. `media`: `{query: style}` applied when the window matches. Colors may be theme tokens. | ```python card = pz.Style(background="panel", border=(1, "border"), radius=8, padding=12) pz.column([...]).style(card | pz.Style(shadow=("#0006", (0, 4), 12))) ``` **Methods and properties** ###### `Style.get` ```python Style.get(key, default=None) ``` Returns a property's value (aliases accepted), or `default` when it is not set. ###### `Style.merge` ```python Style.merge(*styles, **props) ``` A new style with `styles` and `props` on top of this one. ###### `Style.without` ```python Style.without(*keys) ``` A new style without these properties. ###### `Style.text` ```python Style.text() ``` Only the text properties (what a style passes down to the texts inside). ###### `Style.to_dict` ```python Style.to_dict() ``` JSON-friendly properties (gradients and paints become dicts). ###### `Style.from_dict` ```python @classmethod Style.from_dict(data) ``` Builds a style from data made by `to_dict` (aliases accepted). ##### `define_style` ```python define_style(name, *styles, **props) ``` Names a style so widgets can use it by name: `widget.style("card")`. Returns it. | Parameter | Description | | --- | --- | | `name` | The style's name. | | `styles` | Styles to merge. | | `props` | Properties on top. | ##### `stylesheet` ```python stylesheet(rules=None, **classes) ``` Adds CSS-like rules, `{selector: style}`, that widgets get with `widget.classes(...)` and components with `class_=`/`id=`. Selectors: `.class`, `.a.b`, `#id`, a component's class name (`"Rating"`), an optional `:hover`, `:pressed`/`:active` or `:focus`, and comma lists. More specific selectors win (ids, then classes, then names), then later rules. A selector given again replaces its rule. Returns every rule. See [Stylesheets](https://albakore.github.io/pyzed/theming/styles/#stylesheets). | Parameter | Description | | --- | --- | | `rules` | A `{selector: style}` dict. | | `classes` | Classes by keyword: `card={...}` is `".card"`. | ```python pz.stylesheet({ ".card": {"background": "panel", "padding": 16, "radius": 8}, ".card:hover": {"border_color": "accent"}, }) pz.column([...]).classes("card") ``` ##### `clear_stylesheet` ```python clear_stylesheet() ``` Removes every rule added with `pz.stylesheet`. ##### `media` ```python media(query=None, **conditions) ``` Whether the window being drawn matches a media query, like CSS `@media`. See [Responsive styles](https://albakore.github.io/pyzed/theming/styles/#responsive-styles). | Parameter | Description | | --- | --- | | `query` | `"max_width: 600"`, `"(min_width: 768px) and (orientation: landscape)"` or a breakpoint (`"md"`). | | `conditions` | The same by keyword: `min_width`, `max_width`, `min_height`, `max_height`, `orientation`, `theme`. | ```python sidebar = None if pz.media("max_width: 600") else self.sidebar() ``` ##### `viewport` ```python viewport() ``` The size of the window being drawn, `(width, height)` in logical pixels, like `window.innerWidth` and `innerHeight`. ##### `current_breakpoint` ```python current_breakpoint() ``` The widest breakpoint the window reaches: `"xs"` (under 640 px), `"sm"`, `"md"`, `"lg"`, `"xl"` or `"xxl"`. ##### `set_style` ```python set_style(*styles, **props) ``` Sets the global style for every window, under each window's `style`. Without arguments it clears it. | Parameter | Description | | --- | --- | | `styles` | Styles to merge. | | `props` | Properties on top. | ##### `Gradient` ```python class Gradient ``` A linear gradient for backgrounds. Angles follow CSS: 0 up, 90 right, 180 (the default) down. Up to 8 stops. | Parameter | Description | | --- | --- | | `stops` | Colors, spread evenly, or `(offset, color)` pairs with offsets from 0 to 1. | | `angle` | Direction in degrees. | ```python pz.container(None, height=80).style(background=pz.Gradient(["#f97316", "#7c3aed"], angle=90)) ``` **Methods and properties** ###### `Gradient.to_value` ```python Gradient.to_value() ``` `(angle, [(offset, "#rrggbbaa"), ...])`, the form widgets take as `gradient=`. ###### `Gradient.to_dict` ```python Gradient.to_dict() ``` Returns the gradient as JSON-ready data: `{"type": "linear", "angle", "stops"}`. ##### `Mask` ```python class Mask ``` An opacity mask for a `Paint`: how visible the fill is along a direction. On a gradient it follows the gradient's direction. | Parameter | Description | | --- | --- | | `stops` | Opacities (0 to 1), spread evenly, or `(offset, opacity)` pairs. | | `angle` | Direction when the fill is a plain color. | **Methods and properties** ###### `Mask.at` ```python Mask.at(offset) ``` Returns the opacity (0 to 1) at `offset` (0 to 1), interpolating between stops. ###### `Mask.to_dict` ```python Mask.to_dict() ``` Returns the mask as JSON-ready data: `{"angle", "stops"}`. ##### `Paint` ```python class Paint ``` A fill: a color or `Gradient` with an overall opacity and an optional `Mask`. Works anywhere a background does. | Parameter | Description | | --- | --- | | `fill` | A color or a `Gradient`. | | `opacity` | 0 to 1, multiplies the fill's own alpha. | | `mask` | A `pz.Mask`. | ```python pz.Paint("#3b82f6", opacity=0.8, mask=pz.Mask([1, 0])) # fades out downwards ``` **Methods and properties** ###### `Paint.resolve` ```python Paint.resolve() ``` `(color, gradient)`: what a container draws (one of them is `None`). ###### `Paint.to_dict` ```python Paint.to_dict() ``` Returns the paint as JSON-ready data: `fill`, `opacity` and `mask` when there is one. ##### `global_style` ```python global_style() ``` The style set with `set_style` (`None` if none). ##### `named_styles` ```python named_styles() ``` The styles defined with `define_style`, by name. ##### `apply_global` ```python apply_global(view, style=None) ``` `view` with a window's global style: text properties fill in what texts leave unset. ##### `themed` ```python themed(theme, style) ``` `theme` with a global style's text color, background, border color, size and radius. ##### `to_hex` ```python to_hex(color) ``` Any color as `"#rrggbb"` or `"#rrggbbaa"` (theme tokens resolved). ##### `paint_from_dict` ```python paint_from_dict(data) ``` Rebuilds a `Gradient` or `Paint` saved with `to_dict()`. #### Animation ##### `Animation` ```python @dataclass class Animation(keyframes=(), duration=300, delay=0, easing='ease', repeat=1, direction='normal', fill='both', on='mount', category='custom', description='', _frames=()) ``` A keyframe animation, like a CSS `@keyframes` rule with its timing. `keyframes` is a list of `{property: value}` frames (spread evenly; a frame may set `offset` and `easing`) or a dict from offsets (`0` to `1`, `"50%"`, `"from"`, `"to"`) to frames. A property missing at the start or end goes from or to the widget's own value, and `None` names that value explicitly: ```python pz.Animation([{"opacity": 0, "y": 12}, {"opacity": 1, "y": 0}], duration=250) pz.Animation({0: {"scale": 1}, "50%": {"scale": 1.1}, 1: {"scale": 1}}, repeat="infinite") ``` The other fields are the defaults `animate()` options override. `category` and `description` tell editors where and how to show a preset. **Methods and properties** ###### `Animation.properties` ```python Animation.properties # property ``` The properties the keyframes change, in order of appearance. ###### `Animation.total` ```python Animation.total # property ``` Milliseconds from the start to the end (`inf` when it repeats forever). ###### `Animation.with_options` ```python Animation.with_options(**options) ``` A copy with other timing or trigger (`duration`, `easing`, `on`...). ###### `Animation.compile` ```python Animation.compile(**options) ``` The plain data the native side plays (`animate()` calls it). ###### `Animation.to_dict` ```python Animation.to_dict() ``` JSON-ready data (`Animation.from_dict` reads it back). ###### `Animation.from_dict` ```python @classmethod Animation.from_dict(data) ``` Rebuilds an animation saved with `to_dict`. ##### `Trigger` ```python class Trigger ``` Starts an animation from your code, like a button you press in a handler. Keep one on the app, give it to `animate(on=...)` in `view()` and call `fire()` whenever the animation should play (from the start, even if it is still running): ```python class Login(pz.App): def __init__(self): self.wrong = pz.Trigger() def view(self): return ui.input(self.password, self.set_password).animate("shake", on=self.wrong) def submit(self): if not self.check(): self.wrong.fire() # the field shakes now ``` Nothing plays when the widget appears: only `fire()` starts it. `count` says how many times it was fired. `fire` takes and ignores any arguments, so it can be a callback itself: `ui.button("Shake", self.wrong.fire)`. **Methods and properties** ###### `Trigger.fire` ```python Trigger.fire(*_args) ``` Plays the animations that use this trigger, from the start. ##### `define_animation` ```python define_animation(name, animation=None, **options) ``` Names an animation so `animate(name)` plays it; returns it. `define_animation("wiggle", [{"x": 0}, {"x": -4}, {"x": 4}, {"x": 0}], duration=300)` ##### `presence` ```python presence( content, key, *, enter='fade_in', exit='fade_out', id=None, duration=None, easing=None, ) ``` Shows `content` and, when `key` changes, animates the old content out (`exit`) while the new one comes in (`enter`), like page transitions: ```python pz.presence(self.page_view(), key=self.page, enter="slide_in_right", exit="fade_out") ``` `id` tells presences apart (it defaults to where it is called from, so give one when the same line makes several). `duration` and `easing` override both animations'. ##### `stagger` ```python stagger(items, animation='fade_in_up', *, step=40, delay=0, **options) ``` `items` with the same animation, each starting `step` milliseconds after the previous one (a list that cascades in): ```python pz.column(pz.stagger([row(i) for i in items], "fade_in_up", step=30)) ``` ##### `set_reduced_motion` ```python set_reduced_motion(on=True) ``` Turns motion off (`True`) or back on: animations show their resting state at once. By default Pyzed follows the system's "reduce motion" accessibility setting (read when the app starts, see `pyzed.system_reduced_motion`). `True` or `False` overrides it (for example from your app's own settings, or in tests) and `None` follows the system again. The `PYZED_REDUCED_MOTION` environment variable (`1` or `0`) overrides it too. ##### `reduced_motion` ```python reduced_motion() ``` Whether reduced motion is on. ##### `system_reduced_motion` ```python system_reduced_motion() ``` Whether the system asks apps to reduce motion (`None` if unknown). ##### `animation` ```python animation(name) ``` The animation named `name` (a preset or one defined with `define_animation`). ##### `presets` ```python presets(category=None) ``` The named animations (all, or one `category`: entrance, exit, attention, interaction, loop). ##### `ease` ```python ease(easing, t) ``` `easing` at `t` (0 to 1), the curve animations use: `ease("ease_out", 0.5)`. Handy to animate your own drawings (`pz.canvas`) with the same curves. ##### `window_animations` ```python window_animations(window, closing=False) ``` The animation a window plays when it opens (or closes), from its `transition` and `exit_transition` (an empty list when it has none). ##### `PRESETS` Every named animation (the built-in presets and the ones added with `define_animation`), mapped to its `Animation`. See [the preset list](https://albakore.github.io/pyzed/animation/presets/). ##### `CATEGORIES` Preset categories: `entrance`, `exit`, `attention`, `interaction` and `loop`. ##### `EASINGS` Easing names (`linear`, `ease`, `ease_out_cubic`, `ease_out_back`, `spring`...). `animate()` also takes `(x1, y1, x2, y2)` cubic Béziers and `"steps(n)"`. ##### `TRIGGERS` What plays an animation: `mount`, `hover`, `press`, `focus`, `click`, `active` and `manual` (each time `key` changes; a `pz.Trigger` given to `on=` uses it). ##### `PROPERTIES` The properties animations and transitions change: `opacity`, `x`, `y`, `scale`, `background`, `border_color`, `color`, `radius`, `border_width`, `glow` and `glow_color`. ##### `WINDOW_TRANSITIONS` Names for `Window.transition` (`fade`, `zoom`, `pop`, `slide_up`, `slide_down`, `slide_left`, `slide_right`) mapped to their `(opening, closing)` presets. #### Timeline animations ##### `Clip` ```python @dataclass class Clip(name, length=None, tracks=(), calls=(), loop='none', description='') ``` A named timeline of `length` seconds with `tracks` and `calls` (see the module docs). `loop` is `"none"` (plays once and holds the last frame), `"loop"` or `"pingpong"`. `length` defaults to the time of the last key or call. **Methods and properties** ###### `Clip.targets` ```python Clip.targets # property ``` The widget targets its tracks animate, in order. ###### `Clip.attributes` ```python Clip.attributes # property ``` The app attributes its tracks animate (`":score"` -> `"score"`). ###### `Clip.track` ```python Clip.track(path) ``` The track with `path` (`None` when there is none). ###### `Clip.values` ```python Clip.values(time, target=None) ``` `{path: value}` of the enabled tracks at `time` (only `target`'s, as `{property: value}`). ###### `Clip.with_track` ```python Clip.with_track(track) ``` A copy with `track` added, or replacing the track with the same path. ###### `Clip.without_track` ```python Clip.without_track(path) ``` A copy without the track `path`. ###### `Clip.with_key` ```python Clip.with_key(path, time, value, easing=None) ``` A copy with a key at `time` on `path` (replacing one at the same time; adds the track). ###### `Clip.without_key` ```python Clip.without_key(path, time) ``` A copy without the key at `time` on `path`. ###### `Clip.retarget` ```python Clip.retarget(mapping, name=None) ``` A copy whose tracks animate other widgets: `clip.retarget({"card": "panel"})`. ###### `Clip.animation` ```python Clip.animation(target, *, on='mount', **options) ``` `target`'s tracks as a `pz.Animation` (to play without a player, or `define_animation`). ###### `Clip.to_dict` ```python Clip.to_dict() ``` JSON-ready data (`Clip.from_dict` reads it back). ###### `Clip.from_dict` ```python @classmethod Clip.from_dict(data) ``` Rebuilds a clip saved with `to_dict`. ##### `Track` ```python @dataclass class Track(path, keys=(), easing='linear', interpolation='linear', enabled=True) ``` One property changing over time: `Track("card:opacity", {0: 0, 0.5: 1})`. `keys` is `{time: value}`, a list of `(time, value)` or `(time, value, easing)` tuples, or `Key` objects. `easing` is the default curve between keys (`"linear"`, like Godot) and `interpolation="discrete"` holds each value until the next key. `enabled=False` keeps a track in the clip without playing it. **Methods and properties** ###### `Track.target` ```python Track.target # property ``` The widget target (`""` for an app attribute). ###### `Track.property` ```python Track.property # property ``` The property or attribute path it changes. ###### `Track.value_at` ```python Track.value_at(time) ``` The track's value at `time` seconds (`None` when it has no keys). ###### `Track.with_keys` ```python Track.with_keys(keys) ``` A copy with other keys. ###### `Track.to_dict` ```python Track.to_dict() ``` JSON-ready data (`Track.from_dict` reads it back). ###### `Track.from_dict` ```python @classmethod Track.from_dict(data) ``` Rebuilds a track saved with `to_dict`. ##### `Key` ```python @dataclass class Key(time, value, easing=None) ``` A value at a time on a track; `easing` is the curve to the next key (the track's when `None`). **Methods and properties** ###### `Key.to_dict` ```python Key.to_dict() ``` JSON-ready data (`Key.from_dict` reads it back). ###### `Key.from_dict` ```python @classmethod Key.from_dict(data) ``` Rebuilds a key saved with `to_dict`. ##### `Call` ```python @dataclass class Call(time, method, args=()) ``` A method called at `time` seconds: `Call(1.2, "play_sound", "ding")`. `method` is the name of a method of the app (the player's `owner`) or a callable; `args` are passed to it. What it returns (a `Command`, a message...) is handled like a callback's. **Methods and properties** ###### `Call.to_dict` ```python Call.to_dict() ``` JSON-ready data (only calls to methods by name can be saved). ###### `Call.from_dict` ```python @classmethod Call.from_dict(data) ``` Rebuilds a call saved with `to_dict`. ##### `AnimationPlayer` ```python class AnimationPlayer ``` Plays `Clip`s on widgets and app state, with play, pause, seek, speed and a queue. `sources` are clips, `AnimationLibrary` objects and `.pzanim` paths. Give widgets their target with `widget.animate(player["card"])`; tracks of app attributes (`":score"`) and `Call`s run on the player's `owner`, which is the app (or window) that holds the player in an attribute, found automatically, or the one you pass. - `autoplay`: the clip that plays when the player is created. - `speed`: the playback speed of every clip (`play` multiplies it). - `on_started(name)`/`on_finished(name)`: called when a clip starts and when it ends (never for looping clips); they may take no argument. - `fps`: how often app attributes update while a clip with attribute tracks plays. Events: - `on_started(name)`: Called with the name of a clip when it starts. - `on_finished(name)`: Called with the name of a clip when it ends (never for looping clips). **Methods and properties** ###### `AnimationPlayer.add` ```python AnimationPlayer.add(source, *, prefix=None) ``` Adds a clip, a library (its clips as `"name/clip"` when it has a name) or a `.pzanim` path. ###### `AnimationPlayer.remove` ```python AnimationPlayer.remove(name) ``` Removes the clip `name` (stopping it when it is the current one). ###### `AnimationPlayer.names` ```python AnimationPlayer.names # property ``` The names of the clips it can play. ###### `AnimationPlayer.has` ```python AnimationPlayer.has(name) ``` Whether it has a clip called `name`. ###### `AnimationPlayer.clip` ```python AnimationPlayer.clip(name=None) ``` The clip named `name` (the current one by default). ###### `AnimationPlayer.target` ```python AnimationPlayer.target(target, *, only=None) ``` Like `player[target]`; `only` keeps some properties (`("opacity", "x", "y")`), for example to move what sits inside an animated container along with it. ###### `AnimationPlayer.current` ```python AnimationPlayer.current # property ``` The clip playing or paused (`None` when stopped). ###### `AnimationPlayer.playing` ```python AnimationPlayer.playing # property ``` Whether time is moving. ###### `AnimationPlayer.paused` ```python AnimationPlayer.paused # property ``` Whether a clip is paused (stopped clips and finished ones aren't). ###### `AnimationPlayer.finished` ```python AnimationPlayer.finished # property ``` Whether the current clip reached its end. ###### `AnimationPlayer.length` ```python AnimationPlayer.length # property ``` The length of the current clip in seconds (0 when none). ###### `AnimationPlayer.position` ```python AnimationPlayer.position # property ``` Seconds into the current clip. ###### `AnimationPlayer.queued` ```python AnimationPlayer.queued # property ``` The clips that play after the current one, in order. ###### `AnimationPlayer.value` ```python AnimationPlayer.value(path) ``` The value the current clip gives `path` (`"card:opacity"`, `":score"`) right now. ###### `AnimationPlayer.play` ```python AnimationPlayer.play(name=None, *, speed=1.0, backwards=False, from_end=False, restart=False) ``` Plays `name` (the current or paused clip by default) from where it is. ###### `AnimationPlayer.play_backwards` ```python AnimationPlayer.play_backwards(name=None, *, speed=1.0) ``` Plays `name` in reverse from its end (or the current clip from where it is). ###### `AnimationPlayer.pause` ```python AnimationPlayer.pause() ``` Stops time where it is (`play()` or `resume()` goes on). ###### `AnimationPlayer.resume` ```python AnimationPlayer.resume() ``` Goes on from where the clip was paused. ###### `AnimationPlayer.toggle` ```python AnimationPlayer.toggle() ``` Pauses when playing, plays otherwise. ###### `AnimationPlayer.stop` ```python AnimationPlayer.stop(*, reset=False) ``` Stops at the start of the clip (showing its first frame); `reset` lets widgets go back to their own values and forgets the clip. Clears the queue. ###### `AnimationPlayer.seek` ```python AnimationPlayer.seek(seconds) ``` Jumps to `seconds` into the current clip (calls on the way don't run). ###### `AnimationPlayer.advance` ```python AnimationPlayer.advance(seconds) ``` Moves the current clip `seconds` forward (calls run, state updates), like Godot's `advance`. ###### `AnimationPlayer.queue` ```python AnimationPlayer.queue(name) ``` Plays `name` after the current clip ends (or now when nothing plays). ###### `AnimationPlayer.clear_queue` ```python AnimationPlayer.clear_queue() ``` Forgets the queued clips. ###### `AnimationPlayer.subscriptions` ```python AnimationPlayer.subscriptions() ``` What keeps the player's calls, attribute tracks and queue going (added for you when the player is an attribute of the app or a window). ##### `AnimationLibrary` ```python class AnimationLibrary ``` Named clips that are saved together in a `.pzanim` file and reused. `AnimationLibrary([intro, idle], name="ui")`; `lib["intro"]`, `lib.add(clip)`, `lib.remove("idle")`, `lib.save("ui.pzanim")` and `AnimationLibrary.load(path)`. A player given a library with a `name` calls its clips `"ui/intro"`, like Godot. **Methods and properties** ###### `AnimationLibrary.add` ```python AnimationLibrary.add(clip) ``` Adds `clip` (replacing one with its name) and returns it. ###### `AnimationLibrary.remove` ```python AnimationLibrary.remove(name) ``` Removes the clip `name` (nothing happens when there is none). ###### `AnimationLibrary.rename` ```python AnimationLibrary.rename(old, new) ``` Renames the clip `old` to `new` and returns it. ###### `AnimationLibrary.names` ```python AnimationLibrary.names # property ``` The names of its clips, in order. ###### `AnimationLibrary.to_dict` ```python AnimationLibrary.to_dict() ``` The `.pzanim` data (`AnimationLibrary.from_dict` reads it back). ###### `AnimationLibrary.from_dict` ```python @classmethod AnimationLibrary.from_dict(data) ``` Rebuilds a library from `.pzanim` data. ###### `AnimationLibrary.save` ```python AnimationLibrary.save(path) ``` Writes the library as JSON (a `.pzanim` file) and returns the path. ###### `AnimationLibrary.load` ```python @classmethod AnimationLibrary.load(path) ``` Reads a `.pzanim` file (raises `ValueError` when it isn't one). ##### `load_animations` ```python load_animations(path) ``` The library saved in a `.pzanim` file. #### Large data ##### `virtual_column` ```python virtual_column(count, item, *, item_height, spacing=0.0, overscan=4, width=None) ``` A column of `count` rows of the same height that only builds the rows in view: `item(index)` is called when row `index` scrolls into view, and rows that leave it are dropped. A column of a million rows costs the same as one of fifty. Put it in a `scrollable` (it virtualizes against the visible area of the closest one, or the window). | Parameter | Description | | --- | --- | | `count` | Number of rows. | | `item` | Function that receives a row index and returns its widget. | | `item_height` | Height of every row, in pixels. | | `spacing` | Space between rows, in pixels. | | `overscan` | Extra rows built above and below the visible ones, so fast scrolling shows no gaps. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | ```python rows = load_logs() # a list with 500 000 entries pz.scrollable( pz.virtual_column(len(rows), lambda i: pz.text(rows[i]), item_height=20), id="logs", height="fill", ) ``` ##### `memo` ```python memo(function=None, *, maxsize=64) ``` Decorator that caches what a view function returns by its arguments, so the next `view()` reuses the same widget tree while they don't change. Pass everything the subtree depends on as arguments. Lists, dicts and sets compare by contents, other unhashable objects by identity; the current theme and language are always part of the key. `function.cache_clear()` empties its cache. | Parameter | Description | | --- | --- | | `function` | The function to memoize (when used as `@pz.memo`). | | `maxsize` | How many results to keep per function (least recently used are dropped). | ```python @pz.memo def sidebar(files: tuple, selected: str): return pz.column([ui.list_item(f, selected=f == selected) for f in files]) ``` ##### `clear_memos` ```python clear_memos() ``` Empties the cache of every `@pz.memo` function. ##### `memo_stats` ```python memo_stats() ``` Returns `{"hits": n, "misses": m}`: how many calls of memoized functions were answered from the cache and how many built their result. ### Components (pyzed.ui) > API reference for pyzed.ui: every ready-made component, its parameters, defaults and events, for building native Python desktop apps. Source: https://albakore.github.io/pyzed/reference/ui/ Signatures and parameters of every component. To see them with images and examples, go to the [Components](https://albakore.github.io/pyzed/components/overview/) section. They are all functions that return a `Widget` and read their colors from the current theme. Color parameters accept a theme **token** (`"accent"`, `"text_muted"`, `"error"`…) or a hex color. #### Basics ##### `label` ```python label( content, *, color='text', size='md', weight=None, mono=False, italic=False, wrap=True, theme=None, **kwargs, ) ``` Text with a theme color. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `color` | Theme token or hex color. Defaults to `"text"`. | | `size` | `"xs"` (11), `"sm"` (12), `"md"` (13.5), `"lg"` (16), `"xl"` (20), `"2xl"` (26) or a number. | | `weight` | Weight: `"medium"`, `"semibold"`, `"bold"`… | | `mono` | Uses the monospace font (IBM Plex Mono). | | `italic` | Italic. | | `wrap` | Line wrapping. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | | `kwargs` | Passed to `pyzed.text`. | ##### `muted` ```python muted(content, **kwargs) ``` A `label` with the `"text_muted"` color. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `kwargs` | Same as `label`. | ##### `heading` ```python heading(content, *, level=1, theme=None, **kwargs) ``` A semibold heading. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `level` | 1 (26 px), 2 (20 px), 3 (16 px) or 4 (13.5 px). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | | `kwargs` | Same as `label`. | ##### `button` ```python button( content=None, on_press=None, *, icon=None, icon_position='start', icon_color=None, variant='filled', tint=None, size='md', selected=False, disabled=False, full_width=False, keys=None, loading=False, tooltip=None, theme=None, **style, ) ``` A themed button with variants, sizes, an icon and a shortcut. | Parameter | Description | | --- | --- | | `content` | Text or widget. | | `on_press` | Message or callable sent on press. `None` disables the control. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `icon_position` | `"start"` or `"end"`. | | `icon_color` | Icon color: a theme token or a hex color. | | `variant` | `"filled"`, `"ghost"`, `"subtle"`, `"outline"`, `"solid"`, `"tinted"`, or a semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"`. | | `tint` | Color for the `solid` and `tinted` variants. | | `size` | `"xs"`, `"sm"`, `"md"` or `"lg"`. | | `selected` | Marks it as selected. | | `disabled` | Disables it (ignores clicks). | | `full_width` | Fills the full width. | | `keys` | Shortcut shown at the end, written as a string: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `loading` | Shows a spinner in place of the icon and ignores presses (keep a `pz.every(1/30, ...)` subscription while it spins). | | `tooltip` | Text shown on hover. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | | `style` | Any `pyzed.button` style parameter (`bg`, `hover_bg`, `color`…). | ##### `icon_button` ```python icon_button( name, on_press=None, *, size='md', variant='ghost', color=None, selected=False, disabled=False, tooltip=None, keys=None, tint=None, loading=False, indicator=None, theme=None, ) ``` A square button with just an icon. | Parameter | Description | | --- | --- | | `name` | Icon name. | | `on_press` | Message or callable sent on press. `None` disables the control. | | `size` | `"xs"`, `"sm"`, `"md"` or `"lg"`. | | `variant` | As in `ui.button`. Defaults to `"ghost"`. | | `color` | Icon color. | | `selected` | Marks it as selected. | | `disabled` | Disables it (ignores clicks). | | `tooltip` | Tooltip text. | | `keys` | Key binding shown in the tooltip. | | `tint` | Colors it (`"error"`, `"accent"`…). | | `loading` | Swaps the icon for a spinner and ignores presses. | | `indicator` | Adds a small dot in this color at the top right (unread, changes…). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `badge` ```python badge( content, *, tint='accent', variant='soft', icon=None, size='md', on_press=None, theme=None, ) ``` A small pill for statuses and counters. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `tint` | Semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` or a token. | | `variant` | `"soft"`, `"solid"` or `"outline"`. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `size` | `"sm"`, `"md"` or `"lg"`. | | `on_press` | Makes it clickable. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `indicator` ```python indicator(color='accent', *, size=6, theme=None) ``` A colored dot (status, unsaved changes, presence). | Parameter | Description | | --- | --- | | `color` | Token or color. | | `size` | Diameter. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `kbd` ```python kbd(keys, *, size='sm', theme=None) ``` Draws a shortcut as keys: `"ctrl-shift-p"`, `"cmd-k cmd-s"`. Uses symbols on macOS (⌘⇧P) and words elsewhere. | Parameter | Description | | --- | --- | | `keys` | The shortcut. | | `size` | `"xs"` or `"sm"`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `format_keys` ```python format_keys(keys) ``` Splits a shortcut into labels per chord: `format_keys("ctrl-shift-p") == [["Ctrl", "Shift", "P"]]`. The style is controlled by `pyzed.ui.base.KEY_STYLE` (`"auto"`, `"symbols"` or `"words"`). | Parameter | Description | | --- | --- | | `keys` | The shortcut. | ##### `keys_text` ```python keys_text(keys) ``` A shortcut as plain text for a label or a status line: `keys_text("ctrl-shift-p") == "Ctrl+Shift+P"` (`"⌃⇧P"` with macOS symbols), chords separated by a space. Tooltips, buttons and menus take `keys=` instead and draw key caps, so never write the keys into their text. | Parameter | Description | | --- | --- | | `keys` | The shortcut. | ##### `avatar` ```python avatar( initials, *, color=None, size=22, index=0, image=None, status=None, tooltip=None, theme=None, ) ``` A round avatar with initials. | Parameter | Description | | --- | --- | | `initials` | Initials (the first two are used). | | `color` | Background color. | | `size` | Diameter. | | `index` | Picks a collaborator color from the theme (`theme.players`). | | `image` | A picture (path or bytes) instead of the initials. | | `status` | Presence dot: `"online"`, `"away"`, `"busy"`, `"offline"` or a color. | | `tooltip` | Text shown on hover (the person's name). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `tooltip` ```python tooltip(content, text, *, keys=None, position='bottom', theme=None) ``` Wraps `content` with a themed tooltip, optionally with its shortcut. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `text` | Tooltip text. | | `keys` | Shortcut shown at the end, written as a string: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `position` | `"top"`, `"bottom"`, `"left"`, `"right"` or `"cursor"`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `divider` ```python divider(*, vertical=False, color=None, label=None, spacing=0, theme=None) ``` A divider line in the theme color. | Parameter | Description | | --- | --- | | `vertical` | Vertical instead of horizontal. | | `color` | Text or icon color. | | `label` | Muted text in the middle of a horizontal line (`"or"`, a date…). | | `spacing` | Space on both sides of the line. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `card` ```python card( content, *, title=None, actions=(), padding=16, width=None, footer=(), on_press=None, selected=False, elevated=False, theme=None, ) ``` A bordered surface to group content, with an optional title and actions. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `title` | Title. | | `actions` | Extra widgets (usually buttons) shown as actions. | | `padding` | Inner padding. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `footer` | Widgets at the bottom right (buttons). | | `on_press` | Makes the whole card clickable. | | `selected` | Outlines it in the accent color. | | `elevated` | Adds a shadow. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | #### Forms ##### `input` ```python input( value, on_input=None, *, placeholder='', icon=None, on_submit=None, on_clear=None, id=None, size='md', secure=False, width='fill', borderless=False, mono=False, disabled=False, readonly=False, invalid=False, max_length=None, align='start', theme=None, ) ``` A text field. Focus, blur and keys come from the common events: `.on(focus=..., blur=..., key_down=...)`. | Parameter | Description | | --- | --- | | `value` | Current text. | | `on_input` | Callable that receives the new text on every keystroke. | | `placeholder` | Hint text shown when empty. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `on_submit` | Message or callable sent when Enter is pressed. | | `on_clear` | Shows an ✕ while there is text; sent when it is clicked. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `size` | `"sm"`, `"md"` or `"lg"`. | | `secure` | Hides the text. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `borderless` | No border or background (as in the command palette). | | `mono` | Uses the monospace font (IBM Plex Mono). | | `disabled` | Greys it out and ignores typing. | | `readonly` | Selectable and copyable, but it can't be changed. | | `invalid` | Paints the border in the error color. | | `max_length` | Ignores edits longer than this. | | `align` | Text alignment: `"start"`, `"center"` or `"end"`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `search_input` ```python search_input(value, on_input=None, *, placeholder='Search…', **kwargs) ``` An `input` with a magnifying glass icon. | Parameter | Description | | --- | --- | | `value` | Current text. | | `on_input` | Callable that receives the new text on every keystroke. | | `placeholder` | Hint text shown when empty. | | `kwargs` | Same as `input`. | ##### `path_input` ```python path_input( value, on_change=None, *, folder=False, save=False, filters=None, kind=None, directory=None, relative_to=None, title=None, placeholder=None, size='md', mono=True, disabled=False, invalid=False, theme=None, ) ``` A text field for a file or folder path with a button that opens the system's file dialog, so the path never has to be typed (it still can be). | Parameter | Description | | --- | --- | | `value` | Current path. | | `on_change` | Callable that receives the typed or chosen path. | | `folder` | Picks a folder instead of a file. | | `save` | Shows the "Save as" dialog. | | `filters` | `(name, extensions)` pairs: `[("Images", ["png", "jpg"])]`. | | `kind` | Common filters by name: `"image"`, `"audio"`, `"video"`, `"font"`, `"shader"` or `"data"`. | | `directory` | Folder the dialog opens in (by default, the current path's). | | `relative_to` | A chosen path inside this folder is given relative to it. | | `title` | Title of the dialog. | | `placeholder` | Text shown while it is empty. | | `size` | `"sm"`, `"md"` or `"lg"`. | | `mono` | Monospaced text. | | `disabled` | Greys it out. | | `invalid` | Paints the border in the error color. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ```python ui.path_input(self.logo, self.set_logo, kind="image", relative_to=".") ``` ##### `switch` ```python switch( checked, on_toggle=None, *, label=None, size='md', disabled=False, id=None, theme=None, ) ``` An on/off switch. | Parameter | Description | | --- | --- | | `checked` | Current state. | | `on_toggle` | Callable that receives the new `bool` (or a message to send). | | `label` | Label text. | | `size` | `"sm"`, `"md"` or `"lg"`. | | `disabled` | Greys it out and ignores clicks. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `checkbox` ```python checkbox( checked, on_toggle=None, *, label=None, size='md', disabled=False, id=None, theme=None, ) ``` A checkbox. `checked=None` shows the mixed state (part of a group checked); clicking it checks it. | Parameter | Description | | --- | --- | | `checked` | Current state. | | `on_toggle` | Callable that receives the new `bool` (or a message to send). | | `label` | Label text. | | `size` | `"sm"`, `"md"` or `"lg"`. | | `disabled` | Greys it out and ignores clicks. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `slider` ```python slider( value, on_change=None, *, min=0.0, max=1.0, step=0.01, on_release=None, vertical=False, width=None, height=None, disabled=False, show_value=False, id=None, theme=None, ) ``` A slider. | Parameter | Description | | --- | --- | | `value` | Current value. | | `on_change` | Callable that receives the new value. | | `min` | Minimum. | | `max` | Maximum. | | `step` | Step. | | `on_release` | Sent when the drag ends (save or apply there). | | `vertical` | Stands it up. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `disabled` | Greys it out. | | `show_value` | Shows the number next to it: `True`, or a format such as `"{:.0%}"`. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `progress` ```python progress( value, *, max=1.0, tint='accent', height=4, width='fill', label=None, show_value=False, theme=None, ) ``` A thin progress bar. | Parameter | Description | | --- | --- | | `value` | Progress, or `None` for an indeterminate bar (keep a `pz.every(1/30, ...)` subscription while it shows). | | `max` | Maximum value. | | `tint` | Semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` or a token. | | `height` | Thickness. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `label` | Text above the bar. | | `show_value` | Adds the percentage at its right. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `segmented` ```python segmented( options, selected, on_select=None, *, size='md', width=None, disabled=False, id=None, theme=None, ) ``` A group of mutually exclusive buttons. | Parameter | Description | | --- | --- | | `options` | Strings, `(value, label)` or `(value, label, icon)`. | | `selected` | The active value. | | `on_select` | Callable that receives the chosen item. | | `size` | `"sm"`, `"md"` or `"lg"`. | | `width` | `"fill"` stretches the buttons evenly. | | `disabled` | `True`, or the values to disable. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `field` ```python field( title, control, *, description=None, error=None, required=False, vertical=False, theme=None, ) ``` A form row: title and description on the left, control on the right (like a settings screen). | Parameter | Description | | --- | --- | | `title` | Title. | | `control` | The control (switch, input…). | | `description` | Help text. | | `error` | A validation message shown under the title in the error color (see `pz.validation_errors`). | | `required` | Adds a red `*` after the title. | | `vertical` | Puts the control under the title. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | #### Dialogs ##### `dialog_layer` ```python dialog_layer(base, dialog=None, *, on_dismiss=None, theme=None) ``` Shows a dialog centered over `base` with a dimmed backdrop. With `dialog=None` it returns `base` unchanged, so it can always wrap the view. See [Dialogs](https://albakore.github.io/pyzed/guides/i18n/#dialogs). | Parameter | Description | | --- | --- | | `base` | The UI underneath. | | `dialog` | A dialog from the functions below, or `ui.modal`. | | `on_dismiss` | Sent when the backdrop is clicked. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ```python ui.dialog_layer(self.main_view(), ui.confirm("Delete 3 files?", self.delete, self.cancel, danger=True) if self.asking else None, on_dismiss=self.cancel) ``` ##### `alert` ```python alert(message, on_close, *, title=None, kind=None, ok_label=None, width=400, theme=None) ``` A message with one button. | Parameter | Description | | --- | --- | | `message` | Text or widget. | | `on_close` | The button and the close icon. | | `title` | Title. | | `kind` | `"info"`, `"success"`, `"warning"` or `"error"` adds an icon. | | `ok_label` | Button text (default: OK, translated). | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `confirm` ```python confirm( message, on_confirm, on_cancel, *, title=None, confirm_label=None, cancel_label=None, danger=False, kind=None, width=420, theme=None, ) ``` Asks a yes/no question. | Parameter | Description | | --- | --- | | `message` | Text or widget. | | `on_confirm` | The confirm button. | | `on_cancel` | Cancel, the close icon. | | `title` | Title. | | `confirm_label` | Confirm button text. | | `cancel_label` | Cancel button text. | | `danger` | Paints the confirm button red and labels it Delete. | | `kind` | Adds an icon, as in `alert`. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `prompt` ```python prompt( title, value, on_input, on_submit, on_cancel, *, message=None, placeholder='', submit_label=None, cancel_label=None, id=None, width=440, theme=None, ) ``` Asks for a line of text. | Parameter | Description | | --- | --- | | `title` | Title. | | `value` | Current text. | | `on_input` | Receives each edit. | | `on_submit` | Enter or the submit button. | | `on_cancel` | Cancel, the close icon. | | `message` | Text above the field. | | `placeholder` | Hint text shown when empty. | | `submit_label` | Submit button text. | | `cancel_label` | Cancel button text. | | `id` | Field id (for `Command.focus`). | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `choice` ```python choice(message, options, on_cancel, *, title=None, width=440, theme=None) ``` A message with your own buttons. | Parameter | Description | | --- | --- | | `message` | Any Python value, or a callable. | | `options` | `(label, on_press)` pairs; the last one is the main button. | | `on_cancel` | The close icon. | | `title` | Title. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `unsaved_changes` ```python unsaved_changes(name, on_save, on_discard, on_cancel, *, width=440, theme=None) ``` "Save your changes to *name* before closing?" with Don't save, Cancel and Save. | Parameter | Description | | --- | --- | | `name` | File or document name. | | `on_save` | Save. | | `on_discard` | Don't save. | | `on_cancel` | Cancel. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | #### Controls ##### `select` ```python select( options, selected=None, on_select=None, *, placeholder='Select…', width=None, size='md', disabled=False, invalid=False, on_open=None, on_close=None, id=None, theme=None, ) ``` A dropdown with the theme style. Options are displayed with `str()`, so strings, enums, dataclasses or database models all work; `on_select` receives the chosen object, not its label. | Parameter | Description | | --- | --- | | `options` | List of options. | | `selected` | Current option. | | `on_select` | Callable that receives the chosen item. | | `placeholder` | Hint text shown when empty. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `size` | `"sm"`, `"md"` or `"lg"`. | | `disabled` | Greys it out. | | `invalid` | Paints the border in the error color. | | `on_open` | Sent when the menu opens. | | `on_close` | Sent when the menu closes. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `radio_group` ```python radio_group( options, selected, on_select=None, *, horizontal=False, spacing=8, disabled=False, descriptions=None, id=None, theme=None, ) ``` Mutually exclusive options as radio buttons. | Parameter | Description | | --- | --- | | `options` | Strings or `(value, label)` pairs. | | `selected` | Current value. | | `on_select` | Callable that receives the chosen value. | | `horizontal` | In a row instead of a column. | | `spacing` | Space between children, in pixels. | | `disabled` | `True`, or the values to disable. | | `descriptions` | Maps values to a muted line under their label. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `number_field` ```python number_field( value, on_change=None, *, min=None, max=None, step=1, width=120, suffix=None, prefix=None, align='center', decimals=None, disabled=False, invalid=False, id=None, on_submit=None, steppers=True, drag=True, drag_step=None, theme=None, ) ``` A numeric field with −/+ buttons that you can also drag, like Figma's. `on_change(value)` receives the number already clamped to `min`/`max`; anything that can't be parsed as a number is ignored. Press and move the mouse sideways over the number to change it (it keeps going outside the window); double-click it to type a value, and Enter, Escape or a click outside to finish. Once clicked, ↑/↓ step it (Shift: ten steps) and Home/End jump to `min`/`max`. | Parameter | Description | | --- | --- | | `value` | Current value. | | `on_change` | Callable that receives the new value. | | `min` | Minimum. | | `max` | Maximum. | | `step` | Button step. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `suffix` | Unit shown at the end (`"px"`, `"%"`…). | | `prefix` | Text before the number (`"$"`…). | | `align` | Where the number sits between the buttons: `"center"` (default), `"start"` or `"end"`. | | `decimals` | Decimals shown (by default, as many as `step`). | | `disabled` | Greys it out. | | `invalid` | Paints the border in the error color. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `on_submit` | Sent on Enter. | | `steppers` | Shows the −/+ buttons (`False`: a compact field, as in a property grid). | | `drag` | Lets the mouse change it by dragging (`False`: a plain text field). | | `drag_step` | How much each pixel dragged adds (by default `step`, up to 1; steps under 1 take three pixels each). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `split_button` ```python split_button( content, on_press=None, on_menu=None, *, icon=None, variant='filled', tint=None, size='md', disabled=False, tooltip=None, theme=None, ) ``` A button with a separate arrow for more options. `on_press` runs the main action and `on_menu` usually opens a `ui.menu` in a `ui.popover`. | Parameter | Description | | --- | --- | | `content` | Button text. | | `on_press` | Message or callable sent on press. `None` disables the control. | | `on_menu` | Message or callable for the menu button. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `variant` | As in `ui.button`. | | `tint` | As in `ui.button`. | | `size` | `"xs"`, `"sm"`, `"md"` or `"lg"`. | | `disabled` | Disables both halves. | | `tooltip` | Tooltip of the main half. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `toggle_group` ```python toggle_group( options, selected, on_select=None, *, disabled=False, multiple=False, theme=None, ) ``` An icon-only segmented control. | Parameter | Description | | --- | --- | | `options` | `(value, icon_name)` or `(value, icon_name, tooltip)`. | | `selected` | Current value (a collection with `multiple`). | | `on_select` | Callable that receives the chosen item. | | `disabled` | `True`, or the values to disable. | | `multiple` | Each button toggles its value on or off. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `chip` ```python chip( content, *, on_press=None, on_remove=None, selected=False, icon=None, tint='accent', disabled=False, theme=None, ) ``` A compact tag, optionally clickable (filters) and with a button to remove it. | Parameter | Description | | --- | --- | | `content` | Text. | | `on_press` | Message or callable sent on press. `None` disables the control. | | `on_remove` | Shows a ✕ that sends this when clicked. | | `selected` | Marks it as selected. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `tint` | Color when selected (a theme token). | | `disabled` | Greys it out and ignores presses. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `disclosure` ```python disclosure(title, content, *, open, on_toggle=None, end=None, theme=None) ``` A collapsible section: a header with an arrow that shows or hides `content`. You own the open state: change it in `on_toggle`. | Parameter | Description | | --- | --- | | `title` | Title (text or widget). | | `content` | Content. | | `open` | Whether it is open. | | `on_toggle` | Message or callable when the header is clicked. | | `end` | Widgets at the end (right). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `AccordionSection` ```python class AccordionSection ``` One section of `ui.accordion`: `key`, `title`, `content` and optional `icon` and header `end` widget. | Parameter | Description | | --- | --- | | `key` | Key that identifies the subscription (defaults to the message or the function). | | `title` | Title. | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `end` | Widgets at the end (right). | ##### `accordion` ```python accordion( sections, *, open=(), on_toggle=None, icons=None, spacing=8, padding=(6, 12, 10, 12), theme=None, ) ``` Stacked collapsible sections separated by dividers, like the panels of a settings screen. Each header spans the whole width, so the content lines up with the rest of the panel. You own the open state: change it in `on_toggle`. | Parameter | Description | | --- | --- | | `sections` | `ui.AccordionSection(key, title, content, icon=None, end=None)` items (or `(key, title, content)` tuples). | | `open` | The set of open ids, a `{id: bool}` dict, or `True` for all. | | `on_toggle` | Called with the key of the clicked header. | | `icons` | `{id: icon}`: a Lucide icon for each header. | | `spacing` | Space around the dividers between sections. | | `padding` | Padding around the content of an open section. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ```python ui.accordion([ ui.AccordionSection("layout", "Layout", layout_rows, icon="ruler"), ui.AccordionSection("events", "Events", event_rows, icon="zap"), ], open=self.open_sections, on_toggle=lambda key: self.open_sections.symmetric_difference_update({key})) ``` #### Console, errors and help ##### `Log` ```python class Log ``` The entries of a `ui.console`: thread-safe, capped at `limit`, with repeated lines folded into one. `write(level, text)` adds a line; `capture()` (or `with log:`) copies `print` output and tracebacks into it; `pipe(stream)` reads a subprocess's output in a background thread; `text()` returns everything for copying (`text(ids)` only those lines). `write` also takes the line's `source`, `detail` (a traceback) and `origin` (`(path, line)`) and returns the entry; `find(id)` looks one up. | Parameter | Description | | --- | --- | | `limit` | Most entries kept. | | `on_write` | Called after every write (from any thread). | ```python log = ui.Log() with log: print("hello") # still reaches the terminal ui.console(log.entries, on_clear=log.clear) ``` **Methods and properties** ###### `Log.write` ```python Log.write(level, text, *, source='', detail='', origin=None) ``` Adds a line at `level` (from `source`, with `detail` and `origin`), calls `on_write` and returns the entry. ###### `Log.find` ```python Log.find(entry_id) ``` The entry numbered `entry_id`, or `None` when it is gone. ###### `Log.clear` ```python Log.clear() ``` Removes every entry. ###### `Log.counts` ```python Log.counts() ``` Returns how many lines there are per level, counting folded repeats. ###### `Log.text` ```python Log.text(ids=None) ``` Everything (or the entries numbered `ids`) as plain text, for copying. ###### `Log.capturing` ```python Log.capturing # property ``` Whether `sys.stdout`/`sys.stderr` are being copied into the log. ###### `Log.capture` ```python Log.capture() ``` Copies `sys.stdout`/`sys.stderr` into the log (they still reach the terminal). ###### `Log.release` ```python Log.release() ``` Stops capturing and puts the original streams back. ###### `Log.pipe` ```python Log.pipe(stream, level='stdout', on_end=None) ``` Reads a subprocess pipe (bytes or text) line by line into the log, in a background thread. ##### `LogEntry` ```python @dataclass class LogEntry(level, text, time=time.time(), count=1, source='', detail='', origin=None, id=0) ``` One line of a `ui.Log`: `level`, `text`, `time`, `count` (identical lines in a row), `source`, `detail`, `origin`, its number `id`, `clock` (`HH:MM:SS`) and `line()` (the entry as plain text). | Parameter | Description | | --- | --- | | `level` | Level name. | | `text` | The line. | | `time` | When it was written (`time.time()`). | | `count` | Identical lines in a row. | | `source` | What it comes from (`"project"`, `"agent"`...); `ui.console` can show and filter it. | | `detail` | More text shown under the line while it is selected (a traceback). | | `origin` | `(path, line)` of the code it is about. | | `id` | Its number in the log (what a `ui.LogSelection` keeps). | **Methods and properties** ###### `LogEntry.clock` ```python LogEntry.clock # property ``` The entry's local time as `HH:MM:SS`. ###### `LogEntry.line` ```python LogEntry.line(*, details=True) ``` The entry as one line of plain text (its time, level and source, then `detail` below it). ##### `LogSelection` ```python class LogSelection ``` The selected lines of a `ui.console`, like in a file list: a click selects one line, Shift+click a range from the last one clicked and Ctrl+click (Cmd on macOS) adds or removes one. Keep one in your app and pass it to `ui.console(selection=...)`; `select(ids)` selects exactly those (Ctrl+A), `clear()` none, `keep(entries)` forgets lines that are gone, and `ids` are the selected entries' `id`s for `log.text(ids)`. ```python self.selection = ui.LogSelection() ... ui.console(self.log.entries, selection=self.selection, on_context=self.open_menu) # Ctrl+C in on_key: return pz.Command.copy(self.log.text(self.selection.ids)) ``` **Methods and properties** ###### `LogSelection.click` ```python LogSelection.click(entry_id, shown, *, shift=False, ctrl=False) ``` Selects after a click on line `entry_id`; `shown` are the ids of the lines on screen, in order. ###### `LogSelection.select` ```python LogSelection.select(ids) ``` Selects exactly `ids` (all the lines shown, for Ctrl+A). ###### `LogSelection.clear` ```python LogSelection.clear() ``` Selects nothing. ###### `LogSelection.keep` ```python LogSelection.keep(entries) ``` Forgets selected lines that are no longer in `entries` (cleared or dropped). ##### `LOG_LEVELS` ```python LOG_LEVELS(...) ``` The levels `ui.console` colors, in the order of its filter buttons: debug, info, event, stdout, stderr, warning, error. ##### `console` ```python console( entries, *, query='', on_query=None, hidden=(), on_toggle_level=None, on_clear=None, on_copy=None, actions=(), selection=None, on_context=None, on_open=None, sources=(), source=None, on_source=None, show_source=False, placeholder=None, empty=None, font_size=12.5, id=None, theme=None, ) ``` A log view like a browser's dev tools console: a filter box, one toggle per level with its count, copy and clear, and the entries in a monospace list that keeps scrolling to the newest one. | Parameter | Description | | --- | --- | | `entries` | `ui.LogEntry` items (`log.entries`). | | `query` | Filter text. | | `on_query` | Receives each edit of the filter. | | `hidden` | Levels not shown. | | `on_toggle_level` | Called with the level of the clicked toggle. | | `on_clear` | Adds a Clear button. | | `on_copy` | Adds a Copy button. | | `actions` | Extra widgets at the end of the toolbar. | | `selection` | A `ui.LogSelection`: the lines can be selected with the mouse, and a selected line with `detail` shows it below. | | `on_context` | Called with the line right-clicked (`None` under the lines): open a menu there (copy, go to source, clear...). | | `on_open` | Called with the line double-clicked. | | `sources` | `(value, label)` pairs: adds a dropdown that shows the lines of one `source`. | | `source` | The source shown (`None` for all). | | `on_source` | Receives the source picked in the dropdown (`None` for all). | | `show_source` | Tags each line with its source. | | `placeholder` | Text of the empty filter box. | | `empty` | What shows while there are no lines. | | `font_size` | Size of the entries. | | `id` | Scrollable id. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `ErrorInfo` ```python @dataclass class ErrorInfo(kind, message, traceback='', origin=None, where='', time=0.0) ``` An error as `ui.error_card` and `ui.error_dialog` show it: `kind` (the exception's type), `message`, the whole `traceback`, the `origin` `(path, line)` it comes from (the last frame outside the standard library, installed packages and Pyzed), `where` it happened and `time`. `ErrorInfo.from_exception(err, where=...)` makes one (`prefer=` picks which frames may be the origin); `ErrorInfo.current()` describes the exception being handled. `summary` is `Type: message`, `location` is `file.py:12` and `report()` is everything as text for a bug report. | Parameter | Description | | --- | --- | | `kind` | The exception's type name. | | `message` | Its message. | | `traceback` | The whole traceback, as Python prints it. | | `origin` | `(path, line)` of the code it comes from, or `None`. | | `where` | What was running. | | `time` | When it happened (`time.time()`). | ```python try: save(data) except Exception as err: self.error = ui.ErrorInfo.from_exception(err, where="Save") ``` **Methods and properties** ###### `ErrorInfo.from_exception` ```python @classmethod ErrorInfo.from_exception(err, *, where='', prefer=None) ``` Describes `err`. `prefer(path)` picks the frames that may be its origin (the last of them wins); by default, those outside the standard library, installed packages and Pyzed itself. ###### `ErrorInfo.current` ```python @classmethod ErrorInfo.current(*, where='') ``` The exception being handled (inside an `except` block), or `None`. ###### `ErrorInfo.summary` ```python ErrorInfo.summary # property ``` `Type: message` (just the type when the message is empty). ###### `ErrorInfo.location` ```python ErrorInfo.location # property ``` `file.py:12` for the origin, or `""`. ###### `ErrorInfo.report` ```python ErrorInfo.report() ``` Everything as plain text, to copy into a bug report. ##### `error_boundary` ```python error_boundary( build, *, on_error=None, title=None, where='', on_details=None, fallback=None, theme=None, ) ``` Draws `build()`, or an error card when it raises, so one part of the view that fails never takes the whole window down (like React's error boundaries). `on_error(info)` is told about the error each time the part is drawn and fails, so report it once (compare `info.traceback`). | Parameter | Description | | --- | --- | | `build` | A function that returns the part's widget. | | `on_error` | Receives the `ui.ErrorInfo`. | | `title` | Heading of the card (and the error's `where`). | | `where` | What the part is, for the report. | | `on_details` | Adds a Details button; receives the `ui.ErrorInfo` (open a `ui.error_dialog`). | | `fallback` | Your own widget for the error: `fallback(info)`. | | `theme` | Theme override. | ```python sidebar = ui.error_boundary(lambda: self.sidebar(), on_error=self.report, title="Sidebar", on_details=self.show_error) ``` ##### `error_card` ```python error_card(info, *, title=None, on_details=None, actions=(), theme=None) ``` The card `ui.error_boundary` draws in place of what failed: the error, where it comes from and a Copy error button (and Details with `on_details`). | Parameter | Description | | --- | --- | | `info` | A `ui.ErrorInfo`. | | `title` | Heading. | | `on_details` | Adds a Details button. | | `actions` | More buttons after them. | | `theme` | Theme override. | ##### `error_dialog` ```python error_dialog( info, on_close, *, title=None, on_go_to=None, go_to_label=None, on_logs=None, more=0, width=680, theme=None, ) ``` A dialog with an error: its message, where it happened, the file and line it comes from and the traceback in a read-only editor (select and copy any part), with buttons to copy everything, go to the origin, see it among the logs and close. Show it with `ui.dialog_layer` or `ui.layer`. | Parameter | Description | | --- | --- | | `info` | A `ui.ErrorInfo`. | | `on_close` | Close button and Escape. | | `title` | Heading ("Something went wrong"). | | `on_go_to` | Adds the go-to button (open the file at the line). | | `go_to_label` | Its text ("Go to source"). | | `on_logs` | Adds a Show in Logs button. | | `more` | How many other errors came after it (a line says so). | | `width` | Width in pixels or a modal size. | | `theme` | Theme override. | ```python dialog = ui.error_dialog(self.error, self.close_error, on_go_to=self.open_origin) if self.error else None return ui.dialog_layer(self.main_view(), dialog, on_dismiss=self.close_error) ``` ##### `shortcuts` ```python shortcuts(groups, *, query='', on_query=None, height=380, theme=None) ``` A searchable list of key bindings, grouped (for a Keyboard shortcuts dialog). | Parameter | Description | | --- | --- | | `groups` | `[(title, [(label, keys), ...]), ...]`, keys like `"ctrl-s"`. | | `query` | Filter text. | | `on_query` | Adds a search box. | | `height` | Height of the list. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `about` ```python about( name, version='', description='', *, icon='app-window', details=(), links=(), theme=None, ) ``` An About box: icon, name, version, a description, a few details and link buttons. | Parameter | Description | | --- | --- | | `name` | App name. | | `version` | Version text. | | `description` | One or two sentences. | | `icon` | Lucide icon. | | `details` | `(label, value)` rows. | | `links` | `(label, on_press)` buttons. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | #### Data and feedback ##### `callout` ```python callout( title, description=None, *, tint='info', icon=None, actions=(), on_dismiss=None, theme=None, ) ``` An inline notice (a callout or banner). | Parameter | Description | | --- | --- | | `title` | Title (text or widget). | | `description` | Secondary text. | | `tint` | `"info"`, `"success"`, `"warning"`, `"error"` or `"hint"`: picks the color and icon. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `actions` | Extra widgets (usually buttons) shown as actions. | | `on_dismiss` | Adds a close button. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `empty_state` ```python empty_state( title, description=None, *, icon='inbox', actions=(), size='md', height='fill', theme=None, ) ``` A centered placeholder for empty lists, searches with no results, etc. | Parameter | Description | | --- | --- | | `title` | Title. | | `description` | Secondary text. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `actions` | Extra widgets (usually buttons) shown as actions. | | `size` | `"sm"`, `"md"` or `"lg"`. | | `height` | `"fill"` (default), pixels or `"shrink"`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `settings_group` ```python settings_group(title, rows, *, description=None, end=None, theme=None) ``` A titled block of `ui.field` rows separated by lines, like a settings screen. | Parameter | Description | | --- | --- | | `title` | Group title. | | `rows` | The rows (usually `ui.field`). | | `description` | Secondary text. | | `end` | Widget at the right of the title (a Reset button…). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `Column` ```python @dataclass class Column(title, key, width='fill', align='start', render=None, sortable=True) ``` A column of `ui.table`. `key` is a dict key, an attribute name or a function of the row; `render` (optional) turns the value into a widget. `width` is `"fill"`, a number or a portion such as `"fill:2"`. | Parameter | Description | | --- | --- | | `title` | Header (and name used for sorting). | | `key` | How to get the value from each row. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `align` | `"start"`, `"center"` or `"end"`. | | `render` | Function value → widget. | | `sortable` | Whether the header can be clicked to sort. | **Methods and properties** ###### `Column.value` ```python Column.value(row) ``` Returns this column's value for `row`: `key(row)`, a dict item or an attribute. ##### `table` ```python table( columns, rows, *, sort_by=None, descending=False, on_sort=None, on_row_press=None, selected=None, striped=False, row_height=30, height=None, on_row_double_press=None, on_row_context=None, empty=None, theme=None, ) ``` A data table with a fixed header, optional sorting and row selection. Sorting happens here when `sort_by` names a column: clicking a header calls `on_sort(title)` and you update `sort_by`/`descending` in your state. Rows are virtualized: only the ones in view are built, so hundreds of thousands of rows scroll smoothly. | Parameter | Description | | --- | --- | | `columns` | `ui.Column` or plain keys. | | `rows` | Dicts or objects. | | `sort_by` | Title of the column to sort by. | | `descending` | Descending order. | | `on_sort` | Callable that receives the clicked title. | | `on_row_press` | Callable that receives the clicked row. | | `selected` | The row to highlight, or a list or set of rows (multiple selection). | | `striped` | Shaded alternating rows. | | `row_height` | Height of each row. | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `on_row_double_press` | Called with the row on a double click (open, edit…). | | `on_row_context` | Called with the row on a right click (a context menu). | | `empty` | Text or widget shown in place of the rows when there are none. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `stat` ```python stat( title, value, *, delta=None, trend=None, width=None, icon=None, inverse=False, on_press=None, theme=None, ) ``` A dashboard number: title, large value, optional change (`+4.2%`) and sparkline. | Parameter | Description | | --- | --- | | `title` | Title. | | `value` | Current value. | | `delta` | Change in %; green if positive, red otherwise. | | `trend` | Values for the sparkline. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `icon` | Icon before the title. | | `inverse` | Colors a drop green and a rise red (errors, latency…). | | `on_press` | Makes the card clickable. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `sparkline` ```python sparkline(values, *, width=120, height=32, tint='accent', fill=True, theme=None) ``` A tiny line chart for trends. | Parameter | Description | | --- | --- | | `values` | Values. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `tint` | Semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` or a token. | | `fill` | Fills the area under the line. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `bar_chart` ```python bar_chart(values, *, labels=None, width=320, height=160, tint='accent', theme=None) ``` A vertical bar chart with optional labels. | Parameter | Description | | --- | --- | | `values` | Values. | | `labels` | Labels under each bar. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `tint` | Semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` or a token. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `donut` ```python donut(values, *, colors=None, size=96, thickness=14, label=None, theme=None) ``` A donut chart; `label` is drawn in the center. | Parameter | Description | | --- | --- | | `values` | Values. | | `colors` | Colors (defaults to the theme's). | | `size` | Size. | | `thickness` | Ring thickness. | | `label` | Center text. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `spinner` ```python spinner(*, size=16, tint='accent', speed=1.0, theme=None) ``` A spinning arc for loading states. Its angle comes from the clock, so keep a `pz.every(1/30, ...)` subscription active while it is shown. | Parameter | Description | | --- | --- | | `size` | Size. | | `tint` | Semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` or a token. | | `speed` | Turns per second. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `count_badge` ```python count_badge(count, *, tint='accent', max=99, show_zero=True, theme=None) ``` A round counter (notifications, problems…). Shows `99+` above `max`. | Parameter | Description | | --- | --- | | `count` | The number. | | `tint` | Semantic color: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` or a token. | | `max` | Maximum before showing `N+`. | | `show_zero` | With `False`, a count of 0 draws nothing. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `facepile` ```python facepile(people, *, size=22, overlap=6, max_shown=5, theme=None) ``` Overlapping collaborator avatars, as in the title bar of a collaborative editor. Those beyond `max_shown` are grouped into `+N`. | Parameter | Description | | --- | --- | | `people` | Initials or names. | | `size` | Size. | | `overlap` | Overlap in pixels. | | `max_shown` | How many to show. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `diff_stat` ```python diff_stat(added, removed, *, blocks=5, theme=None) ``` `+12 −3` with GitHub-style colored blocks. | Parameter | Description | | --- | --- | | `added` | Lines added. | | `removed` | Lines removed. | | `blocks` | Number of blocks. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `keybinding_hint` ```python keybinding_hint(text, keys, *, theme=None) ``` A muted hint like “Open file Ctrl P” for footers and empty states. | Parameter | Description | | --- | --- | | `text` | Text. | | `keys` | Shortcut shown at the end, written as a string: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `markdown` ```python markdown(source, *, size=None, on_link=None, width=None, selectable=None, theme=None) ``` `pz.markdown` with the theme's text, link and code colors. | Parameter | Description | | --- | --- | | `source` | The Markdown text. | | `size` | Size. | | `on_link` | Callable that receives the URL when a link is clicked. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `selectable` | Same as in `pz.markdown`; the selection uses the theme's `selection` color. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `split` ```python split( first, second, *, ratio=0.5, on_resize=None, vertical=False, min_size=80, theme=None, **kwargs, ) ``` `pz.split` with the divider in the theme's border color (accent while dragging). | Parameter | Description | | --- | --- | | `first` | First pane. | | `second` | Second pane. | | `ratio` | Proportion of the first pane (0–1). | | `on_resize` | Callable that receives the new ratio. | | `vertical` | Vertical orientation. | | `min_size` | Minimum size of each pane. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | | `kwargs` | Same as `pz.split`. | #### Lists, tree and menus ##### `list_item` ```python list_item( content, on_press=None, *, icon=None, icon_color=None, start=None, end=None, selected=False, focused=False, indent=0, height=26, color=None, inset=False, disabled=False, on_right_press=None, on_double_press=None, description=None, tooltip=None, theme=None, ) ``` A list row: icon, text and a slot at the end. | Parameter | Description | | --- | --- | | `content` | Text or widget. | | `on_press` | Message or callable sent on press. `None` disables the control. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `icon_color` | Icon color: a theme token or a hex color. | | `start` | Widgets at the start (left). | | `end` | Widgets at the end (right). | | `selected` | Marks it as selected. | | `focused` | Draws a focus border. | | `indent` | Left indentation, in pixels. | | `height` | Row height. | | `color` | Text color. | | `inset` | With margin and rounded corners (menus, palettes). | | `disabled` | Disables it (ignores clicks). | | `on_right_press` | Right click. | | `on_double_press` | Sent on a double click (open, rename…). | | `description` | A muted second line (the row grows to 40 px unless `height` is set). | | `tooltip` | Text shown on hover. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `list_header` ```python list_header(title, *, end=None, theme=None) ``` A section header for lists. | Parameter | Description | | --- | --- | | `title` | Title. | | `end` | Widgets at the end (right). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `TreeNode` ```python @dataclass class TreeNode(name, children=None, id=None, status=None, icon=None, icon_color=None) ``` A file or folder for `ui.tree`. Folders have `children` (a list). `status` is `"modified"`, `"created"`, `"deleted"`, `"conflict"` or `"ignored"`. `TreeNode.from_paths(paths, status=None)` builds the tree from paths like `"src/main.rs"`, with folders first and the git status propagated to folders. | Parameter | Description | | --- | --- | | `name` | Display name. | | `children` | Children (`None` for a file). | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `status` | Git status. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `icon_color` | Icon color: a theme token or a hex color. | **Methods and properties** ###### `TreeNode.is_folder` ```python TreeNode.is_folder # property ``` Whether the node is a folder (it has a `children` list, even if empty). ###### `TreeNode.from_paths` ```python @classmethod TreeNode.from_paths(paths, status=None) ``` Builds a tree from `"src/main.rs"`-style paths. Folders sort first. ###### `TreeNode.sort` ```python TreeNode.sort() ``` Sorts the children recursively: folders first, then by name ignoring case. ##### `tree` ```python tree( nodes, *, expanded, selected=None, on_toggle=None, on_select=None, on_open=None, on_context=None, focused=None, indent=14, item_height=24, theme=None, ) ``` A collapsible file tree, like a code editor's project panel. | Parameter | Description | | --- | --- | | `nodes` | List of `TreeNode`. | | `expanded` | Set of open folder ids. | | `selected` | One id or a collection of ids (multiple selection). | | `on_toggle` | Called with the id of the clicked folder. | | `on_select` | Called with the id of the clicked file. | | `on_open` | Called with the id of a file that is double-clicked. | | `on_context` | Called with the id of a row that is right-clicked (show a `ui.menu` there). | | `focused` | Outlines the row the keyboard is on. | | `indent` | Left indentation, in pixels. | | `item_height` | Height of each row. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `MenuItem` ```python @dataclass class MenuItem(label, on_press=None, icon=None, keys=None, disabled=False, danger=False, checked=None, description=None, submenu=False) ``` An entry of `ui.menu`. | Parameter | Description | | --- | --- | | `label` | Label text. | | `on_press` | Message or callable sent on press. `None` disables the control. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `keys` | Shortcut shown at the end, written as a string: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `disabled` | Disables it (ignores clicks). | | `danger` | In red (destructive actions). | | `checked` | Shows a check mark (`True`/`False`) or nothing (`None`). | | `description` | A muted second line. | | `submenu` | Ends the row with a chevron; open the next menu beside it in `on_press`. | ##### `menu` ```python menu(items, *, width=240, max_height=None, theme=None) ``` A floating context menu. Use `ui.SEPARATOR` between groups; a plain string is a header. | Parameter | Description | | --- | --- | | `items` | List of `MenuItem`, `ui.SEPARATOR` or strings. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `max_height` | Scrolls a menu taller than this. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `SEPARATOR` Separator for `ui.menu`. ##### `virtual_list` ```python virtual_list( items, render, *, item_height=26, spacing=0, width='fill', height='fill', padding=None, overscan=4, id=None, on_scroll=None, theme=None, ) ``` A scrolling list that only builds the rows in view, for thousands or millions of items. `render(item)` returns the widget for one item and is called when it scrolls into view. Every row has the same height. | Parameter | Description | | --- | --- | | `items` | The items: a list or any sequence (other iterables are turned into a list). | | `render` | Function that receives an item and returns its widget. | | `item_height` | Height of every row, in pixels. | | `spacing` | Space between children, in pixels. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `padding` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. | | `overscan` | Extra rows built above and below the visible ones. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `on_scroll` | Callable that receives the scroll viewport, as in `pz.scrollable`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | #### Navigation ##### `Tab` ```python @dataclass class Tab(id, label, icon=None, icon_color=None, dirty=False, preview=False, status=None, pinned=False, tooltip=None) ``` A tab. `Tab.for_file(path, **kwargs)` creates it with the file's name and icon. | Parameter | Description | | --- | --- | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `label` | Label text. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `icon_color` | Icon color: a theme token or a hex color. | | `dirty` | Unsaved changes (shows a dot). | | `preview` | Preview tab (in italics). | | `status` | Git status (tints the name). | | `pinned` | Shows only the icon, has no close button and goes first in `tab_bar`. | | `tooltip` | Text shown on hover (the full path…). | **Methods and properties** ###### `Tab.for_file` ```python @classmethod Tab.for_file(path, **kwargs) ``` A tab for a file `path`: its name as label and the matching file icon and color. ##### `tab` ```python tab(item, *, active, on_select=None, on_close=None, height=32, dimmed=False, theme=None) ``` A single tab. You normally use `tab_bar`. | Parameter | Description | | --- | --- | | `item` | The `Tab`. | | `active` | Whether it is the active one. | | `on_select` | Callable that receives the chosen item. | | `on_close` | Message or callable sent when the X is pressed. | | `height` | Height. | | `dimmed` | Mutes the label (the tab being dragged). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `tab_bar` ```python tab_bar( tabs, active, *, on_select=None, on_close=None, start=(), end=(), height=32, on_drag=None, on_drop=None, on_drag_over=None, dragging=None, drop_index=None, on_context=None, on_new=None, theme=None, ) ``` A row of tabs with optional controls at the start and end (navigation, split, +). The close button appears on the active tab and on hover. Tabs can be dragged to reorder them or out of the window: see [Draggable tabs](https://albakore.github.io/pyzed/guides/windows/#draggable-tabs-and-detachable-windows). | Parameter | Description | | --- | --- | | `tabs` | List of `Tab`. | | `active` | Id of the active tab. | | `on_select` | Callable that receives the chosen item. | | `on_close` | Message or callable sent when the X is pressed. | | `start` | Widgets at the start (left). | | `end` | Widgets at the end (right). | | `height` | Height. | | `on_drag` | Called with the tab id when a tab starts being dragged. | | `on_drop` | Called with the tab id and the `Event` when the dragged tab is released: `event.window_x`/`window_y` (outside the window if it was dropped outside) and `event.screen`. | | `on_drag_over` | While `dragging` is set: called with the insertion index under the pointer, or `None` when it leaves the bar. | | `dragging` | Id of the tab being dragged (dims it and turns on `on_drag_over`). | | `drop_index` | Draws the insertion mark before the tab at this index (`len(tabs)`: after the last one). | | `on_context` | Called with the id of a tab that is right-clicked. | | `on_new` | Sent on a double click of the empty space after the tabs. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `breadcrumbs` ```python breadcrumbs( parts, *, separator='›', on_press=None, max_items=None, on_more=None, theme=None, ) ``` `src › main.rs › fn main`. Parts can be strings or `(icon, text)` tuples. | Parameter | Description | | --- | --- | | `parts` | The parts. | | `separator` | Separator. | | `on_press` | Called with the index of the clicked part. | | `max_items` | Keeps the first part and the last ones and shows `…` for the rest. | | `on_more` | Makes the `…` clickable. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `toolbar` ```python toolbar(start=(), end=(), *, center=None, height=32, theme=None) ``` The bar below the tabs: breadcrumbs on the left and buffer actions on the right. | Parameter | Description | | --- | --- | | `start` | Widgets at the start (left). | | `end` | Widgets at the end (right). | | `center` | Kept in the middle of the bar whatever the widths of both sides. | | `height` | Height. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | #### Workspace ##### `workspace` ```python workspace( center, *, title_bar=None, left=None, right=None, bottom=None, status_bar=None, left_width=240, right_width=320, bottom_height=220, on_resize=None, theme=None, ) ``` The layout of an editor-style window: title bar, left, right and bottom docks, center and status bar. Pass `None` to hide a dock. | Parameter | Description | | --- | --- | | `center` | Central content (usually tabs + editor). | | `title_bar` | Title bar (`ui.title_bar`). | | `left` | Left dock. | | `right` | Right dock. | | `bottom` | Bottom dock. | | `status_bar` | Status bar (`ui.status_bar`). | | `left_width` | Width of the left dock. | | `right_width` | Width of the right dock. | | `bottom_height` | Height of the bottom dock. | | `on_resize` | Called with `(side, size)` when the user finishes resizing a dock by dragging its inner border. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `title_bar` ```python title_bar(start=(), end=(), *, center=None, height=34, theme=None) ``` The top bar: project and branch pickers on the left, account and actions on the right. | Parameter | Description | | --- | --- | | `start` | Widgets at the start (left). | | `end` | Widgets at the end (right). | | `center` | Centered widget. | | `height` | Height. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `title_button` ```python title_button( content, on_press=None, *, icon=None, muted=False, chevron=False, theme=None, ) ``` A compact button for the title bar (`pyzed ▾`, `main`…). | Parameter | Description | | --- | --- | | `content` | Text. | | `on_press` | Message or callable sent on press. `None` disables the control. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `muted` | Muted text. | | `chevron` | Adds a ▾. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `status_bar` ```python status_bar(start=(), end=(), *, height=28, theme=None) ``` The bottom status bar. | Parameter | Description | | --- | --- | | `start` | Widgets at the start (left). | | `end` | Widgets at the end (right). | | `height` | Height. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `status_item` ```python status_item( content=None, on_press=None, *, icon=None, color=None, icon_color=None, selected=False, tooltip=None, keys=None, theme=None, ) ``` An icon and/or text in the status bar (diagnostics, cursor position, language…). | Parameter | Description | | --- | --- | | `content` | Text. | | `on_press` | Message or callable sent on press. `None` disables the control. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `color` | Text or icon color. | | `icon_color` | Icon color: a theme token or a hex color. | | `selected` | Marks it as selected. | | `tooltip` | Tooltip text. | | `keys` | Shortcut shown at the end, written as a string: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `panel` ```python panel( content, *, header=None, width=None, height=None, side='left', resizable=True, on_resize=None, min_size=120, theme=None, ) ``` A dockable panel. `side` decides which edge gets the border line; with a numeric `width` (`height` for the bottom) that border can be dragged to resize it. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `header` | Header (`ui.panel_header`). | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `side` | `"left"`, `"right"` or `"bottom"`. | | `resizable` | `False` keeps the size fixed. | | `on_resize` | Called with the new size when a drag ends. | | `min_size` | Smallest size when resizing. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `panel_header` ```python panel_header(title, actions=(), *, icon=None, height=32, theme=None) ``` A panel header: icon, title and actions. | Parameter | Description | | --- | --- | | `title` | Title. | | `actions` | Extra widgets (usually buttons) shown as actions. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `height` | Height. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | #### Window frame ##### `window_frame` ```python window_frame( content, *, title=None, menus=None, menu_button=True, start=(), center=None, end=(), style=None, window=None, height=None, resize_border=6.0, resize_edges=True, bg=None, theme=None, ) ``` The whole frame of a window opened with `frame="custom"`: a title bar that moves the window (double click maximizes, right click opens the system menu on Windows), the minimize, maximize and close buttons in the style of a platform, menus on the left, and borders that resize the window. | Parameter | Description | | --- | --- | | `content` | The window's content, under the title bar. | | `title` | Title shown in the bar. By default the window's `title`. | | `menus` | Menus on the left of the bar: a list of `ui.Menu`. | | `menu_button` | Shows the menus behind a ☰ button (`False` always shows their names). | | `start` | Items after the menus (project, branch…). | | `center` | Your own widget in the middle instead of the title (a search field, Run and Stop…); the bar still drags on both sides of it. | | `end` | Items before the window buttons. | | `style` | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"` or `"kde"`. By default the window's `frame`, or this system's. | | `window` | The window the buttons act on. By default the one whose `view()` is running. | | `height` | Height of the bar. By default the platform's. | | `resize_border` | Width in pixels of the borders that resize the window. | | `resize_edges` | `False` leaves the resize borders to you (`pz.window_edges`), to put them over menus and dialogs. | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ```python class Notes(pz.App): frame = "custom" def view(self): return ui.window_frame(self.editor(), menus=[ui.Menu("File", [ui.MenuItem("Open", self.open)])], end=[ui.icon_button("settings", self.settings)]) ``` ##### `window_title_bar` ```python window_title_bar( title=None, *, style=None, start=(), center=None, end=(), on_drag=None, on_minimize=None, on_maximize=None, on_close=None, on_menu=None, maximized=False, focused=True, width='fill', height=None, bg=None, border=True, theme=None, ) ``` A window's title bar alone: window buttons in the style of `style` (on the left for macOS), `start` items, the title and `end` items. Without callbacks it only shows, which is how Pyzed Design previews a form in the frame of each platform. | Parameter | Description | | --- | --- | | `title` | Title (centered, or on the left on Windows). | | `style` | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"` or `"kde"` (`None`: this system's). | | `start` | Items at the start. | | `center` | Widget in the middle instead of the title; the bar still drags on both sides of it. | | `end` | Items at the end. | | `on_drag` | Called when the empty part of the bar is dragged (return `Command.drag_window()`). | | `on_minimize` | Minimize button. | | `on_maximize` | Maximize button and double click on the bar. | | `on_close` | Close button. | | `on_menu` | Right click on the bar. | | `maximized` | Shows the restore symbol. | | `focused` | `False` dims it like a window in the background. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `height` | Height. By default the platform's. | | `bg` | Background color (`"#rrggbb[aa]"`, `(r, g, b[, a])` or `"transparent"`). | | `border` | Line under the bar. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `window_controls` ```python window_controls( style=None, *, on_minimize=None, on_maximize=None, on_close=None, maximized=False, focused=True, height=None, theme=None, ) ``` The minimize, maximize and close buttons in the style of a platform. A button without a callback is drawn but does nothing. | Parameter | Description | | --- | --- | | `style` | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"` or `"kde"` (`None`: this system's). | | `on_minimize` | Minimize button. | | `on_maximize` | Maximize button. | | `on_close` | Close button. | | `maximized` | Shows the restore symbol. | | `focused` | `False` dims them. | | `height` | Height. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `Menu` ```python @dataclass class Menu(label, items=[]) ``` A menu of a window frame's menu bar: a name and its items (`ui.MenuItem`, `ui.SEPARATOR` or a header string). | Parameter | Description | | --- | --- | | `label` | The menu's name. | | `items` | Its entries: `ui.MenuItem`, `ui.SEPARATOR` or a header string. | ```python ui.Menu("File", [ui.MenuItem("Open…", self.open, keys="ctrl-o"), ui.SEPARATOR, ui.MenuItem("Quit", self.close)]) ``` ##### `system_platform` ```python system_platform() ``` The style of this system's window buttons: `"macos"`, `"windows"`, `"kde"`, `"ubuntu"` or `"gnome"` (other Linux desktops). Under WSL it is `"windows"`. #### Overlays ##### `layer` ```python layer( base, overlay=None, *, on_dismiss=None, backdrop=None, align_x='center', align_y='start', offset=72, ) ``` Stacks `overlay` on top of `base`. Clicking outside sends `on_dismiss`. With `overlay=None` it returns `base` unchanged. | Parameter | Description | | --- | --- | | `base` | The UI underneath. | | `overlay` | What floats on top (modal, palette, menu). | | `on_dismiss` | Message or callable sent on close. | | `backdrop` | Color of the backdrop that dims the UI. | | `align_x` | Horizontal alignment: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `align_y` | Vertical alignment: `"start"`/`"top"`, `"center"`, `"end"`/`"bottom"`. | | `offset` | Distance from the top when `align_y` is `"start"`. | ##### `modal` ```python modal( content, *, title=None, on_close=None, width=480, footer=(), description=None, icon=None, max_height=None, theme=None, ) ``` A dialog. Use it as the `overlay` of `ui.layer`. | Parameter | Description | | --- | --- | | `content` | Content: a `Widget`, a string or a number (converted to text). | | `title` | Title. | | `on_close` | The close button; also sent by Escape while the dialog has focus. | | `width` | Pixels or `"sm"` (360), `"md"` (480), `"lg"` (640) or `"xl"` (820). | | `footer` | Footer buttons, aligned to the right. | | `description` | Muted line under the title. | | `icon` | Icon before the title. | | `max_height` | Scrolls content taller than this, keeping the header and footer in view. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `PaletteItem` ```python @dataclass class PaletteItem(label, on_press=None, keys=None, icon=None, detail=None) ``` An entry of `command_palette` or `picker`. | Parameter | Description | | --- | --- | | `label` | Label text. | | `on_press` | Message or callable sent on press. `None` disables the control. | | `keys` | Shortcut shown at the end, written as a string: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `detail` | Secondary text (for example, a file path). | ##### `command_palette` ```python command_palette( query, on_query, commands, *, selected=0, on_submit=None, placeholder='Execute a command…', input_id='command-palette', theme=None, **kwargs, ) ``` A command palette: fuzzy-filters `commands` by `query` and shows their shortcuts. | Parameter | Description | | --- | --- | | `query` | Search text. | | `on_query` | Callable that receives the new text. | | `commands` | List of `PaletteItem`. | | `selected` | Highlighted index (for arrow-key navigation). | | `on_submit` | Message or callable sent when Enter is pressed. | | `placeholder` | Hint text shown when empty. | | `input_id` | Field id (for `Command.focus`). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | | `kwargs` | Same as `picker`. | ##### `picker` ```python picker( query, on_query, items, *, selected=0, placeholder='Search…', input_id='picker', on_submit=None, width=560, max_height=340, empty_text='No matches', theme=None, ) ``` A searchable list on a floating surface. The basis of the palette and the file finder. | Parameter | Description | | --- | --- | | `query` | Search text. | | `on_query` | Callable that receives the new text. | | `items` | `(PaletteItem, positions)` pairs, for example from `fuzzy_filter`. | | `selected` | Highlighted index. | | `placeholder` | Hint text shown when empty. | | `input_id` | Field id. | | `on_submit` | Message or callable sent when Enter is pressed. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `max_height` | Maximum height in pixels. | | `empty_text` | Text shown when there are no results. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `fuzzy_match` ```python fuzzy_match(query, candidate) ``` Returns `(score, positions)` if every letter of `query` appears in order in `candidate`; otherwise `None`. Rewards word starts and consecutive letters. | Parameter | Description | | --- | --- | | `query` | Search text. | | `candidate` | Candidate text. | ##### `fuzzy_filter` ```python fuzzy_filter(query, items, key=) ``` Filters and sorts `items` by fuzzy score. Returns `[(item, positions), ...]`. | Parameter | Description | | --- | --- | | `query` | Search text. | | `items` | Items. | | `key` | Function that returns the text of each item. | ##### `highlighted` ```python highlighted(text, positions, *, color, match_color, size=13.5, weight=None) ``` Text with the characters at `positions` highlighted (fuzzy search matches). | Parameter | Description | | --- | --- | | `text` | Text. | | `positions` | Indices to highlight. | | `color` | Text or icon color. | | `match_color` | Color of the matches. | | `size` | Size. | | `weight` | Text weight. | ##### `toast` ```python toast( title, message=None, *, kind='info', icon=None, on_dismiss=None, actions=(), width=340, progress=None, on_press=None, theme=None, ) ``` A notification. | Parameter | Description | | --- | --- | | `title` | Title. | | `message` | Secondary text. | | `kind` | `"info"`, `"success"`, `"warning"` or `"error"`. | | `icon` | Name of a Lucide icon (`pyzed icons` lists them). | | `on_dismiss` | Message or callable sent on close. | | `actions` | Extra widgets (usually buttons) shown as actions. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `progress` | 0 to 1: a thin bar at the bottom (time left, a download). | | `on_press` | Makes the card clickable. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `toasts` ```python toasts(base, items, *, spacing=8) ``` Shows `items` (usually `toast(...)`) stacked in the bottom-right corner over `base`. | Parameter | Description | | --- | --- | | `base` | The UI underneath. | | `items` | List of toasts. | | `spacing` | Space between children, in pixels. | ##### `popover` ```python popover(base, content=None, *, x=0, y=0, on_dismiss=None, align_x='start') ``` Shows `content` (a `ui.menu`, a card…) over `base` at `(x, y)`. Clicking anywhere else calls `on_dismiss`. For a right-click menu, store the mouse position with `mouse_area(on_move=...)` and open the popover from `on_right_press` (see `examples/context_menu.py`). | Parameter | Description | | --- | --- | | `base` | The UI underneath. | | `content` | What floats (or `None`). | | `x` | Horizontal position. | | `y` | Vertical position. | | `on_dismiss` | Message or callable sent on close. | | `align_x` | Horizontal alignment: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | #### Code ##### `editor` ```python editor( id, text='', *, language=None, path=None, on_change=None, on_cursor=None, markers=None, font_size=None, line_height=None, gutter=True, wrap=False, active_line=True, height='fill', placeholder=None, annotations=None, show_whitespace=False, tab_size=None, hard_tabs=False, backspace_unindent=True, repeat_delay=None, repeat_rate=None, capture_keys=(), on_scroll=None, auto_close=True, folding=True, minimap=False, cursor_shape='bar', cursor_blink=True, cursor_width=2.0, cursor_color=None, on_zoom=None, on_action=None, highlights=None, match_brackets=True, selection_highlight=True, font=None, keymap=None, scrollbar=True, readonly=False, on_hover=None, hover_delay=500, sticky_scroll=0, theme=None, ) ``` A code editor: highlighting with the theme colors, line numbers and git markers. | Parameter | Description | | --- | --- | | `id` | Identifies the buffer (use the file path). | | `text` | Initial text. | | `language` | Language. If missing, it is inferred from `path`. | | `path` | File path. | | `on_change` | Callable that receives the new value. | | `on_cursor` | Callable that receives `(line, column)`. | | `markers` | `{line: "created" | "modified" | "deleted"}`. | | `font_size` | Font size (defaults to the theme's). | | `line_height` | Line height. | | `gutter` | Shows line numbers. | | `wrap` | Line wrapping. | | `active_line` | Background color of the cursor line. | | `height` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `placeholder` | Hint text shown when empty. | | `annotations` | `{line: "text"}` or `{line: ("text", color)}`: virtual text at the end of the line (code lens, diagnostics). | | `show_whitespace` | Draws spaces as `·` and tabs as `→`. | | `tab_size` | Tab inserts spaces up to the next multiple; Enter keeps the indentation (one more level after `:`); Backspace removes a level. | | `hard_tabs` | With `tab_size`: Tab, Enter and indent (Ctrl+]) use tab characters instead of spaces. | | `backspace_unindent` | With `tab_size`: Backspace in leading spaces removes a whole level (`False`: one space). | | `repeat_delay` | Milliseconds Backspace and Delete wait, held down, before repeating (`None`: the system's). | | `repeat_rate` | Deletions per second at most while Backspace or Delete is held down (`None`: the system's pace). | | `capture_keys` | Keys the editor leaves to the app's `on_key` (`"up"`, `"enter"`…), e.g. while a completion list is open. | | `on_scroll` | Called on scroll. | | `auto_close` | Closes brackets and quotes as you type (and wraps the selection). | | `folding` | Fold chevrons in the gutter; Ctrl+Shift+[ folds and Ctrl+Shift+] unfolds. | | `minimap` | `True` (or a width in pixels) shows a minimap on the right. | | `cursor_shape` | `"bar"`, `"block"`, `"underline"` or `"hollow"`. | | `cursor_blink` | Whether the cursor blinks. | | `cursor_width` | Width of the bar cursor. | | `cursor_color` | Cursor color (the theme's player color by default). | | `on_zoom` | Callable that receives `1`, `-1` or `0` for Ctrl+= / Ctrl+- / Ctrl+0 and Ctrl+wheel. | | `on_action` | Callable that receives the name of each editor action that runs (`"undo"`, `"select_next"`…), e.g. to count them. | | `highlights` | Ranges painted behind the text in the theme's search color: `[(line, column, end_line, end_column)]`, from 1, with exclusive ends. | | `match_brackets` | Outlines the bracket that matches the one next to the cursor (Ctrl+M jumps to it). | | `selection_highlight` | Highlights the other occurrences of the selected text (Ctrl+D adds the next one as a cursor). | | `font` | Typeface: `"mono"` by default; `"sans"` makes a multi-line text box, like a chat's message box. | | `keymap` | Zed-style `{binding: action}` dict (`{"ctrl-shift-d": "duplicate_line_down"}`); `None` as the action removes a default key. Build it from per-action overrides with `pyzed.ui.editor.keymap_for`. | | `scrollbar` | `False` hides the scrollbars. | | `readonly` | `True` lets the text be selected and copied but not changed (a log, a traceback). | | `on_hover` | Callable that receives `{line, column, start, end, x, y, height}` (the word under the mouse, from 1, and its place in the editor) after the mouse rests on it, and `None` when it leaves (hover cards). | | `hover_delay` | Milliseconds the mouse rests on a word before `on_hover` is called. | | `sticky_scroll` | How many lines of the enclosing blocks (`class`, `def`, `if`…) stay pinned at the top while scrolling (`0`: none). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `language_token` ```python language_token(path) ``` Returns the highlighter language token for a path (`"main.rs"` → `"rs"`). | Parameter | Description | | --- | --- | | `path` | File path. | ##### `terminal` ```python terminal(lines, *, font_size=13, id=None, theme=None) ``` A read-only terminal view with ANSI colors. It stays scrolled to the end. | Parameter | Description | | --- | --- | | `lines` | Text with ANSI escapes, or a list of lines of `(text, color)` segments. | | `font_size` | Font size. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ##### `ansi` ```python ansi(text) ``` Converts text with ANSI colors into lines of `(text, color_name)` segments. | Parameter | Description | | --- | --- | | `text` | Text with ANSI escapes. | #### Color picker ##### `ColorPicker` ```python class ColorPicker ``` A component that picks a color with alpha, a linear gradient (stops you add, drag and remove, plus an angle) or a fill with an opacity mask. `on_change` gets a `"#rrggbb[aa]"` string, a `pz.Gradient` or a `pz.Paint`, all valid as a `background`. Pass the picked value back as `value`. See [Styles](https://albakore.github.io/pyzed/theming/styles/#color-picker). | Parameter | Description | | --- | --- | | `value` | A color, theme token, `pz.Gradient` or `pz.Paint`. | | `alpha` | Shows the alpha bar and field. | | `gradients` | Shows the Solid/Gradient switch. | | `masks` | Shows the mask editor. | | `swatches` | Quick colors (default: the theme's accent and status colors). | | `on_change` | Receives `event.value`. | ```python ui.ColorPicker(value=self.fill, on_change=lambda e: setattr(self, "fill", e.value)) ``` **Methods and properties** ###### `ColorPicker.parts` ```python ColorPicker.parts() ``` `(stops, angle, mask, opacity)`: one stop and no angle for a plain color. ###### `ColorPicker.build` ```python ColorPicker.build(stops, angle, mask, opacity) ``` Turns `(stops, angle, mask, opacity)` back into a color, `Gradient` or `Paint`. ###### `ColorPicker.publish` ```python ColorPicker.publish(*parts) ``` Sets the value from parts, drops any half-typed hex text and emits `change`. ###### `ColorPicker.current` ```python ColorPicker.current() ``` Returns the hex color being edited (the selected stop's color). ###### `ColorPicker.set_color` ```python ColorPicker.set_color(color) ``` Replaces the selected stop's color with `color` and publishes the result. ###### `ColorPicker.hsva` ```python ColorPicker.hsva() ``` Returns the edited color as `(h, s, v, a)`, each 0 to 1. ###### `ColorPicker.pick_sv` ```python ColorPicker.pick_sv(event) ``` Sets saturation and value from a press or drag on the saturation/value area. ###### `ColorPicker.pick_hue` ```python ColorPicker.pick_hue(event) ``` Sets the hue from a press or drag on the hue bar. ###### `ColorPicker.pick_alpha` ```python ColorPicker.pick_alpha(event) ``` Sets the alpha from a press or drag on the alpha bar. ###### `ColorPicker.type_hex` ```python ColorPicker.type_hex(text) ``` Handles typing in the hex field: applies the text once it is a valid hex color. ###### `ColorPicker.set_alpha_percent` ```python ColorPicker.set_alpha_percent(value) ``` Sets the alpha from the percent field (0 to 100). ###### `ColorPicker.set_mode` ```python ColorPicker.set_mode(mode) ``` Switches between `"Solid"` and `"Gradient"`. ###### `ColorPicker.set_angle` ```python ColorPicker.set_angle(value) ``` Sets the gradient's angle in degrees (or the mask's, on a plain color). ###### `ColorPicker.select_stop` ```python ColorPicker.select_stop(index) ``` Selects the gradient stop at `index` for editing. ###### `ColorPicker.move_stop` ```python ColorPicker.move_stop(index, event) ``` Moves a gradient stop to where the bar is pressed or by a drag's `dx`. ###### `ColorPicker.add_stop` ```python ColorPicker.add_stop(event) ``` Adds a gradient stop where the bar is clicked, with the color the gradient has there. ###### `ColorPicker.remove_stop` ```python ColorPicker.remove_stop() ``` Removes the selected gradient stop (a gradient keeps at least two). ###### `ColorPicker.toggle_mask` ```python ColorPicker.toggle_mask(on) ``` Adds a mask fading from opaque to transparent when `on`, or removes the mask. ###### `ColorPicker.select_mask_stop` ```python ColorPicker.select_mask_stop(index) ``` Selects the mask stop at `index` for editing. ###### `ColorPicker.move_mask_stop` ```python ColorPicker.move_mask_stop(index, event) ``` Moves a mask stop to where the bar is pressed or by a drag's `dx`, keeping stops sorted. ###### `ColorPicker.add_mask_stop` ```python ColorPicker.add_mask_stop(event) ``` Adds a mask stop where the bar is clicked, with the opacity the mask has there (at most 8). ###### `ColorPicker.set_mask_opacity` ```python ColorPicker.set_mask_opacity(value) ``` Sets the selected mask stop's opacity (0 to 1). ###### `ColorPicker.remove_mask_stop` ```python ColorPicker.remove_mask_stop() ``` Removes the selected mask stop (a mask keeps at least two). ###### `ColorPicker.marker` ```python ColorPicker.marker(x, y, color, *, size=14, selected=True, **events) ``` A round handle of `color` centered on `(x, y)`. ###### `ColorPicker.area` ```python ColorPicker.area(layers, height, markers, on_pick, *, width=248) ``` A rounded strip of stacked layers that reports where it is pressed and dragged. ###### `ColorPicker.sv_area` ```python ColorPicker.sv_area() ``` The saturation/value square for the current hue, with a marker on the current color. ###### `ColorPicker.hue_bar` ```python ColorPicker.hue_bar() ``` The rainbow hue bar with a marker on the current hue. ###### `ColorPicker.alpha_bar` ```python ColorPicker.alpha_bar() ``` The alpha bar (over a checkerboard) with a marker on the current alpha. ###### `ColorPicker.stops_bar` ```python ColorPicker.stops_bar(points, preview, selected, on_select, on_move, on_add, *, colors=True) ``` A bar previewing `preview` with a draggable handle per `(offset, value)` point. ##### `color_swatch` ```python color_swatch(value, *, size=20, on_press=None) ``` A small square showing a color, gradient or paint over a checkerboard. | Parameter | Description | | --- | --- | | `value` | What to show. | | `size` | Side in pixels. | | `on_press` | Makes it clickable. | ##### `checkerboard` ```python checkerboard(width, height, size=6) ``` A light/dark checkerboard to draw under translucent colors. | Parameter | Description | | --- | --- | | `width` | Width. | | `height` | Height. | | `size` | Cell size. | #### Dates and times ##### `Calendar` ```python class Calendar ``` A component with a month to pick a day on (`mode="single"`), a range (`"range"`: the first click starts it, the second ends it) or several days (`"multiple"`). The title opens a grid of months, then of years. Once clicked it takes the keyboard: arrows move, Enter picks, Page Up/Down change month. See the [Calendar](https://albakore.github.io/pyzed/library/calendar/) page. | Parameter | Description | | --- | --- | | `value` | A date (`datetime.date` or `"2026-10-22"`), a `(start, end)` pair or a list of dates, by `mode`. | | `mode` | `"single"`, `"range"` or `"multiple"`. | | `min` | The earliest day that can be picked. | | `max` | The latest day that can be picked. | | `disabled` | Days that cannot be picked: dates, weekday names (`"sunday"`) or `"weekends"`. | | `first_weekday` | `"monday"`, `"sunday"` or `"saturday"`. | | `months` | Months shown side by side. | | `week_numbers` | Shows the ISO week number of each row. | | `show_outside` | Shows the days of the months around. | | `today_button` | Adds a Today link. | | `on_change` | Receives `event.value` (a date, a tuple or a list). | | `on_month` | Receives `event.value`, the `(year, month)` shown. | ```python ui.Calendar(self.day, on_change=lambda e: setattr(self, "day", e.value)) ui.Calendar(self.stay, mode="range", months=2, on_change=lambda e: setattr(self, "stay", e.value)) ``` ##### `DatePicker` ```python class DatePicker ``` A component: a date field that opens a calendar below it, in a popup that panels never cut off. It can be typed in `format` (Enter or leaving the field reads it); once clicked, ↑/↓ move the date a day and Page Up/Down a month without opening anything, Alt+↓ opens the calendar and Escape closes it. See the [Date picker](https://albakore.github.io/pyzed/library/date-picker/) page. | Parameter | Description | | --- | --- | | `value` | The date (`datetime.date`, ISO text or `None`). | | `format` | How it is written: `"DD/MM/YYYY"`, `"MMM D, YYYY"`... (see `format_date`). | | `placeholder` | Hint while empty (default: the format). | | `min` | The earliest date. | | `max` | The latest date. | | `disabled_dates` | Days that cannot be picked (as in `Calendar`). | | `first_weekday` | `"monday"`, `"sunday"` or `"saturday"`. | | `week_numbers` | Shows week numbers in the calendar. | | `clearable` | Shows an x that empties it. | | `today_button` | Adds a Today link to the calendar. | | `disabled` | Greys it out. | | `invalid` | Paints the border in the error color. | | `on_change` | Receives `event.value`: a `datetime.date` or `None`. | | `on_open` | The calendar opened. | | `on_close` | The calendar closed. | ```python ui.DatePicker(self.due, format="DD/MM/YYYY", on_change=lambda e: setattr(self, "due", e.value)) ``` ##### `DateRangePicker` ```python class DateRangePicker ``` A component: a field for a start and an end date, with two months and quick ranges (Today, Last 7 days, This month...) in its popup. Typed text is two dates separated by `separator`. See the [Date range picker](https://albakore.github.io/pyzed/library/date-range-picker/) page. | Parameter | Description | | --- | --- | | `value` | `(start, end)`: dates, ISO text or `None`. | | `format` | How each date is written. | | `separator` | Text between the two dates. | | `placeholder` | Hint while empty. | | `min` | The earliest date. | | `max` | The latest date. | | `disabled_dates` | Days that cannot be picked. | | `first_weekday` | `"monday"`, `"sunday"` or `"saturday"`. | | `months` | Months shown side by side. | | `presets` | Shows the quick ranges. | | `clearable` | Shows an x that empties it. | | `disabled` | Greys it out. | | `invalid` | Paints the border in the error color. | | `on_change` | Receives `event.value`: `(start, end)`. | | `on_open` | The calendar opened. | | `on_close` | The calendar closed. | ```python ui.DateRangePicker(self.period, on_change=lambda e: setattr(self, "period", e.value)) ``` ##### `TimePicker` ```python class TimePicker ``` A component: a time field with columns of hours and minutes (and seconds, and AM/PM with a 12-hour `format`) in a popup. Once clicked, ↑/↓ change it by `minute_step`. See the [Time picker](https://albakore.github.io/pyzed/library/time-picker/) page. | Parameter | Description | | --- | --- | | `value` | The time (`datetime.time`, `"14:30"` or `None`). | | `format` | `"HH:mm"`, `"HH:mm:ss"`, `"h:mm A"`... | | `placeholder` | Hint while empty. | | `minute_step` | Minutes between the ones offered. | | `min` | The earliest time. | | `max` | The latest time. | | `clearable` | Shows an x that empties it. | | `disabled` | Greys it out. | | `invalid` | Paints the border in the error color. | | `on_change` | Receives `event.value`: a `datetime.time` or `None`. | | `on_open` | The list opened. | | `on_close` | The list closed. | ```python ui.TimePicker(self.alarm, format="h:mm A", minute_step=15, on_change=lambda e: setattr(self, "alarm", e.value)) ``` ##### `format_date` ```python format_date(value, fmt='YYYY-MM-DD', *, language=None) ``` Writes a date with a pattern of tokens: `YYYY` 2026, `YY` 26, `MMMM` October, `MMM` Oct, `MM` 10, `M` 10, `DD` 05, `D` 5, `dddd` Monday, `ddd` Mon, `HH`/`hh` hours, `mm` minutes, `ss` seconds, `A` AM/PM and `[text]` kept as is. A pattern with `%` is a `strftime` one. Month and day names follow the language of `pyzed.i18n`. | Parameter | Description | | --- | --- | | `value` | A date, a datetime, ISO text or `None` (gives `""`). | | `fmt` | The pattern. | | `language` | A language for the names instead of the current one. | ```python ui.format_date(day, "dddd, D MMMM YYYY") # "Monday, 5 October 2026" ``` ##### `parse_date` ```python parse_date(text, fmt='YYYY-MM-DD', *, language=None) ``` Reads a date written with a pattern of `format_date` (month names in the current language or English); ISO `YYYY-MM-DD` always works. Returns `None` when the text is not a date. | Parameter | Description | | --- | --- | | `text` | The text. | | `fmt` | The pattern. | | `language` | The language of the names. | ##### `format_time` ```python format_time(value, fmt='HH:mm') ``` Writes a time (or `"14:30"`) with a pattern such as `"h:mm A"`. | Parameter | Description | | --- | --- | | `value` | A `datetime.time` or text. | | `fmt` | The pattern. | ##### `parse_time` ```python parse_time(text, fmt='HH:mm') ``` Reads a time: `"14:30"`, `"2:30 PM"`, `"14:30:05"`, with `fmt` first. `None` when it is not one. | Parameter | Description | | --- | --- | | `text` | The text. | | `fmt` | The pattern. | #### System ##### `file_input` ```python file_input( value, on_change=None, *, mode='open', filters=None, title=None, directory=None, file_name='', placeholder=None, button=None, editable=True, disabled=False, invalid=False, id=None, width='fill', theme=None, ) ``` A path field with a button that opens the system's file dialog, so nobody types a path by hand. See the [File picker](https://albakore.github.io/pyzed/library/file-input/) page. | Parameter | Description | | --- | --- | | `value` | Current value. | | `on_change` | Receives the path (a list with `"open_many"`). | | `mode` | `"open"` (a file), `"open_many"` (a list), `"save"` or `"folder"`. | | `filters` | Files shown: `{"Images": "png jpg"}` or `"Images: png, jpg"`. | | `title` | The dialog's title. | | `directory` | Where the dialog opens (default: the folder of the current path). | | `file_name` | The name suggested when saving. | | `placeholder` | Hint text shown when empty. | | `button` | Text for the button instead of a folder icon. | | `editable` | `False` keeps the path from being typed. | | `disabled` | Disables it (ignores clicks). | | `invalid` | Paints the border in the error color. | | `id` | Stable identifier used to focus or scroll it with a `Command`. | | `width` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). | | `theme` | Theme to use instead of the current one (`pyzed.theme.current()`). | ```python ui.file_input(self.photo, self.set_photo, filters={"Images": "png jpg"}) ``` ##### `pick` ```python pick(then, *, mode='open', title=None, filters=None, directory=None, file_name='') ``` The system's file dialog as a `Command`: return it from a callback and `then` receives what the user chose (a path, a list of paths with `"open_many"`, or `None` when they cancel). `ui.file_input` uses it. | Parameter | Description | | --- | --- | | `then` | Called with the result. | | `mode` | `"open"`, `"open_many"`, `"save"` or `"folder"`. | | `title` | The dialog's title. | | `filters` | Files shown: `{"Images": "png jpg"}` or `"Images: png, jpg"`. | | `directory` | Where the dialog opens. | | `file_name` | The name suggested when saving. | ```python ui.button("Open…", lambda: ui.system.pick(self.load, filters="Text: txt, md")) ``` ##### `DropZone` ```python class DropZone ``` A component that takes files dragged from the system's file manager and dropped on the window, and opens the file dialog on a click. It lights up while files are dragged over the window and keeps the dropped paths in `files`. The window's `on_file_drop` hook hands each file to the zone under the pointer, else to one whose `accept` lists its type. See the [Drop zone](https://albakore.github.io/pyzed/library/drop-zone/) page. | Parameter | Description | | --- | --- | | `text` | The main text (default: Drop files here). | | `hint` | The line under it. | | `icon_name` | Its icon. | | `accept` | Extensions (`["png", "jpg"]`), patterns (`"*.tar.gz"`) or `"folder"`; empty takes everything. | | `multiple` | `False` keeps only the last file. | | `browse` | A click opens the file dialog. | | `show_files` | Lists the files under the text (each with an x). | | `files` | The dropped paths. | | `disabled` | Takes nothing. | | `live` | `False` draws it without taking files (the designer's canvas). | | `on_drop` | Receives `event.value`: one dropped path (once per file). | | `on_files` | Receives `event.value`: the list of files. | | `on_reject` | Receives a path it does not accept. | | `on_enter` | Files are dragged over the window. | | `on_leave` | The drag left without dropping. | ```python ui.DropZone(accept=["png", "jpg"], on_drop=lambda e: self.photos.append(e.value)) ``` #### Agent ##### `ToolCall` ```python class ToolCall ``` A component for one action of an AI agent, drawn like in Zed's Agent panel: a line with its icon, what it does and where (it unfolds to show the input and the output), or a card for edits and commands, with the diff or the output. While `status` is `"waiting"` it asks right on the card: Always allow, Allow or Reject. See [AI agents](https://albakore.github.io/pyzed/editor/ai-agents/). | Parameter | Description | | --- | --- | | `title` | What it does ("Edit file"). | | `detail` | What it works on, in a monospace font. | | `kind` | ACP's kind: `read`, `edit`, `delete`, `move`, `search`, `execute`, `think`, `fetch` or `other` (its icon). | | `status` | `pending`, `running`, `waiting`, `done`, `failed` or `rejected`. | | `input` | The arguments (a dict or text). | | `output` | The answer (text or a dict). | | `images` | PNG bytes shown under the output. | | `diffs` | The edits it made: `{"path", "old", "new"}` dicts, shown as colored diffs (see `ui.diff_view`). | | `progress` | What it printed so far (a command's output); its last lines show under the header while it runs. | | `allow_always` | Shows Always allow. | | `theme` | Theme. | | `on_allow` | Allow was pressed. | | `on_allow_always` | Always allow was pressed. | | `on_deny` | Reject was pressed. | ```python ui.ToolCall(title="Edit file", detail="ui/main.py", kind="edit", status=self.status, on_allow=lambda e: self.answer(True), on_deny=lambda e: self.answer(False)) ``` **Methods and properties** ###### `ToolCall.toggle` ```python ToolCall.toggle() ``` Unfolds or folds the input and output. ###### `ToolCall.card` ```python ToolCall.card # property ``` Whether it is drawn as a card: an edit, a command, or a call waiting for an OK. ##### `Plan` ```python class Plan ``` A component for an agent's plan: a checklist with `done/total` in its header, which folds it. | Parameter | Description | | --- | --- | | `steps` | `{"step": text, "status": "pending" | "in_progress" | "done"}` dicts. | | `running` | Spins the step in progress. | | `theme` | Theme. | ```python ui.Plan(steps=[{"step": "Add the form", "status": "done"}, {"step": "Wire it", "status": "in_progress"}]) ``` **Methods and properties** ###### `Plan.toggle` ```python Plan.toggle() ``` Folds or unfolds the steps. ##### `EditsBar` ```python class EditsBar ``` A component listing the files an agent changed, like Zed's edits bar: `3 files changed` with Reject all and Keep all, unfolding to one row per file with its diff stat, Reject and Keep. | Parameter | Description | | --- | --- | | `files` | Dicts or objects with `path` and optional `added` and `removed` line counts. | | `theme` | Theme. | | `on_keep` | Keep all. | | `on_reject` | Reject all. | | `on_keep_file` | Keep on a file (`event.value` is its path). | | `on_reject_file` | Reject on a file. | | `on_open` | A file was clicked. | ```python ui.EditsBar(files=[{"path": "ui/main.py", "added": 12, "removed": 3}], on_keep=lambda e: self.keep()) ``` **Methods and properties** ###### `EditsBar.toggle` ```python EditsBar.toggle() ``` Shows or hides the row of each file. ##### `ThreadHistory` ```python class ThreadHistory ``` A component for past conversations: a search box and the threads grouped by day (Today, Yesterday, This week, This month, Older), each with a delete button. | Parameter | Description | | --- | --- | | `threads` | Dicts or objects with `id`, `title`, `updated` (a timestamp) and an optional `detail`. | | `selected` | The id of the open thread. | | `theme` | Theme. | | `on_open` | A thread was picked (`event.value` is its id). | | `on_delete` | Delete was pressed on a thread. | ```python ui.ThreadHistory(threads=self.threads, on_open=lambda e: self.open(e.value)) ``` **Methods and properties** ###### `ThreadHistory.set_query` ```python ThreadHistory.set_query(text) ``` Filters the threads by `text`. ###### `ThreadHistory.visible` ```python ThreadHistory.visible() ``` The threads whose title or detail has every word of the search. ##### `AuthPrompt` ```python class AuthPrompt ``` A component that asks to sign in, like Zed when an agent needs it: one button per way to sign in, then, while one runs, what is happening (the browser or a terminal is open), a code to type when there is one, Open the page again and Cancel. | Parameter | Description | | --- | --- | | `title` | The heading. | | `detail` | A line under it. | | `methods` | Dicts with `id`, `name`, optional `description` and `icon`; the first is the main one. | | `waiting` | The id of the method in progress (`""` when none). | | `waiting_text` | What to show while waiting. | | `code` | A code to type (device sign-in). | | `url` | The page that was opened. | | `error` | An error to show. | | `icon_name` | The icon. | | `theme` | Theme. | | `on_pick` | A method was picked (`event.value` is its id). | | `on_cancel` | Cancel. | | `on_open_url` | Open the page again. | | `on_copy` | The code was copied. | ```python ui.AuthPrompt(title="Sign in to Gemini", methods=[{"id": "google", "name": "Log in with Google"}], on_pick=self.sign_in) ``` ##### `ServiceRow` ```python class ServiceRow ``` A component for one thing an agent uses (an MCP server, an external agent, a skill), like the rows of Zed's agent settings: a status dot, the name and command, an on/off switch, a main button and a `⋯` menu. | Parameter | Description | | --- | --- | | `name` | Name. | | `detail` | The command or URL, in a monospace font. | | `summary` | A line about it. | | `icon_name` | The icon. | | `status` | `running`, `starting`, `error`, `off`, `installed`, `missing` or `update` (the dot's color). | | `status_text` | The dot's tooltip; shown under the name for errors and updates. | | `version` | A version or a source. | | `enabled` | The switch (`None` hides it). | | `busy` | Shows a spinner. | | `primary` | Text of the main button (Update, Install…). | | `actions` | `ui.MenuItem` entries of the `⋯` menu. | | `theme` | Theme. | | `on_toggle` | The switch was flipped. | | `on_primary` | The main button was pressed. | ```python ui.ServiceRow(name="github", detail="npx -y @modelcontextprotocol/server-github", status="running", enabled=True) ``` **Methods and properties** ###### `ServiceRow.toggle_menu` ```python ServiceRow.toggle_menu() ``` Opens or closes the `⋯` menu. ##### `Catalog` ```python class Catalog ``` A component for things to install with a search box (agents from a registry, servers, skills…): each row shows Install, Installed or Update to a newer version. | Parameter | Description | | --- | --- | | `items` | Dicts or objects with `id`, `name` and optional `version`, `description`, `installed` and `update`. | | `busy` | The id being installed (a spinner). | | `height` | Height of the list. | | `theme` | Theme. | | `on_install` | Install or Update was pressed (`event.value` is the id). | ```python ui.Catalog(items=self.registry, busy=self.installing, on_install=lambda e: self.install(e.value)) ``` **Methods and properties** ###### `Catalog.set_query` ```python Catalog.set_query(text) ``` Filters the items by `text`. ###### `Catalog.visible` ```python Catalog.visible() ``` The items whose id, name or description has every word of the search. ##### `context_meter` ```python context_meter(used, limit, *, theme=None) ``` How full a model's context window is, like Zed's token counter: a small ring and `48k / 200k`, yellow past 80% and red past 95%. | Parameter | Description | | --- | --- | | `used` | Tokens in use. | | `limit` | The model's window. | | `theme` | Theme. | ##### `format_tokens` ```python format_tokens(count) ``` A short token count: `950`, `4.5k`, `48k`, `1.25M`. | Parameter | Description | | --- | --- | | `count` | Tokens. | ##### `diff_view` ```python diff_view(diff, *, theme=None, max_lines=120) ``` An edit as Zed shows it in a thread: the file's name, then the changed lines (green added, red removed) with a few lines around them. | Parameter | Description | | --- | --- | | `diff` | A dict with `path`, `old` and `new` (the texts before and after). | | `theme` | Theme. | | `max_lines` | Lines shown before `… N more lines`. | ```python ui.diff_view({"path": "main.py", "old": "x = 1\n", "new": "x = 2\n"}) ``` #### Icon picker ##### `IconPicker` ```python class IconPicker ``` A component that picks a Lucide icon: a search box (names and tags), a category drop-down and a grid that only builds the rows in view. Enter picks the first match. `on_change` gets the icon's name (`""` when cleared); pass it back as `value`. See the [Icon picker](https://albakore.github.io/pyzed/library/icon-picker/) page. | Parameter | Description | | --- | --- | | `value` | The picked icon's name. | | `columns` | Icons per row. | | `height` | Height of the grid. | | `clearable` | Shows a button that clears the icon. | | `autofocus` | Focuses the search box when it appears. | | `on_change` | Receives `event.value`. | ```python ui.IconPicker(value=self.icon, on_change=lambda e: setattr(self, "icon", e.value)) ``` **Methods and properties** ###### `IconPicker.set_query` ```python IconPicker.set_query(text) ``` Filters the grid by `text` (names and tags). ###### `IconPicker.set_group` ```python IconPicker.set_group(key) ``` Shows only the icons of category `key` (`""` for every icon). ###### `IconPicker.matches` ```python IconPicker.matches() ``` The icons shown: those of the chosen category that match the search, best first. ###### `IconPicker.pick` ```python IconPicker.pick(name) ``` Selects `name` and emits `change`. ##### `icon_picker_dialog` ```python icon_picker_dialog( value, on_pick, *, on_close=None, title=None, columns=10, clearable=True, key='icon-picker', theme=None, ) ``` A dialog with an `IconPicker` that picks on a click; use it as the dialog of `ui.dialog_layer`. The search box gets the focus. | Parameter | Description | | --- | --- | | `value` | The current icon. | | `on_pick` | Called with the clicked icon's name (`""` when cleared). | | `on_close` | The close button. | | `title` | Default: "Choose an icon", translated. | | `columns` | Icons per row. | | `clearable` | Shows the "No icon" button. | | `key` | Component key, for several pickers in one view. | | `theme` | Theme. | ```python ui.dialog_layer(page, ui.icon_picker_dialog(self.icon, self.pick, on_close=self.close) if self.picking else None, on_dismiss=self.close) ``` ##### `icon_grid` ```python icon_grid( names, on_pick, *, selected=None, columns=8, cell=34, height=288, id=None, theme=None, ) ``` The bare grid of icons, virtualized: only the rows in view are built, so thousands of icons cost the same as a few. | Parameter | Description | | --- | --- | | `names` | Icon names, in order. | | `on_pick` | Called with the clicked name. | | `selected` | The highlighted name. | | `columns` | Icons per row. | | `cell` | Side of each cell in pixels. | | `height` | Height of the grid. | | `id` | Id of the scrollable, for `Command.scroll_to`. | | `theme` | Theme. | #### Console and help ##### `reference_browser` ```python reference_browser( browser, *, on_open_url=None, sidebar_width=270, width='fill', height='fill', theme=None, ) ``` The built-in reference, like Godot's: a searchable class list next to the page that is open. `browser` is a `pyzed.reference.Browser` (keep it in your app's state); clicking entries, links and the back/forward buttons changes it. `on_open_url(url)` gets the links that leave the reference (the website, GitHub...): ```python self.help = reference.Browser("pyzed.ui.button") ... ui.reference_browser(self.help, on_open_url=open_in_browser) ``` Events: - `on_open_url(url)`: Called with the URL of a link that leaves the reference (open it in the browser). ### Themes > API reference for pyzed Theme: the built-in themes, color tokens, deriving custom themes and color utilities for Python GUIs. Source: https://albakore.github.io/pyzed/reference/theme/ See the [theming](https://albakore.github.io/pyzed/theming/themes/) guide for the list of tokens and how to create your own. #### Theme ##### `Theme` A theme: an immutable dataclass with color tokens (surfaces, borders, elements, text, status, editor, syntax, collaborator and ANSI colors) and metrics (`radius`, `radius_sm`, `ui_font_size`, `buffer_font_size`). The `is_dark` property tells whether it is dark. **Methods and properties** ###### `Theme.is_dark` ```python Theme.is_dark # property ``` Whether the theme's appearance is dark. ##### `derive` ```python derive(self, **changes) ``` Returns a copy with some tokens changed. The `syntax` and `ansi` dicts are merged instead of replaced. | Parameter | Description | | --- | --- | | `changes` | Tokens to change. | ```python ocean = pz.themes.ONE_DARK.derive(name="Ocean", accent="#4fb3ff", syntax={"keyword": "#ff79c6"}) ``` ##### `color` ```python color(self, name) ``` Resolves a token (`"accent"`, `"text_muted"`…) to its color. Hex colors and `"transparent"` pass through unchanged. | Parameter | Description | | --- | --- | | `name` | Token or color. | ##### `tokens` ```python tokens(self) ``` All tokens as a dict. ##### `syntax_scopes` ```python syntax_scopes(self) ``` Maps the theme's syntax tokens to TextMate scopes for the highlighter. ##### `palette` ```python palette(self) ``` The base palette passed to Iced for its internal styles. Internal use. ##### `with_accent` ```python with_accent(self, accent) ``` A copy with a different accent color and everything that depends on it (focus, selection, links, primary buttons, text on accent). | Parameter | Description | | --- | --- | | `accent` | The color. | ##### `generate` ```python generate( name, *, background, text, accent, appearance=None, error=None, warning=None, success=None, syntax=None, radius=6.0, ) ``` Creates a complete theme from a few colors. Surfaces, borders, hover states and muted text are mixed from `background` and `text`; the appearance is inferred from the background brightness if not given. | Parameter | Description | | --- | --- | | `name` | Name. | | `background` | Editor/background color. | | `text` | Text color. | | `accent` | Accent color. | | `appearance` | `"dark"` or `"light"`. | | `error` | Error color. | | `warning` | Warning color. | | `success` | Success color. | | `syntax` | Extra syntax colors. | | `radius` | Corner radius. | ```python pz.Theme.generate("Custom", background="#1b1d23", text="#e6e6e6", accent="#ff8a65") ``` ##### `from_zed` ```python from_zed(source, name=None) ``` Reads a theme from a Zed theme family file (`.json`) or a dict. A family usually has several (Ayu Dark/Light/Mirage); `name` picks one (the first by default). | Parameter | Description | | --- | --- | | `source` | Path, JSON text or dict. | | `name` | Name of the theme within the family. | ##### `to_dict` ```python to_dict(self) ``` A JSON-ready dict; `Theme.from_dict` reads it back. ##### `from_dict` ```python from_dict(data, *, base=None) ``` Creates a theme from `to_dict`. Missing tokens come from `base` (One Dark or One Light depending on `appearance`). | Parameter | Description | | --- | --- | | `data` | The tokens. | | `base` | Base theme. | ##### `save` ```python save(self, path) ``` Saves the theme as JSON (Pyzed's own format). | Parameter | Description | | --- | --- | | `path` | File path. | ##### `load` ```python load(path) ``` Reads a theme written with `save` (for Zed themes use `from_zed`). | Parameter | Description | | --- | --- | | `path` | File path. | #### Built-in themes ##### `themes` Namespace with the built-in themes (the One, Ayu and Gruvbox families) and the theme registry. Built-in themes are also available as attributes: `themes.ONE_DARK`, `themes.ONE_LIGHT`, `themes.AYU_DARK`, `themes.AYU_LIGHT`, `themes.AYU_MIRAGE`, `themes.GRUVBOX_DARK`, `themes.GRUVBOX_LIGHT`… ##### `all` ```python all() ``` List of all built-in themes. ##### `names` ```python names() ``` Names of all registered themes. ##### `get` ```python get(name) ``` Looks up a theme by name (case-insensitive). Raises `KeyError` if it doesn't exist. | Parameter | Description | | --- | --- | | `name` | Name. | ##### `register` ```python register(theme) ``` Adds (or replaces) a theme so it can be used by name. | Parameter | Description | | --- | --- | | `theme` | The theme. | ##### `load_zed` ```python load_zed(source) ``` Loads and registers every theme in a Zed family file. | Parameter | Description | | --- | --- | | `source` | Path, JSON text or dict. | ##### `auto` ```python auto(light='One Light', dark='One Dark') ``` A theme that follows the system's light/dark mode. | Parameter | Description | | --- | --- | | `light` | Theme (or name) for light mode. | | `dark` | Theme (or name) for dark mode. | ##### `AutoTheme` What `themes.auto` returns: a `(light, dark)` pair. `pick(dark)` returns the right one. **Methods and properties** ###### `AutoTheme.pick` ```python AutoTheme.pick(dark) ``` Returns the dark theme when `dark` is true, else the light one. #### Utilities (pyzed.theme) ##### `current` ```python current() ``` The theme components use when no `theme=` is passed. ##### `use` ```python use(theme) ``` Changes the current theme. `App` does this before every `view()`, so you normally don't need it. | Parameter | Description | | --- | --- | | `theme` | The theme. | ##### `alpha` ```python alpha(color, value) ``` Returns `color` with its opacity multiplied by `value`. | Parameter | Description | | --- | --- | | `color` | Hex color. | | `value` | Factor from 0 to 1. | ```python pz.theme.alpha("#74ade8", 0.2) # "#74ade833" ``` ##### `mix` ```python mix(a, b, amount=0.5) ``` Mixes two colors: `amount=0` is `a` and `amount=1` is `b`. | Parameter | Description | | --- | --- | | `a` | First color. | | `b` | Second color. | | `amount` | Proportion of `b`. | ##### `resolve` ```python resolve(value, *, dark=True) ``` Turns an `App.theme` value (theme, name, `AutoTheme` or `None`) into a `Theme`. | Parameter | Description | | --- | --- | | `value` | The value. | | `dark` | Whether the system is in dark mode. | ##### `lighten` ```python lighten(color, amount=0.1) ``` Lightens a color. | Parameter | Description | | --- | --- | | `color` | Hex color. | | `amount` | Amount from 0 to 1. | ##### `darken` ```python darken(color, amount=0.1) ``` Darkens a color. | Parameter | Description | | --- | --- | | `color` | Hex color. | | `amount` | Amount from 0 to 1. | ##### `luminance` ```python luminance(color) ``` Relative luminance (WCAG) of a color, from 0 to 1. | Parameter | Description | | --- | --- | | `color` | Hex color. | ##### `contrast` ```python contrast(a, b) ``` WCAG contrast ratio between two colors (1 to 21). | Parameter | Description | | --- | --- | | `a` | First color. | | `b` | Second color. | ##### `readable_on` ```python readable_on(background, light='#fafafa', dark='#1c2025') ``` Returns `light` or `dark`, whichever has more contrast on `background`. | Parameter | Description | | --- | --- | | `background` | Background color. | | `light` | Light text. | | `dark` | Dark text. | ### Drawing (pyzed.draw) > API reference for pyzed.draw: shapes, paths, text and transforms for drawing custom 2D graphics on a pz.canvas in Python desktop apps. Source: https://albakore.github.io/pyzed/reference/draw/ `pyzed.draw` shapes are plain dicts you pass to [`pz.canvas`](https://albakore.github.io/pyzed/reference/widgets/#canvas). Angles are in degrees and `y` grows downwards. See the [drawing with canvas](https://albakore.github.io/pyzed/guides/canvas/) guide. ```python from pyzed import draw pz.canvas([ draw.background("#1e2127"), draw.circle(100, 100, 40, fill="#74ade8"), draw.text(100, 180, "Hello", color="#dce0e5", align_x="center"), ], width=200, height=200) ``` #### Shapes ##### `background` ```python background(color) ``` Fills the whole canvas. | Parameter | Description | | --- | --- | | `color` | Color. | ##### `rect` ```python rect(x, y, w, h, *, fill=None, stroke=None, width=1.0, radius=0.0, dash=None) ``` A rectangle, filled and/or stroked. | Parameter | Description | | --- | --- | | `x` | Left. | | `y` | Top. | | `w` | Width. | | `h` | Height. | | `fill` | Fill color. | | `stroke` | Stroke color. | | `width` | Stroke width. | | `radius` | Corner radius. | | `dash` | Dash pattern, e.g. `[6, 4]`. | ##### `rects` ```python rects(items, *, fill) ``` Many rectangles filled with one color in a single shape (fast for grids and bars). | Parameter | Description | | --- | --- | | `items` | `(x, y, w, h)` tuples. | | `fill` | Color. | ##### `circle` ```python circle(x, y, r, *, fill=None, stroke=None, width=1.0, dash=None) ``` A circle centered at `(x, y)`. | Parameter | Description | | --- | --- | | `x` | Center x. | | `y` | Center y. | | `r` | Radius. | | `fill` | Fill color. | | `stroke` | Stroke color. | | `width` | Stroke width. | | `dash` | Dash pattern, e.g. `[6, 4]`. | ##### `line` ```python line(x1, y1, x2, y2, *, stroke='#ffffff', width=1.0, dash=None, cap=None) ``` A straight line. | Parameter | Description | | --- | --- | | `x1` | Start x. | | `y1` | Start y. | | `x2` | End x. | | `y2` | End y. | | `stroke` | Stroke color. | | `width` | Stroke width. | | `dash` | Dash pattern, e.g. `[6, 4]`. | | `cap` | `"round"`, `"square"` or `"butt"`: shape of the line ends. | ##### `polyline` ```python polyline( points, *, stroke='#ffffff', width=1.0, closed=False, fill=None, dash=None, cap='round', ) ``` Joins `points` with straight lines (`closed=True` closes the shape). | Parameter | Description | | --- | --- | | `points` | List of `(x, y)`. | | `stroke` | Stroke color. | | `width` | Stroke width. | | `closed` | Closes the shape. | | `fill` | Fill color. | | `dash` | Dash pattern, e.g. `[6, 4]`. | | `cap` | `"round"`, `"square"` or `"butt"`: shape of the line ends. | ##### `polygon` ```python polygon(points, *, fill=None, stroke=None, width=1.0) ``` A closed shape through `points`. | Parameter | Description | | --- | --- | | `points` | List of `(x, y)`. | | `fill` | Fill color. | | `stroke` | Stroke color. | | `width` | Stroke width. | ##### `arc` ```python arc(x, y, r, start, end, *, stroke=None, fill=None, width=1.0, pie=False, cap=None) ``` A circular arc from `start` to `end` degrees (0 = 3 o'clock, clockwise). `pie=True` closes it through the center (pie slice). | Parameter | Description | | --- | --- | | `x` | Center x. | | `y` | Center y. | | `r` | Radius. | | `start` | Start angle. | | `end` | End angle. | | `stroke` | Stroke color. | | `fill` | Fill color. | | `width` | Stroke width. | | `pie` | Closes through the center. | | `cap` | `"round"`, `"square"` or `"butt"`: shape of the line ends. | ##### `path` ```python path(ops, *, fill=None, stroke=None, width=1.0, dash=None, cap=None) ``` A free-form path with SVG-like operations: `("M", x, y)` move, `("L", x, y)` line, `("Q", cx, cy, x, y)` quadratic curve, `("C", c1x, c1y, c2x, c2y, x, y)` cubic curve, `("A", cx, cy, r, start, end)` arc, `("Z",)` close. | Parameter | Description | | --- | --- | | `ops` | List of operations. | | `fill` | Fill color. | | `stroke` | Stroke color. | | `width` | Stroke width. | | `dash` | Dash pattern, e.g. `[6, 4]`. | | `cap` | `"round"`, `"square"` or `"butt"`: shape of the line ends. | ##### `text` ```python text( x, y, content, *, color='#ffffff', size=14, font='sans', weight=None, align_x='start', align_y='top', ) ``` Text anchored at `(x, y)`; `align_x`/`align_y` choose which side of the text sits on that point. | Parameter | Description | | --- | --- | | `x` | X position. | | `y` | Y position. | | `content` | The text. | | `color` | Color. | | `size` | Size. | | `font` | Family. | | `weight` | Weight. | | `align_x` | `"start"`, `"center"` or `"end"`. | | `align_y` | `"top"`, `"center"` or `"bottom"`. | ##### `image` ```python image(source, x, y, w, h, *, rotation=0.0, opacity=1.0, pixelated=False, size=None) ``` A bitmap (sprite) drawn in the box `(x, y, w, h)`. Decoded once per source and cached, so drawing it every frame is cheap. See [Making games](https://albakore.github.io/pyzed/games/). | Parameter | Description | | --- | --- | | `source` | A path, encoded image bytes (PNG, JPEG…) or raw RGBA bytes with `size`. | | `x` | Left. | | `y` | Top. | | `w` | Width. | | `h` | Height. | | `rotation` | Degrees, around its center. | | `opacity` | From 0 to 1. | | `pixelated` | Nearest-neighbor scaling, for pixel art. | | `size` | `(width, height)` of raw RGBA bytes. | #### Transforms ##### `translate` ```python translate(x, y) ``` Offsets the shapes that come after it. | Parameter | Description | | --- | --- | | `x` | Horizontal. | | `y` | Vertical. | ##### `rotate` ```python rotate(degrees) ``` Rotates the shapes that come after it around the current origin. | Parameter | Description | | --- | --- | | `degrees` | Degrees. | ##### `scale` ```python scale(x, y=None) ``` Scales the shapes that come after it. | Parameter | Description | | --- | --- | | `x` | Horizontal factor. | | `y` | Vertical factor (defaults to `x`). | ##### `save` ```python save() ``` Saves the current transform (use it with `restore`). ##### `restore` ```python restore() ``` Goes back to the transform saved by the last `save`. ##### `group` ```python group(shapes, *, x=0.0, y=0.0, rotation=0.0, scale_by=None) ``` Draws `shapes` translated, rotated and scaled without affecting the following shapes. Returns a list: expand it with `*`. | Parameter | Description | | --- | --- | | `shapes` | The shapes. | | `x` | X offset. | | `y` | Y offset. | | `rotation` | Degrees. | | `scale_by` | Scale. | ### Icons > API reference for pyzed.icons: the bundled Lucide icon set, icon names and file-type icon utilities for Python desktop interfaces. Source: https://albakore.github.io/pyzed/reference/icons/ Pyzed bundles the [Lucide](https://lucide.dev) font (ISC license) with about 2000 icons. Search names with `pyzed icons ` or on the Lucide website. #### pyzed.icons ##### `icon` ```python icon(name, *, size=16, color=None) ``` An icon widget. Also available as `pyzed.icon`. | Parameter | Description | | --- | --- | | `name` | Icon name. | | `size` | Size. | | `color` | Color (defaults to the text color). | ##### `glyph` ```python glyph(name) ``` Returns an icon's character, to use in any text with `font="icons"`. Raises `ValueError` if it doesn't exist. | Parameter | Description | | --- | --- | | `name` | Icon name. | ##### `names` ```python names() ``` Sorted list of all icon names. ##### `search` ```python search(query, among=None) ``` Icons matching a search by name and tag, best first: exact names, then names that start with the query, names with every word and icons found by their tags (`search("person")` finds `user`). | Parameter | Description | | --- | --- | | `query` | Words that must all appear in the name or the tags. | | `among` | Only search these names (default: every icon). | ```python icons.search("arrow up")[:3] # ["arrow-up", "arrow-up-01", "arrow-up-10"] ``` ##### `tags` ```python tags(name) ``` The search tags of an icon, from Lucide (`tags("user")` is `["person", "account", "contact"]`). | Parameter | Description | | --- | --- | | `name` | Icon name. | ##### `category` ```python category(key, among=None) ``` The icons of a category of `CATEGORIES`, sorted. An icon can be in several. | Parameter | Description | | --- | --- | | `key` | A key of `CATEGORIES` (`"arrows"`, `"files"`, `"people"`...). | | `among` | Only look at these names. | ##### `CATEGORIES` Groups for browsing icons (arrows, text, files, layout, media, communication, people, devices, development, shapes, weather, commerce, maps, status), each with the words of names and tags that put an icon in it. ##### `for_file` ```python for_file(name) ``` Returns `(icon, color_or_None)` for a file name, as in the project panel of a code editor. | Parameter | Description | | --- | --- | | `name` | File name. | ##### `language_for` ```python language_for(name) ``` Human-readable language name of a file (`"main.py"` → `"Python"`), handy for the status bar. | Parameter | Description | | --- | --- | | `name` | File name. | ### CLI > Every pyzed command-line command: create projects, manage dependencies, run and build apps, generate icons and start the MCP server. Source: https://albakore.github.io/pyzed/reference/cli/ Installing Pyzed adds the `pyzed` command (also available as `python -m pyzed`). Commands that work on a project look for the nearest `pyproject.toml` from the current folder up. See [Projects with uv](https://albakore.github.io/pyzed/guides/projects/). #### Projects ##### `pyzed new ` Creates a uv project in a new folder: `uv init --package`, the `[tool.pyzed]` table, `ui/main.pzform` with its handlers, `pyzed-ui` as a dependency, `ruff` and `ty` as dev dependencies, and a git repository. | Option | Effect | | --- | --- | | `--app` | Backend next to `pyproject.toml` instead of in `src/` (`uv init --app`). | | `--python VERSION` | Python version of the project. | | `--no-git` | No git repository. | | `--no-sync` | Only write `pyproject.toml` (no resolving or installing). | | `--script` | A single `main.py` with a workspace, sidebar and command palette, plus `requirements.txt`; no uv. | ##### `pyzed init [path]` Adds Pyzed to the uv project at `path` (default: the current folder), with the same options as `new` except `--script`. If there is no `pyproject.toml`, it runs `uv init` first. Existing code is not modified. #### Dependencies | Command | Does | | --- | --- | | `pyzed add PACKAGE...` | `uv add`. A constraint (`"httpx==0.27.2"`) changes the version of a package already there. `--dev`, `--group NAME`, `--editable` (local paths). | | `pyzed remove PACKAGE...` | `uv remove`, in the group the package is in. | | `pyzed upgrade [PACKAGE...]` | Upgrades the given packages (every one when none is given) within their constraints and syncs. | | `pyzed sync` | `uv sync`. | | `pyzed deps [--outdated]` | Lists direct dependencies, their group and locked version (`--outdated`: only those with a newer release). | | `pyzed check [--fix]` | `ruff check` and `ty check`; exits with 1 if either fails. | #### Running | Command | Does | | --- | --- | | `pyzed design [form or folder]` | Opens the [visual designer](https://albakore.github.io/pyzed/editor/). Inside a project and without a form, opens its main form. | | `pyzed run [form] [--reload]` | Runs a form with its handlers; default: the project's main form. `--reload` updates the window on save. | | `pyzed dev script.py [--watch PATH]` | Runs a script with hot reload: saving a file updates the open window. | #### `pyzed build` Packages the project for the current platform with PyInstaller or Nuitka. See [How building works](https://albakore.github.io/pyzed/packaging/build/) and, in Pyzed Design, [Exporting your app](https://albakore.github.io/pyzed/editor/exporting/). | Option | Effect | | --- | --- | | `--backend pyinstaller\|nuitka` | The packager (default: `backend` in `pyproject.toml`, else PyInstaller). | | `--onefile` | A single executable instead of a folder. | | `--console` | Keep the console window. | | `--name NAME` | Name of the executable. | | `--archive zip\|tar.gz` | Also pack the app, named for the self-updater (`shop-windows-x86_64.zip`). | | `--target TARGET` | Fail unless this computer builds `TARGET` (`windows-x64`, `macos-arm64`, `linux-x64`...); CI uses it. | | `--dry-run` | Print the backend's command. | | `--workflow` | Write `.github/workflows/build.yml`, which builds every target of `targets` on GitHub Actions. | #### `pyzed icons [filter]` Lists the names of the bundled Lucide icons that contain `filter`. ```bash pyzed icons arrow ``` #### `pyzed mcp` Runs the [MCP server for AI agents](https://albakore.github.io/pyzed/guides/mcp/) over stdio, until the client disconnects. It needs the `mcp` extra: `pip install "pyzed-ui[mcp]"`. | Option | Effect | | --- | --- | | `--transport streamable-http` | Serve over HTTP instead of stdio. | | `--allow-code` | Offer `screenshot` and the project tools over HTTP too (they are always on over stdio). | | `--root PATH` | The folder the project tools work in (by default the current one). | ### Visual designer (pyzed.designer) > API reference for pyzed.designer: run, open and build forms from Python code, and register toolbox components and templates. Source: https://albakore.github.io/pyzed/reference/designer/ `pyzed.designer` is the code side of the [visual designer](https://albakore.github.io/pyzed/editor/): run `.pzform` files, open forms as windows, build forms in code, and extend the toolbox with your own components and templates. [Designed components](https://albakore.github.io/pyzed/editor/component-designer/) (`.pzcomp`) are loaded from here too. ```python from pyzed import designer designer.run("ui/main.pzform", reload=True) ``` #### Running forms ##### `run` ```python run(path, *, reload=None, splash=None) ``` Runs the form at `path` with all the logic wired in the designer. `reload=True` turns on hot reload: saving the form (from the designer or by hand) or its handlers module updates the running window without restarting it. `None` follows `PYZED_RELOAD`, like `pz.run`. `splash` (a `pz.Splash`) is shown before the form's window opens (see `App.splash`). ##### `load` ```python load(path) ``` Reads a `.pzform` file. ##### `open_form` ```python open_form(path) ``` Loads a `.pzform` and its handlers into a `FormApp` ready to `run()` or `screenshot()`. If the form belongs to a project (see `pyzed.project`), the project is activated first so its handlers can import the backend and the packages of its `.venv`. ##### `open_window` ```python open_window(form, *, data=None, handlers=None, folder=None) ``` A `Command` that opens `form` in a new window (see `form_window` for the arguments). ##### `form_window` ```python form_window(form, *, data=None, handlers=None, folder=None) ``` A `FormWindow` for `form` (a `.pzform` path or name, or a `Form`), ready to open. A form loaded from a file gets its handlers module; a `Form` built in code uses `handlers` (a dict of functions). `data` reaches the new form as `form.data`. ##### `new_form` ```python new_form( path, *, title=None, template='blank', width=None, height=None, overwrite=False, ) ``` Creates `path` (`.pzform` added if missing) and its handlers module from `template`. The form is named after the file (`order-details.pzform` -> `OrderDetails`). Returns the form's path. Raises `FileExistsError` if it exists, unless `overwrite`. ##### `save` ```python save(form, path) ``` Writes `form` to `path` (`.pzform` is added when there is no suffix). ##### `open_document` ```python open_document(path) ``` Opens a `.pzform` as a `Document`, a `.pzcomp` as a `SceneDocument` or a `.pzshad` as a `ShaderDocument`. ##### `design` ```python design(path=None, *, theme=None) ``` Opens the visual designer, optionally on a `.pzform` file (created on first save). #### Forms and controls ##### `Form` ```python @dataclass class Form(name='Form1', title='Form1', width=640.0, height=420.0, theme='One Dark', accent=None, background=None, radius=None, font_size=14.0, resizable=True, grid=8, style={}, controls=[], events={}, code=None, animations=[], autoplay='') ``` A window designed visually: its settings, its controls (in z-order) and its events. `theme` is any registered theme name; `accent`, `radius` and `font_size` tweak it without writing a theme file. `style` is the form's global style (`pz.Style.to_dict()`), applied to the whole window. `grid` is the designer's snapping step. **Methods and properties** ###### `Form.get` ```python Form.get(name) ``` Returns the control called `name`, or `None` if there is none. ###### `Form.names` ```python Form.names() ``` Returns the names of all controls, in z-order. ###### `Form.children` ```python Form.children(name) ``` The controls directly inside `name` (the form itself when `None`), in z-order. ###### `Form.descendants` ```python Form.descendants(name) ``` Every control inside `name`, at any depth (parents before their children). ###### `Form.ancestors` ```python Form.ancestors(name) ``` The containers holding `name`, nearest first. ###### `Form.walk` ```python Form.walk(parent=None, depth=0) ``` `(control, depth)` in drawing order: each control, then what it contains. ###### `Form.is_inside` ```python Form.is_inside(name, container) ``` Whether control `name` is inside `container`, at any depth. ###### `Form.unique_name` ```python Form.unique_name(base) ``` The first free `base1`, `base2`... (Visual Basic style). ###### `Form.add` ```python Form.add(control) ``` Appends `control` (on top of the z-order) and returns it. ###### `Form.remove` ```python Form.remove(names) ``` Removes controls and everything inside them. ###### `Form.rename` ```python Form.rename(old, new) ``` Renames a control and returns the name actually used (made unique and valid). ###### `Form.reorder` ```python Form.reorder(names, where) ``` Moves controls to the `"front"` or `"back"` of the z-order. ###### `Form.to_dict` ```python Form.to_dict() ``` Returns the form as JSON-ready data: `{"format", "form": settings, "controls": [...]}`. ###### `Form.from_dict` ```python @classmethod Form.from_dict(data) ``` Builds a form from saved `data` (see `to_dict`), ignoring unknown settings and repairing broken parents; raises `ValueError` if it is not a Pyzed form. ###### `Form.to_json` ```python Form.to_json() ``` Returns the form as indented JSON text (the contents of a `.pzform` file). ###### `Form.from_json` ```python @classmethod Form.from_json(text) ``` Builds a form from the JSON `text` of a `.pzform` file. ###### `Form.copy` ```python Form.copy() ``` Returns a deep copy of the form and its controls. ##### `Control` ```python @dataclass class Control(name, type, x=0.0, y=0.0, width=100.0, height=28.0, props={}, events={}, anchor=('left', 'top'), visible=True, enabled=True, locked=False, parent=None, layout={}, style={}, animations=[], bind='', rules={}, hidden_in_editor=False) ``` One component placed on a form. `x`, `y`, `width` and `height` are in logical pixels relative to the form's client area, or to the top-left corner of `parent` when the control sits inside a container (a panel, a group box, a flex row...). `props` holds the component's own properties (`text`, `checked`...) and `events` maps event names (`"click"`) to handler function names. `layout` holds options read by the parent's layout, such as `grow` in a flex row or `span` in a grid. `style` holds CSS-like style properties (`pz.Style.to_dict()`: background, border, radius, font...) drawn on top of the props. `animations` lists the control's animations (see `designer.motion`). `bind` names the field of `form.data` the control shows (and edits, for inputs), and `rules` what its value must be (see `designer.binding`). `hidden_in_editor` hides it on the designer's canvas only (the outline's eye with the "editor" setting); the running form ignores it. **Methods and properties** ###### `Control.min_size` ```python Control.min_size # property ``` `(min_width, min_height)` from `layout` (`0` means no limit). ###### `Control.max_size` ```python Control.max_size # property ``` `(max_width, max_height)` from `layout` (`0` means no limit). ###### `Control.pivot` ```python Control.pivot # property ``` The point rotation and scale turn around, and that stays put when a size limit stops the control from stretching: `(0, 0)` is the top-left corner, `(0.5, 0.5)` (the default) the center. ###### `Control.rect` ```python Control.rect # property ``` The control's `(x, y, width, height)`, relative to its parent (or the form). ###### `Control.to_dict` ```python Control.to_dict() ``` Returns the control as JSON-ready data; empty `parent`, `layout` and `style` are left out. ###### `Control.from_dict` ```python @classmethod Control.from_dict(data) ``` Builds a control from saved `data`, ignoring unknown keys and invalid anchors. ###### `Control.copy` ```python Control.copy() ``` Returns a deep copy of the control. ##### `Event` ```python @dataclass class Event(sender, name, value=None) ``` What a handler receives besides the form: who raised it, which event and its value. ##### `FormApp` ```python class FormApp ``` A form as the main application window. `path` (set by `open_form`) is the `.pzform` file; with hot reload on, saving it or its handlers module updates the running window. **Methods and properties** ###### `FormApp.on_start` ```python FormApp.on_start() ``` Watches the form and its handlers module for hot reload (when it is on), then raises `load`. ##### `FormWindow` ```python class FormWindow ``` A form in an extra window (`form.open(...)`, `designer.open_window` and the designer's preview). **Methods and properties** ###### `FormWindow.on_open` ```python FormWindow.on_open() ``` Raises the form's `load` event when the window opens. ##### `Document` ```python class Document ``` A form open in the designer. `code` is the text of the handlers module, kept in memory and written next to the form on `save`. `selection` holds control names, primary first. **Methods and properties** ###### `Document.clipboard` ```python Document.clipboard # property ``` The copied controls (as `Control.to_dict()` data), shared by every document. ###### `Document.open` ```python @classmethod Document.open(path) ``` Loads the form at `path` and the text of its handlers module (if the file exists) into a new document. ###### `Document.code_path` ```python Document.code_path # property ``` Where the handlers module is saved, or `None` while the form has no file. ###### `Document.display_name` ```python Document.display_name # property ``` The name shown for the document: its file name, or `.pzform` if unsaved. ###### `Document.save` ```python Document.save(path=None) ``` Writes the form and its handlers module; returns the form path. ###### `Document.commit` ```python Document.commit(group=None) ``` Records an undo step after an edit (`group` merges repeated edits of one field). ###### `Document.undo` ```python Document.undo() ``` Goes back one undo step; returns `False` if there was nothing to undo. ###### `Document.redo` ```python Document.redo() ``` Redoes one undone step; returns `False` if there was nothing to redo. ###### `Document.selected` ```python Document.selected # property ``` The selected controls, primary first (names that no longer exist are skipped). ###### `Document.primary` ```python Document.primary # property ``` The primary (first) selected control, or `None` when nothing is selected. ###### `Document.select` ```python Document.select(names, *, add=False) ``` Selects the controls `names` (one name, several, or `None` for none); unknown names are ignored. ###### `Document.select_all` ```python Document.select_all() ``` Selects every control of the form. ###### `Document.is_selected` ```python Document.is_selected(name) ``` Whether the control `name` is selected. ###### `Document.add` ```python Document.add(type_, x, y, width=None, height=None, *, snap=True, parent=None) ``` Places a new `type_` control (snapped to the grid) and selects it. ###### `Document.rects` ```python Document.rects() ``` The absolute rectangle of every control (see `layout.resolve`). ###### `Document.can_contain` ```python Document.can_contain(container, name) ``` Whether control `name` can be moved into `container` (`None` is the form, which takes anything): it must be a container other than the control itself and not inside it. ###### `Document.reparent` ```python Document.reparent(names, parent, *, index=None, at=None, commit=True, rects=None) ``` Moves controls into `parent` (or onto the form with `None`), keeping them where they are. ###### `Document.wrap` ```python Document.wrap(type_, names=None, props=None) ``` Puts controls (the selection by default) inside a new container of `type_` around them, with `props` set on it (an invisible panel for a group). ###### `Document.ungroup` ```python Document.ungroup(names=None) ``` Takes the children out of the selected containers (`names`), where they are on screen, and removes the emptied containers; the children become the selection. Returns the removed containers' names. ###### `Document.move_out` ```python Document.move_out(name) ``` Moves a control out of its container, into the container's own parent. ###### `Document.set_layout` ```python Document.set_layout(name, key, value) ``` Sets an option the parent's layout reads (`grow`, `span`). ###### `Document.delete` ```python Document.delete() ``` Removes the selected controls (and what is inside them); returns the removed names. ###### `Document.copy` ```python Document.copy() ``` Copies the selection, with whatever is inside the selected containers. ###### `Document.clipboard_text` ```python Document.clipboard_text() ``` The copied controls as JSON for the system clipboard, so another form, another Pyzed Design window or a text file can take them (see `paste_text`). ###### `Document.parse_clipboard` ```python @staticmethod Document.parse_clipboard(text) ``` The controls in clipboard `text` written by `clipboard_text`, or `None` when it holds something else. Controls of a type this Pyzed does not know are left out. ###### `Document.paste_text` ```python Document.paste_text(text, offset=16) ``` Pastes the controls in system clipboard `text` (see `clipboard_text`); returns the new names, or `None` when the text holds no Pyzed controls. ###### `Document.cut` ```python Document.cut() ``` Copies the selection to the clipboard and deletes it; returns how many controls were copied. ###### `Document.paste` ```python Document.paste(offset=16) ``` Adds copies of the clipboard (shifted by `offset`) without their events; they keep their names when those are free in this form. ###### `Document.duplicate` ```python Document.duplicate() ``` Pastes a copy of the selection right away, leaving the clipboard as it was; returns the new names. ###### `Document.size` ```python Document.size # property ``` The form's `(width, height)` in pixels. ###### `Document.nudge` ```python Document.nudge(dx, dy) ``` Moves the selected controls (those not placed by a layout) by `dx`, `dy` pixels, kept inside their container when they share one; repeated nudges merge into one undo step. ###### `Document.align` ```python Document.align(edge, *, to_form=False) ``` Aligns the selected controls on `edge` (see `geometry.align`): to each other, or to the form with `to_form` or when only one control is selected. ###### `Document.center` ```python Document.center(axis='both') ``` Centers the selection (as a group) in the form. ###### `Document.distribute` ```python Document.distribute(axis) ``` Spaces the selected controls evenly along `axis` (see `geometry.distribute`). ###### `Document.same_size` ```python Document.same_size(which='both') ``` Gives the selected controls the primary one's width, height or both (`which`). ###### `Document.snap_to_grid` ```python Document.snap_to_grid() ``` Snaps the selected controls (or all of them if nothing is selected) to the form's grid. ###### `Document.reorder` ```python Document.reorder(where) ``` Moves the selected controls in the z-order (`where`: see `Form.reorder`). ###### `Document.set_rect` ```python Document.set_rect(name, rect, *, group=None) ``` Sets the position and size of control `name` from `rect` `(x, y, width, height)`, rounded to whole pixels; edits with the same `group` merge into one undo step. ###### `Document.set_prop` ```python Document.set_prop(name, key, value) ``` Sets property `key` of control `name` to `value` (repeated edits merge into one undo step). ###### `Document.set_style` ```python Document.set_style(name, key, value) ``` Sets one style property of a control, or of the form's global style when `name` is `None`; `None` removes it. ###### `Document.set_animations` ```python Document.set_animations(name, animations, *, group=None) ``` Replaces the animations of control `name` (see `designer.motion`). Edits with the same `group` in a row undo together (typing a duration). ###### `Document.set_clip` ```python Document.set_clip(clip, *, old=None, group=None) ``` Stores the `pz.Clip` `clip` in the form (in place of the clip `old` when it is renamed). ###### `Document.remove_clip` ```python Document.remove_clip(name) ``` Removes the clip `name` (and stops it playing when the form opens). ###### `Document.set_autoplay` ```python Document.set_autoplay(name) ``` Plays the clip `name` when the form opens (`""`: none). ###### `Document.clear_style` ```python Document.clear_style(name) ``` Removes every style property of a control (or of the form's global style). ###### `Document.set_field` ```python Document.set_field(name, key, value) ``` Sets a `Control` attribute (x, y, width, height, visible, enabled, locked, anchor); a container's new width or height moves what is inside it as its anchors say. ###### `Document.set_bind` ```python Document.set_bind(name, bind) ``` Binds control `name` to the field (or, for display controls, the expression) `bind` of `form.data`; `None` or `""` unbinds it. ###### `Document.set_rule` ```python Document.set_rule(name, rule, value) ``` Sets validation `rule` (`required`, `min_length`, `pattern`...) of control `name`; `None`, `""` or `False` removes it. ###### `Document.toggle_anchor` ```python Document.toggle_anchor(name, edge) ``` Adds `edge` to the anchors of control `name`, or removes it if it was there. ###### `Document.parent_size` ```python Document.parent_size(name) ``` The design size of what control `name` is anchored to: its container, or the form. ###### `Document.set_anchor` ```python Document.set_anchor(name, anchor, *, move=False, margin=0.0) ``` Anchors control `name` with a preset (`"top_right"`, `"full"`...) or edges. With `move` it is also moved (and, on stretched axes, sized) to where the anchor points, `margin` pixels in. ###### `Document.set_offset` ```python Document.set_offset(name, side, value) ``` Sets the distance from the `side` (`left`, `top`, `right`, `bottom`) of the container to control `name`; the opposite side stays where it is, so the control grows or shrinks. ###### `Document.set_pivot` ```python Document.set_pivot(name, x, y) ``` Sets the pivot of control `name` (`0` to `1` on each axis; the center is the default). ###### `Document.set_form` ```python Document.set_form(key, value) ``` Sets attribute `key` of the form (a new `name` is made a valid identifier first). ###### `Document.reflow` ```python Document.reflow(parent, size) ``` After the form (`parent` `None`) or container `parent` was resized from `size`, moves and sizes what is inside it the way the anchors say (unless `follow_anchors` is off). ###### `Document.rename` ```python Document.rename(old, new) ``` Renames a control, its selection entry and the handlers that follow the naming convention. ###### `Document.owner` ```python Document.owner(name) ``` The display name and event map of a control, or of the form when `name` is `None`. ###### `Document.events_of` ```python Document.events_of(name) ``` Returns the events of control `name`, or the form's events when `name` is `None`. ###### `Document.wire` ```python Document.wire(name, event) ``` Connects `event` to a handler (creating its stub if needed) and returns the function name. ###### `Document.unwire` ```python Document.unwire(name, event, *, delete_code=False) ``` Disconnects `event`; with `delete_code` an untouched stub is removed too. ###### `Document.rename_handler` ```python Document.rename_handler(old, new) ``` Points the events wired to handler `old` at `new` (after its function was renamed in the code); returns whether any event used it. ###### `Document.handler_source` ```python Document.handler_source(handler) ``` Returns the source of `handler` in the code, or a new stub if it is not there yet. ###### `Document.set_handler_source` ```python Document.set_handler_source(handler, source) ``` Replaces one handler's code (raises `SyntaxError` and changes nothing if it is invalid). ###### `Document.set_code` ```python Document.set_code(source) ``` Replaces the whole text of the handlers module and marks the document as changed. #### Data binding and validation ##### `check` ```python check(value, rules, type_='input') ``` The message of the first rule `value` breaks, or `None` when it passes them all. `required` (a checked box for check boxes and switches), `min_length`, `max_length`, `pattern` (a regular expression the whole text must match), `min` and `max`. `message`, when set, replaces the built-in message. Empty values only fail `required`. ##### `translate` ```python translate(value, language=None) ``` `value` with `@key` texts looked up in the current (or given) language; lists are translated item by item and `@@` stands for a literal `@`. ##### `read` ```python read(data, path, default=None) ``` The field at `path` in `data` (dicts, lists and objects), or `default` when it is missing. ##### `write` ```python write(data, path, value) ``` Sets the field at `path` in `data`, creating missing dicts on the way. ##### `has` ```python has(data, path) ``` Whether `data` has a field at `path`. ##### `evaluate` ```python evaluate(expression, data, **extra) ``` The value of `bind` for `data`: the field when it is a path, else the Python expression evaluated over the data's fields. Raises on a missing field or a bad expression. ##### `bound_prop` ```python bound_prop(type_) ``` The property a control of `type_` binds (`None` if it cannot be bound). ##### `two_way` ```python two_way(type_) ``` Whether a control of `type_` writes the user's changes back to the data. ##### `rules_for` ```python rules_for(type_) ``` The validation rules that make sense for `type_` (none for display controls). ##### `load_texts` ```python load_texts(path) ``` Loads the translation catalogs of the form at `path`: `locales/` next to it and in the folders above, up to the project root. Returns the languages found. ##### `BIND_PROPS` The property each control type binds, and whether the user edits it (two-way): `{type: (property, two_way)}`. #### Toolbox ##### `Component` ```python @dataclass class Component(type, label, icon, category, render, size=(120, 28), props=(), events=(), description='', visual=True, container=None, child_layout=(), children=(), role=None) ``` A toolbox entry: how a control of this `type` looks, behaves and is configured. **Methods and properties** ###### `Component.default_event` ```python Component.default_event # property ``` The event opened by double-clicking the control in the designer. ###### `Component.all_events` ```python Component.all_events # property ``` The control's own events followed, for visual components, by the common mouse events. ###### `Component.defaults` ```python Component.defaults() ``` Returns a new dict of the default value of each property, by name. ###### `Component.prop` ```python Component.prop(name) ``` Returns the property called `name`, or `None` if the component has none. ###### `Component.create` ```python Component.create(name, x=0, y=0, width=None, height=None) ``` A new control of this type with default properties. ##### `Prop` ```python @dataclass class Prop(name, kind, default=None, label='', choices=(), min=None, max=None, step=1.0, group='Appearance', type='', accept='') ``` A property shown in the designer's property grid. `kind` picks the editor: text, multiline, number, bool, color, choice (from `choices`), list (one item per line), rows (comma-separated rows, the first one the header), icon (a Lucide icon name), file or folder (a path with a button that opens the system's dialog; `accept` names the kind of file, like `"image"`, or its extensions, like `"wgsl pzshad"`) or typed (any annotation in `type`: unions, optionals, dicts, tuples...). **Methods and properties** ###### `Prop.title` ```python Prop.title # property ``` The label shown in the property grid: `label`, or the name with underscores as spaces. ##### `RenderContext` ```python class RenderContext ``` What a component's `render` needs: the theme and a way to raise events. `live` is `False` in the designer, where callbacks are inert placeholders so controls look enabled but do nothing. **Methods and properties** ###### `RenderContext.on` ```python RenderContext.on(control, event, prop=None) ``` A callback for `event`; when `prop` is given the value is also stored there. ###### `RenderContext.set` ```python RenderContext.set(control, prop, value) ``` Stores `value` in `prop` of a running control without raising an event (a tree's open folders). ###### `RenderContext.key` ```python RenderContext.key(control) ``` A stable id for widgets with internal state (editors, inputs). ##### `register` ```python register(component) ``` Adds (or replaces) a component in the toolbox. ##### `component` ```python component(type_) ``` Returns the registered component for `type_`; raises `KeyError` listing the known types. ##### `all_components` ```python all_components() ``` Returns every registered component, in registration order. ##### `categories` ```python categories() ``` Returns the registered components grouped by `category` (toolbox sections), in registration order. ##### `Template` ```python @dataclass class Template(name, label, icon, description, build=None, size=(640, 420), handlers=(), form=None) ``` A starting point for new forms: a `build` function that fills an empty `Form`, or a saved form. ##### `register_template` ```python register_template(template) ``` Adds (or replaces, by name) a template offered in the "New form" dialog; returns it. ##### `templates` ```python templates() ``` Returns the registered form templates, in registration order. #### Designed components ##### `new_component` ```python new_component( name='Component1', path=None, *, width=240, height=120, label='', icon='component', category='Custom', description='', extends=None, ) ``` A new designed component (not saved yet; call `save()`): its scene and an empty class. `extends` is the `.pzcomp` of a component to inherit from: the new class derives from it and starts with its controls, properties, events and methods. ##### `load_component` ```python load_component(path) ``` Imports the class of the component saved at `path` (fresh, so edits are picked up). Defining the class registers it in the toolbox, replacing an older version. Returns the class, or `None` (with the reason in `errors`) if its code fails to import. ##### `import_component` ```python import_component(scene) ``` The class of the designed component saved at `scene`, to inherit from it. A relative path is relative to the module calling it, so a component can extend another one next to it: ```python Rating = import_component("rating.pzcomp") class BigRating(Rating, scene="big_rating.pzcomp"): ... ``` The class is imported once and reused until its code changes. ##### `discover` ```python discover(root=None, *, fresh=True) ``` Imports every designed component under `root` (the current folder by default). Components may use each other, so a failed import is retried once the others loaded (`errors` keeps what still failed). With `fresh=False` a component whose code did not change since it was last imported is kept as it is. ##### `SceneDocument` ```python class SceneDocument ``` A component open in the designer: its scene, its class code, selection and undo. Every command of `Document` works (placing, moving, aligning controls...). Events of controls become methods of the class (`def ok_click(self, event)`), and the component's own events are its lifecycle methods (`setup`, `on_mount`...). Properties and custom events are read from the class and written back to it. **Methods and properties** ###### `SceneDocument.scene` ```python SceneDocument.scene # property ``` The component's scene (the document's `form`, typed as a `Scene`). ###### `SceneDocument.base_file` ```python SceneDocument.base_file # property ``` The `.pzcomp` this component extends, or `None`. ###### `SceneDocument.base` ```python SceneDocument.base # property ``` The scene this component extends, with everything it inherits itself (`None` if none). ###### `SceneDocument.inherited` ```python SceneDocument.inherited(name) ``` `True` for a control that comes from the base component. ###### `SceneDocument.own_scene` ```python SceneDocument.own_scene() ``` What this component's own `.pzcomp` holds: its controls, and the inherited ones it changed. ###### `SceneDocument.set_base` ```python SceneDocument.set_base(path) ``` Makes this component inherit from the one saved at `path` (`None` inherits from nothing). ###### `SceneDocument.base_info` ```python SceneDocument.base_info() ``` The classes this component inherits from (nearest first) as `(scene file name, class info)`. ###### `SceneDocument.all_members` ```python SceneDocument.all_members # property ``` This component's properties and the ones it inherits (bindings can use both). ###### `SceneDocument.info` ```python SceneDocument.info # property ``` The class as last parsed (the last good parse while the code has a syntax error). ###### `SceneDocument.code_ok` ```python SceneDocument.code_ok # property ``` Whether the class code currently parses (`info` falls back to the last good parse otherwise). ###### `SceneDocument.members` ```python SceneDocument.members # property ``` The component's properties and private state as declared in its class, in order (a copy). ###### `SceneDocument.member` ```python SceneDocument.member(name) ``` Returns the declared property or state called `name`, or `None`. ###### `SceneDocument.unique_member` ```python SceneDocument.unique_member(base='value') ``` Returns `base` (made an identifier), or `base2`, `base3`... if it is taken by a member, method, control or reserved name. ###### `SceneDocument.add_member` ```python SceneDocument.add_member(name='', type_='str', default=None, *, public=True, description='') ``` Declares a new property (`public=False` makes it private state). ###### `SceneDocument.update_member` ```python SceneDocument.update_member(member, **changes) ``` Changes a property (`name`, `type`, `default`, `public`, `description`); returns its name. ###### `SceneDocument.set_member_source` ```python SceneDocument.set_member_source(name, source) ``` Sets the default of property `name` to the Python expression `source` (`[1, 2]`, `Point(x=1)`...). ###### `SceneDocument.remove_member` ```python SceneDocument.remove_member(name) ``` Removes property `name` from the class. ###### `SceneDocument.move_member` ```python SceneDocument.move_member(name, offset) ``` Moves property `name` by `offset` places in the class's declaration order (clamped). ###### `SceneDocument.custom_events` ```python SceneDocument.custom_events # property ``` The events the component declares, as `{name: description}` (a copy). ###### `SceneDocument.add_event` ```python SceneDocument.add_event(name='changed', description='') ``` Declares a new event called `name` (made unique) with `description`; returns its name. ###### `SceneDocument.update_event` ```python SceneDocument.update_event(name, *, new_name=None, description=None) ``` Renames event `name` to `new_name` (if valid and free) and/or changes its `description`, keeping its place; returns the event's name. Raises `KeyError` if it does not exist. ###### `SceneDocument.remove_event` ```python SceneDocument.remove_event(name) ``` Removes the declared event `name` from the class. ###### `SceneDocument.set_class_name` ```python SceneDocument.set_class_name(name) ``` Renames the component's class (made a valid class name) in the code and the scene; returns the new name. ###### `SceneDocument.set_description` ```python SceneDocument.set_description(text) ``` Sets the component's description, also as its class's docstring. ###### `SceneDocument.set_scene` ```python SceneDocument.set_scene(key, value) ``` Sets how the component shows in the toolbox (`label`, `icon`, `category`) or its size. ###### `SceneDocument.set_binding` ```python SceneDocument.set_binding(control, prop, expression) ``` Makes `prop` of `control` follow `expression` (`None` or `""` removes the binding). ###### `SceneDocument.add_method` ```python SceneDocument.add_method(name='do_something', args='self', doc='') ``` Adds a reusable method (not an event handler) to the class; returns its name. ##### `Scene` ```python @dataclass class Scene(name='Component1', title='Component1', width=240.0, height=120.0, theme='One Dark', accent=None, background=None, radius=None, font_size=14.0, resizable=False, grid=8, style={}, controls=[], events={}, code=None, animations=[], autoplay='', label='', icon='component', category='Custom', description='', extends='', bindings={}) ``` The layout of a designed component: a `Form` whose `name` is the class name. `bindings` maps a control name to `{control property: expression}`; the expression (usually a component property, such as `value`, or any Python expression over them, such as `f"{value} stars"`) is evaluated on every render. **Methods and properties** ###### `Scene.class_name` ```python Scene.class_name # property ``` The name of the component's class (the scene's `name`). ###### `Scene.bound` ```python Scene.bound(control) ``` Returns the bindings of `control` as `{property: expression}` (empty if it has none). ##### `SceneComponent` ```python class SceneComponent ``` Base class of the components made in the designer's component mode. `scene` is the `.pzcomp` file with the layout, relative to the module. The toolbox label, icon, category, description and default size come from the scene unless the class sets them. **Methods and properties** ###### `SceneComponent.scene_path` ```python @classmethod SceneComponent.scene_path() ``` Returns the `.pzcomp` file the class is tied to, or `None`. ###### `SceneComponent.scene_model` ```python @classmethod SceneComponent.scene_model() ``` The scene as currently saved (or previewed by the designer), or `None`. ###### `SceneComponent.ctx` ```python SceneComponent.ctx # property ``` A render context for this instance, in the theme of its scene (what `ControlProxy.focus()` needs). ##### `render` ```python render(control, ctx) ``` Draws `control` at its size (its own props, falling back to the component defaults). A control whose fields, theme, language and animations are the same as last time gets its last drawing back, so a form with hundreds of controls only rebuilds the ones that changed. Controls made from a `pz.Component` keep their own cache (see `pyzed.component`). #### Layout helpers ##### `align` ```python align(controls, edge, *, to=None) ``` Lines controls up on `edge` (left, center, right, top, middle, bottom). With `to` (e.g. the form rectangle) they align to it; otherwise to the bounds of the selection, or to the form when a single control is given via `to`. ##### `distribute` ```python distribute(controls, axis) ``` Spaces three or more controls evenly along `"horizontal"` or `"vertical"`. ##### `same_size` ```python same_size(controls, which='both', *, reference=None) ``` Gives every control the `"width"`, `"height"` or `"both"` of `reference` (the first one). ##### `snap` ```python snap(value, grid, enabled=True) ``` Rounds `value` to the nearest multiple of `grid` (unchanged if disabled). ##### `smart_guides` ```python smart_guides(rect, others, size, threshold=4.0) ``` Snaps a moving rectangle to the edges and centers of `others` and of the form. Returns `(dx, dy, lines)`: the correction to apply and the guide lines to draw (as thin `(x1, y1, x2, y2)` boxes), like the alignment guides of Figma or Qt Designer. ##### `anchored` ```python anchored(control, design, actual) ``` Where a control goes when its form is resized from `design` to `actual` size. Like WinForms anchors: an edge anchored to the form keeps its distance to that side; anchored to both sides it stretches; anchored to neither it keeps its relative position (centered controls stay centered). The control's minimum and maximum size and its pivot (`Control.layout`) apply too; see `pyzed.anchors.place`. #### Designer app ##### `Designer` ```python class Designer ``` The visual form editor. ##### `EditorSettings` ```python @dataclass class EditorSettings(theme='', font_family='', font_size=14.0, tab_size=4, hard_tabs=False, convert_indentation=False, backspace_unindent=True, repeat_delay=500.0, repeat_rate=20.0, show_whitespace=False, wrap=False, line_numbers=True, highlight_line=True, autocomplete=True, code_lens=True, inline_diagnostics=True, format_on_apply=False, auto_close=True, folding=True, minimap=True, cursor_shape='bar', cursor_blink=True, cursor_width=2.0, line_height='standard', scrollbar=True, match_brackets=True, selection_highlight=True, hover=True, sticky_scroll=True, ty=True, ruff=True, keymap={}) ``` EditorSettings(theme: 'str' = '', font_family: 'str' = '', font_size: 'float' = 14.0, tab_size: 'int' = 4, hard_tabs: 'bool' = False, convert_indentation: 'bool' = False, backspace_unindent: 'bool' = True, repeat_delay: 'float' = 500.0, repeat_rate: 'float' = 20.0, show_whitespace: 'bool' = False, wrap: 'bool' = False, line_numbers: 'bool' = True, highlight_line: 'bool' = True, autocomplete: 'bool' = True, code_lens: 'bool' = True, inline_diagnostics: 'bool' = True, format_on_apply: 'bool' = False, auto_close: 'bool' = True, folding: 'bool' = True, minimap: 'bool' = True, cursor_shape: 'str' = 'bar', cursor_blink: 'bool' = True, cursor_width: 'float' = 2.0, line_height: 'str' = 'standard', scrollbar: 'bool' = True, match_brackets: 'bool' = True, selection_highlight: 'bool' = True, hover: 'bool' = True, sticky_scroll: 'bool' = True, ty: 'bool' = True, ruff: 'bool' = True, keymap: 'Dict[str, str]' = ) **Methods and properties** ###### `EditorSettings.key_repeat` ```python EditorSettings.key_repeat # property ``` The editor's `repeat_delay` and `repeat_rate` (`None` where the system's key repeat stays). ###### `EditorSettings.to_dict` ```python EditorSettings.to_dict() ``` Returns the settings as a JSON-ready dict. ###### `EditorSettings.from_dict` ```python @classmethod EditorSettings.from_dict(data) ``` Builds settings from saved `data`, keeping defaults for missing or invalid values and clamping the tab size, font size and cursor shape to valid ones. ##### `History` ```python class History ``` Snapshots of a form: `push` after an edit, then `undo`/`redo` to move through them. **Methods and properties** ###### `History.push` ```python History.push(form, group=None) ``` Records the form's current state; returns `False` if nothing changed. ###### `History.steps` ```python History.steps # property ``` How many undo steps there are (a mark for `merge`). ###### `History.merge` ```python History.merge(mark) ``` Makes every step recorded since `mark` (`steps` before a batch of edits) a single undo step. ###### `History.reset` ```python History.reset(form) ``` Forgets all undo and redo steps and starts again from `form`'s current state. ###### `History.can_undo` ```python History.can_undo # property ``` Whether there is a step to undo. ###### `History.can_redo` ```python History.can_redo # property ``` Whether there is an undone step to redo. ###### `History.undo` ```python History.undo() ``` Steps back one snapshot; returns the previous form (a new object) or `None` if there is none. ###### `History.redo` ```python History.redo() ``` Steps forward one snapshot; returns the restored form (a new object) or `None` if there is none. ##### `theme_for` ```python theme_for(form, theme_name=None) ``` The theme a form runs with: its base theme (or `theme_name`, to preview it with another one) plus accent, radius, font and background tweaks. ##### `COMMON_EVENTS` Events every control can handle in the designer's Events dock. ##### `FORM_EVENTS` Events of the form itself (`load`, `close`, `resize`, `key`...). ### Services (pyzed.services) > API reference for pyzed.services: timers, shortcuts, file watchers, processes, background workers, web requests, the clipboard, settings, file dialogs, notifications and tray icons for Python desktop apps. Source: https://albakore.github.io/pyzed/reference/services/ Services raise events but draw nothing: they are the non-visual components of Pyzed Design ([the tray under the form](https://albakore.github.io/pyzed/editor/non-visual-components/)) and work the same in a `pz.App` or a script. Handlers go in `on_=`; events raised by a background thread reach them on the app's thread. See the [Services guide](https://albakore.github.io/pyzed/guides/services/). ```python watcher = pz.FileWatcher("inbox", pattern="*.csv", on_created=self.import_file) watcher.start() ``` #### Time and keys ##### `Timer` ```python class Timer ``` Raises `tick` (with the count) every `interval` milliseconds while it is running; `repeat=False` ticks once. Return `subscriptions()` from your app's `subscriptions`. ##### `Shortcut` ```python class Shortcut ``` Raises `activate` when one of its `keys` is pressed (`"ctrl-s"`, `"Ctrl+S"`, several separated by commas; `mod-` is Cmd on macOS and Ctrl elsewhere). Call `handle(combo)` from `on_key`. #### Files and programs ##### `FileWatcher` ```python class FileWatcher ``` Watches a folder or a file and raises `created`, `modified` and `deleted` (a path) and `change` (the changed paths), checking every `interval` milliseconds. `start()`, `stop()`, `running`. ##### `Process` ```python class Process ``` Runs a program and raises `output` and `error_output` for every line it prints, `finished` with its exit code, `started` and `failed`. `start()`, `write()`, `kill()`, `wait()`. ##### `Worker` ```python class Worker ``` Runs `work(value)` in a background thread with `run(value)`; `report()` raises `progress`, the result arrives in `done` and an exception in `failed`. `cancel()` sets `cancelled`. #### Network ##### `HttpRequest` ```python class HttpRequest ``` Calls a web address in the background: `send()` raises `response` with an `HttpResponse` (error statuses included) or `failed` when there was no answer. `json=` sends JSON. ##### `HttpResponse` ```python @dataclass class HttpResponse(status, content=b'', headers={}, url='') ``` A web server's answer: `status`, `ok`, `headers`, `content` (bytes), `text` and `json()`. #### System ##### `Clipboard` ```python class Clipboard ``` The system clipboard: `copy(text)` and `paste()` return commands; with `watch=True` it raises `change` when something new is copied. ##### `Settings` ```python class Settings ``` Values kept between runs: a dict saved as JSON in the user's settings folder at every change, with `defaults`; raises `change` with `(key, value)`. ##### `FileDialog` ```python class FileDialog ``` The system's file dialog: `show()` returns the command that opens it in `mode` `"open"`, `"open_many"`, `"save"` or `"folder"`; raises `chosen` or `cancelled`. ##### `Notification` ```python class Notification ``` A desktop notification: `show(text)` shows it and returns whether it could; raises `shown` or `failed`. ##### `TrayIcon` ```python class TrayIcon ``` An icon in the system tray with a `tooltip` and a `menu` (a list, or text with one item per line: `-` separator, `[x]` check, indented submenu): `show()` returns whether it could, `hide()`, `checked(item)`; raises `click`, `double_click` (Windows), `menu` and `failed`. On Linux it is a StatusNotifierItem, without GTK. ##### `TrayItem` ```python @dataclass class TrayItem(text, id=None, enabled=True, checked=None, items=[]) ``` An entry of a `TrayIcon` menu: `text`, `id` (what `menu` receives), `enabled`, `checked` and `items` for a submenu. #### Operating system (pyzed.system) ##### `SystemInfo` ```python class SystemInfo ``` What the computer is: `os` (`"Windows"`, `"macOS"`, `"Linux"`), `os_version`, `machine`, `hostname`, `user`, `home`, `language`, `cpu_count`, `python_version`, `memory_total`, plus `memory_available`, `disk_free` and `disk_total` read when asked. `env(name)` reads a variable, `folder("documents")` finds the user's folders. With `interval` above 0, `start()` raises `update` with `snapshot()` every so often. ##### `Launcher` ```python class Launcher ``` Opens files, folders and links with the app the system uses for them: `open(target)`, `reveal(path)` (selected in the file manager) and `email(to, subject, body)`; raises `opened` or `failed`. ##### `NetworkMonitor` ```python class NetworkMonitor ``` Notices when the computer goes online or offline by reaching `host` every `interval` milliseconds in the background: `online`, `check()`, `start()`, `stop()`; raises `change`, `online` and `offline`. ##### `PowerMonitor` ```python class PowerMonitor ``` The battery: `percent`, `plugged` and `has_battery`; every `interval` milliseconds raises `change`, `low` (under `low_level` while unplugged), `plugged` and `unplugged`. ### Games (pyzed.game) > API reference for pyzed.game: held keys, movement, collisions, pixel sprites, particles and screen shake for real-time games on a pz.canvas. Source: https://albakore.github.io/pyzed/reference/game/ `pyzed.game` holds small helpers for real-time games: held keys, movement, collisions, pixel sprites, particles and screen shake. They are plain Python built on [`pyzed.draw`](https://albakore.github.io/pyzed/reference/draw/), so they also work in screenshots and tests. See [Making games](https://albakore.github.io/pyzed/games/) for complete examples. ```python from pyzed import draw, game def tick(self, dt): # pz.frames(self.tick) in subscriptions() self.x += game.axis(("left", "a"), ("right", "d")) * 300 * dt self.sparks.update(dt) def view(self): return pz.canvas([draw.background("#0b0e14"), *game.glow(self.x, 200, 18, "#74ade8"), *self.sparks.shapes()], width=400, height=300) ``` #### Keys ##### `pressed` ```python pressed() ``` The keys held down right now, as a `frozenset` of names without modifiers (`"left"`, `"space"`, `"a"`…). Also available as `pz.pressed()`. ##### `is_pressed` ```python is_pressed(*keys, held=None) ``` Whether any of `keys` is held down (`held` replaces `pressed()`, for tests). Also available as `pz.is_pressed()`. ##### `axis` ```python axis(negative, positive, *, held=None) ``` `-1`, `0` or `1` from two groups of keys: `axis(("left", "a"), ("right", "d"))`. ##### `vector` ```python vector( left=('left', 'a'), right=('right', 'd'), up=('up', 'w'), down=('down', 's'), *, held=None, ) ``` A direction `(x, y)` of length 0 or 1 from four groups of keys (arrows and WASD by default); diagonals are normalized. #### Math ##### `clamp` ```python clamp(value, low, high) ``` `value` limited to `[low, high]`. ##### `lerp` ```python lerp(a, b, t) ``` The value `t` of the way from `a` to `b` (`t` from 0 to 1). #### Collisions ##### `Rect` ```python @dataclass class Rect(x, y, w, h) ``` An axis-aligned box (`x`, `y` is the top-left corner) with `left`, `right`, `top`, `bottom`, `center`, `overlaps(other)`, `contains(x, y)`, `moved(dx, dy)` and `shape(**paint)` (a `draw.rect`). ##### `circle_rect` ```python circle_rect(cx, cy, r, rect) ``` Where a circle hits a `Rect`: the push-out direction `(nx, ny)`, or `None`. A ball bounces with `if nx: vx = abs(vx) * nx` and `if ny: vy = abs(vy) * ny`. #### Drawing ##### `rgba` ```python rgba(color, alpha=1.0) ``` `color` (`"#rrggbb[aa]"` or a tuple) as an `(r, g, b, a)` tuple with `alpha` applied. ##### `glow` ```python glow(x, y, radius, color, *, layers=4, strength=0.5, core=True) ``` A soft light: translucent circles growing around `(x, y)` (bloom on a canvas). `strength` is the opacity of the innermost halo; `core=False` leaves out the solid circle. ##### `sprite` ```python sprite(rows, palette, x=0.0, y=0.0, *, scale=1.0, flip=False) ``` Pixel art from text: each character of `rows` is a pixel colored by `palette` (missing characters are transparent). One shape per color, so a sprite stays cheap whatever its size. #### Effects ##### `Particles` ```python class Particles ``` Sparks, smoke, explosions: short-lived dots that move, fall and fade out. Call `burst(x, y, …)` when something happens, `update(dt)` every frame and draw `shapes()`; `gravity`, `drag` and `limit` tune them. ##### `Shake` ```python class Shake ``` Screen shake: `kick(8)` on a hit, `update(dt)` every frame, and start the canvas shapes with `*shake.shapes()` (a translate) so everything moves together. ### Shader graphs (pyzed.shading) > API reference for pyzed.shading: load .pzshad shader graphs, change their variables, and build node graphs in code that compile to the WGSL of pz.shader. Source: https://albakore.github.io/pyzed/reference/shading/ `pyzed.shading` is the node-based shader system behind the designer's [Shader view](https://albakore.github.io/pyzed/shading/shader-designer/): `.pzshad` graphs of nodes that compile to the WGSL of [`pz.shader`](https://albakore.github.io/pyzed/reference/widgets/). See [Shader graphs](https://albakore.github.io/pyzed/shading/shader-graphs/) for a tour. ```python from pyzed import shading fire = shading.load("shaders/fire.pzshad") fire["speed"] = 2.0 # a mutable variable: no recompiling pz.shader(fire, width=320, height=200) # or pz.shader("shaders/fire.pzshad") ``` #### Using shaders ##### `load` ```python load(path, *, fresh=False) ``` The `Shader` of a `.pzshad` file, cached by path (`fresh=True` reads it again). It reloads by itself when the file changes. ##### `Shader` ```python class Shader ``` A shader graph ready to draw, with its own values for the variables: `shader["speed"] = 2`, `shader.set(...)`, `shader.variables`, `shader.wgsl` and `shader.widget(...)`. `pz.shader` takes one directly. ##### `widget` ```python widget(source, **options) ``` A `pz.shader` widget for a `.pzshad` path, a `Graph` or a `Shader`. #### Building graphs ##### `Graph` ```python class Graph ``` A shader graph: variables, nodes and the links between them. Call a node type on it to add one (`g.noise(scale=4)`, `g.sin(x)`), pick outputs with `node["name"]` and combine them with arithmetic. `save`, `compile`, `arrange`, `connect` and the variable methods edit it. ##### `new` ```python new(name='shader') ``` A new graph with UV feeding a gradient into the Output node: the designer's Blank template. ##### `load_graph` ```python load_graph(path) ``` The `Graph` of a `.pzshad` file, to edit it. ##### `Variable` ```python @dataclass class Variable(name, type='float', value=None, mutable=True, min=None, max=None, step=None, description='') ``` A variable of a shader: `name`, `type` (float, int, bool, vec2, vec3, vec4, color, texture), `value`, `mutable` (a uniform) or constant, `min`, `max`, `step` and `description`. ##### `Node` ```python @dataclass class Node(id, type, x=0.0, y=0.0, inputs={}, params={}, title='', preview=True) ``` A node of a graph: its `type`, position, `inputs` (fixed values of unconnected inputs), `params` (settings), `title` and whether its `preview` shows. ##### `NodeRef` ```python class NodeRef ``` What `Graph.add` returns: `ref["port"]` is an output to wire elsewhere; arithmetic uses its first output. ##### `GraphError` ```python class GraphError ``` Raised for wires that can't connect (types, cycles), unknown node types and bad variable names. #### Compiling ##### `compile` ```python compile(graph, target=None, *, values=None) ``` Compiles a graph (or one node's output, for its preview) to WGSL: the `Compiled` code, uniforms, textures and problems by node. ##### `Compiled` ```python @dataclass class Compiled(code, uniforms={}, textures={}, names={}, errors={}, lines={}, uses_time=False) ``` The result of `compile`: `code`, `uniforms`, `textures`, `names` (the uniform of each variable), `errors` by node id, `uses_time`, and `check()`, which runs the GPU compiler and puts its messages on the node that wrote the line. #### The node catalog ##### `register` ```python register(node_type, *, preset=True) ``` Adds a node type of your own to the catalog (and the designer's Shading sidebar). ##### `NodeType` ```python @dataclass class NodeType(id, title, category, description, inputs=(), outputs=(), params=(), exprs={}, functions=(), min_type='bool', build=None, ports=None, resolve=None, label=None, icon='', keywords='') ``` A node type: id, title, category, inputs, outputs, settings and how it writes its WGSL. ##### `Preset` ```python @dataclass class Preset(type, title, category, description, params={}, keywords='') ``` An entry of the Shading sidebar: a node type with some settings (`func:sin` is the Function node set to sin). ## Examples ### Examples > Ready-to-run Pyzed example apps: to-do lists, a code editor, Game of Life, clock, stopwatch, multiple windows, live data streams and more. Source: https://albakore.github.io/pyzed/examples/ All examples live in [`examples/`](https://github.com/albakore/pyzed/tree/main/examples). Run them from the repository root: .py `} python={` {py} -m pip install pyzed-ui {py} examples/.py `} /> | Example | What it shows | Tutorial | | --- | --- | --- | | [`counter.py`](https://github.com/albakore/pyzed/blob/main/examples/counter.py) | The bare minimum: state, messages and callables. | [Quickstart](https://albakore.github.io/pyzed/getting-started/quickstart/) | | [`todo.py`](https://github.com/albakore/pyzed/blob/main/examples/todo.py) | Forms, lists, filters, focus and shortcuts. | [To-do list](https://albakore.github.io/pyzed/tutorials/basic/todo-app/) | | [`downloads.py`](https://github.com/albakore/pyzed/blob/main/examples/downloads.py) | `Command.perform`, `pz.post` from threads, progress and notifications. | [Background tasks](https://albakore.github.io/pyzed/tutorials/intermediate/background-tasks/) | | [`mini_editor.py`](https://github.com/albakore/pyzed/blob/main/examples/mini_editor.py) | A code editor with a file tree and tabs in ~150 lines. | [Mini editor](https://albakore.github.io/pyzed/tutorials/advanced/code-editor/) | | [`gallery.py`](https://github.com/albakore/pyzed/blob/main/examples/gallery.py) | Every component, interactive, with a theme picker. | [Components](https://albakore.github.io/pyzed/components/overview/) | | [`zed_editor.py`](https://github.com/albakore/pyzed/blob/main/examples/zed_editor.py) | Full editor: file finder, palette, docks, terminal, themes and git markers. | | | [`tutorial/`](https://github.com/albakore/pyzed/tree/main/examples/tutorial/) | Five steps: window, state, layout and themes, async, notes app. | [5-step tutorial](https://albakore.github.io/pyzed/tutorials/basic/step-by-step/) | | [`todos.py`](https://github.com/albakore/pyzed/blob/main/examples/todos.py) | Iced's *todos*: add, edit, complete, filter and save. | | | [`game_of_life.py`](https://github.com/albakore/pyzed/blob/main/examples/game_of_life.py) | `pz.canvas` with thousands of cells and `pz.every` as the engine. | [Canvas](https://albakore.github.io/pyzed/guides/canvas/) | | [`games/snake.py`](https://github.com/albakore/pyzed/blob/main/examples/games/snake.py) | Snake on a fixed timer, key queue and a glass game-over card. | [Snake](https://albakore.github.io/pyzed/games/snake/) | | [`games/breakout.py`](https://github.com/albakore/pyzed/blob/main/examples/games/breakout.py) | 60 fps loop with `pz.frames`, held keys, collisions, particles and shake. | [Breakout](https://albakore.github.io/pyzed/games/breakout/) | | [`games/neon_invaders.py`](https://github.com/albakore/pyzed/blob/main/examples/games/neon_invaders.py) | A space shooter: pixel-art sprites, neon glow and a HUD with glass and bloom. | [Neon Invaders](https://albakore.github.io/pyzed/games/neon-invaders/) | | [`clock.py`](https://github.com/albakore/pyzed/blob/main/examples/clock.py) | Analog clock drawn on a canvas. | [Canvas](https://albakore.github.io/pyzed/guides/canvas/) | | [`stopwatch.py`](https://github.com/albakore/pyzed/blob/main/examples/stopwatch.py) | Stopwatch with laps and shortcuts. | [Async](https://albakore.github.io/pyzed/guides/async/) | | [`download_progress.py`](https://github.com/albakore/pyzed/blob/main/examples/download_progress.py) | Concurrent downloads with `async` generators and progress. | [Async](https://albakore.github.io/pyzed/guides/async/) | | [`websocket_stream.py`](https://github.com/albakore/pyzed/blob/main/examples/websocket_stream.py) | Live data with `pz.listen` and an `async` generator. | [Async](https://albakore.github.io/pyzed/guides/async/) | | [`multi_window.py`](https://github.com/albakore/pyzed/blob/main/examples/multi_window.py) | Several `pz.Window` with shared state and different themes. | [Multiple windows](https://albakore.github.io/pyzed/guides/windows/) | | [`styling.py`](https://github.com/albakore/pyzed/blob/main/examples/styling.py) | Theme picker, system mode, `Theme.generate` and accents. | [Themes](https://albakore.github.io/pyzed/theming/themes/) | | [`image_viewer.py`](https://github.com/albakore/pyzed/blob/main/examples/image_viewer.py) | `pz.image` and the zoomable `pz.image_viewer`. | [Content](https://albakore.github.io/pyzed/components/content/#images) | | [`markdown_viewer.py`](https://github.com/albakore/pyzed/blob/main/examples/markdown_viewer.py) | Markdown editor with live preview. | [Content](https://albakore.github.io/pyzed/components/content/) | | [`qr_code.py`](https://github.com/albakore/pyzed/blob/main/examples/qr_code.py) | Generates a QR code as you type. | [Content](https://albakore.github.io/pyzed/components/content/) | | [`panes.py`](https://github.com/albakore/pyzed/blob/main/examples/panes.py) | Resizable panes with nested `ui.split`. | [Content](https://albakore.github.io/pyzed/components/content/#resizable-panels) | | [`context_menu.py`](https://github.com/albakore/pyzed/blob/main/examples/context_menu.py) | Context menu with `ui.popover` and `ui.menu`. | [Content](https://albakore.github.io/pyzed/components/content/#popover) | | [`screenshots.py`](https://github.com/albakore/pyzed/blob/main/examples/screenshots.py) | Regenerates the documentation screenshots. | [Tests and screenshots](https://albakore.github.io/pyzed/guides/testing/) | #### Integrations [`examples/integrations/`](https://github.com/albakore/pyzed/tree/main/examples/integrations) holds desktop apps built on backend and data libraries. Each one is a uv project: `cd` into it and run `uv run main.py`. | Example | Libraries | Tutorial | | --- | --- | --- | | [`fastapi_heroes`](https://github.com/albakore/pyzed/tree/main/examples/integrations/fastapi_heroes) | FastAPI, SQLModel, httpx | [FastAPI](https://albakore.github.io/pyzed/integrations/fastapi/) | | [`django_polls`](https://github.com/albakore/pyzed/tree/main/examples/integrations/django_polls) | Django, httpx | [Django](https://albakore.github.io/pyzed/integrations/django/) | | [`sqlalchemy_quickstart`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlalchemy_quickstart) | SQLAlchemy 2.0, aiosqlite | [SQLAlchemy](https://albakore.github.io/pyzed/integrations/sqlalchemy/) | | [`sqlalchemy_contacts`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlalchemy_contacts) | SQLAlchemy 2.0 | [SQLAlchemy](https://albakore.github.io/pyzed/integrations/sqlalchemy/#many-rows-the-contacts-demo) | | [`sqlmodel_heroes`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlmodel_heroes) | SQLModel | [SQLModel](https://albakore.github.io/pyzed/integrations/sqlmodel/) | | [`sqlmodel_inventory`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlmodel_inventory) | SQLModel, Pydantic | [SQLModel](https://albakore.github.io/pyzed/integrations/sqlmodel/#one-model-for-the-form-and-the-table-the-inventory-demo) | | [`httpx_pypi`](https://github.com/albakore/pyzed/tree/main/examples/integrations/httpx_pypi) | httpx, Pydantic | [httpx](https://albakore.github.io/pyzed/integrations/httpx/) | | [`httpx_download`](https://github.com/albakore/pyzed/tree/main/examples/integrations/httpx_download) | httpx | [httpx](https://albakore.github.io/pyzed/integrations/httpx/#streaming-downloads-with-progress) | | [`pydantic_signup`](https://github.com/albakore/pyzed/tree/main/examples/integrations/pydantic_signup) | Pydantic | [Pydantic](https://albakore.github.io/pyzed/integrations/pydantic/) | | [`aiosqlite_journal`](https://github.com/albakore/pyzed/tree/main/examples/integrations/aiosqlite_journal) | aiosqlite | [Other libraries](https://albakore.github.io/pyzed/integrations/other-libraries/#aiosqlite) | #### Code editor Shortcuts: Ctrl+Shift+P palette, Ctrl+P file finder, Ctrl+S save, Ctrl+B, Ctrl+J and Ctrl+Alt+B docks. (Image: A code editor built with Pyzed, in One Dark) | Command palette | File finder (Gruvbox) | | --- | --- | | (Image: Palette) | (Image: File finder) | #### Component gallery (Image: Gallery in One Light) #### Downloads (Image: Downloads) #### Iced demos Python recreations of the [Iced demos](https://github.com/iced-rs/iced/tree/master/examples). | Game of Life | Styling | | --- | --- | | (Image: Game of Life) | (Image: Styling) | | Clock | Stopwatch | | --- | --- | | (Image: Clock) | (Image: Stopwatch) | | Todos | QR code | | --- | --- | | (Image: Todos) | (Image: QR code) | | Image viewer | Markdown | | --- | --- | | (Image: Image viewer) | (Image: Markdown) | ##### Multiple windows (Image: Multiple windows) #### AI and Blocks | Example | What it shows | Page | | --- | --- | --- | | [`ai_chat.py`](https://github.com/albakore/pyzed/blob/main/examples/ai_chat.py) | A streaming AI chat over any OpenAI-compatible API, with a demo mode that needs no network. | [Building AI apps](https://albakore.github.io/pyzed/ai/ai-apps/) | | [`pomodoro.py`](https://github.com/albakore/pyzed/blob/main/examples/pomodoro.py) | The timer an agent builds with the MCP tools in the prompting guide. | [Prompting guide](https://albakore.github.io/pyzed/ai/prompting/) | | [`blocks/`](https://github.com/albakore/pyzed/tree/main/examples/blocks) | Nine ready-made screens: login, dashboard, settings, data table, chat, IDE, onboarding, music player, kanban. | [Blocks](https://albakore.github.io/pyzed/blocks/) | The screenshots on this page are generated with `python docs/scripts/example_shots.py`. ## FAQ ### Frequently asked questions > Pyzed FAQ: what it is, how it compares with tkinter, Kivy, PyQt and Flet, license, platforms, performance, the visual designer and AI agent support. Source: https://albakore.github.io/pyzed/faq/ Short answers to the questions people ask most about Pyzed. Each one links to the page that explains it in full. #### What is Pyzed? Pyzed is an open-source Python framework for building native desktop applications. Your Python code describes the interface; [Iced](https://iced.rs), a GUI toolkit written in Rust, lays it out and draws it with the GPU (or a software renderer when there is none). It ships more than 90 components inspired by the [Zed](https://zed.dev) editor (tabs, file tree, command palette, code editor, terminal, tables, charts, dialogs, a workspace with docks) and Pyzed Design, a drag-and-drop visual designer. See the [introduction](https://albakore.github.io/pyzed/getting-started/introduction/). #### Is Pyzed related to the ZED camera's pyzed package? No. Pyzed (`pip install pyzed-ui`, `import pyzed as pz`) is a Python GUI framework for desktop apps, and its name comes from the [Zed](https://zed.dev) editor that inspired its components. The `pyzed` module of the Stereolabs ZED SDK (`pyzed.sl`) is an unrelated library for ZED stereo cameras. Both use the `pyzed` import name, so they cannot be installed in the same environment. #### How do I install Pyzed? Run `pip install pyzed-ui` or `uv add pyzed-ui`, then `import pyzed as pz`. The package is called `pyzed-ui` on PyPI but is imported as `pyzed`. It needs Python 3.10 or newer, and the prebuilt wheels for Windows, macOS and Linux include the Rust engine, so there is nothing to compile. See [Installation](https://albakore.github.io/pyzed/getting-started/installation/). #### Is Pyzed free and open source? Yes. Pyzed and Pyzed Design are free and released under the MIT license, so you can use them in commercial and closed-source applications. The code is on [GitHub](https://github.com/albakore/pyzed). #### Which platforms does Pyzed support? Windows (x64 and ARM), macOS (Intel and Apple Silicon) and Linux (x86_64 and ARM, glibc 2.28 or newer, and musl for Alpine). On Alpine the audio engine renders offline only, without sound devices. Mobile and the web are not targets yet. See [Stability and known limitations](https://albakore.github.io/pyzed/guides/stability/). #### How is Pyzed different from tkinter? tkinter ships with Python and is fine for small utilities, but its widgets look dated, styling is limited and complex layouts take a lot of code. Pyzed draws a modern, themeable interface with the GPU, comes with high-level components such as a code editor, tables, trees, a command palette and notifications, has a visual designer, and uses a declarative `view()`/state model instead of mutating widgets by hand. tkinter needs no extra install; Pyzed needs `pip install pyzed-ui`. #### How is Pyzed different from Kivy? Kivy is aimed at touch and mobile apps and uses its own KV language. Pyzed is aimed at desktop apps (dense, keyboard-driven interfaces like editors, dashboards and tools), is written in plain Python with no extra language, and renders through Rust and Iced. If you need Android or iOS, Kivy is the better fit today. #### How is Pyzed different from PyQt and PySide? Qt is a large, mature C++ toolkit with native-looking widgets and a huge API. Pyzed is much smaller and simpler: you describe the interface from your state with plain Python functions, styles work like CSS, and components look modern out of the box. Pyzed is MIT licensed, while PyQt is GPL or commercial and PySide is LGPL. Qt still has more widgets, accessibility support and years of production use. #### How is Pyzed different from Flet? Flet builds the interface with Flutter and talks to it from Python, and can also target the web and mobile. Pyzed runs the interface in the same process through a Rust extension, with no separate client, and focuses on desktop apps with an IDE-like look, a visual designer and a code editor component. #### Is Pyzed fast? Layout, text shaping, syntax highlighting and drawing all happen in Rust, and the widget tree your `view()` returns is made of cheap immutable Rust objects. The GIL is released while the event loop runs, so your Python threads keep working. For large data, virtual lists and tables show hundreds of thousands of rows and `@pz.memo` skips rebuilding parts of the view that did not change. See [Performance](https://albakore.github.io/pyzed/guides/performance/) and [How it works](https://albakore.github.io/pyzed/guides/how-it-works/). #### Is there a visual GUI designer? Yes. [Pyzed Design](https://albakore.github.io/pyzed/editor/) lets you draw windows and reusable components by drag and drop, edit their properties and styles, write event handlers in a Python editor with completion and diagnostics, and run them with F5. Forms are saved as a readable `.pzform` file plus a plain Python file. [Download it](https://albakore.github.io/pyzed/download/) for Windows, macOS or Linux. #### Can I make interfaces with effects and animations? Yes. Any widget can be animated with presets, keyframes and transitions, and controls can have rotation, blur, glow, bloom, glass and opacity effects, with light, dark and custom themes. See [Animation and effects](https://albakore.github.io/pyzed/animation/) and [Themes](https://albakore.github.io/pyzed/theming/themes/). #### Does Pyzed work with AI coding agents? Yes. `pyzed mcp` (install `pyzed-ui[mcp]`) is an [MCP server](https://albakore.github.io/pyzed/guides/mcp/) that gives agents such as Cursor, VS Code or Zed the exact signature of every component, these docs, best practices, code checks and screenshots of the result. These docs are also published for language models at [llms.txt](https://albakore.github.io/pyzed/llms.txt) and [llms-full.txt](https://albakore.github.io/pyzed/llms-full.txt), and Pyzed Design has a built-in [Agent panel](https://albakore.github.io/pyzed/editor/ai-agents/). #### Can I add AI features to my own app? Yes. [`pyzed.agents`](https://albakore.github.io/pyzed/guides/agents/) talks to Anthropic, OpenAI, Gemini, OpenRouter, Ollama and OpenAI-compatible servers, lets the model call your Python functions as tools, streams its answer into the interface and keeps API keys in the system keychain. #### How do I ship a Pyzed app to users? `pyzed build` packages a project as a standalone app (a folder or a single file) for Windows, macOS or Linux that runs on machines without Python. It uses PyInstaller underneath; build each platform on that platform or in a CI matrix. See [Packaging](https://albakore.github.io/pyzed/packaging/build/). #### Can I test a Pyzed app without a window? Yes. `app.screenshot(path)` renders the current state off-screen with the software renderer, with no display or GPU, so it works in CI. Your state and handlers are plain Python and are tested like any other code. See [Testing](https://albakore.github.io/pyzed/guides/testing/). #### Does Pyzed support screen readers? Not yet. Pyzed respects the system's reduce-motion setting and supports keyboard shortcuts, but screen reader support depends on Iced and is still missing. See [Accessibility](https://albakore.github.io/pyzed/guides/accessibility/). #### Do I need to know Rust? No. You only write Python: classes, functions and lambdas. Rust is only needed if you build Pyzed itself from source or [contribute](https://albakore.github.io/pyzed/contributing/) a new native widget. ## Contributing ### Contributing to Pyzed > Contribute to Pyzed: set up a Python and Rust development environment, run the checks CI runs, and follow the commit and pull request conventions. Source: https://albakore.github.io/pyzed/contributing/ Thanks for helping! This section is the developer's guide to Pyzed: how to build it from source, how the code is organized, how to [add a component to the visual editor](https://albakore.github.io/pyzed/contributing/adding-components/), how to [document it](https://albakore.github.io/pyzed/contributing/documenting-components/) and how to [edit this site](https://albakore.github.io/pyzed/contributing/docs/). The short version also lives in [`CONTRIBUTING.md`](https://github.com/albakore/pyzed/blob/main/CONTRIBUTING.md) at the root of the repository. #### Development setup You need **Python 3.10+**, **Rust 1.88+** (`rustup update` upgrades an older toolchain) and, on Linux, the usual X11/Wayland development libraries. Pyzed is a Rust extension (PyO3 + Iced) with a Python package on top, built with [maturin](https://www.maturin.rs). With [uv](https://docs.astral.sh/uv/): ```bash git clone https://github.com/albakore/pyzed cd pyzed uv sync # creates .venv, builds the Rust core (release) and installs pyzed editable uv run pytest tests uv run python examples/gallery.py uv run pyzed design # the visual editor, Pyzed Design ``` With pip: ```bash python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install maturin pytest ty maturin develop --release # builds the Rust core and installs pyzed in the venv pytest tests ``` - Always build with `--release`: debug builds of Iced are too slow to use a real window. - On Linux a build from source plays no sound by default, so it builds without extra system packages: `pyzed.audio` renders offline and on its silent clock. For audio devices and MIDI, install ALSA's headers (`sudo apt install libasound2-dev pkg-config`; Fedora: `sudo dnf install alsa-lib-devel`) and build with `maturin develop --release --features alsa` (with uv, keep `MATURIN_PEP517_ARGS="--features alsa"` exported and run `uv sync --reinstall-package pyzed-ui`). - Changes to Python files under `python/pyzed/` are picked up directly (the package is installed in editable mode). - After changing **Rust** code under `src/`, build again: `maturin develop --release` (with uv, the next `uv run` or `uv sync` rebuilds the core on its own). #### Checks Run the same checks as CI before you push: ```bash cargo fmt --check # Rust formatting cargo clippy -- -D warnings # Rust lints, warnings are errors pytest tests # or: uv run pytest tests ``` The test suite runs off-screen: windows are rendered with `App.screenshot` into PNG files, so it needs no GPU or display. `tests/test_visual.py` compares screenshots with reference images; when you change how a component looks, regenerate them with `pytest tests/test_visual.py --update-snapshots` (see [Testing](https://albakore.github.io/pyzed/contributing/testing/)). Optional extras: ```bash uv run ty check # type checks the Python package python docs/scripts/gen_api.py --check # public names without API documentation ``` CI runs the lint job, the tests on Linux, macOS and Windows, a preview of the release notes and the **PR title** check. Documentation-only changes skip the build. #### The docs site The documentation is an [Astro Starlight](https://starlight.astro.build) site in `docs/`: ```bash cd docs npm install npm run dev # http://localhost:4321/pyzed/ npm run build # the static site in docs/dist (what GitHub Pages publishes) ``` Both scripts first run `scripts/sync-changelog.mjs`, which turns `CHANGELOG.md` into the [What's new](https://albakore.github.io/pyzed/changelog/) page. See [Editing the documentation](https://albakore.github.io/pyzed/contributing/docs/) for the layout of the site, translations and the scripts that regenerate the reference and the images. #### Project architecture ```mermaid flowchart LR subgraph RS["src/ (Rust, PyO3 + Iced)"] W["widget.rs
Widget, Kind"] --> R["render.rs
Kind → iced::Element"] A["app.rs
messages, Command, run, screenshot"] end subgraph PY["python/pyzed/"] UI["ui/
ready-made components"] --> CORE["pyzed._core"] D["designer/
Pyzed Design"] --> UI APP["app.py, theme.py,
component.py…"] --> CORE end CORE --> W ``` | Path | What it holds | | --- | --- | | `src/` | Rust core (PyO3 + Iced): the widget tree, the renderer bridge, the runtime and the code editor | | `python/pyzed/` | Python package: `App`, themes, icons, `pz.Component`, CLI | | `python/pyzed/ui/` | The ready-made components (`ui.button`, `ui.select`, `ui.table`…), plain functions returning widgets | | `python/pyzed/designer/` | The visual editor: form model, toolbox registry, document commands, runtime, panels | | `tests/` | pytest suite (off-screen, no GPU) | | `examples/` | runnable examples and the tutorials' code | | `docs/` | this site, with its generator scripts in `docs/scripts/` | | `scripts/` | release and maintenance scripts | Read [How it works](https://albakore.github.io/pyzed/guides/how-it-works/) for the bridge between Python and Iced and [Pyzed Design → How it is built](https://albakore.github.io/pyzed/editor/files-and-scripting/#how-it-is-built) for the classes of the editor. #### Commits and pull requests Pyzed follows [FastAPI](https://github.com/fastapi/fastapi)'s style: every commit on `main`, that is, every **PR title**, starts with a [gitmoji](https://gitmoji.dev) followed by a short sentence in the imperative, in English, with no trailing period: ```text ✨ Add a Rating component to the designer 🐛 Fix line numbers when the editor wraps lines 📝 Document the Rating component ``` PRs are merged with **Squash and merge**, so the title becomes the commit on `main` and the line in the release notes. The emoji picks the section of the notes: | Emoji | Use it for | Release notes section | | --- | --- | --- | | 💥 | A breaking change (also explain it in the PR) | Breaking Changes | | ✨ | A new feature, widget or component | Features | | 🐛 🚑️ 🔒️ | A bug fix, a critical hotfix, a security fix | Fixes | | ♻️ ⚡️ 🎨 🔥 | Refactors, performance, structure, removing code | Refactors | | ⬆️ ⬇️ 📌 | Upgrading, downgrading or pinning a dependency | Upgrades | | 📝 ✏️ | Documentation, typos | Docs | | 🌐 | Translations of the docs | Translations | | ✅ 👷 💚 🔧 🔨 | Tests, CI, configuration, development scripts | Internal | The **PR title** workflow (`.github/workflows/pr-title.yml`) rejects titles that do not start with a gitmoji (or a shortcode such as `:sparkles:`) and a space. The workflow for a change: 1. Fork the repository and create a branch from `main` (`feature/rating`, `fix/editor-scroll`, `docs/tutorial-typo`). For anything bigger than a small fix, open an issue first. 2. Keep each PR focused on one change. Add or update tests for behavior changes and docs for user-facing ones. 3. Run `cargo fmt --check`, `cargo clippy -- -D warnings` and `pytest tests`. 4. Open the PR with a gitmoji title and fill in the template: what changes, how it was tested (screenshots for UI changes) and the checklist. Link the issue it closes (`Closes #123`). 5. Push follow-up commits to the same branch; there is no need to squash them yourself. Commits must be authored by you: do not add `Co-authored-by` trailers or generated-by footers. #### Where to go next - [Adding a component to the editor](https://albakore.github.io/pyzed/contributing/adding-components/): from a `pyzed.ui` function to a toolbox entry with properties, events and tests, with a worked example. - [Documenting a component](https://albakore.github.io/pyzed/contributing/documenting-components/): docstrings, API reference, component library, screenshots, translations and the changelog. - [Editing the documentation](https://albakore.github.io/pyzed/contributing/docs/): how this site is organized and built. - [Testing](https://albakore.github.io/pyzed/contributing/testing/): testing `ui` components and the visual regression tests with their reference images. ### Adding a component to the editor > Add a component to Pyzed step by step: a pyzed.ui function and its Rust widget, a toolbox entry with properties and events, saved forms and tests. Source: https://albakore.github.io/pyzed/contributing/adding-components/ This guide walks through adding a new control to **Pyzed Design**, the visual editor in `python/pyzed/designer/`, end to end. The running example is a **Rating** control: a row of stars that raises a `change` event with the number of the clicked star. (Image: The Rating control running in a form) #### How a component travels Everything the editor knows about a control comes from one `Component` record in `python/pyzed/designer/components.py`. The same `render` function draws the control on the canvas (inert) and in the running app (live), so what you design is exactly what runs. ```mermaid flowchart LR UI["pyzed.ui function
(ui/*.py)"] --> SPEC["designer.Component
+ Prop, events"] SPEC -->|register| REG["registry
categories()"] REG --> TB["Toolbox
(panels.toolbox)"] TB -->|drag / Document.add| CV["Canvas
RenderContext (inert)"] CV --> PG["Property grid
Prop.kind editors"] CV -->|Document.save| PZ[".pzform
props + events"] CV -->|Document.wire| PY["handlers .py
control_event() stub"] PZ --> RT["FormApp / pyzed run
LiveContext"] PY --> RT ``` | Stage | Code | What happens | | --- | --- | --- | | Widget | `python/pyzed/ui/` | A plain function returns a `Widget` built from `pyzed._core` (Rust). | | Toolbox entry | `designer/components.py` | `Component(...)` describes the type, label, icon, category, size, props and events; `register()` adds it. | | Toolbox | `designer/panels.py` | `toolbox()` lists `categories()` with each entry's `icon` and `label` (translated with `tr`) and its `description` as tooltip. | | Canvas | `designer/surface.py` | Draws every control with `components.render(control, RenderContext(...))`; callbacks are inert. | | Property grid | `designer/panels.py` | One editor per `Prop`, chosen by its `kind`, grouped by its `group`. | | Saving | `designer/model.py` | `Control.to_dict()` writes `type`, geometry, `props` and `events` into the `.pzform` JSON. | | Handlers | `designer/document.py`, `designer/code.py` | `Document.wire(name, event)` names the handler `_` and appends a stub to the `.py` file. | | Running | `designer/runtime.py` | `LiveContext.on()` stores the new value in the prop and calls the wired handler with `(form, event)`. | :::tip[Built in, or just for your app?] This guide adds a **built-in** component to Pyzed itself. To use a control only in your own app you need none of the repository changes: call `designer.register(...)` from your code (see the [worked example](#worked-example-a-rating-control)), write a [`pz.Component`](https://albakore.github.io/pyzed/guides/components/) class (it joins the toolbox on its own), or draw one in the [component designer](https://albakore.github.io/pyzed/editor/component-designer/). The user-facing side is described in [Files, templates and scripting](https://albakore.github.io/pyzed/editor/files-and-scripting/#adding-your-own-components). ::: #### 1. The `pyzed.ui` component Most designer controls wrap a function from `pyzed.ui`, so the control is also available to people who write their UI in code. Put it in the module that fits (`inputs.py`, `controls.py`, `display.py`…) and follow the conventions of the package: - It is a **plain function** returning a `Widget`; the value comes first, then the callback, then keyword-only options. - The last parameter is `theme: Optional[Theme] = None`, resolved with `t = _theme(theme)`; every color comes from the theme (`t.color("accent")`, `t.text_muted`, `t.ghost_hover`…), never a hard-coded hex. - Callbacks receive the new value; with no callback the control is read only. - A one-line docstring that says what it draws, then what the callback receives. ```python # python/pyzed/ui/controls.py def rating( value: float, on_change: Any = None, *, max: int = 5, # noqa: A002 - mirrors ui.progress size: float = 16, tint: str = "accent", theme: Optional[Theme] = None, ) -> _core.Widget: """A row of ``max`` stars with the first ``value`` lit, in the ``tint`` color. ``on_change`` is called with the number of the clicked star (1 to ``max``); without it the stars are read only. """ t = _theme(theme) stars = [] for i in range(1, int(max) + 1): lit = i <= value star = _icon("star", size=size, color=t.color(tint) if lit else alpha(t.text_muted, 0.5)) stars.append(_core.button(star, (lambda i=i: on_change(i)) if on_change else None, padding=2, bg="transparent", hover_bg=t.ghost_hover, radius=t.radius_sm)) return _core.row(stars, spacing=0, align="center") ``` `controls.py` already imports `_core`, `_icon`, `alpha`, `Theme` and `_theme`. Then export it from `python/pyzed/ui/__init__.py`: add `rating` to the `from .controls import (...)` list **and** to `__all__`. Every name in `__all__` must be documented (see [Documenting a component](https://albakore.github.io/pyzed/contributing/documenting-components/)). ##### When the widget needs Rust You only touch `src/` when no existing building block can draw the control: a new Iced widget, a native interaction or something that must be fast. `pyzed._core` exposes a small set of primitives (`text`, `row`, `column`, `container`, `button`, `text_input`, `slider`, `progress_bar`, `mouse_area`, `stack`, `svg`, `code_editor`…), and most `ui` components, like Rating, are compositions of them. A native widget is wired in four places. This is how `progress_bar` does it: 1. **A variant of `Kind`** in `src/widget.rs`, holding plain Rust data (the widget tree is immutable and lives in Rust): ```rust pub enum Kind { // ... ProgressBar { min: f32, max: f32, value: f32, width: Option, height: Option, style: ToggleStyle, radius: f32 }, } ``` 2. **A `#[pyfunction]` builder** in `src/widget.rs` (or `src/extra.rs`) that converts Python values with the helpers in `src/style.rs` and returns `new(py, Kind::...)`. Callbacks are stored as `PyMsg` (`PyMsg::maybe(&on_change)`): ```rust #[pyfunction] #[pyo3(signature = (value, *, min=0.0, max=1.0, width=None, height=None, accent=None, bg=None, radius=2.0))] pub fn progress_bar(py: Python<'_>, value: f32, min: f32, max: f32, width: Any<'_>, height: Any<'_>, accent: Any<'_>, bg: Any<'_>, radius: f32) -> PyResult> { new(py, Kind::ProgressBar { min, max, value, width: style::opt_length(&width)?, height: style::opt_length(&height)?, style: toggle_style(&accent, &bg, &None, &None, &None)?, radius }) } ``` 3. **Registration** in the module's `register()` function (`widget::register`, called from `src/lib.rs`): `m.add_function(wrap_pyfunction!(progress_bar, m)?)?;`. It is then `pyzed._core.progress_bar`. 4. **A match arm** in `build()` in `src/render.rs` that turns the `Kind` into an `iced::Element` every frame. Interactions become a `Message` (`Message::Py(msg)`, `Message::Slide(msg, value)`, `Message::Toggle`…), which `src/app.rs` turns into a call of the Python callback. Rebuild with `maturin develop --release`, run `cargo fmt` and `cargo clippy -- -D warnings`, and wrap the new primitive in a `pyzed.ui` function as above. [How it works](https://albakore.github.io/pyzed/guides/how-it-works/) explains the bridge in more detail. #### 2. The toolbox entry Built-in controls live in `python/pyzed/designer/components.py`: a render function and an entry in the tuple that is registered at the end of the module. ##### The render function ```python def _rating(c: Control, ctx: RenderContext) -> Any: p = c.props return _fill(rating(_num(p["value"]), ctx.on(c, "change", "value") if c.enabled else None, max=int(_num(p["max"], 5)), size=_num(p["size"], 16), tint=p["tint"], theme=ctx.theme), c) ``` (`from ..ui.controls import rating`, or call it as `ui.rating` once it is exported.) - `c` is the `Control` being drawn. `c.props` already has every property: `components.render()` merges the component's defaults under the saved values, so `p["max"]` never raises `KeyError`, even for forms saved before the property existed. - **Values from the property grid or the JSON can be strings or `None`.** Coerce them with the module's helpers: `_num(value, default)`, `_lines(value)` for `list` props, `_align(value)`. - The function is given the control's size: fill it (`_fill(widget, c)` wraps the widget in a `width="fill", height="fill"` container, aligned to the start and centered vertically). - **Use `ctx.on(c, event, prop)` for every callback.** In the designer it returns an inert function. At run time (`LiveContext`) it stores the callback's argument in `c.props[prop]` and then calls the handler wired to `event`, with that value as `event.value`. Pass `None` when the control is disabled (`c.enabled`). - `ctx.theme` is the form's theme; `ctx.live` is `False` on the canvas (use it to draw design-time hints, as the layout containers do); `ctx.key(c)` is a stable id for widgets with internal state (text inputs, editors). - Do not draw the user's style yourself: `components.render()` applies the control's **Style** section (`designer/styling.py`) on top of what you return. ##### The `Component` record ```python Component( type="rating", label="Rating", icon="star", category="Common", render=_rating, size=(132, 28), props=(Prop("value", "number", 3, min=0, max=10, group="Data"), Prop("max", "number", 5, min=1, max=10, group="Data"), Prop("size", "number", 16, min=8, max=48), Prop("tint", "choice", "accent", choices=TINTS)), events=("change",), description="A row of stars to rate something; change gives the number of stars.", ), ``` Add it to the `for _component in (...)` tuple, next to the controls of its category; the toolbox shows the categories, and the components inside them, in registration order. | Field | Meaning | | --- | --- | | `type` | Stable id saved in `.pzform` files and used to name new controls (`rating1`, `rating2`…). **Never rename it** once released: old forms would fail to load (`KeyError: unknown component type`). | | `label` | Name in the toolbox and at the top of the property grid. | | `icon` | A [Lucide](https://lucide.dev) icon name, used in the toolbox and the outline (`pyzed icons` lists them). | | `category` | Toolbox section: `Common`, `Display`, `Containers`, `Layout`, `Navigation`, `Components`, or a new one. | | `render` | `render(control, ctx) -> Widget`, as above. | | `size` | Default `(width, height)` when dropped on the form. | | `props` | The properties shown in the property grid, in order (see below). | | `events` | The control's own events. The first one is the **default event**, opened by double-clicking the control. | | `description` | One sentence, shown as the toolbox tooltip and under the label in the property grid. | | `visual` | `False` for controls that draw nothing at run time (like `timer`): they get no mouse events. | | `container` | `"free"`, `"flex"` or `"grid"` when other controls can be dropped inside it (see `designer/layout.py`). | | `child_layout` | Layout options given to new controls of this type, such as `(("grow", 1),)` for the spacer. | ##### Properties `Prop(name, kind, default, label="", choices=(), min=None, max=None, step=1.0, group="Appearance")`: | `kind` | Editor in the property grid | Value | | --- | --- | --- | | `text` | single-line text box | `str` | | `multiline` | multi-line text box | `str` | | `number` | number box with steppers (`min`, `max`, `step`) | number, or `None` when the default is `None` | | `bool` | switch | `bool` | | `color` | color swatch and picker | theme token or hex string (`""` for none) | | `choice` | drop-down of `choices` | `str` | | `list` | list editor, one item per line | `str` with one item per line | | `rows` | table editor, comma-separated rows, the first one the header | `str` | | `icon` | Lucide icon picker | icon name (`""` for none) | - `group` places it in a section of the grid: `Appearance` (default), `Data` (the values a user changes, like `text`, `value`, `selected`), `Layout` or `Behavior`. - The grid shows `label`, or the name with underscores as spaces (`line_numbers` → *Line numbers*). - Name the prop that changes when the user interacts after what it holds (`value`, `checked`, `selected`, `text`) and pass that name as the third argument of `ctx.on`, so `form.rating1.value` is always current. ##### Events - Name events with the existing verbs when they fit: `click`, `double_click`, `change`, `submit`, `release`, `tick`. `EVENT_DOCS` in `components.py` holds the docstring of the generated stub for each event name; add an entry if you introduce a new one. - Every visual component also gets `COMMON_EVENTS` (`mouse_enter`, `mouse_leave`, `right_click`) automatically (`Component.all_events`); the runtime wraps the control in a `mouse_area` when they are wired. Do not list them yourself. ##### Translations and hot reload - The designer's own texts are translated in `python/pyzed/designer/locales/es.json`. Add the Spanish label and any new category or property title under `"designer"` (the English text is the key): `"Rating": "Valoración"`. Property titles are shared, so `Value`, `Max`, `Size` and `Tint` are already there. - If users change a property by interacting (like Rating's `value`), add it to `_VALUE_PROPS` in `python/pyzed/designer/runtime.py` (`"rating": ("value",)`): hot reload then keeps what the user picked when the form or its code is saved. #### 3. What the editor saves and generates Placing a Rating, naming it and double-clicking it gives this in the `.pzform` file (only `type`, the geometry, `props` and `events` depend on your component): ```json { "name": "rating1", "type": "rating", "x": 16, "y": 48, "width": 132, "height": 28, "props": { "value": 3, "max": 5, "size": 16, "tint": "accent" }, "events": { "change": "rating1_change" }, "anchor": ["left", "top"], "visible": true, "enabled": true, "locked": false } ``` and this stub in the form's handlers module (`Document.wire` → `code.ensure_function`, with the docstring from `EVENT_DOCS`): ```python def rating1_change(form, event): """Runs when the value of rating1 changes (``event.value``).""" pass ``` At run time, handlers read and write the props as attributes: `form.rating1.value`, `form.rating1.max = 10`, `form.rating1.hide()`. Because `props` are saved as plain JSON, keep their values JSON-friendly (strings, numbers, booleans, `None`), and when you add a property later give it a default that keeps old forms looking the same. #### 4. Tests Tests live in `tests/` and run off-screen. `test_every_component_has_defaults_and_renders` in `tests/test_designer.py` already renders every registered component with its defaults, so a broken `render` fails right away. Add tests for what is specific to your control: ```python import pyzed as pz from pyzed import designer, ui from pyzed.designer.document import Document from pyzed.designer.runtime import FormApp def test_rating_ui_component(): assert isinstance(ui.rating(3, lambda stars: None, max=5), pz.Widget) assert isinstance(ui.rating(0), pz.Widget) # read only def test_rating_in_the_toolbox(): spec = designer.component("rating") assert spec in designer.categories()["Common"] assert spec.default_event == "change" control = spec.create("stars", 0, 0) assert control.props == {"value": 3, "max": 5, "size": 16, "tint": "accent"} assert isinstance(designer.render(control, designer.RenderContext(pz.themes.ONE_DARK)), pz.Widget) def test_rating_runs_its_handler(tmp_path): doc = Document(designer.Form(name="review")) doc.add("rating", 16, 16) handler = doc.wire("rating1", "change") doc.set_handler_source(handler, "def rating1_change(form, event):\n form.rating1.max = 10\n") path = doc.save(tmp_path / "review.pzform") assert designer.load(path)["rating1"].events == {"change": "rating1_change"} rt = designer.open_form(path).runtime rt.ctx.on(rt.form["rating1"], "change", "value")(4) # what a click on the 4th star does assert rt.form["rating1"].props["value"] == 4 assert rt.form["rating1"].props["max"] == 10 assert rt.error is None def test_rating_screenshot(tmp_path): form = designer.Form(width=200, height=60) form.add(designer.component("rating").create("stars", 16, 16)) FormApp(form).screenshot(tmp_path / "rating.png", scale=1) assert (tmp_path / "rating.png").stat().st_size > 0 ``` - `rt.ctx.on(control, event, prop)(value)` simulates the user: it is exactly the callback the widget calls. - `Document` drives the editor without a window: `add`, `set_prop`, `wire`, `save`, `undo`… - `FormApp(...).screenshot(path, scale=1)` and `designer.Designer(path).screenshot(...)` render the running form and the whole editor off-screen. - The `conftest.py` fixture keeps the editor's preferences out of your real config folder. Run `pytest tests/test_designer.py -q` while you work and the whole suite before pushing. #### Worked example: a Rating control This is the complete example, as a single file you can run without touching the repository: it builds the `ui` function, registers the toolbox entry, creates a form with `Document`, wires the event, runs the form and takes screenshots. (In a pull request, the pieces go to the files shown in the steps above.) ```python # rating.py """A Rating control for Pyzed Design.""" from typing import Any, Optional from pyzed import _core from pyzed.designer import Component, Prop, register from pyzed.designer.components import TINTS, RenderContext from pyzed.designer.model import Control from pyzed.icons import icon as _icon from pyzed.theme import Theme, alpha from pyzed.ui.base import _theme def rating( value: float, on_change: Any = None, *, max: int = 5, # noqa: A002 - mirrors ui.progress size: float = 16, tint: str = "accent", theme: Optional[Theme] = None, ) -> _core.Widget: """A row of ``max`` stars with the first ``value`` lit, in the ``tint`` color. ``on_change`` is called with the number of the clicked star (1 to ``max``); without it the stars are read only. """ t = _theme(theme) stars = [] for i in range(1, int(max) + 1): lit = i <= value star = _icon("star", size=size, color=t.color(tint) if lit else alpha(t.text_muted, 0.5)) stars.append(_core.button(star, (lambda i=i: on_change(i)) if on_change else None, padding=2, bg="transparent", hover_bg=t.ghost_hover, radius=t.radius_sm)) return _core.row(stars, spacing=0, align="center") def _rating(c: Control, ctx: RenderContext) -> Any: p = c.props widget = rating(p["value"], ctx.on(c, "change", "value") if c.enabled else None, max=int(p["max"]), size=p["size"], tint=p["tint"], theme=ctx.theme) return _core.container(widget, width="fill", height="fill", align_y="center") RATING = register(Component( "rating", "Rating", "star", "Common", _rating, (132, 28), (Prop("value", "number", 3, min=0, max=10, group="Data"), Prop("max", "number", 5, min=1, max=10, group="Data"), Prop("size", "number", 16, min=8, max=48), Prop("tint", "choice", "accent", choices=TINTS)), ("change",), "A row of stars to rate something; change gives the number of stars.", )) ``` ```python # demo.py from pathlib import Path import rating # noqa: F401 - registers the component from pyzed import designer from pyzed.designer import Document HERE = Path(__file__).resolve().parent doc = Document(designer.Form(name="review", title="Review", width=360, height=150)) doc.add("label", 16, 16, 320, 24).props["text"] = "How was your stay?" doc.add("rating", 16, 48) doc.add("label", 16, 88, 320, 24) doc.rename("label2", "status") handler = doc.wire("rating1", "change") # "rating1_change", with a stub in review.py doc.set_handler_source(handler, ( "def rating1_change(form, event):\n" ' form.status.text = f"You gave it {event.value} stars"\n')) path = doc.save(HERE / "review.pzform") # writes review.pzform and review.py app = designer.open_form(path) rt = app.runtime rt.ctx.on(rt.form["rating1"], "change", "value")(4) # a click on the fourth star assert rt.form["status"].props["text"] == "You gave it 4 stars" app.screenshot(HERE / "rating-run.png", scale=2) # the image at the top of this page editor = designer.Designer(path) editor.doc.select("rating1") editor.screenshot(HERE / "rating-designer.png", size=(1280, 760), scale=2) ``` `python demo.py` writes the form, its handlers and the two screenshots. To use the control interactively, ```python import rating # noqa: F401 from pyzed import designer designer.design("review.pzform") # the editor, with Rating in the toolbox # designer.run("review.pzform") # or run the form ``` The selected Rating on the canvas, with its **Data** and **Appearance** properties in the grid: (Image: The Rating control selected in Pyzed Design) #### Checklist - [ ] `pyzed.ui` function with a docstring, exported in `ui/__init__.py` and `__all__` - [ ] Rust changes (if any) formatted and clippy-clean, rebuilt with `maturin develop --release` - [ ] `Component` entry in `designer/components.py`, with a stable `type`, a Lucide icon and a one-sentence description - [ ] Callbacks through `ctx.on(c, event, prop)`; values coerced with `_num`/`_lines` - [ ] Spanish texts in `designer/locales/es.json`; interactive props in `_VALUE_PROPS` - [ ] Tests in `tests/`, and `pytest tests` passes - [ ] Documentation: see [Documenting a component](https://albakore.github.io/pyzed/contributing/documenting-components/) - [ ] PR titled `✨ Add a Rating component` ### Documenting a component > Document a new Pyzed component: docstrings, the API reference, the component library page, screenshots, translations and the changelog. Source: https://albakore.github.io/pyzed/contributing/documenting-components/ A component is finished when people can find it and learn it without reading its code. Document it in the same pull request that adds it; reviewers check it. This page continues the Rating example from [Adding a component to the editor](https://albakore.github.io/pyzed/contributing/adding-components/). ```mermaid flowchart LR DS["Docstring
(the code)"] --> API["api_docs_en.py
api_docs_es.py"] --> GEN["gen_api.py"] --> REF["API reference
reference/*.md"] LIB["library_data.py"] --> LS["library_shots.py"] --> IMG["public/library/*.webp"] LIB --> GL["gen_library.py"] --> PAGES["Component library pages"] CS["component_shots.py"] --> CIMG["src/assets/components/*.png"] --> GUIDE["Components guides"] CL["CHANGELOG.md
[Unreleased]"] --> NEW["What's new"] ``` | What | Where | Generated by | | --- | --- | --- | | Docstring | the function or class in `python/pyzed/` | written by hand | | API reference | `docs/src/content/docs/[es/]reference/*.md` | `docs/scripts/gen_api.py` from `api_docs_en.py` / `api_docs_es.py` | | Component library | the *Component library* section of the site | `docs/scripts/gen_library.py` from `docs/scripts/library_data.py` | | Library previews (animated) | `docs/public/library/*.webp` | `docs/scripts/library_shots.py` | | Guide illustrations | `docs/src/assets/components/*.png` | `docs/scripts/component_shots.py` | | Editor screenshots | `docs/screenshots/*.png` | `examples/screenshots.py` | | Release notes | `CHANGELOG.md` → [What's new](https://albakore.github.io/pyzed/changelog/) | `docs/scripts/sync-changelog.mjs` on every build | Install Pyzed in your environment first (`uv sync` or `maturin develop --release`): every script imports the package, so signatures and images always match the code. #### 1. Docstrings Docstrings are the source of truth: the editor shows them, completion shows them and the API reference falls back to them. The repository's style: - **The first line is a full sentence** that says what it is or does, in the present tense: *"A row of ``max`` stars with the first ``value`` lit, in the ``tint`` color."* The first paragraph is what `gen_api.py` uses as a summary. - Then, if needed, a short paragraph about **callbacks and behavior**: what `on_change` receives, what happens without it. - Last, an **`Events:` section** that lists every `on_*` parameter with what its handler receives, written as a call (``` ``on_change(stars)`` ```, ``` ``on_press()`` ```). Editors show it on hover, and it is the same list as the **Events** section of the library page. - Code names use reST double backticks (```` ``value`` ````) and examples a `::` block; `gen_api.py` converts both to Markdown (an inline code span and a fenced `python` block). - No `Args:`/`Returns:` sections: parameters are described in `api_docs_*.py` (below), so the docstring stays short. - Designer `Prop`s have no docstring: the `description` of the `Component` (one sentence) is the toolbox tooltip and the subtitle in the property grid. ```python def rating(value: float, on_change: Any = None, *, max: int = 5, size: float = 16, tint: str = "accent", theme: Optional[Theme] = None) -> _core.Widget: """A row of ``max`` stars with the first ``value`` lit, in the ``tint`` color. ``on_change`` is called with the number of the clicked star (1 to ``max``); without it the stars are read only. Events: ``on_change(stars)``: Called with the number of the clicked star. """ ``` Widgets written in Rust (`pz.button`, `pz.slider`…) carry the same text as `///` comments above their `#[pyfunction]`; `scripts/gen_stubs.py` copies them into `_core.pyi` after `maturin develop`. #### 2. API reference The pages under [API reference](https://albakore.github.io/pyzed/reference/ui/) are generated. Never edit `docs/src/content/docs/reference/*.md` by hand: describe the function in `docs/scripts/api_docs_en.py` and its translation in `docs/scripts/api_docs_es.py`, then regenerate. Each file has a `PAGES` list; each page has `sections`, a list of `(title, {name: entry})` pairs. Add the entry to the section where it belongs (for `pyzed.ui` components: `Basics`, `Forms`, `Lists, tree and menus`, `Navigation`, `Workspace`, `Overlays`, `Code`): ```python # docs/scripts/api_docs_en.py, page "ui", section "Forms" "pyzed.ui.rating": { "desc": "A row of stars to rate something.", "params": { "max": "Number of stars.", "size": "Size of each star, in pixels.", "on_change": "Called with the number of the clicked star (1 to `max`). Without it the stars are read only.", }, }, ``` ```python # docs/scripts/api_docs_es.py, page "ui", section "Formularios" "pyzed.ui.rating": { "desc": "Una fila de estrellas para valorar algo.", "params": { "max": "Número de estrellas.", "size": "Tamaño de cada estrella, en píxeles.", "on_change": "Recibe el número de la estrella pulsada (de 1 a `max`). Sin él, las estrellas son de solo lectura.", }, }, ``` - **The signature is not written by hand**: `gen_api.py` reads it with `inspect`, from Python and from the Rust functions too (PyO3 exposes their signatures). - Parameters missing from `params` are looked up in `COMMON` at the top of the file (`value`, `tint`, `theme`, `width`, `on_press`…), so only describe what is specific. - An entry can also have `"example"` (a code block) and `"extra"` (Markdown after the table). - Designer API (`pyzed.designer.*`) entries are listed with `_doc(...)`, which uses the docstring. ```bash python docs/scripts/gen_api.py # both languages python docs/scripts/gen_api.py --lang en # only English (or --lang es) python docs/scripts/gen_api.py --check # lists public names without documentation ``` `--check` looks at everything exported in `__all__` of `pyzed`, `pyzed.ui` and `pyzed.designer`. CI runs it and reports what is missing as warnings, so a new export without an entry is visible in the PR. #### 3. The component library The [Component library](https://albakore.github.io/pyzed/library/) has one page per component, in the spirit of shadcn/ui or Chakra UI: a description, a preview in both themes with a **Preview/Code** tab pair, "When to use", usage, examples, tips, an API table and related components, plus a gallery of all of them grouped by category. The pages are generated from one data file, `docs/scripts/library_data.py`, by `docs/scripts/gen_library.py`; never edit `library/*.mdx` by hand. Document the function in the API reference first (step 2): the library's API tables reuse `gen_api.py`, so the signature and the parameter descriptions come from `api_docs_.py`. Then add an entry with `add(...)` to `library_data.py` (the order of the file does not matter; pages are sorted by name). Every text is a `T(en, es)` pair: ```python add( slug="rating", name="Rating", category="forms", api=["pyzed.ui.rating"], blurb=T("A row of stars to rate something.", "Una fila de estrellas para valorar algo."), desc=T( "Stars the user clicks to give a score. `on_change` receives the number of the clicked star; " "without it the stars are read only.", "Estrellas en las que el usuario hace clic para puntuar. `on_change` recibe el número de la " "estrella pulsada; sin él, las estrellas son de solo lectura.", ), when=T( ["To collect a quick score (reviews, feedback).", "To show a score next to an item, read only."], ["Para recoger una puntuación rápida (reseñas, opiniones).", "Para mostrar una puntuación junto a un elemento, en solo lectura."], ), preview={"code": """pz.column([ ui.rating(4, on_rate), ui.rating(3, on_rate, tint="warning", size=20), ui.rating(7, max=10), ], spacing=10)""", "size": (720, 200)}, usage="""class Review(pz.App): def __init__(self): self.stars = 0 def view(self): return ui.rating(self.stars, lambda stars: setattr(self, "stars", stars))""", related=["slider", "segmented"], badge="new", ) ``` | Key | Meaning | | --- | --- | | `slug` | URL and file name: `library/.mdx` and `public/library/.*.webp`. | | `name` | Title of the page and of its gallery card. | | `category` | A key of `CATEGORIES` (`actions`, `display`, `forms`, `feedback`, `data`, `lists`, `navigation`, `overlays`, `dialogs`, `workspace`, `code`, `layout`, `content`). | | `api` | The public names documented on the page, the main one first. | | `blurb`, `desc`, `when` | The gallery line, the lead paragraph and the "When to use" bullets. | | `preview` | `{"code": ..., "size": (w, h), "fill": False, "start": None}`: the snippet shown in the Code tab **and** drawn as the preview. | | `usage` | Optional fuller snippet, usually an `App` showing where the state lives. | | `examples` | Optional list of `{"title": T(...), "text": T(...), "code": ..., "size": (w, h)}`, each with its own image (`"shot": False` for code only). | | `notes` | Optional "Tips" (Markdown). | | `related`, `badge` | Optional related slugs; `"new"` adds a badge in the sidebar and the gallery. | **Events.** Every page gets an **Events** section, also generated: a table of the component's own events (its `on_*` parameters, with the description of the API reference), the common events of `.on()` and a **Handling the events** example in three tabs: imperative, declarative and functional. What each handler receives (`(text)`, `(x, y)`…) and the three examples live in `docs/scripts/library_events.py`: ```python HANDLERS = {"on_change": "(value)", "ui.rating.on_change": "(stars)", ...} ex("rating", """class Review(pz.App): ...""", # imperative: methods change self """@dataclass\nclass Rate: ...""", # declarative: messages and update() """@dataclass(frozen=True)\nclass State: ...""") # functional: view(state, set_state) ``` The imperative and declarative examples end with an `App` class; the functional one defines `view(state, set_state)` and a frozen `State`. `tests/test_library_events.py` runs the three of every page, and `--check` fails when a component with events has no examples. The page [Events](https://albakore.github.io/pyzed/library/events/) lists every component with its number of events. Snippets run with `pz`, `ui` and `draw` already imported; any other free name (`on_rate`, `save`…) is a do-nothing callback, so write them the way a reader would write them in an app; **the last expression is the widget**. Prose is Markdown inside MDX: write `{`, `}` and `<` only inside backticks. Render the previews, generate the pages and check them: ```bash python docs/scripts/library_shots.py rating # public/library/rating.dark.webp and rating.light.webp python docs/scripts/gen_library.py # library/*.mdx in both languages python docs/scripts/gen_library.py --check # coverage and consistency, writes nothing ``` Previews are animated WebP recorded off-screen at scale 2 with [`anim_webp.py`](https://albakore.github.io/pyzed/contributing/docs/#animated-captures), centered on the theme background, in One Dark and One Light (examples: `-..webp`; each with a still `*.still.webp` for reduced motion). By default the pointer hovers and presses the middle of the preview, and each variant is hovered and pressed (or clicked and typed into when it has a text field); a clip in which nothing changes plays a theme cycle instead. Give the new component a script in `docs/scripts/library_anim.py` when it does more than that (open a menu, drag a slider, type a query); `library_shots.py --probe rating` prints what a script can aim at. `--check` fails when a name of `pyzed.ui.__all__` is on no library page, when an entry points to an unknown name, category, related slug or clip, when a clip is missing or not animated, or when a generated page is out of date. Commit the clips and `public/library/manifest.json` with the data file. #### 4. Screenshots for the guides The topic guides under *Components* (`docs/src/content/docs/components/*.md`) show one image per page, drawn by `docs/scripts/component_shots.py`. To illustrate the new component there, add a function that returns the widgets and an entry in `SHOTS` with the image size in logical pixels: ```python def rating(): return pz.column([ ui.rating(3, noop), ui.rating(4, noop, tint="warning", size=20), ui.rating(2, max=10), ], spacing=10) SHOTS = { # ... "rating": (rating, (300, 140)), } ``` ```bash python docs/scripts/component_shots.py rating # docs/src/assets/components/rating.png ``` and reference it from the page with a relative path: `![Rating](../../../assets/components/rating.png)` (one more `../` in the Spanish page). The large screenshots of the editor (`docs/screenshots/`) are made by `examples/screenshots.py`. Its `designer-components.png` places **every registered component** on one form, so regenerate it when you add one to the toolbox: ```bash python examples/screenshots.py ``` Tips for good images: - Show the component **in context and in its states** (empty, selected, disabled, each variant), not just the default. - Use realistic text ("How was your stay?"), never "foo" or "test". - Keep the size tight: the scripts render at scale 2, so 300×140 logical pixels is a 600×280 image. - One Dark is the default theme of the images; the library pages also have One Light. #### 5. Guides and the editor docs Besides the generated pages, update the hand-written ones the component affects: - The **Components** guide page of its family (`components/forms.md`, `components/controls.md`…): add it to the example and to the *Component / Purpose* table. - The list of built-in controls in [Visual designer → Components](https://albakore.github.io/pyzed/guides/designer/#components) and the [Toolbox and canvas](https://albakore.github.io/pyzed/editor/toolbox-and-canvas/) page of the Pyzed Design manual, when it joins the toolbox. - The [Pyzed Design](https://albakore.github.io/pyzed/editor/) manual, if the control adds editor behavior (a new property editor, a new kind of container). #### 6. Translations The site is English first, with a Spanish translation under `docs/src/content/docs/es/` at the same paths. For every page you add or change: - Write the English page, then the Spanish one with the same file name. - Links include the locale: `/pyzed/guides/designer/` in English, `/pyzed/es/guides/designer/` in Spanish. - Relative image paths have **one more `../`** in Spanish pages, because they are one folder deeper. - Anchors come from the heading text, so a link to a section of a Spanish page uses the Spanish heading (`/pyzed/es/guides/components/#en-el-diseñador`). - Generated content has both languages in its data: `api_docs_es.py`, the Spanish fields of `library_data.py`, and the editor's own texts in `python/pyzed/designer/locales/es.json`. If you cannot write the Spanish version, say so in the PR: a missing page falls back to English with a notice, and a maintainer or another contributor can translate it in a 🌐 PR. #### 7. The changelog Release notes are generated from the PR titles when a version is released, so a `✨ Add a Rating component` title is usually enough. For a change that deserves more than one line, write the note by hand under `## [Unreleased]` in `CHANGELOG.md`, in the section it belongs to: ```markdown ## [Unreleased] ### Features - **Rating**: a row of stars in `pyzed.ui` (`ui.rating`) and in the designer's toolbox, with a `change` event. ``` Notes written under `[Unreleased]` are kept and merged when the release is cut. The site's [What's new](https://albakore.github.io/pyzed/changelog/) page is rebuilt from this file on every docs build. #### 8. Build and check the site ```bash cd docs npm install npm run dev # http://localhost:4321/pyzed/, reloads as you edit npm run build # the static site in docs/dist, as published on GitHub Pages ``` Before opening the PR: - `npm run build` finishes without errors (it fails on broken Markdown, MDX or missing images). - `python docs/scripts/gen_api.py --check` does not list your new names, and `python docs/scripts/gen_library.py --check` passes. - The new pages show up in the sidebar in both languages, and their links and images work in both. - The images were regenerated with the scripts, not taken by hand. See [Editing the documentation](https://albakore.github.io/pyzed/contributing/docs/) for the structure of the site, mermaid diagrams and the release flow. ### Editing the documentation > How the Pyzed documentation site is organized, how to preview it, how translations work and how to regenerate the API reference and screenshots. Source: https://albakore.github.io/pyzed/contributing/docs/ The site is built with [Astro Starlight](https://starlight.astro.build) and lives in `docs/`. It is published to GitHub Pages on every push to `main` and on every release. ``` docs/ ├── astro.config.mjs # sections (topics), redirects, theme, languages ├── screenshots/ # large screenshots (examples/screenshots.py) ├── scripts/ │ ├── api_docs_en.py # English API descriptions │ ├── api_docs_es.py # Spanish API descriptions │ ├── gen_api.py # generates reference/*.md in both languages │ ├── library_data.py # the component library: one entry per component │ ├── gen_library.py # generates library/*.mdx in both languages │ ├── shading_nodes_data.py # node catalog texts and examples (en/es) │ ├── gen_shading_nodes.py # generates shading/nodes/*.mdx in both languages │ ├── anim_webp.py # records animated WebP captures (shared library + CLI) │ ├── library_anim.py # what each component's animated preview does │ ├── library_shots.py # generates public/library/*.webp (animated previews) │ ├── component_shots.py # generates src/assets/components/*.png │ ├── example_shots.py # generates example and tutorial screenshots │ └── sync-changelog.mjs # CHANGELOG.md → What's new page (both languages) + stable version └── src/content/docs/ # the pages (Markdown / MDX) ├── guides/ ... # English (default language, site root) └── es/ # Spanish translations, same paths ``` #### Preview ```bash cd docs npm install npm run dev # http://localhost:4321/pyzed/ ``` #### Languages The documentation is multilingual. English is the default language and is served at the site root; each translation lives in a folder named after its locale: | Language | Files | URL | | --- | --- | --- | | English | `src/content/docs/guides/canvas.md` | `/pyzed/guides/canvas/` | | Spanish | `src/content/docs/es/guides/canvas.md` | `/pyzed/es/guides/canvas/` | - A translation uses **the same path** as the English page, under `es/`. - When you add a page, add it in both languages. If a translation is missing, Starlight shows the English page with a notice instead of a 404. - Internal links include the locale: `/pyzed/guides/...` in English pages and `/pyzed/es/guides/...` in Spanish ones. - Relative paths to images and examples have one more `../` in the Spanish files, since they are one folder deeper. #### Sections and navigation The top-level sections (Learn, Library, Designer, Shading, Theming, Distribute, Showcase, Integrations, AI and Reference) are the `topics` list in `astro.config.mjs`, using the [starlight-sidebar-topics](https://starlight-sidebar-topics.netlify.app) plugin. Each one is a link in the header navbar (`src/components/Header.astro`) and has its own sidebar; on phones they appear at the top of the menu. A page belongs to the section whose sidebar lists it, usually through the folder it lives in: | Section | Folders | | --- | --- | | Learn | `getting-started/`, `tutorials/{basic,intermediate,advanced}/`, `guides/`, `why/`, `examples`, `faq` | | Library | `library/`, `blocks/` (and the older `components/`) | | Designer | `editor/`, `download` | | Shading, Theming, Distribute | `shading/`, `theming/`, `packaging/` | | Showcase | `showcase/`, `games/` | | Integrations, AI | `integrations/`, `ai/` | | Reference | `reference/`, `changelog`, `contributing/` | - The overview of a section gets a hero when its frontmatter has `section: { eyebrow, icon, lead }` (`src/components/PageTitle.astro`). - Link to pages with `` (`src/components/NavCards.astro`); a card without `href` shows as "coming soon". - Show screenshots and short recordings with `` (`src/components/Media.astro`): an image from `src/assets` (optionally with a `light` version) or an mp4/webm loop with a `poster`, plus a caption. - Write terminal commands with `` `` (`src/components/Commands.astro`, in an `.mdx` page). It draws the `uv`, `pip` and `python` tabs (synced across the site) and splits each into macOS/Linux and Windows PowerShell blocks when they differ. Write `{py}`, `{venv}`, `{activate}`, `{vpy}` and `{env NAME=value}` where the systems differ; the component fills in `python3`/`py`, `source .venv/bin/activate`/`.venv\Scripts\Activate.ps1` and so on. Commands that only exist on one system go in `` instead. - Theme swatches come from `src/components/theme-tokens.json` (written by `docs/scripts/gen_theme_tokens.py`), and the theming screenshots from `docs/scripts/theming_shots.py`. - **Moving a page:** `git mv` it in both languages, add the old path to `MOVED` in `astro.config.mjs` so the old URL redirects, and update the links to it (`git grep` the old path, including `docs/scripts` and `python/`). #### Diagrams Write diagrams as `mermaid` code blocks; the `astro-mermaid` integration renders them on the page and follows the light/dark theme: ````md ```mermaid flowchart LR view["App.view()"] --> tree[Widget tree] --> render[render.rs] --> iced[Iced] ``` ```` #### Regenerating the reference and images These need Pyzed installed (`uv sync`, or `maturin develop --release`). For a new component, follow [Documenting a component](https://albakore.github.io/pyzed/contributing/documenting-components/). ```bash python docs/scripts/gen_api.py # API reference, both languages python docs/scripts/gen_api.py --lang en # English only (or --lang es) python docs/scripts/gen_api.py --check # reports public names without documentation python docs/scripts/gen_shading_nodes.py # shader node catalog, both languages python docs/scripts/gen_shading_nodes.py --check # fails if the pages are stale or an example breaks python docs/scripts/library_shots.py # animated previews of the component library python docs/scripts/component_shots.py # component illustrations python docs/scripts/example_shots.py # example and tutorial screenshots python examples/screenshots.py # large screenshots ``` When you add a public function, describe its parameters in `docs/scripts/api_docs_en.py` and `docs/scripts/api_docs_es.py`, then regenerate the reference. CI runs `gen_api.py --check`. #### Animated captures `docs/scripts/anim_webp.py` records a Pyzed view as a looping animated WebP, with a scripted pointer, keyboard and clock. Every component of the [library](https://albakore.github.io/pyzed/library/) is recorded with it (`library_shots.py`), and any other page can use it for its own clips: animations, audio widgets, a form being filled in. It uses the same off-screen renderer as `pz.screenshot` (no window, display or GPU), through `pyzed._core.record`: the pointer's position is passed to every frame, so hover and pressed states and tooltips show up; events (press, release, wheel, keys, typed text) reach the widgets between frames; and time moves by exactly one frame per frame (animations, spinners, carets and presences follow it), so a clip comes out the same on every machine, however long a frame takes. ```python import pyzed as pz from pyzed import ui from anim_webp import record_webp, hover, press, type_text, drag, wait, leave class Login(pz.App): def __init__(self): self.email = "" def view(self): return pz.column([ ui.input(self.email, lambda v: setattr(self, "email", v), placeholder="Email"), ui.button("Sign in", lambda: None, variant="solid"), ], spacing=8, width=280) record_webp( Login(), [press("input"), type_text("ada@example.com"), hover("Sign in"), press(), wait(600), leave()], out="docs/public/media/login.webp", size=(360, 160), # canvas, in logical pixels (recorded at 2x) theme="One Light", crop="content", # trim to what changes, plus a margin poster="docs/public/media/login.still.webp", ) ``` `record_webp` takes an `App` (instance or class) or a function that returns the widget. Steps: | Step | What it does | | --- | --- | | `wait(ms)` | time passes (animations, spinners and carets move) | | `move(target, ms)`, `hover(target, ms, hold)` | the pointer glides to `target` (and stays) | | `press(target=None, hold)`, `click` | a left click, held for `hold` ms | | `drag(start, end, ms)` | press, glide, release (sliders, splitters, color fields) | | `type_text(text, cps)` | types into the focused field | | `key(name, mods="")` | one key: `"Enter"`, `"Tab"`, `"Down"`, `"a"` with `"ctrl"`... | | `scroll(lines, target=None)` | the wheel over `target` | | `leave(ms)` | the pointer leaves (hover states end) | | `call(fn)`, `set_state(**attrs)` | change the app's state between frames | | `theme(name)` | switch the app's theme | A **target** is a point `(x, y)`, a text shown in the view (`"Sign in"`: its center), `"input"` / `"input:1"` (the first / second text field), `"center"` (the middle of what is drawn), `"@0.25,0.5"` (a point relative to what is drawn), `(target, dx, dy)` to aim beside one, or `fn(ctx) -> (x, y)`. Widgets drawn without text (switches, sliders, dropdown items) are aimed at with points. Encoding: identical consecutive frames are merged into one longer frame, the clip loops forever, and frames are lossy (`quality=80`, `method=6`) or lossless, whichever is smaller (WebP's `allow_mixed`), which keeps text sharp. Aim for **150 KB or less** per clip and never more than **400 KB**; shorten the clip, crop it or lower `fps` (20 by default) if it grows. Views can read `clip_time()` (seconds since the clip started) to move things that would otherwise stand still. The lower-level pieces are there too: `record_frames()` (the frames, as RGBA), `images()`, `crop_box()`, `encode_webp()`, `fit_webp()` (lowers the quality until the file fits a size; `record.py` encodes its `anim=True` clips with it), `info(path)` (frames, duration and size of a WebP). From the command line: ```bash python docs/scripts/anim_webp.py out.webp --code 'ui.button("Save", None, variant="solid")' \ --steps 'hover("Save"); press(); wait(500)' --size 320x120 --crop --theme "One Light" ``` **On a page**, serve the files as they are, from `docs/public/`: Astro's image pipeline would turn an animated WebP into a still picture. `src/components/AnimatedImage.astro` writes the markup: the `` with its `width`, `height`, `alt`, `loading="lazy"` and `decoding="async"`, and the still frame for readers who ask for reduced motion: ```html Typing an email and signing in ``` `` uses it for the clips in `public/media/`, and the library's `src/components/library/Clip.astro` for the ones in `public/library/` (sizes from `public/library/manifest.json`). What each library preview does is in `docs/scripts/library_anim.py`; `library_shots.py --probe ` prints the texts and fields a script can aim at. #### What's new and versions ```mermaid flowchart LR C["Commits (gitmoji)"] --> PR["Pull request"] --> CI["CI: lint, tests, wheels"] PR --> MAIN["main"] MAIN --> CL["CHANGELOG.md"] T["tag vX.Y.Z"] --> R["GitHub Release + PyPI"] CL --> S["sync-changelog.mjs"] --> WEB["What's new + version badge"] MAIN --> D["docs.yml → GitHub Pages"] ``` `docs/scripts/sync-changelog.mjs` generates the [What's new](https://albakore.github.io/pyzed/changelog/) page in both languages from `CHANGELOG.md` (repository root) on every build. Each version is a `## [X.Y.Z] - YYYY-MM-DD` heading; versions with a suffix (`1.0.0-rc.1`, `0.2.0b1`) are marked as pre-releases. The most recent one without a suffix is shown as the **latest stable** release in the header, the home page and the What's new page. ### Testing > How the Pyzed test suite is organized, how to test UI components without a window and how visual regression tests and reference images work. Source: https://albakore.github.io/pyzed/contributing/testing/ The test suite lives in `tests/` and runs with pytest, off-screen: it needs no GPU and no display. ```bash pytest tests # everything (uv run pytest tests) pytest tests -m "not visual" # skip the screenshot comparisons pytest tests/test_ui_overlays.py # one file ``` #### Testing `ui` components The components in `pyzed.ui` are plain functions that return widgets. Widgets are opaque Rust objects, so to look inside them use the `ui_tree` helper: it swaps `pyzed._core` for a recorder while the test runs, and every widget a component builds becomes a `Node` with its `kind`, positional `args` and keyword `props`. ```python import pytest import pyzed as pz from pyzed.ui import navigation as nav from ui_tree import install @pytest.fixture def tree(monkeypatch): return install(monkeypatch) def test_tab_bar_selects_by_id(tree): selected = [] bar = nav.tab_bar([nav.Tab("a", "a.py")], "a", on_select=selected.append, theme=pz.themes.ONE_DARK) bar.button_with("a.py").args[1]() # press the tab assert selected == ["a"] assert bar.props["bg"] == pz.themes.ONE_DARK.tab_bar ``` `Node.find(kind, where)`, `find_all`, `walk`, `texts()` and `button_with(text)` search the tree. Pass an explicit `theme=` so the expected colors do not depend on the current theme. To check that a component really renders, build it inside an `App` and call `App.screenshot` (see the end of `tests/test_ui_workspace.py`). #### Visual regression tests `tests/test_visual.py` renders every scene in `tests/visual/scenes.py` with `App.screenshot` and compares it, pixel by pixel, with its reference image in `tests/visual/references/`. A pixel counts as changed when a channel differs by more than 16; the test fails when more than 0.05% of the pixels change. The references are rendered on **Linux x86-64 with the CPU renderer** (tiny-skia; `tests/conftest.py` sets `WGPU_BACKEND=dx12`, which has no backend on Linux, so iced falls back to it). Other platforms rasterize text slightly differently (ARM too), so there the comparisons are skipped unless `PYZED_VISUAL=1` is set. The visual tests need Pillow (`pip install pillow`; it is in the `test` group of `pyproject.toml`). ##### When a comparison fails The test writes two images to `tests/visual/output/` (ignored by git): - `.actual.png`: what the code renders now. - `.diff.png`: the reference, faded, with the changed pixels in red. In CI they are uploaded as the **visual-diffs-…** artifact of the Linux test job. ##### Updating the references If the change is intended (you changed a component's look on purpose, or merged a change that did), regenerate the references and commit them with your change: ```bash pytest tests/test_visual.py --update-snapshots # every scene that changed pytest tests/test_visual.py --update-snapshots -k palette # only the scenes matching "palette" git add tests/visual/references ``` `--update-snapshots` only rewrites the images that really changed, so unrelated references stay untouched. Run it on Linux (or in the Linux CI job's environment) so the images match what CI renders. When two branches change the same reference, resolve the conflict by regenerating it after the merge. ##### Adding a scene Add a function to `tests/visual/scenes.py` that returns an `App`, decorated with `@scene`: ```python @scene("badges", size=(320, 120)) def badges(): return _app(lambda: pz.container(pz.row([ui.badge("new", tint="accent"), ui.count_badge(3)], spacing=8), padding=16)) ``` Then create its reference with `pytest tests/test_visual.py --update-snapshots -k badges` and commit the PNG. Keep scenes small and deterministic: no clocks, random data or files outside the repository. Animations are rendered in their final state. A reference whose scene was removed makes `test_every_reference_has_a_scene` fail, so delete the image together with the scene. ### SEO and llms.txt > How the Pyzed docs are optimized for search engines and AI answer engines: meta tags, social cards, JSON-LD, hreflang, sitemap, robots.txt and llms.txt. Source: https://albakore.github.io/pyzed/contributing/seo/ The documentation is built to be found by search engines (Google, Bing...) and to be read and cited by AI answer engines and coding assistants. Almost everything is generated at build time from the pages themselves, so a new page gets it for free. Your part is to write a good `title` and `description`. #### Writing a good description Every page needs a `description` in its frontmatter. It becomes the ``, the text under the title in search results and social previews, the summary of the page in `llms.txt` and the `description` of its structured data. - **120 to 160 characters.** Shorter ones get padded with a generic sentence; longer ones are cut by search engines. - **Say what the reader gets**, with the words people search for: *Python desktop GUI*, *native*, *Rust*, *visual designer*, *tkinter alternative*, the component or feature name. Use them naturally, once. - **Unique per page**, and translated (not copied) in the Spanish page. - Quote it when it contains a colon: `description: "Pyzed themes: One, Ayu and Gruvbox..."`. Component pages in the library are generated: their description is the component's `blurb` from `docs/scripts/library_data.py` plus a sentence added by `seo_description()` in `gen_library.py`. The changelog descriptions live in `docs/scripts/sync-changelog.mjs`. #### What is generated | What | Where it comes from | | --- | --- | | ``, meta description, canonical URL, Open Graph and Twitter cards, keywords | `docs/src/routeData.ts` (Starlight [route middleware](https://starlight.astro.build/guides/route-data/)) | | Social card images (1200×630) | `docs/scripts/gen_og.py` writes `docs/public/og/<section>[.es].png` and the app icons; re-run it after changing screenshots | | JSON-LD structured data | `routeData.ts`: `Organization` and `WebSite` everywhere, `SoftwareSourceCode` and `SoftwareApplication` (Pyzed Design, with the version from `src/generated/release.json`) on the home page, `BreadcrumbList` and `TechArticle` on every docs page, `FAQPage` on the [FAQ](https://albakore.github.io/pyzed/faq/) | | `hreflang` alternates | Starlight, filtered by `routeData.ts` so a page only lists the languages it is really translated to. An untranslated page shown in Spanish points its canonical URL to the English one and is `noindex` | | `sitemap-index.xml` | `@astrojs/sitemap` in `astro.config.mjs`, with both languages and each page's `<lastmod>` from the git history of its file (the docs workflow checks out the whole history; the same history gives the *Last updated* date of every page) | | `robots.txt`, `humans.txt`, `.well-known/security.txt`, `manifest.webmanifest` | Static files in `docs/public/` | | `llms.txt`, `llms-full.txt`, `llms-small.txt`, `llms/<section>.txt`, `es/llms.txt`, `es/llms-full.txt` | Endpoints in `docs/src/pages/`, built from the content collection by `docs/src/lib/llms.ts` | | `<page>.md` (raw Markdown of every page) | `docs/src/pages/[...page].md.ts` | #### llms.txt and Markdown pages [llms.txt](https://llmstxt.org) is a plain Markdown map of a site for language models. Pyzed publishes: - [`/pyzed/llms.txt`](https://albakore.github.io/pyzed/llms.txt): what Pyzed is, key facts, and every page with a one-line description, grouped by section. Spanish: [`/pyzed/es/llms.txt`](https://albakore.github.io/pyzed/es/llms.txt). - [`/pyzed/llms-full.txt`](https://albakore.github.io/pyzed/llms-full.txt): the whole English documentation in one file. Spanish: [`/pyzed/es/llms-full.txt`](https://albakore.github.io/pyzed/es/llms-full.txt). - [`/pyzed/llms-small.txt`](https://albakore.github.io/pyzed/llms-small.txt): only what is needed to write an app (getting started, tutorials, guides, AI, FAQ). - `/pyzed/llms/<section>.txt`: one section, for example [`llms/library.txt`](https://albakore.github.io/pyzed/llms/library.txt). - Every page as Markdown: replace the trailing slash with `.md`, for example [`/pyzed/guides/layout.md`](https://albakore.github.io/pyzed/guides/layout.md) (`/pyzed/index.md` for the home page). Each HTML page links to it with `<link rel="alternate" type="text/markdown">`. The Markdown is cleaned up: frontmatter, imports and MDX components are removed, tabs and asides become plain text, the example files that tutorials import with `?raw` are inlined as code blocks, images keep only their alt text, and links become absolute. New sections appear automatically; to give one a name and a place in the order, add it to `SECTIONS` in `docs/src/lib/llms.ts`. Check the result after building: ```bash cd docs npm run build less dist/llms.txt dist/guides/layout.md ``` #### Other checks - **One `h1` per page.** Starlight renders the title as the `h1`; start the content at `##`. - **Alt text for every image**: `![What the screenshot shows](...)`. It is also what assistants read in place of the image. - **Internal links** use the full path with the base: `/pyzed/guides/layout/` (`/pyzed/es/...` in Spanish). - **robots.txt** only works at the root of a host. While the docs are served from `albakore.github.io/pyzed/`, the copy in `docs/public/robots.txt` documents the policy (all search engines and AI crawlers are welcome); the sitemap is also linked from every page, and can be submitted in Google Search Console and Bing Webmaster Tools. #### Getting indexed Search engines only show pages they know about, and a new project has no links pointing to it yet. Two things happen on every deploy of `main` (`.github/workflows/docs.yml`): - **IndexNow.** `docs/scripts/indexnow.py` hashes every page in the sitemap, publishes the hashes as `page-hashes.json` and, once the site is deployed, sends the pages that differ from the live copy to [IndexNow](https://www.indexnow.org). Bing (which also feeds ChatGPT search, Copilot and DuckDuckGo), Yandex, Seznam, Naver and Yep then crawl them within hours. The key is the file `docs/public/<key>.txt`; it is public by design. - **Ownership checks.** The repository variables `GOOGLE_SITE_VERIFICATION`, `BING_SITE_VERIFICATION` and `YANDEX_VERIFICATION` (*Settings → Secrets and variables → Actions → Variables*) become the `google-site-verification`, `msvalidate.01` and `yandex-verification` meta tags of every page. To register the site once: 1. In [Google Search Console](https://search.google.com/search-console), add the URL-prefix property `https://albakore.github.io/pyzed/`, choose **HTML tag** and save the `content` value as the `GOOGLE_SITE_VERIFICATION` variable. Re-run the **Docs** workflow, verify, and submit `https://albakore.github.io/pyzed/sitemap-index.xml` under **Sitemaps**. 2. In [Bing Webmaster Tools](https://www.bing.com/webmasters), import the site from Search Console (or use the `BING_SITE_VERIFICATION` variable the same way) and submit the same sitemap. 3. Give the project links search engines trust: the repository's **About → Website** pointing to the docs and topics such as `python`, `gui`, `desktop-app`, `rust`, `iced`; the PyPI page (its links come from `[project.urls]` in `pyproject.toml`); and posts or listings that link to the docs. Pyzed shares its import name with the `pyzed` module of the Stereolabs ZED SDK, so the docs say clearly that the package is `pyzed-ui` and name what people search for: *Python GUI*, *desktop app*, *Rust*, *Iced*. ## Changelog ### What's new > Pyzed changelog and release notes: new components, designer features, improvements, fixes and breaking changes in every version of the Python GUI framework. Source: https://albakore.github.io/pyzed/changelog/ <!-- Generated by docs/scripts/sync-changelog.mjs from CHANGELOG.md. Do not edit by hand. --> The latest stable release is **1.9.0** (2026-10-22). ```bash pip install --upgrade pyzed-ui==1.9.0 ``` Release notes are also on [GitHub Releases](https://github.com/albakore/pyzed/releases). #### [1.9.0] - 2026-10-22 <span class="release-tag stable">Latest stable</span> ##### Highlights - **A code editor that knows your code**: hover cards with types and docs, go to definition, find references, rename across the project, quick fixes (ruff, ty, auto-imports, `uv add`) and sticky scroll. - **A Figma-style Properties panel**: numbers you drag, position and size first, auto layout for flex and grid containers, a margin/padding box model, file and folder pickers and animations on events. - **Dates, files and the system**: date, range and time pickers, a calendar, a file picker, a drop zone, popups and `pyzed.system` (system info, launcher, network and power monitors). - **Settings that follow you**: one Settings dialog with search, user profiles, per-project settings, Reset to default and the project's renderer (GPU or CPU) in `pyproject.toml`. ##### Features - ✨ IDE features in the code editor: hover, go to definition (F12, Ctrl+click), type definition, back/forward, find references, rename symbol (F2), quick fixes (Ctrl+.) and sticky scroll; the language servers fall back to the designer's Python when the project has no `.venv`. PR [#183](https://github.com/albakore/pyzed/pull/183) by [@albakore](https://github.com/albakore). - ✨ Component icons take the color of their role (layout, input, action, navigation, display, media, system, custom) in the toolbox, the outline, the tray and the Properties/Events headers, and the Design tab is now called Toolbox. PR [#184](https://github.com/albakore/pyzed/pull/184) by [@albakore](https://github.com/albakore). - ✨ One Settings dialog with search, user profiles, per-project settings and Reset to default; a project picks its renderer (GPU or CPU, graphics API, GPU and vsync) in `pyproject.toml`, and the MCP server reads and changes it. PR [#185](https://github.com/albakore/pyzed/pull/185) by [@albakore](https://github.com/albakore). - ✨ Keyboard focus for selects, switches, check boxes, sliders, segmented controls and radio groups, `pz.focusable` for any control, and tooltip shortcuts shown as key caps from the keymap. PR [#186](https://github.com/albakore/pyzed/pull/186) by [@albakore](https://github.com/albakore). - ✨ A Figma-style Properties panel: draggable numbers, Position first with align buttons, Text/Fill/Border/Spacing sections, auto layout, group and ungroup (Ctrl+G, Ctrl+Shift+G), path pickers for every file or folder property and animations on events. PR [#188](https://github.com/albakore/pyzed/pull/188) by [@albakore](https://github.com/albakore). - ✨ `ui.Calendar`, `ui.DatePicker`, `ui.DateRangePicker`, `ui.TimePicker`, `ui.file_input`, `ui.DropZone`, `pz.popup` and `pyzed.system` (`SystemInfo`, `Launcher`, `NetworkMonitor`, `PowerMonitor`), with the toolbox split into Forms, Date & time and System. PR [#189](https://github.com/albakore/pyzed/pull/189) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 A WSL project opened from Windows still lists its tools when Windows can't reach the distro. PR [#180](https://github.com/albakore/pyzed/pull/180) by [@albakore](https://github.com/albakore). - 🐛 A project's components load whenever it opens, and a missing one is drawn as a warning instead of breaking the form. PR [#182](https://github.com/albakore/pyzed/pull/182) by [@albakore](https://github.com/albakore). - 🐛 Windows updates no longer keep the old version: the installer clears the old files before copying, and Restart to update stops child processes, retries the folder swap and says so if it fails. PR [#192](https://github.com/albakore/pyzed/pull/192) by [@albakore](https://github.com/albakore). - 🐛 Component descriptions show without raw reST marks: inline code in the Properties header and clean tooltips. PR [#187](https://github.com/albakore/pyzed/pull/187) by [@albakore](https://github.com/albakore). ##### Refactors - ♻️ Custom events have their own place in the Events tab, apart from the built-in ones. PR [#181](https://github.com/albakore/pyzed/pull/181) by [@albakore](https://github.com/albakore). #### [1.8.1] - 2026-10-22 ##### Highlights - **Updates that get out of the way**: the update button moves to the top right of the title bar with a subtle blink, new versions download in the background, and Pyzed Design restarts cleanly on Windows after updating. ##### Features - ✨ The update button sits in the title bar next to View, Help and Settings and blinks softly while it waits; the version found by the automatic check downloads in the background, the check runs again every 4 hours while the designer is open, a finished download is reused and old downloads are cleaned up. PR [#178](https://github.com/albakore/pyzed/pull/178) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Restarting after an update on Windows no longer fails with `'NoneType' object has no attribute 'flush'` or opens several console windows. PR [#178](https://github.com/albakore/pyzed/pull/178) by [@albakore](https://github.com/albakore). ##### Docs - 📝 The README is in English (Spanish in `README.es.md`), the repository has a `CITATION.cff`, and the docs sitemap carries real dates, unique titles and image descriptions. PR [#177](https://github.com/albakore/pyzed/pull/177) by [@albakore](https://github.com/albakore). #### [1.8.0] - 2026-10-22 ##### Highlights - **A faster designer**: Pyzed draws only what changed (cached components, form controls and panels), skips rebuilds for pointer moves that change nothing and redraws big forms for less. - **Projects that stay in sync**: Pyzed Design warns when a project needs a newer `pyzed-ui` or a sync, and the Dependencies tab keeps itself up to date. ##### Features - ✨ Pyzed Design warns when a project needs a newer `pyzed-ui` or a sync, and keeps the Dependencies tab up to date. PR [#172](https://github.com/albakore/pyzed/pull/172) by [@albakore](https://github.com/albakore). - ✨ Hidden dialogs and drawers stay off the designer canvas until one is selected. PR [#171](https://github.com/albakore/pyzed/pull/171) by [@albakore](https://github.com/albakore). - ✨ Select and copy text in the Agent, Logs and Console panels, and close any tab. PR [#173](https://github.com/albakore/pyzed/pull/173) by [@albakore](https://github.com/albakore). - ⚡️ Pointer moves that change nothing no longer rebuild the view, and big forms are cheaper to redraw. PR [#174](https://github.com/albakore/pyzed/pull/174) by [@albakore](https://github.com/albakore). - ⚡️ Only what changed is drawn again: cached components, form controls and designer panels. PR [#175](https://github.com/albakore/pyzed/pull/175) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 New projects are no longer created in the installed app's folder (where they failed, or were deleted by the next update): Pyzed Design offers the last project's folder or `~/Pyzed Projects`, and refuses folders it cannot write to with a clear message. PR [#170](https://github.com/albakore/pyzed/pull/170) by [@albakore](https://github.com/albakore). ##### Docs - 📝 The docs reach search engines and AI answer engines sooner: new and changed pages are sent to IndexNow on every deploy, Google, Bing and Yandex ownership checks come from repository variables, the PyPI page links the docs, and the FAQ tells Pyzed apart from the ZED camera SDK's `pyzed` module. PR [#176](https://github.com/albakore/pyzed/pull/176) by [@albakore](https://github.com/albakore). #### [1.7.0] - 2026-10-22 1.6.0 was tagged but never published (its wheels failed to build), so this release carries all of it. ##### Highlights - **Sound in your apps**: `pyzed.audio`, an audio engine in Rust with 23 modules joined by cables (oscillators, filters, effects, sampler, drums, sequencer, MIDI) and low-latency output on every system, plus waveform, spectrum, knobs, faders, a webcam view and a VCV-style rack with animated cables. - **Export your app**: Pyzed Design builds apps for Windows, macOS and Linux on x64 and ARM with PyInstaller or Nuitka, with an icon and a splash screen. An Android prototype builds APKs too. - **Components that do work**: timers, shortcuts, file watchers, processes, workers and web requests as non-visual components in Pyzed Design, and `pyzed.services` in code. - **Hot reload that keeps your app running**: the state stays and new attributes, initial values and methods are picked up without restarting. - **A fuller designer**: every library component in the toolbox (Dock included) with its properties and events, the startup form, any Pyzed file open in the editor, and animations you start from code with an easier timeline. - **Tabs anywhere and WSL as Windows**: any tab goes to any dock or window of its own, and under WSL Pyzed opens its windows as native Windows programs. ##### Features - ✨ Put any tab anywhere: panels next to the forms, forms in docks, and a clearer tab drag. PR [#136](https://github.com/albakore/pyzed/pull/136) by [@albakore](https://github.com/albakore). - ✨ Interface and code fonts and sizes in the editor settings, and duplicating lines on every cursor. PR [#135](https://github.com/albakore/pyzed/pull/135) by [@albakore](https://github.com/albakore). - ✨ Non-visual components in Pyzed Design and `pyzed.services`: timers, shortcuts, file watchers, processes, workers, web requests and more. PR [#138](https://github.com/albakore/pyzed/pull/138) by [@albakore](https://github.com/albakore). - ✨ An audio engine, `pyzed.audio`: modules joined by cables, low-latency devices (WASAPI, CoreAudio, PulseAudio/PipeWire, ALSA), MIDI, offline rendering to WAV and analysis for visualizers. PR [#139](https://github.com/albakore/pyzed/pull/139) by [@albakore](https://github.com/albakore). - ✨ Audio UI components, a webcam view, a VCV-style rack with animated cables, audio demos and the Audio docs section. PR [#142](https://github.com/albakore/pyzed/pull/142) by [@albakore](https://github.com/albakore). - ✨ Windows open as native Windows windows when Pyzed runs in WSL (`pyzed windows setup`, `PYZED_WINDOWS=0` for WSLg). PR [#150](https://github.com/albakore/pyzed/pull/150) by [@albakore](https://github.com/albakore). - ✨ Every demo in the welcome gallery of Pyzed Design: audio, games, animation, styling, shaders, AI and more. PR [#153](https://github.com/albakore/pyzed/pull/153) by [@albakore](https://github.com/albakore). - ⚡️ Faster views with shared button parts, and the CPU renderer when the GPU is emulated. PR [#157](https://github.com/albakore/pyzed/pull/157) by [@albakore](https://github.com/albakore). - ✨ Export apps from Pyzed Design for Windows, macOS and Linux on x64 and ARM, with PyInstaller or Nuitka, icons and a splash screen. PR [#158](https://github.com/albakore/pyzed/pull/158) by [@albakore](https://github.com/albakore). - ✨ Android prototype: build APKs of Pyzed apps. PR [#159](https://github.com/albakore/pyzed/pull/159) by [@albakore](https://github.com/albakore). - ✨ Hot reload keeps the state and picks up new attributes, initial values and methods without restarting. PR [#164](https://github.com/albakore/pyzed/pull/164) by [@albakore](https://github.com/albakore). - ✨ Editor: choose the startup form, open every Pyzed file, indent with tabs and pace a held Backspace. PR [#166](https://github.com/albakore/pyzed/pull/166) by [@albakore](https://github.com/albakore). - ✨ The library's missing components in the designer's toolbox (Dock included), with their properties and events. PR [#165](https://github.com/albakore/pyzed/pull/165) by [@albakore](https://github.com/albakore). - ✨ Clearer animations: start them from code, an easier timeline and a "Start here" guide. PR [#167](https://github.com/albakore/pyzed/pull/167) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Claude Code, Codex and other agents start when Pyzed Design is opened from the desktop's menu (Node.js found where nvm, volta and fnm put it), **Retry** no longer repeats the message, and Pyzed's MCP server is added to Zed's settings even when they have comments. PR [#137](https://github.com/albakore/pyzed/pull/137) by [@albakore](https://github.com/albakore). - 🐛 The silent audio clock keeps real time when the system timer wakes late. PR [#144](https://github.com/albakore/pyzed/pull/144) by [@albakore](https://github.com/albakore). - 🐛 Pyzed builds from source on Linux without ALSA's headers: audio devices and MIDI come with the `alsa` feature, which the released wheels turn on. PR [#149](https://github.com/albakore/pyzed/pull/149) by [@albakore](https://github.com/albakore). - 🐛 Claude Code and Codex run their current ACP adapters, so the new models and effort levels show. PR [#151](https://github.com/albakore/pyzed/pull/151) by [@albakore](https://github.com/albakore). - 🐛 Your checkout and everything Pyzed Design starts run on Windows when they come from WSL. PR [#152](https://github.com/albakore/pyzed/pull/152) by [@albakore](https://github.com/albakore). - 🐛 The Windows core builds without Windows' folders on PATH, so maturin does not pick the Microsoft Store's `python.exe`. PR [#154](https://github.com/albakore/pyzed/pull/154) by [@albakore](https://github.com/albakore). - 🐛 Pyzed Design on Windows no longer freezes when it opens or creates a WSL project. PR [#155](https://github.com/albakore/pyzed/pull/155) by [@albakore](https://github.com/albakore). - 🐛 The agents' local servers no longer print dropped connections (`WinError 10054`) on Windows. PR [#156](https://github.com/albakore/pyzed/pull/156) by [@albakore](https://github.com/albakore). - 🐛 A release stays a draft until its files are attached, and the download page links only files that exist. PR [#163](https://github.com/albakore/pyzed/pull/163) by [@albakore](https://github.com/albakore). ##### Refactors - ♻ The new app template is centered with a flex box now that `pz.box` centers on the cross axis. PR [#132](https://github.com/albakore/pyzed/pull/132) by [@albakore](https://github.com/albakore). - ♻ The agent settings show the everyday ones first and fold the rest under **Advanced**. PR [#137](https://github.com/albakore/pyzed/pull/137) by [@albakore](https://github.com/albakore). ##### Docs - 📝 Game tutorials and the Animation docs section. PR [#146](https://github.com/albakore/pyzed/pull/146) by [@albakore](https://github.com/albakore). - 🍱 Pyzed Design captures retaken with the event tray. PR [#148](https://github.com/albakore/pyzed/pull/148) by [@albakore](https://github.com/albakore). - 📝 The events of every component, with imperative, declarative and functional examples. PR [#168](https://github.com/albakore/pyzed/pull/168) by [@albakore](https://github.com/albakore). ##### Internal - 📦 `pyzed-ui` installs only from the prebuilt wheels, never compiling the Rust core. PR [#140](https://github.com/albakore/pyzed/pull/140) by [@albakore](https://github.com/albakore). - ✅ The real-time audio test waits for the audio clock instead of a fixed sleep. PR [#141](https://github.com/albakore/pyzed/pull/141) by [@albakore](https://github.com/albakore). - 👷 CI decides which jobs run from `git diff`, so pull requests with over 300 files still run their tests. PR [#147](https://github.com/albakore/pyzed/pull/147) by [@albakore](https://github.com/albakore). - 💚 The release wheels build and test again: tests no longer share styles or need maturin, and the musllinux wheels build without ALSA. PRs [#162](https://github.com/albakore/pyzed/pull/162) and [#163](https://github.com/albakore/pyzed/pull/163) by [@albakore](https://github.com/albakore). #### [1.5.0] - 2026-10-20 ##### Highlights - **Program apps like the web**: hover, pressed and focus styles with transitions, CSS-like stylesheets, `pz.box` (block, flex, grid and stack layouts), media queries with `pz.viewport()`, and components with children and context. - **Errors never take the editor down**: a Logs panel, error boundaries, an error dialog and a selectable console. - **Agents you can follow**: every agent step shows live, each agent's modes and models in the composer, and Pyzed's MCP server is added to the coding agents and editors on this computer. ##### Features - ✨ Every agent step shows live, each agent's modes, models and options in the composer, a Thinking toggle, remembered Always allow permissions, and Pyzed's MCP server added to the AI agents and editors of this computer (Claude Desktop, Cursor, Windsurf, Gemini CLI, Codex, VS Code, Zed and more). PR [#123](https://github.com/albakore/pyzed/pull/123) by [@albakore](https://github.com/albakore). - ✨ Logs panel, error boundaries and an error dialog, a selectable console, the `ui.error_boundary`, `ui.error_card`, `ui.error_dialog` and `ui.ErrorInfo` components and a `readonly` code editor. PR [#124](https://github.com/albakore/pyzed/pull/124) by [@albakore](https://github.com/albakore). - ✨ State styles and transitions, CSS-like stylesheets, `pz.box`, media queries and breakpoints with `pz.viewport()`, children, context and `animate=` for components. PR [#125](https://github.com/albakore/pyzed/pull/125) by [@albakore](https://github.com/albakore). - ⚡ Pyzed Agent designs faster: it starts out knowing the toolbox, themes, `pz.ui` components and best practices, gets the open document with each message and lays out a whole form with one `add_controls` call. PR [#123](https://github.com/albakore/pyzed/pull/123) by [@albakore](https://github.com/albakore). - ✨ The agent's steps go to the Logs panel: each message, tool call and result, files, permissions and errors. PR [#126](https://github.com/albakore/pyzed/pull/126) by [@albakore](https://github.com/albakore). - ✨ MCP tools for timeline animations (`.pzanim`) and component states. PR [#127](https://github.com/albakore/pyzed/pull/127) by [@albakore](https://github.com/albakore). - ✨ The MCP writes apps like a web page: boxes, stylesheets, media queries and nesting components. PR [#128](https://github.com/albakore/pyzed/pull/128) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 `pz.box` shrinks, centers on the cross axis and sizes its children correctly. PR [#129](https://github.com/albakore/pyzed/pull/129) by [@albakore](https://github.com/albakore). - 🐛 Recent projects stay in `PYZED_CONFIG_DIR` when it is set. PR [#125](https://github.com/albakore/pyzed/pull/125) by [@albakore](https://github.com/albakore). ##### Docs - 📝 The error and AI agent components in the component library. PR [#130](https://github.com/albakore/pyzed/pull/130) by [@albakore](https://github.com/albakore). #### [1.4.0] - 2026-10-20 1.3.0 was tagged but never published: its Windows wheel build failed a test that is fixed in this version. Everything listed under 1.3.0 ships for the first time in 1.4.0. ##### Highlights - **Detachable panels and work layouts**: drag any panel out of Pyzed Design, stack panels together and save layouts for each kind of work. - **A Zed-like Agent panel**: new-thread menu, thread history, settings inside the panel, the ACP agent registry, skills, keep or reject each edit, external agent sign-in, follow-agent, a context meter, notifications, and threads and rules per project. - **Timeline animations**: `pz.Clip`, `pz.Track`, `pz.Call` and `AnimationPlayer`, saved as `.pzanim` and edited in the new Animation panel of `.pzform` files. - **Pyzed themes**: Midnight, Graphite, Forest, Rosé, Dawn and Glacier, plus property editors for each kind of value. - **Zed-like keymap**: `keymap.json` with contexts and a keymap editor. ##### Features - ✨ Zed-like keymap with contexts in `keymap.json` and a keymap editor. PR [#108](https://github.com/albakore/pyzed/pull/108) by [@albakore](https://github.com/albakore). - ✨ Forms kept in the project, mark or change the startup file and clone repositories. PR [#113](https://github.com/albakore/pyzed/pull/113) by [@albakore](https://github.com/albakore). - ✨ Detachable and stackable panels and work layouts. PR [#105](https://github.com/albakore/pyzed/pull/105), PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - ✨ Toolbar groups; New component moves to the toolbox and the New dialog. PR [#106](https://github.com/albakore/pyzed/pull/106), PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - ✨ Run/Stop and a search field in the middle of the title bar. PR [#107](https://github.com/albakore/pyzed/pull/107), PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - ✨ Zed-like Agent panel with history, settings, ACP registry, skills and keep/reject edits, and the reusable `ToolCall`, `Plan`, `EditsBar`, `ThreadHistory`, `AuthPrompt`, `ServiceRow`, `Catalog` and `context_meter` components. PR [#109](https://github.com/albakore/pyzed/pull/109), PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - ✨ Timeline animations with `pz.Clip`, `pz.Track`, `pz.Call`, `AnimationPlayer` and `.pzanim`. PR [#110](https://github.com/albakore/pyzed/pull/110), PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - ✨ States, sizes and events for the `ui` components; `pick_list` gains `on_open` and `on_close`. PR [#111](https://github.com/albakore/pyzed/pull/111), PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - ✨ WSL projects from Windows. PR [#115](https://github.com/albakore/pyzed/pull/115), PR [#119](https://github.com/albakore/pyzed/pull/119) by [@albakore](https://github.com/albakore). - ✨ External agent sign-in like Zed, follow-agent, context meter and notifications. PR [#116](https://github.com/albakore/pyzed/pull/116), PR [#119](https://github.com/albakore/pyzed/pull/119) by [@albakore](https://github.com/albakore). - ✨ Pyzed theme family and property editors for each kind of value. PR [#117](https://github.com/albakore/pyzed/pull/117), PR [#119](https://github.com/albakore/pyzed/pull/119) by [@albakore](https://github.com/albakore). - ✨ Animation panel in `.pzform`. PR [#118](https://github.com/albakore/pyzed/pull/118), PR [#119](https://github.com/albakore/pyzed/pull/119) by [@albakore](https://github.com/albakore). - ✨ Agents work on the active project: threads per project and project rules (`AGENTS.md`, `.rules`, `CLAUDE.md`...) in `pyzed.agents.rules`. PR [#120](https://github.com/albakore/pyzed/pull/120) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Agent sign-in on Windows sends the POST body correctly. PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - 🐛 Clone target folder on Windows paths and the agent registry cache on Windows. PR [#119](https://github.com/albakore/pyzed/pull/119) by [@albakore](https://github.com/albakore). #### [1.3.0] - 2026-10-20 ##### Highlights - **Shader graphs**: build shaders from nodes in the new Shader view of Pyzed Design, like Godot's visual shaders, saved as `.pzshad` and compiled to WGSL (`pyzed.shading`). - **Pyzed Design as an app**: native installers for Windows (per user, no admin), macOS (`.dmg`) and Linux (`install.sh`), a preview update channel and a welcome window with 24 example projects. - **Agents that build whole apps**: sign in to Claude in the browser or with a Claude subscription, `run_app` and a plan checklist in the Agent panel, and MCP tools that plan an app (`plan_app`), show it live while it is built and work in the project's environment (`.venv`, WSL). - **Projects in the title bar**: the project, branch and environment, with a project picker for open and recent projects. - **New docs site**: sections with a header navbar, every Designer view documented, a shader node catalog, basic, intermediate and advanced tutorials, every variant of each component and retaken screenshots and clips. ##### Features - ✨ Update the MCP server with everything new up to 1.2.0 and add project tools to build whole apps. PR [#86](https://github.com/albakore/pyzed/pull/86) by [@albakore](https://github.com/albakore). - ✨ Sign in to Claude in the browser, use a Claude subscription, and let agents build apps and control the editor. PR [#87](https://github.com/albakore/pyzed/pull/87) by [@albakore](https://github.com/albakore). - ✨ Add a welcome window with example projects to Pyzed Design, and run commands without console windows on Windows. PR [#88](https://github.com/albakore/pyzed/pull/88) by [@albakore](https://github.com/albakore). - 📦 Make Pyzed Design an installable app: native installers, a preview channel, and templates and translations as data. PR [#89](https://github.com/albakore/pyzed/pull/89) by [@albakore](https://github.com/albakore). - ✨ Add node-based shader graphs (`.pzshad`) and a Shader view to Pyzed Design, like Godot's visual shaders. PR [#90](https://github.com/albakore/pyzed/pull/90) by [@albakore](https://github.com/albakore). - ✨ Open pages and sign-ins in the Windows browser when Pyzed Design runs under WSL. PR [#92](https://github.com/albakore/pyzed/pull/92) by [@albakore](https://github.com/albakore). - ✨ Guide agents with `plan_app`, show the app live while they build it, and run project tools in the project's environment. PR [#102](https://github.com/albakore/pyzed/pull/102) by [@albakore](https://github.com/albakore). - ✨ Show the project, branch and environment in the title bar with a project picker. PR [#103](https://github.com/albakore/pyzed/pull/103) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Fix the white Retro screen shader template and tidy shader graph layouts. PR [#91](https://github.com/albakore/pyzed/pull/91) by [@albakore](https://github.com/albakore). - 🐛 Keep the folder browser working next to folders without permission, and close a dialog that fails to draw. PR [#93](https://github.com/albakore/pyzed/pull/93) by [@albakore](https://github.com/albakore). - 🐛 Fix `ui.button` style overrides such as `radius`, rotated images and the alpha of SVG tints. PR [#101](https://github.com/albakore/pyzed/pull/101) by [@albakore](https://github.com/albakore). ##### Docs - 💄 Restyle the docs and split them into sections with a header navbar. PR [#94](https://github.com/albakore/pyzed/pull/94) by [@albakore](https://github.com/albakore). - 📝 Document every Designer view and add a shader node catalog with recipes. PR [#95](https://github.com/albakore/pyzed/pull/95) by [@albakore](https://github.com/albakore). - 📝 Expand Theming and Distribute, and show uv/pip/python commands per platform. PR [#96](https://github.com/albakore/pyzed/pull/96) by [@albakore](https://github.com/albakore). - 📝 Add basic, intermediate and advanced tutorials and turn the Showcase into a gallery. PR [#97](https://github.com/albakore/pyzed/pull/97) by [@albakore](https://github.com/albakore). - 📸 Retake every docs screenshot, add node captures and short clips, and place them in the pages. PR [#98](https://github.com/albakore/pyzed/pull/98) by [@albakore](https://github.com/albakore). - 📝 Show every variant of each library component, with plain-words intros and a gallery index. PR [#99](https://github.com/albakore/pyzed/pull/99) by [@albakore](https://github.com/albakore). - 📝 Update the README with Pyzed Design, shaders, games, AI, integrations and new screenshots. PR [#100](https://github.com/albakore/pyzed/pull/100) by [@albakore](https://github.com/albakore). ##### Internal - 👷 Speed up CI: cache the docs images, run jobs only for the files they check, a smaller PR matrix and parallel tests. PR [#104](https://github.com/albakore/pyzed/pull/104) by [@albakore](https://github.com/albakore). #### [1.2.0] - 2026-10-20 ##### Highlights - **Games**: a game loop (`pz.frames`), held keys (`pz.pressed`), `on_key_up`, sprites and `pyzed.game`, with Snake, Breakout and Neon Invaders demos and tutorials. - **Shaders**: `pz.shader` paints a widget with a WGSL function on the GPU, with showcase demos (game HUD, main menu, inventory, glass dashboard). - **Integrations**: tutorials and runnable demos for FastAPI, SQLAlchemy, SQLModel, Django, httpx and Pydantic, with `pz.validation_errors` and `ui.field(error=)`. - **A stronger AI agent**: images, external MCP servers, external agents through ACP (Gemini CLI, Codex...), GitHub Copilot, Google Vertex AI, Claude through identity federation, sign-in from Pyzed Design and a Zed-like composer. - **Pyzed Design feels like Zed**: the project's files with Zed's right-click menu, any text file in the code editor, tabs you drag out into their own windows, a custom window frame, anchors that follow while you design, a compact details panel for properties and an icon picker. - **Faster by default**: `import pyzed` takes 78 ms instead of 178 ms, lighter view rebuilds and mimalloc in the native core. - **Why Pyzed**: a new docs section comparing Pyzed with tkinter, Qt, Kivy, Flet and others, with measured benchmarks. ##### Features - ✨ Edit component properties of any type in a compact details panel, kept in sync with the code. PR [#61](https://github.com/albakore/pyzed/pull/61) by [@albakore](https://github.com/albakore). - ✨ Add an icon picker with search and categories to the library and Pyzed Design. PR [#62](https://github.com/albakore/pyzed/pull/62) by [@albakore](https://github.com/albakore). - ✨ Sign in to AI providers from Pyzed Design and add a Zed-like agent composer. PR [#63](https://github.com/albakore/pyzed/pull/63) by [@albakore](https://github.com/albakore). - ✨ Show the project's files in Pyzed Design, with file search, a Dependencies tab and a folder browser. PR [#65](https://github.com/albakore/pyzed/pull/65) by [@albakore](https://github.com/albakore). - ✨ Add anchor presets, offsets, size limits, pivot and rotation to Position, and `pz.anchor_layout`. PR [#66](https://github.com/albakore/pyzed/pull/66) by [@albakore](https://github.com/albakore). - ✨ Add images, MCP servers, external agents, GitHub and Google sign-in and Claude identity federation to the AI agent. PR [#68](https://github.com/albakore/pyzed/pull/68) by [@albakore](https://github.com/albakore). - ✨ Add a game loop, held keys and sprites, with three game demos and tutorials. PR [#69](https://github.com/albakore/pyzed/pull/69) by [@albakore](https://github.com/albakore). - ✨ Add `pz.shader` and showcase demos: game HUD, main menu, inventory, glass dashboard and shaders. PR [#70](https://github.com/albakore/pyzed/pull/70) by [@albakore](https://github.com/albakore). - ✨ Add integration demos and tutorials for FastAPI, SQLAlchemy, SQLModel, Django and httpx, with `pz.validation_errors`, `ui.field(error=)` and object options in `ui.select`. PR [#71](https://github.com/albakore/pyzed/pull/71) by [@albakore](https://github.com/albakore). - ✨ Open any text file from the Project tab in the code editor, with a tab per file. PR [#76](https://github.com/albakore/pyzed/pull/76) by [@albakore](https://github.com/albakore). - ✨ Follow anchors while designing: resizing the form, a component or a container moves what is inside. PR [#77](https://github.com/albakore/pyzed/pull/77) by [@albakore](https://github.com/albakore). - ✨ Drag tabs out into their own windows and stack them back in Pyzed Design. PR [#79](https://github.com/albakore/pyzed/pull/79) by [@albakore](https://github.com/albakore). - ✨ Give the Project tree Zed's right-click menu: cut, copy, paste, duplicate, undo, trash, terminal, .gitignore and permalinks, with its keys. PR [#81](https://github.com/albakore/pyzed/pull/81) by [@albakore](https://github.com/albakore). - ✨ Add custom window frames: own title bar, platform window buttons, Zed-like menus and resize borders, used by Pyzed Design. PR [#83](https://github.com/albakore/pyzed/pull/83) by [@albakore](https://github.com/albakore). - ✨ Wheel scrolling, configurable key bindings and a sectioned settings dialog for the code editor. PR [#84](https://github.com/albakore/pyzed/pull/84) by [@albakore](https://github.com/albakore). - ⚡ Start faster and rebuild views with less work by default. PR [#82](https://github.com/albakore/pyzed/pull/82) by [@albakore](https://github.com/albakore). - 💄 Normalize margins and spacing across Pyzed Design's panels with shared spacing tokens. PR [#80](https://github.com/albakore/pyzed/pull/80) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Restart Pyzed Design after an update even while background work is running. PR [#60](https://github.com/albakore/pyzed/pull/60) by [@albakore](https://github.com/albakore). - 🐛 Write Optional props as `X | None` on every Python version. PR [#64](https://github.com/albakore/pyzed/pull/64) by [@albakore](https://github.com/albakore). - 🐛 Pick color properties with the color picker and keep half-typed colors from breaking the view. PR [#67](https://github.com/albakore/pyzed/pull/67) by [@albakore](https://github.com/albakore). - 🐛 `ui.select` hands `on_select` the chosen object (a model, an enum) instead of its label, as documented, and `Command.perform` calls its `then` when the function returns `None`. PR [#71](https://github.com/albakore/pyzed/pull/71) by [@albakore](https://github.com/albakore). - 🐛 Build with Rust 1.88 again: drop the if-let match guard and check the MSRV in CI. PR [#75](https://github.com/albakore/pyzed/pull/75) by [@albakore](https://github.com/albakore). - 🐛 Keep completion rows inside the list and let the list scroll. PR [#78](https://github.com/albakore/pyzed/pull/78) by [@albakore](https://github.com/albakore). ##### Docs - 📝 Add SEO/GEO, llms.txt, Why Pyzed with measured benchmarks, AI agents and Blocks docs. PR [#74](https://github.com/albakore/pyzed/pull/74) by [@albakore](https://github.com/albakore). - 📝 Redo the integration demos following the official tutorials, with an architectures page. PR [#73](https://github.com/albakore/pyzed/pull/73) by [@albakore](https://github.com/albakore). - 📝 Document the window frame, the editor's `keymap` and `scrollbar`, held keys and `on_key_up`, `pyzed.game` and the release secrets, and re-measure Pyzed's benchmarks with 1.2.0. PR [#85](https://github.com/albakore/pyzed/pull/85) by [@albakore](https://github.com/albakore). ##### Internal - 👷 Bundle the Agent panel's GitHub and Google sign-in apps into the release builds from Actions secrets. PR [#72](https://github.com/albakore/pyzed/pull/72) by [@albakore](https://github.com/albakore). #### [1.1.0] - 2026-10-19 ##### Highlights - **Large data**: virtual lists and tables that only build the rows in view (200 000 rows in 0.2 ms per view) and `@pz.memo` to reuse view subtrees. - **Real effects**: animate rotation, size, padding and font size, with real blur, backdrop blur and bloom on the GPU and in software. - **A better Pyzed Design**: copy and paste between forms, data binding and validation, and a theme and language preview. - **Accessibility**: Tab focus between fields, the system's reduce motion setting and a screen reader guide. - **Input methods**: Chinese, Japanese and Korean input in the code editor. - **Safer by default**: private agent threads, hidden secret files, a hardened sign-in and checked updates. - **More platforms**: wheels for musllinux, tested on Python 3.10 to 3.14 and on ARM Linux, Pyzed Design builds for Linux ARM and Intel Macs, and build attestations. ##### Breaking Changes - ⬆️ Pyzed now requires Python 3.10 or newer. PR [#52](https://github.com/albakore/pyzed/pull/52) by [@albakore](https://github.com/albakore). ##### Features - ✨ Add virtual lists and tables and memoized view subtrees for large data. PR [#55](https://github.com/albakore/pyzed/pull/55) by [@albakore](https://github.com/albakore). - ✨ Add copy and paste between forms, data binding and validation, and theme/language preview to Pyzed Design. PR [#56](https://github.com/albakore/pyzed/pull/56) by [@albakore](https://github.com/albakore). - ✨ Support input methods (IME) for Chinese, Japanese and Korean in the code editor. PR [#57](https://github.com/albakore/pyzed/pull/57) by [@albakore](https://github.com/albakore). - ✨ Animate rotation, size, padding and font size, with real blur, backdrop blur and bloom. PR [#58](https://github.com/albakore/pyzed/pull/58) by [@albakore](https://github.com/albakore). - ♿ Add Tab focus between fields, follow the system's reduce motion setting and document screen reader support. PR [#54](https://github.com/albakore/pyzed/pull/54) by [@albakore](https://github.com/albakore). ##### Fixes - 🔒️ Keep agent threads private (`0600`, with a *Keep conversations* switch), hide secret files such as `.env` and keys from agents, ignore forged sign-in callbacks and refuse updates without a checksum. PR [#52](https://github.com/albakore/pyzed/pull/52) by [@albakore](https://github.com/albakore). - 🐛 Read component props on Python 3.14, where annotations are lazy. PR [#52](https://github.com/albakore/pyzed/pull/52) by [@albakore](https://github.com/albakore). ##### Internal - 👷 Test on Python 3.10, 3.12 and 3.14 and on ARM Linux; build musllinux wheels, Pyzed Design for Linux ARM and Intel Macs, optional signing and notarization, and build provenance attestations. PR [#52](https://github.com/albakore/pyzed/pull/52) by [@albakore](https://github.com/albakore). - ✅ Add visual regression tests and tests for workspace, navigation, overlays and agent threads. PR [#53](https://github.com/albakore/pyzed/pull/53) by [@albakore](https://github.com/albakore). #### [1.0.1] - 2026-10-19 ##### Fixes - 🔒️ AI agents refuse tools marked `writes=True` when `confirm_writes` is on (the default) and no `confirm` callback was given, instead of running them without asking. Pass `confirm=` or set `confirm_writes=False` to keep the old behavior. - 🔒️ `pyzed mcp --transport streamable-http` no longer offers the `screenshot` tool, which runs the code it receives, unless you add `--allow-code`. Over stdio nothing changes. - 🐛 Pyzed Design refuses a `.pzform` or component saved by a newer Pyzed instead of opening it and losing what it does not know. - 🏷️ Type stubs for the Rust core (`_core.pyi`) and `py.typed`, so editors and type checkers see every widget and command. ##### Docs - 📝 Add the *Stability and known limitations* guide: the versioning and deprecation policy and what Pyzed does not do yet. #### [1.0.0] - 2026-10-19 First stable version. From 1.0.0 on, the public API follows semantic versioning: breaking changes only land in a new major version. ##### Highlights - **Animations and effects**: animate any widget property, with ready-made animations, transitions between windows and visual effects such as glow, bloom, glass and transparency, in the library and in Pyzed Design. - **AI agents**: an Agent panel in Pyzed Design and `pyzed.agents` in the library, to write code and generate components and forms, with browser login or personal tokens kept in the system keychain. `pyzed mcp` also exposes Pyzed to external agents. - **Built-in reference**: a searchable, Godot-style reference of every component and function, inside Pyzed Design and from the library. - **A Zed-grade code editor**: Zed's actions and keys, multiple cursors, find and replace. - **Updates**: Pyzed Design updates itself like Zed, with a What's new tab. - **New docs site**: a component library, the Pyzed Design manual and a download page. ##### Features - ✨ Add Zed-style updates to Pyzed Design with a What's new tab. PR [#40](https://github.com/albakore/pyzed/pull/40) by [@albakore](https://github.com/albakore). - ✨ Add an MCP server for AI agents (`pyzed mcp`). PR [#41](https://github.com/albakore/pyzed/pull/41) by [@albakore](https://github.com/albakore). - ✨ Add Zed's editor actions, keys, multiple cursors, find and replace, and opt-in telemetry. PR [#44](https://github.com/albakore/pyzed/pull/44) by [@albakore](https://github.com/albakore). - ✨ Add AI agents to Pyzed Design and the library (`pyzed.agents`). PR [#47](https://github.com/albakore/pyzed/pull/47) by [@albakore](https://github.com/albakore). - ✨ Add a built-in, searchable reference to the library and the designer, like Godot's. PR [#46](https://github.com/albakore/pyzed/pull/46) by [@albakore](https://github.com/albakore). - ✨ Add animations, transitions and visual effects (glow, bloom, glass) to widgets, windows and Pyzed Design. PR [#48](https://github.com/albakore/pyzed/pull/48) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Fix the MCP docs index after the docs redesign and document self-updates. PR [#45](https://github.com/albakore/pyzed/pull/45) by [@albakore](https://github.com/albakore). ##### Docs - 📝 Redesign the docs site with a component library, the Pyzed Design manual and a download page. PR [#42](https://github.com/albakore/pyzed/pull/42) by [@albakore](https://github.com/albakore). - 📝 Add the reference browser to the component library. PR [#49](https://github.com/albakore/pyzed/pull/49) by [@albakore](https://github.com/albakore). ##### Translations - 🌐 Translate the designer's context menu, code panel, color picker and status bar, and rename it to Pyzed Design. PR [#43](https://github.com/albakore/pyzed/pull/43) by [@albakore](https://github.com/albakore). #### [0.4.0] - 2026-10-19 ##### Highlights - **Component Designer**: draw reusable components with typed properties, state, methods and events, inherit from them and use them like any widget, with a Component view, tabs and a New component button. - **A much richer designer**: containers, dockable panels, menus, templates, a debug console, a Style section with a floating color picker, list and table editors, drag and drop from the outline, and a code editor with completion, diagnostics, folding and a minimap. - **Styles and layout**: CSS-like styles for every widget with global styles, gradients and masks, plus track grids, flex, resizable panels and a Zed-style dock. - **Apps and projects**: common events for every widget, translations, ready-made dialogs, uv projects and `pyzed build` to package an app. - **Full API docs**: every function, method, class and component is documented. ##### Features - ✨ Add uv projects, a Project tab in the designer and `pyzed build`. PR [#18](https://github.com/albakore/pyzed/pull/18) by [@albakore](https://github.com/albakore). - ✨ Add common events for every widget, custom components and quiet Linux/WSL startup. PR [#19](https://github.com/albakore/pyzed/pull/19) by [@albakore](https://github.com/albakore). - ✨ Add completion, diagnostics, code lens and settings to the designer's code editor. PR [#20](https://github.com/albakore/pyzed/pull/20) by [@albakore](https://github.com/albakore). - ✨ Add track grids, flex, resizable panels and a Zed-style dock. PR [#25](https://github.com/albakore/pyzed/pull/25) by [@albakore](https://github.com/albakore). - ✨ Add a component designer mode: draw reusable components with properties, state, methods and events. PR [#24](https://github.com/albakore/pyzed/pull/24) by [@albakore](https://github.com/albakore). - ✨ Add translations, ready-made dialogs and opening forms from code. PR [#26](https://github.com/albakore/pyzed/pull/26) by [@albakore](https://github.com/albakore). - ✨ Add auto-closing pairs, code folding, a minimap, font zoom and cursor shapes to the code editor. PR [#27](https://github.com/albakore/pyzed/pull/27) by [@albakore](https://github.com/albakore). - ✨ Add containers, dockable panels, menus, templates, a debug console and translations to the designer. PR [#28](https://github.com/albakore/pyzed/pull/28) by [@albakore](https://github.com/albakore). - ✨ Drag components and outline rows onto the form, and open context menus where the pointer is. PR [#30](https://github.com/albakore/pyzed/pull/30) by [@albakore](https://github.com/albakore). - ✨ Make designed components responsive, add a New component button and component inheritance. PR [#31](https://github.com/albakore/pyzed/pull/31) by [@albakore](https://github.com/albakore). - ✨ Add a grip tab to select and drag containers in the designer. PR [#32](https://github.com/albakore/pyzed/pull/32) by [@albakore](https://github.com/albakore). - ✨ Add CSS-like styles for every widget, global styles, gradients, masks and a color picker. PR [#33](https://github.com/albakore/pyzed/pull/33) by [@albakore](https://github.com/albakore). - ✨ Add a Style section and the form's global style to the designer, with the color picker. PR [#35](https://github.com/albakore/pyzed/pull/35) by [@albakore](https://github.com/albakore). - ✨ Add a Component view with tabs, typed properties, reusable methods and child access. PR [#37](https://github.com/albakore/pyzed/pull/37) by [@albakore](https://github.com/albakore). - ✨ Float the color picker, add list and table editors, and line up the Properties and Events docks. PR [#36](https://github.com/albakore/pyzed/pull/36) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Silence "MESA: error: ZINK" with `MESA_LOG=null` (`silent` is not a Mesa option). PR [#22](https://github.com/albakore/pyzed/pull/22) by [@albakore](https://github.com/albakore). - 🐛 Make a fixed grid height the height of each row with `columns=<int>`. PR [#29](https://github.com/albakore/pyzed/pull/29) by [@albakore](https://github.com/albakore). ##### Upgrades - ➕ Add ty to the development dependencies. PR [#23](https://github.com/albakore/pyzed/pull/23) by [@albakore](https://github.com/albakore). ##### Docs - 📝 Document setting up Pyzed with uv. PR [#21](https://github.com/albakore/pyzed/pull/21) by [@albakore](https://github.com/albakore). - 📝 Document every function, method, class and component. PR [#38](https://github.com/albakore/pyzed/pull/38) by [@albakore](https://github.com/albakore). #### [0.3.1] - 2026-10-18 ##### Fixes - 🐛 Read `pyzed.__version__` from the package metadata. PR [#17](https://github.com/albakore/pyzed/pull/17) by [@albakore](https://github.com/albakore). #### [0.3.0] - 2026-10-18 ##### Highlights - **Pyzed Designer**: a Visual Basic / Qt Designer style form editor (`pyzed design app.pzform`) with a toolbox, dot-grid snapping, alignment tools, property grid, events and per-event code editing. **F5** runs the form with all its logic. - **Hot reload**: `pyzed dev main.py` updates the open window on every save and keeps the app's state. - **Bilingual docs**: the site is in English by default, with a full Spanish translation under `/es/`. ##### Features - ✨ Add visual form designer with event code editing, grid snapping and alignment. PR [#15](https://github.com/albakore/pyzed/pull/15) by [@albakore](https://github.com/albakore). - ✨ Add hot reload for running apps (`pyzed dev`, `pz.run(App, reload=True)`, `PYZED_RELOAD=1`). PR [#14](https://github.com/albakore/pyzed/pull/14) by [@albakore](https://github.com/albakore). ##### Upgrades - ⬆️ Bump pyo3 from 0.27.2 to 0.30.1. PR [#11](https://github.com/albakore/pyzed/pull/11) by [@dependabot](https://github.com/apps/dependabot). - ⬆️ Bump two-face from 0.4.5 to 0.5.2. PR [#10](https://github.com/albakore/pyzed/pull/10) by [@dependabot](https://github.com/apps/dependabot). - ⬆️ Bump png from 0.17.16 to 0.18.1. PR [#9](https://github.com/albakore/pyzed/pull/9) by [@dependabot](https://github.com/apps/dependabot). - ⬆️ Bump the GitHub Actions group with 7 updates. PR [#12](https://github.com/albakore/pyzed/pull/12) by [@dependabot](https://github.com/apps/dependabot). ##### Docs - 📝 README images now show up on PyPI. ##### Translations - 🌐 Make the docs bilingual (English by default, Spanish under `/es`), with Mermaid diagrams and new screenshots. PR [#13](https://github.com/albakore/pyzed/pull/13) by [@albakore](https://github.com/albakore). ##### Internal - 👷 Speed up CI, gitmoji release notes, PyPI README images and contribution guidelines. PR [#8](https://github.com/albakore/pyzed/pull/8) by [@albakore](https://github.com/albakore). - 👷 Deploy the docs site only from `main`, not from release tags. PR [#7](https://github.com/albakore/pyzed/pull/7) by [@albakore](https://github.com/albakore). #### [0.2.0] - 2026-10-18 First version published on PyPI, as `pyzed-ui`. ##### Breaking Changes - 💥 `App.run()` raises an error when it is called a second time in the same process. ##### Features - ✨ Several windows per app: open and close extra windows, window hooks and window commands (`Window`). - ✨ Async support: coroutines, generators and async generators from `update` or `Command.perform`. - ✨ Subscriptions: timers with `pz.every` and streams with `pz.listen`. - ✨ New widgets: `image` (bitmaps), `image_viewer`, `canvas` with `pz.draw`, `rich_text`, `markdown`, `qr_code`, `pick_list`, `radio`, `split`, `pin`, `grid` and `responsive`; gradients and more style options. - ✨ New `pyzed.ui` components: `select`, `radio_group`, `number_field`, `split_button`, `chip`, `disclosure`, `callout`, `table`, `facepile`, `diff_stat`, charts, `spinner`, `popover`, `markdown`, `split` and `stat`. - ✨ Theming: import Zed themes, bundled Ayu, Gruvbox and One themes, `Theme.generate()` and automatic light/dark switching. - ✨ Editor: horizontal scrolling with a sticky line-number gutter, correct line numbers with `wrap`, active line, cursor autoscroll and PageUp/PageDown. ##### Fixes - 🐛 Images are clipped and rounded correctly in every renderer, including off-screen screenshots without a GPU. ##### Docs - 📝 Documentation site with guides, tutorials, API reference and component illustrations. - 📝 Examples based on Iced's demos (todos, game of life, clock, stopwatch, downloads, websocket, multiple windows, image viewer, markdown, QR code, styling, context menu, panes) and a five-step tutorial. ##### Internal - 👷 CI and release workflows: wheels for Linux, macOS and Windows published on PyPI. #### [0.1.0] - 2026-10-18 First version (not published on PyPI). ##### Features - ✨ Elm-style runtime on top of Iced 0.14 (`App`, `update`, `view`, `on_start`, `on_key`). - ✨ Messages can be any Python value, or callables that are called directly. - ✨ `Command`: `exit`, `focus`, `set_editor_text`, `set_editor_cursor`, `scroll_to_end`, `batch`, `message` and `perform` (work on a thread). - ✨ `pz.post()` to send messages from other threads. - ✨ Base widgets: `text`, `column`, `row`, `container`, `center`, `button`, `text_input`, `scrollable`, `space`, `rule`, `mouse_area`, `stack`, `opaque`, `hover`, `tooltip`, `checkbox`, `toggler`, `slider`, `progress_bar`, `code_editor`, `svg` and `icon`. - ✨ About 40 Zed-style components in `pyzed.ui`: buttons, forms, lists, file tree, tabs, workspace with docks, command palette, toasts, code editor and terminal. - ✨ One Dark, One Light and Gruvbox Dark themes with Zed's color tokens, and `Theme.derive()`. - ✨ Bundled IBM Plex fonts and Lucide icons. - ✨ Off-screen screenshots with `app.screenshot()` (no GPU or display needed). - ✨ CLI: `pyzed new` and `pyzed icons`.