# Pyzed (essentials) > 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. | ## 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. ## 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. ## 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/). ## 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.