# Pyzed > Pyzed es un framework de código abierto (MIT) para crear GUIs de escritorio nativas en Python. Python describe la interfaz; Iced, un toolkit de GUI acelerado por GPU escrito en Rust, la organiza y la dibuja. Busca ser un framework de frontend para apps de escritorio (más adelante móvil y web), con más de 90 componentes cuyo aspecto se inspira en el editor Zed, temas, shaders de GPU por nodos, empaquetado para todas las plataformas de escritorio y Pyzed Design, un diseñador visual de arrastrar y soltar al estilo de Visual Basic, Qt Designer y Godot. ## Empezar ### Introducción > Qué es Pyzed: un framework de GUI de escritorio para Python renderizado de forma nativa en Rust (Iced), con componentes modernos listos para usar, y cuándo usarlo. Source: https://albakore.github.io/pyzed/es/getting-started/introduction/ **Pyzed** es una librería para crear interfaces de escritorio en Python con el rendimiento de [Iced](https://iced.rs), un toolkit de GUI escrito en Rust. Incluye una colección de componentes inspirados en el editor [Zed](https://zed.dev): pestañas, árbol de archivos, paleta de comandos, editor de código, terminal, menús, notificaciones y un workspace con paneles acoplables. (Image: Un editor de código hecho con Pyzed (tema One Light)) :::note[Dónde está Pyzed hoy] Pyzed 1.x es estable para apps de escritorio en Windows, macOS y Linux: su API pública sigue el versionado semántico. `pyzed.agents`, `pyzed.mcp` y las animaciones siguen siendo provisionales, y móvil y web están planificados pero todavía no están disponibles. Mira [Estabilidad y limitaciones conocidas](https://albakore.github.io/pyzed/es/guides/stability/). ::: El estilo de la interfaz se inspira en el editor Zed, y Pyzed Design, el diseñador visual, en los editores de formularios y escenas de Godot, Unreal Engine, Visual Basic y Qt Designer. #### Cómo funciona Tu código Python describe **qué** se ve. Rust se encarga de **cómo** se dibuja. ``` Python Rust (pyzed._core, PyO3) Iced 0.14 App.view() ──► árbol de Widget ──► render.rs: Widget → iced::Element ──► layout + wgpu/tiny-skia App.update() ◄── mensajes ◄────── app.rs: Message, Command → Task ◄──── eventos (ratón, teclado) ``` - `view()` devuelve un árbol de `Widget`. Son objetos inmutables de Rust, así que construirlo es barato. - Cuando el usuario interactúa, Pyzed llama a tu `update(mensaje)` o directamente al callable que pasaste (`on_press=self.guardar`). Después vuelve a llamar a `view()`. - El layout, el texto, el resaltado de sintaxis y el dibujado ocurren en Rust. - El GIL se libera mientras corre el bucle de eventos, así que tus hilos de Python siguen trabajando. #### Cuándo usar Pyzed - Herramientas de escritorio, paneles internos, editores y utilidades que deben sentirse rápidas. - Interfaces densas al estilo de un IDE, con muchos paneles, listas y atajos de teclado. - Apps que quieres probar o documentar con capturas generadas sin pantalla. #### Qué necesitas saber Python básico: clases, funciones y lambdas. No hace falta saber Rust ni Iced. Continúa con la [instalación](https://albakore.github.io/pyzed/es/getting-started/installation/) o salta directamente al [inicio rápido](https://albakore.github.io/pyzed/es/getting-started/quickstart/). ¿Dudas sobre la licencia, las plataformas o cómo se compara Pyzed con tkinter, Kivy, PyQt o Flet? Mira las [preguntas frecuentes](https://albakore.github.io/pyzed/es/faq/). ### Instalación > Instala Pyzed con pip o uv en Windows, macOS y Linux. Los wheels precompilados incluyen el motor en Rust: solo necesitas Python 3.10 o superior. Source: https://albakore.github.io/pyzed/es/getting-started/installation/ Pyzed necesita **Python 3.10 o superior**. #### Desde PyPI - **uv** gestiona el proyecto por ti: [Crear un proyecto](https://albakore.github.io/pyzed/es/getting-started/new-project/) empieza una app nueva así, y es lo que usa el resto de esta documentación. Instálalo desde [docs.astral.sh/uv](https://docs.astral.sh/uv/). - **pip** instala en un *entorno virtual* (`.venv`), una carpeta que separa los paquetes de este proyecto del resto de tu equipo. Actívalo en cada terminal nueva antes de ejecutar tu app. - **python** instala en el Python de tu equipo, sin nada más que aprender. En Windows, `py` es el lanzador de Python que trae el instalador de [python.org](https://www.python.org/downloads/); en macOS y Linux el comando suele ser `python3`. El paquete se llama `pyzed-ui` en PyPI (el nombre `pyzed` ya estaba ocupado), pero se importa como `pyzed`. Los wheels precompilados para Linux, macOS y Windows incluyen el motor de Rust, así que no necesitas compilar nada. **Solo wheels precompilados:** En PyPI solo hay wheels, compilados y probados por el pipeline de releases para Linux (x86_64 y ARM, glibc y musl), macOS (Intel y Apple Silicon) y Windows (x64 y ARM). Un mismo wheel sirve para cualquier Python desde 3.10. En otra plataforma la instalación se detiene con un error en lugar de compilar; ahí instala desde el código fuente (abajo). #### Desde el código fuente Esto compila la extensión de Rust, así que necesitas [Rust 1.88 o superior](https://rustup.rs) (`rustup update` actualiza uno más antiguo) y las dependencias de sistema de tu plataforma: **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:** Instala [Rust con rustup](https://rustup.rs) y las *Build Tools for Visual Studio* (carga de trabajo "Desarrollo para el escritorio con C++"). #### Desarrollar Pyzed Para trabajar en Pyzed, clona el repositorio. Necesitas [Rust 1.88 o superior](https://rustup.rs) (`rustup update` actualiza uno más antiguo) y las dependencias del sistema de arriba. - `uv sync` también instala el grupo `test` (maturin y pytest). La primera compilación tarda unos minutos. - Los cambios en Python (`python/pyzed/`) se usan directamente. - Con uv, el siguiente `uv run` o `uv sync` después de cambiar código de Rust (`src/`) recompila el núcleo solo; con pip, vuelve a ejecutar `maturin develop --release`. Usa `--release`: la build de depuración es mucho más lenta al usar la ventana. - `uv sync --extra designer` añade ty y ruff para el editor de código del diseñador. - En Linux, una build desde el código no suena por defecto, así que compila sin paquetes extra del sistema: `pyzed.audio` renderiza offline y con su reloj silencioso. Para dispositivos de audio y MIDI, instala las cabeceras de ALSA (`sudo apt install libasound2-dev pkg-config`; Fedora: `sudo dnf install alsa-lib-devel`) y compila con `maturin develop --release --features alsa` (con uv, deja exportado `MATURIN_PEP517_ARGS="--features alsa"` y ejecuta `uv sync --reinstall-package pyzed-ui`). #### Linux y WSL Pyzed ajusta algunas cosas antes de abrir la primera ventana: - **Silencia los avisos del driver de la GPU.** Mesa imprime mensajes como `libEGL warning: failed to get driver name for fd -1` o `MESA: error: ZINK: failed to choose pdev` mientras el renderer busca un backend que funcione. No afectan a nada, así que Pyzed define `EGL_LOG_LEVEL=fatal` y `MESA_LOG=null`. Con `PYZED_GPU_LOGS=1` vuelven a aparecer. - **En WSL usa X11.** El compositor Wayland de WSLg corta la conexión al maximizar o redimensionar una ventana, y eso imprime `Io error: Broken pipe (os error 32)`. En WSL, Pyzed usa X11 (a través de XWayland). Con `PYZED_WAYLAND=1` se mantiene Wayland. - **En WSL abre las páginas en el navegador de Windows.** WSL no suele tener navegador propio, así que los inicios de sesión (los de los agentes de IA), la documentación y los demás enlaces se abren en tu navegador predeterminado de Windows. Las páginas que vuelven a Pyzed en `127.0.0.1` siguen llegando gracias al reenvío de localhost de WSL. Los programas que necesitan una terminal, como el inicio de sesión de Claude Code, se abren en Windows Terminal (o en una consola) dentro de tu distribución de WSL. Con `BROWSER` eliges el navegador tú. Las variables que definas tú (`EGL_LOG_LEVEL`, `MESA_LOG`, `WAYLAND_DISPLAY`, `BROWSER`...) nunca se tocan. #### WSL: ventanas nativas de Windows En WSL, Pyzed abre cada ventana **como un programa nativo de Windows**, no a través de WSLg: `pyzed design`, `pyzed run`, `pyzed dev`, tus scripts que llaman a `pz.run`, Ejecutar en Pyzed Design y las vistas en vivo de los agentes de IA. La ventana es una ventana de Windows de verdad, dibujada con los drivers de GPU de Windows, así que va más rápida y se acopla, escala y aparece en la barra de tareas como cualquier otra aplicación. Tu código y tu proyecto siguen en WSL: sigues editando en Linux (en Zed, por ejemplo) y la recarga en caliente recoge cada guardado. Solo necesita [uv en Windows](https://docs.astral.sh/uv/getting-started/installation/). Pyzed puede instalarlo por ti, desde WSL: ```bash uv run pyzed windows setup # instala uv en Windows si falta y prepara el proyecto uv run pyzed windows # muestra qué se usa y dónde ``` Cómo funciona: - Windows lee el proyecto a través de `\\wsl.localhost\\...` y arranca en la misma carpeta. - Windows tiene **su propio entorno**, en `%LOCALAPPDATA%\pyzed\wsl\`, con la misma versión de Python, las dependencias del proyecto en las versiones de `uv.lock`, lo que necesita el editor (agentes de IA, MCP, servidores de lenguaje) y la wheel de `pyzed-ui` de la misma versión (siempre una wheel ya compilada: en Windows no se compila nada). El `.venv` del proyecto sigue siendo el de Linux. Se crea la primera vez (unos segundos) y de nuevo solo cuando cambian las dependencias. - El comando en WSL espera a la ventana: su salida, la consola de Pyzed Design y el código de salida son los de la aplicación, y al detenerlo (Detener, Ctrl+C) la ventana se cierra. Sin uv (ni el lanzador `py` de Python) en Windows, las ventanas se abren por WSLg como antes, con un aviso que explica cómo prepararlo. Para usar siempre WSLg, define `PYZED_WINDOWS=0` o añade `--wslg` a `pyzed design`, `run` o `dev`. | Variable | Qué hace | | --- | --- | | `PYZED_WINDOWS=0` | Abre las ventanas por WSLg. | | `PYZED_WINDOWS_ENV=NOMBRE,NOMBRE` | Más variables para la aplicación en Windows (claves de API...). Las `PYZED_*` siempre pasan. | | `PYZED_WINDOWS_WHEEL` | Ruta o URL de la wheel de `pyzed-ui` para Windows (ver abajo). | | `PYZED_WINDOWS_TARGET` | Target de Rust para el núcleo de un checkout (`x86_64-pc-windows-gnu` o `-msvc`). | :::note[Si trabajas en Pyzed] Desde un checkout de Pyzed (`uv sync` en el repositorio), Windows ejecuta **tu checkout**: sus archivos Python se importan desde WSL, así que cada cambio se nota, y su núcleo en Rust se compila para Windows dentro de WSL (compilación cruzada), una vez por cada cambio en las fuentes de Rust (la primera compilación tarda unos minutos; en Windows no se compila nada). Necesita un compilador cruzado, que se instala una vez: ```bash sudo apt install mingw-w64 # o clang y lld, para el target MSVC de las wheels publicadas ``` Sin él, las ventanas se abren por WSLg con un aviso que lo explica. `PYZED_WINDOWS_WHEEL` (la ruta o URL de una wheel de Windows) evita la compilación, y `PYZED_WINDOWS_TARGET` elige el target de Rust. ::: Cuando Pyzed Design corre así en Windows, todo lo que inicia corre también en Windows: Ejecutar, el script del proyecto, las vistas previas y el audio usan los dispositivos de Windows. Crear o clonar un proyecto en una carpeta de WSL usa el uv y el git de Linux, así que el proyecto y su `.venv` siguen siendo de Linux. #### Comprobar la instalación La primera línea muestra la versión; la segunda, los nombres de iconos que contienen "folder". ### Inicio rápido > Tu primera ventana de escritorio nativa en Python con Pyzed en cinco minutos: instala el paquete, escribe una app contador y ejecútala. Source: https://albakore.github.io/pyzed/es/getting-started/quickstart/ #### 1. Una ventana ```python title="hola.py" import pyzed as pz class Hola(pz.App): title = "Hola" size = (400, 240) def view(self): return pz.center(pz.text("Hola, Pyzed", size=24)) pz.run(Hola) ``` Una app es una clase que hereda de `pz.App`. Su método `view()` devuelve los widgets que se ven. `pz.run()` abre la ventana y bloquea hasta que se cierra. #### 2. Estado y mensajes El estado vive en tu objeto. Los widgets interactivos reciben un **mensaje**: cualquier valor de Python. Cuando el usuario pulsa el botón, Pyzed llama a `update(mensaje)` y vuelve a dibujar. ```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: El contador) #### 3. Callables en lugar de mensajes Si el mensaje es un callable (una lambda o un método), Pyzed lo **llama directamente** y no pasa por `update`. Para apps pequeñas suele ser lo más cómodo: ```python ui.button("Guardar", self.guardar) # llama a self.guardar() ui.button("Borrar", lambda: self.borrar(id)) # con argumentos ``` Los controles que producen un valor (`on_input`, `on_toggle`, `on_change`…) llaman al callable con ese valor: ```python ui.input(self.nombre, lambda texto: setattr(self, "nombre", texto), placeholder="Tu nombre") ``` #### 4. Componentes y temas `pyzed.ui` trae componentes listos para usar. Todos leen el tema actual: ```python class MiApp(pz.App): theme = pz.themes.ONE_LIGHT def cambiar_tema(self): self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark" ``` #### Siguiente - [Crear un proyecto con `pyzed new`](https://albakore.github.io/pyzed/es/getting-started/new-project/) - [Tutorial: lista de tareas](https://albakore.github.io/pyzed/es/tutorials/basic/todo-app/) - [Guía de la arquitectura](https://albakore.github.io/pyzed/es/guides/architecture/) ### Crear un proyecto > Genera una app de escritorio de Pyzed como proyecto estándar de uv con un comando: la UI en su propia carpeta y ruff, ty y git listos para usar. Source: https://albakore.github.io/pyzed/es/getting-started/new-project/ Pyzed instala el comando `pyzed`. Crea un proyecto completo con [uv](https://docs.astral.sh/uv/), una herramienta que instala paquetes de Python y mantiene cada proyecto en su propio entorno (las pestañas pip y python instalan uv por ti): Esto genera un proyecto de uv estándar: ``` mi-app/ ├── pyproject.toml # dependencias (pyzed-ui; ruff y ty para desarrollo) + [tool.pyzed] ├── uv.lock ├── .venv/ ├── .git/ ├── src/mi_app/ # backend └── ui/ # forms (.pzform) y sus handlers de eventos (.py) ``` La carpeta de UI separa la interfaz del resto del código, y sus handlers pueden importar el backend, cualquier módulo del proyecto y todas las dependencias. ¿Ya tienes un proyecto creado con `uv init`? Ejecuta `pyzed init` dentro. Consulta [Proyectos con uv](https://albakore.github.io/pyzed/es/guides/projects/) para la estructura, los comandos de dependencias y la pestaña Project del diseñador, y [Compilar y distribuir](https://albakore.github.io/pyzed/es/packaging/build/) para empaquetar la app en cada plataforma. #### Una app de un solo archivo Para empezar desde código en lugar del diseñador, `--script` crea un único `main.py` sin uv: La plantilla ya incluye: - Un `ui.workspace` con barra de título, panel izquierdo y barra de estado. - Navegación entre páginas con `ui.list_item`. - Una paleta de comandos que se abre con Ctrl+Shift+P y se cierra con Esc. - Un botón para alternar entre tema claro y oscuro. #### Buscar iconos Pyzed incluye los ~2000 iconos de [Lucide](https://lucide.dev). Para buscar nombres: Úsalos con `pz.icon("folder")` o con el parámetro `icon=` de los componentes. #### Ejecutar como módulo `python -m pyzed` es equivalente al comando `pyzed`: ## Tutoriales ### Tutoriales > Aprende Pyzed creando apps de escritorio reales en Python paso a paso, con el diseñador visual y en código: tutoriales básicos, intermedios y avanzados, cada uno con código que puedes ejecutar. Source: https://albakore.github.io/pyzed/es/tutorials/ Cada tutorial termina en un programa que puedes ejecutar, y su código completo está en la carpeta [`examples`](https://github.com/albakore/pyzed/tree/main/examples) del repositorio. Cada uno dice qué vas a construir, cuánto tarda, qué necesitas saber antes y qué vas a aprender. ```mermaid flowchart LR B["Básico
dibujar una ventana · un formulario · layout · temas · Python básico"] --> I["Intermedio
enlace de datos · páginas · tablas · diálogos · componentes · hilos · shaders"] I --> A["Avanzado
editor · juegos · grafos de shader · backends · empaquetado · agente de IA"] ``` **¿Nunca programaste?:** Empieza por **Tu primera ventana en Pyzed Design**: la dibujas con el mouse y la ejecutas con F5, sin código. Cada tutorial siguiente añade un poco de Python y explica cada línea. #### Básicos De un lienzo vacío a tus primeras apps, con el diseñador y en código. #### Intermedios Datos reales, varias pantallas y tus propias piezas reutilizables. #### Avanzados Aplicaciones completas, juegos y cómo publicarlas. #### Ejecutar los ejemplos Clona el repositorio (o descárgalo como zip) y ejecuta cualquier ejemplo desde su carpeta con la herramienta que uses. `uv` crea el entorno por ti; con `pip`, primero crea y activa un entorno virtual. Cada tutorial tiene un bloque como este con su propio archivo: Los formularios hechos con el diseñador (`.pzform`) se ejecutan con `pyzed run` en lugar de `python`: **Sobre esta sección:** Los tutoriales están probados: cada app de ejemplo de `examples/tutorial` se importa, se dibuja fuera de pantalla y se ejercita en la suite de tests, así que el código de estas páginas funciona con la versión actual de Pyzed. Se están añadiendo grabaciones cortas de cada tutorial. **¿Quieres hacer videojuegos?:** La ruta [Crear videojuegos](https://albakore.github.io/pyzed/es/games/) del Showcase va desde un primer bucle de juego hasta plataformas, shooters y puzles completos, con ejemplos animados y consejos. **Tip:** ¿Nuevo en Python? Lee primero [Instalación](https://albakore.github.io/pyzed/es/getting-started/installation/) y el [Inicio rápido](https://albakore.github.io/pyzed/es/getting-started/quickstart/) para ver una primera ventana en cinco minutos. ¿Buscas apps terminadas para estudiar? Mira el [Showcase](https://albakore.github.io/pyzed/es/showcase/). ### Enlace de datos y validación > Enlaza los controles de un formulario de Pyzed Design a form.data, añade reglas de validación sin código, muestra una expresión en vivo en una etiqueta y valida todo el formulario en una línea. Source: https://albakore.github.io/pyzed/es/tutorials/intermediate/data-binding/ En [Un formulario que hace algo](https://albakore.github.io/pyzed/es/tutorials/basic/first-form/) cada campo se leía y se revisaba a mano. Aquí los controles hacen ese trabajo solos: cada uno está **enlazado** a un campo de los datos del formulario, lleva sus propias **reglas** y muestra su propio mensaje de error. El Python que queda por escribir es un solo `if`. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un formulario de registro que se valida solo | 25 minutos | Intermedio · un poco de Python | [Un formulario que hace algo](https://albakore.github.io/pyzed/es/tutorials/basic/first-form/) | #### Qué vas a aprender - **Enlazar** un campo a una clave de `form.data` (en los dos sentidos: muestra el valor y guarda lo que escribe el usuario). - Mostrar una **expresión en vivo** en una etiqueta (`f"Nice to meet you, {name}!"`). - Añadir **reglas**: obligatorio, un patrón, una longitud mínima, un número mínimo, tu propio mensaje. - Validarlo todo con `form.validate()`, y leer `form.data` y `form.errors`. #### Pasos 1. **Dibuja el formulario.** Un formulario nuevo llamado `signup` con un **Título**, una **Etiqueta de texto** llamada `greeting`, tres **Cuadros de texto** (`your_name`, `email`, `password`, este último con **Contraseña** activado), un **Número** (*Number box*) llamado `age`, una **Casilla** (*Check box*) llamada `terms` (texto `I accept the terms`), una etiqueta pequeña `summary`, una etiqueta `status` y un **Botón** llamado `create`. 2. **Enlaza los campos.** Selecciona `your_name` y, en la sección **Datos** (*Data*) de Propiedades, escribe `name` en **Enlazar a** (*Bind to*). Haz lo mismo con los demás: | Control | Enlazar a | Reglas (sección Datos) | | --- | --- | --- | | `your_name` | `name` | **Obligatorio** | | `email` | `email` | **Obligatorio**, **Patrón** `[^@\s]+@[^@\s]+\.[a-z]{2,}`, **Mensaje** `Enter a valid email` | | `password` | `password` | **Obligatorio**, **Longitud mínima** 8 | | `age` | `age` | **Mínimo** 13, **Mensaje** `You must be 13 or older` | | `terms` | `terms` | **Obligatorio** (una casilla solo pasa si está marcada), **Mensaje** `Please accept the terms` | 3. **Muestra una expresión en vivo.** Un control de solo lectura (etiqueta, título, insignia...) puede enlazarse a una **expresión de Python** sobre los datos, que se recalcula en cada redibujado. Selecciona `greeting` y pon en **Enlazar a**: ```python f"Nice to meet you, {name}!" if name else "Fill in the form below." ``` Y en `summary`: ```python f"{len(password)} characters in your password" ``` 4. **Ejecútalo** (F5) y escribe. El saludo sigue a tu nombre letra a letra, y cada campo revisa sus reglas **mientras escribes**: un borde rojo y una burbuja con el mensaje debajo. 5. **Valida al hacer clic.** Haz doble clic en `create` y escribe el manejador: ```python def create_click(form, event): if not form.validate(): # revisa cada regla y muestra cada mensaje 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` es un dict normal que llenan los controles enlazados. Guárdalo, envíalo a una API o pásalo a otro formulario. **Cargar datos existentes:** Para editar un registro en lugar de crear uno, asígnalo: `form.data = {"name": "Ada", "email": "ada@example.com"}` en el manejador `load` del formulario llena cada control enlazado. Otro formulario también puede pasarlo: `form.open("signup", data=customer)`. **Errores del servidor:** Las reglas atrapan errores de tipeo. Para comprobaciones que solo puede hacer un servidor ("ese correo ya existe"), pon el mensaje tú: `form.email.error = "Already registered"`. `None` lo borra. #### Código completo `signup.pzform` guarda el diseño, los enlaces y las reglas; `signup.py`, solo el botón: `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']}!" ``` #### Eventos de este tutorial Los controles enlazados casi no necesitan eventos: cada cambio va solo a `form.data`. El formulario tiene un handler, `create_click`, que llama a `form.validate()` y lee `form.data`. ##### Prueba más eventos ```python def form_load(form, event): # empieza en la primera caja return form.name.focus() def terms_change(form, event): # una casilla: event.value es True o False form.create.enabled = event.value def email_change(form, event): # comprueba un campo mientras se escribe form.email.validate() # muestra (o quita) su mensaje al momento ``` #### Siguiente - [Enlace de datos y validación](https://albakore.github.io/pyzed/es/editor/data-binding/) en el manual del diseñador: rutas anidadas (`customer.address.city`), tablas, traducciones con `@claves` y vista previa en otros idiomas. - [Errores de validación de Pydantic, Django o FastAPI](https://albakore.github.io/pyzed/es/integrations/overview/). - [Una app con varias vistas](https://albakore.github.io/pyzed/es/tutorials/intermediate/navigation/). ### Tu primera ventana en Pyzed Design > Dibuja tu primera ventana de escritorio con Pyzed Design, el diseñador visual: arrastra controles, cambia sus propiedades y ejecuta el formulario con F5, sin escribir código. Source: https://albakore.github.io/pyzed/es/tutorials/basic/first-window/ Vas a construir una ventana pequeña que te saluda por tu nombre. La vas a **dibujar** con el mouse, como en Visual Basic o Qt Designer, y a ejecutarla con una tecla. No hay que escribir código hasta el último paso (que es opcional), y ese paso es una línea. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Una ventana con un título, un cuadro de texto y un botón | 15 minutos | Básico · sin código | Pyzed Design (abajo) | #### Qué vas a aprender - Abrir Pyzed Design y crear un **formulario** (una ventana de tu app). - Colocar controles desde las **Herramientas** y cambiarlos en el panel **Propiedades**. - Hacer que un control se estire cuando la ventana crece, con **anclajes**. - Guardar y **ejecutar** el formulario con F5. - Conectar un botón a un **manejador de eventos** de Python (opcional). #### Antes de empezar Pyzed Design es el editor visual de Pyzed. Instálalo de una de estas formas: **uv:** ```bash title="Windows, macOS y Linux" uv tool install "pyzed-ui[designer]" # instala el comando pyzed pyzed design hello.pzform ``` **pip:** ```bash title="macOS y 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="Con Pyzed ya instalado" python -m pyzed.designer hello.pzform # lo mismo, sin el comando pyzed en el PATH ``` **¿No tienes Python?:** Descarga la app **Pyzed Design** desde la página de [Descarga](https://albakore.github.io/pyzed/es/download/). Trae su propio Python y `uv`, así que no hay que instalar nada más. **El editor en español:** Pyzed Design usa el idioma de tu sistema. En esta página los nombres de paneles y propiedades aparecen en español, con el nombre en inglés entre paréntesis la primera vez, por si tu editor está en inglés. #### Pasos 1. **Crea el formulario.** En Pyzed Design pulsa Ctrl+N (o **☰ → Nueva**). Elige la tarjeta **Formulario** (*Form*), escribe `hello` como **Nombre** (*Name*), `Hello` como **Título** (*Title*), deja la plantilla **En blanco** (*Blank*) y pulsa **Crear** (*Create*). Si abriste el editor con `pyzed design hello.pzform`, el formulario vacío ya está abierto. 2. **Añade un título.** En las **Herramientas** (*Toolbox*) de la izquierda, busca **Título** (*Heading*; escríbelo en **Buscar componentes…**) y arrástralo al formulario. Con él seleccionado, mira el panel **Propiedades** (*Properties*) de la derecha y cambia: - **Nombre**: `headline` (pulsa Enter). Así lo llamará tu código más adelante. - **Texto** (*Text*): `Hello, Pyzed!` 3. **Añade una etiqueta y un cuadro de texto.** Arrastra una **Etiqueta de texto** (*Label*) debajo del título y pon en su **Texto** `Type your name and press the button.` y en su **Color** `text_muted` (un gris más suave que sigue al tema). Luego arrastra un **Cuadro de texto** (*Text box*) debajo, llámalo `your_name` y pon en su **Texto de ayuda** (*Placeholder*) `Your name` y en su **Icono** (*Icon*) `user`. 4. **Añade el botón.** Arrastra un **Botón** (*Button*) debajo del cuadro de texto, llámalo `greet`, pon en su **Texto** `Greet`, en su **Variante** (*Variant*) `solid` y en su **Icono** `hand`. 5. **Alinea todo.** Arrastra los controles hasta que las **guías inteligentes** rojas muestren que sus bordes izquierdos coinciden. Selecciona varios (arrastra un recuadro alrededor) y usa **Alinear a la izquierda** (*Align left*) en la barra de herramientas para una línea perfecta. 6. **Haz que se estire.** Selecciona el cuadro de texto y abre la sección **Posición** (*Position*) de Propiedades. En la cuadrícula de presets de anclaje elige el que estira a **izquierda y derecha**. Haz lo mismo con el título y la etiqueta. Para el botón elige **arriba · derecha**, así sigue al borde derecho. 7. **Guarda y ejecuta.** Pulsa Ctrl+S: el editor escribe `hello.pzform` (el diseño) y `hello.py` (su código, todavía vacío). Pulsa F5: tu formulario se abre como una ventana de verdad. Cambia su tamaño y mira cómo se estira el cuadro de texto. **Hay nombres ocupados:** Un control no puede llamarse `name`, `title`, `size`, `data` ni `window`: tu código usa esas palabras para el propio formulario (`form.title` es el título de la ventana). Por eso el cuadro de texto es `your_name` y el título `headline`. #### Extra: que el botón haga algo 1. **Haz doble clic en el botón** en el lienzo. El panel **Eventos** (*Events*) conecta su evento `click` a una función nueva, `greet_click`, y la abre en el **editor de código** de abajo. 2. **Escribe una línea** (dos, si quieres) dentro de la función: ```python def greet_click(form, event): form.headline.text = f"Hello, {form.your_name.text}!" ``` `form` es la ventana en ejecución: cada control es un atributo (`form.your_name`), y al cambiar una propiedad (`.text`) se redibuja enseguida. Escribe `form.` y el editor te lista tus controles. 3. **Conecta también Enter.** Selecciona el cuadro de texto, abre **Eventos** y haz doble clic en su evento `submit` (pulsar Enter). En la nueva función `your_name_submit`, llama al manejador del botón: ```python def your_name_submit(form, event): return greet_click(form, event) ``` 4. **Guarda** (Ctrl+S). Si el formulario sigue en ejecución se actualiza en el sitio; si no, pulsa F5 otra vez. Escribe tu nombre y pulsa **Greet**. #### Los archivos terminados El formulario son dos archivos juntos: `hello.pzform` (un JSON que describe la ventana, lo escribe el diseñador) y `hello.py` (tu código). Los dos están en [`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) ``` Ejecuta el ejemplo terminado sin abrir el diseñador: **De dónde vienen las ideas:** El lienzo, las herramientas y la grilla de propiedades siguen a los diseñadores RAD clásicos (Visual Basic, el diseñador de WinForms, Qt Designer); los anclajes funcionan como los de WinForms, Godot y Unreal Engine. El aspecto del editor está inspirado en el editor de código Zed. #### Eventos de este tutorial | Control | Evento | Handler | Qué hace aquí | | --- | --- | --- | --- | | `greet` (Botón) | `click` | `greet_click` | Escribe el saludo en `headline`. | | `your_name` (Caja de texto) | `submit` | `your_name_submit` | Enter hace lo mismo que el botón. | ##### Prueba más eventos Haz doble clic en un evento del panel **Eventos** para crear su handler y pega el cuerpo: ```python def your_name_change(form, event): # cada tecla: event.value es el texto form.greet.enabled = bool(event.value.strip()) def headline_double_click(form, event): # una etiqueta tiene click y double_click form.headline.text = "¡Hola!" form.your_name.text = "" def form_key(form, event): # cualquier tecla en la ventana: event.value es la combinación if event.value == "escape": return form.close() ``` Todo control tiene además `mouse_enter`, `mouse_leave` y `right_click`. La lista completa de cada control está en [Eventos y handlers](https://albakore.github.io/pyzed/es/editor/events/), y cómo reciben sus eventos formularios, apps y componentes en [Apps, ventanas, formularios y componentes](https://albakore.github.io/pyzed/es/guides/classes/). #### Siguiente - [Un formulario que hace algo](https://albakore.github.io/pyzed/es/tutorials/basic/first-form/): varios campos, validación y la consola. - [El manual de Pyzed Design](https://albakore.github.io/pyzed/es/editor/): cada panel del editor en detalle. - [Herramientas y lienzo](https://albakore.github.io/pyzed/es/editor/toolbox-and-canvas/): todas las formas de colocar, seleccionar y alinear controles. ### Un mini editor de código > Tutorial: crea un editor de código en Python con Pyzed: workspace, árbol de archivos, pestañas, resaltado, guardado y paleta de comandos. Source: https://albakore.github.io/pyzed/es/tutorials/advanced/code-editor/ Vamos a construir un editor de código en unas 150 líneas: abre una carpeta real, muestra el árbol de archivos, abre pestañas, edita con resaltado de sintaxis, guarda con Ctrl+S y tiene paleta de comandos. Las piezas del workspace (docks, pestañas, paleta) son las mismas con las que está hecho Pyzed Design; su aspecto está inspirado en el editor Zed. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un editor de código que funciona sobre una carpeta | 45 minutos | Avanzado · Python | [Listas y tablas](https://albakore.github.io/pyzed/es/tutorials/intermediate/lists-tables/), [Navegación](https://albakore.github.io/pyzed/es/tutorials/intermediate/navigation/) | #### Qué vas a aprender - Convertir una carpeta en un árbol de archivos (`ui.TreeNode.from_paths`) y abrir archivos en pestañas. - Editar con resaltado de sintaxis (`ui.editor`) y llevar la cuenta de los cambios sin guardar. - Montar un workspace completo: barra de título, docks y barra de estado. - Añadir una paleta de comandos con búsqueda difusa y atajos de teclado. (Image: El mini editor) 1. **Leer la carpeta.** `TreeNode.from_paths` convierte una lista de rutas en el árbol del panel de proyecto. Saltamos carpetas pesadas como `.git` o `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() # carpetas abiertas ``` 2. **Abrir y cerrar pestañas.** El estado son las rutas abiertas, la activa y el texto de cada una. El id de cada pestaña es su ruta: ```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. **El editor.** Usamos la ruta como `id` del editor: así cada archivo conserva su cursor y su historial de deshacer al cambiar de pestaña. `on_change` marca el archivo como modificado: ```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) # la pestaña muestra un punto ``` 4. **El workspace.** `ui.workspace` coloca la barra de título, el dock izquierdo, el centro y la barra de estado. Pasar `None` en `left` oculta el 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. **Atajos.** Normalizamos Cmd a Ctrl para que funcione igual en 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. **Paleta de comandos.** Se abre con Ctrl+Shift+P, enfoca su campo y flota sobre el workspace con `ui.layer`: ```python if combo == "ctrl-shift-p": self.palette_open, self.query = True, "" return Command.focus("command-palette") # al final de 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: La paleta de comandos) #### Código completo `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:** Pasa una carpeta para abrirla: `python examples/mini_editor.py ruta/a/una/carpeta`. Sin ella abre la carpeta actual. Para una versión más completa (buscador de archivos, panel de terminal, outline, temas, marcadores de git) mira [`examples/zed_editor.py`](https://github.com/albakore/pyzed/blob/main/examples/zed_editor.py) y su página en el [Showcase](https://albakore.github.io/pyzed/es/showcase/apps/#editor-de-proyectos). #### Eventos de este tutorial | Widget | Evento | El handler recibe | | --- | --- | --- | | `ui.tab_bar` | `on_select`, `on_close` | El id de la pestaña (su ruta). | | `ui.editor` | `on_change`, `on_cursor` | El texto; la línea y la columna. | | `ui.tree` | `on_toggle`, `on_select` | La ruta de la carpeta o del archivo. | | `ui.status_item` | `on_press` | Nada. | | `ui.command_palette` | `on_query`, `on_submit` | La búsqueda; nada. | | `ui.layer` | `on_dismiss` | Nada (un clic fuera de la paleta). | | La app | `on_key` | La combinación (`ctrl-shift-p`, `ctrl-b`…). | ##### Prueba más eventos ```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, # doble clic on_context=lambda path: setattr(self, "menu_for", path)) # clic derecho: un menú ui.tab_bar(tabs, self.active, on_select=lambda p: setattr(self, "active", p), on_close=self.close_tab, on_new=self.new_file) # doble clic después de las pestañas ``` #### Siguiente - [Un bucle de juego](https://albakore.github.io/pyzed/es/tutorials/advanced/game-loop/) con `pyzed.game`. - [Workspace](https://albakore.github.io/pyzed/es/library/workspace/), [Tree](https://albakore.github.io/pyzed/es/library/tree/) y [Command palette](https://albakore.github.io/pyzed/es/library/command-palette/) en la biblioteca de componentes. ### Un bucle de juego > Escribe un pequeño juego en tiempo real con Pyzed: un bucle por fotograma con pz.frames, teclas mantenidas, colisiones, brillo, partículas y temblor de pantalla, y un HUD hecho con widgets normales. Source: https://albakore.github.io/pyzed/es/tutorials/advanced/game-loop/ Las apps esperan al usuario; los juegos no. Un juego **actualiza el mundo muchas veces por segundo**, pulses una tecla o no. Este tutorial construye *Dodge*: caen bloques, te mueves a izquierda y derecha, recoges estrellas y el juego se acelera. Unas 130 líneas, y cada pieza se puede reutilizar. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un juego arcade que se puede jugar | 40 minutos | Avanzado · Python | [Una app con varias vistas](https://albakore.github.io/pyzed/es/tutorials/intermediate/navigation/) | #### Qué vas a aprender - Ejecutar código **en cada fotograma** con `pz.frames(callback)` y mover cosas según *velocidad × dt*. - Leer las teclas **mantenidas** (`game.axis`) en lugar de pulsaciones sueltas. - Detectar **colisiones** con `game.Rect`. - Dibujar en un `pz.canvas` y añadir **brillo**, **partículas** y **temblor de pantalla**. - Apilar un **HUD** y una tarjeta de *Game over*, hechos con widgets normales, sobre el canvas. #### Pasos 1. **El bucle.** Devuelve `pz.frames(self.tick)` desde `subscriptions()` y Pyzed llama a `tick(dt)` antes de cada fotograma (60, 120 o 144 veces por segundo, sincronizado con la pantalla). `dt` es el tiempo desde el fotograma anterior, en segundos. Cuando el juego termina no devuelves nada y el bucle se detiene, así un juego en pausa no usa CPU: ```python def subscriptions(self): return [] if self.over else [pz.frames(self.tick)] ``` 2. **Mueve según velocidad × dt.** Las velocidades están en píxeles por segundo. Multiplicar por `dt` hace que el juego vaya a la misma velocidad en cualquier pantalla: ```python self.x += game.axis(("left", "a"), ("right", "d")) * 380 * dt # -1, 0 o 1 × velocidad × tiempo self.x = game.clamp(self.x, 0, W - PLAYER_W) ``` `game.axis` lee las teclas pulsadas **ahora mismo**: mantén la flecha y la paleta sigue moviéndose. 3. **Crea cosas con un temporizador.** Una cuenta atrás en segundos que se reinicia al llegar a cero. Se acorta con el tiempo, así que el juego se vuelve más difícil: ```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. **Colisiones.** `game.Rect` es una caja con `overlaps`, `contains` y `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) # temblor de pantalla self.sparks.burst(*player.center, count=40, color=["#f87171", "#fca5a5"]) ``` 5. **Dibuja el mundo.** Una lista de figuras de `pyzed.draw` en un `pz.canvas`. `game.glow` añade luz suave, `sparks.shapes()` dibuja las partículas y `shake.shapes()` mueve todo un poco después de un golpe: ```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 y Game over.** No se dibujan en el canvas: son widgets. `pz.stack` pone la tarjeta de *Game over* (un panel de cristal con un botón) sobre el canvas, y los marcadores van encima: ```python play = pz.stack([self.board(), self.overlay()], width=W, height=H) ``` 7. **Reinicia.** `on_key` recibe las pulsaciones sueltas; Enter reinicia el estado y, como `over` vuelve a ser falso, `subscriptions()` devuelve el bucle y el juego empieza de nuevo. **Hazlo tuyo:** Cambia un número cada vez y mira qué pasa: la velocidad de la paleta (380), la gravedad de las chispas (`Particles(gravity=300)`), el intervalo de aparición, o da a los bloques una `rotation` con `draw.group`. Ejecútalo con `pyzed dev examples/tutorial/dodge.py` para ver cada cambio al guardar. **¿Paso fijo o bucle por fotograma?:** Los juegos de cuadrícula como Snake avanzan una celda cada vez a ritmo constante: usa `pz.every(segundos, callback)`. Los juegos de acción que se mueven con suavidad usan `pz.frames`. La sección [Videojuegos](https://albakore.github.io/pyzed/es/games/) tiene uno de cada. #### Código completo `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) ``` #### Eventos de este tutorial | Origen | Evento | Handler | | --- | --- | --- | | `subscriptions()` | cada fotograma | `pz.frames(self.tick)`: `tick(dt)` recibe los segundos desde el anterior. | | El teclado | teclas mantenidas | `game.axis(("left", "a"), ("right", "d"))` las lee dentro de `tick`. | | La app | `on_key` | Enter vuelve a empezar después de chocar. | Un juego lee en cada fotograma las teclas que están *pulsadas* en lugar de reaccionar a cada pulsación; `on_key` es para acciones puntuales. ##### Prueba más eventos ```python def on_key(self, combo): if combo == "enter" and self.over: self.reset() elif combo == "p": # pausa: deja de devolver la suscripción de fotogramas 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) # clic para reiniciar ``` #### Siguiente - [Hacer videojuegos](https://albakore.github.io/pyzed/es/games/): Snake, Breakout y Neon Invaders, paso a paso. - [Un grafo de shader propio](https://albakore.github.io/pyzed/es/tutorials/advanced/shader-graph/) para un fondo por GPU. ### Un formulario que hace algo > Construye un formulario de contacto en Pyzed Design: cuadros de texto, un desplegable y un área de texto, un botón Enviar conectado a Python, un contador de caracteres en vivo y mensajes en la consola. Source: https://albakore.github.io/pyzed/es/tutorials/basic/first-form/ Un **formulario de contacto**: el usuario escribe un nombre, un correo y un mensaje, elige un tema y pulsa **Send**. El formulario revisa lo escrito, muestra un mensaje amable, cuenta los caracteres mientras escribes e imprime en la consola lo que "envió". Es la forma de la mayoría de las apps de gestión: campos, un botón y unas pocas líneas de Python que reaccionan. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un formulario de contacto con comprobaciones y avisos | 25 minutos | Básico · un poco de Python | [Tu primera ventana](https://albakore.github.io/pyzed/es/tutorials/basic/first-window/) | #### Qué vas a aprender - Usar varios tipos de campo: **Cuadro de texto**, **Desplegable** (*Drop-down*) y **Área de texto** (*Text area*). - Leer en el código lo que escribió el usuario: `form.email.text`, `form.topic.selected`. - Reaccionar **mientras el usuario escribe** con un evento `change`, y a un clic con `click`. - Dar avisos: cambiar el texto y el color de una etiqueta, mover el cursor con `focus()`. - Ejecutar código al abrir la ventana (el evento `load` del formulario) y leer la salida de `print` en la **Consola**. #### Pasos 1. **Crea el formulario.** Ctrl+N → **Formulario**, nómbralo `contact`, con título `Contact us`. Selecciona un lugar vacío del lienzo: el panel Propiedades muestra ahora el **formulario**. Pon **Ancho** (*Width*) en 480 y **Alto** (*Height*) en 500. 2. **Añade los campos.** Para cada campo arrastra una **Etiqueta de texto** (su título) y el control debajo: | Texto de la etiqueta | Control | Nombre | Propiedades | | --- | --- | --- | --- | | Name | Cuadro de texto | `your_name` | Texto de ayuda `Ada Lovelace` | | Email | Cuadro de texto | `email` | Texto de ayuda `ada@example.com`, Icono `mail` | | Topic | Desplegable | `topic` | Opciones `Question`, `Bug report`, `Idea`; Seleccionado `Question` | | (ninguna) | Área de texto | `message` | Texto de ayuda `How can we help?` | Las **Opciones** (*Options*) de un desplegable son una lista: cada elemento es su propio campo, y **+** añade uno. 3. **Añade el botón y dos etiquetas para los avisos.** Un **Botón** llamado `send` (texto `Send`, variante `solid`, icono `send`) en la esquina inferior derecha, una **Etiqueta de texto** pequeña llamada `counter` (texto `0 / 500`, tamaño `xs`) debajo del mensaje, y una **Etiqueta de texto** vacía llamada `status` junto al botón. 4. **Ancla todo.** Cuadros de texto y etiquetas: estirar a **izquierda y derecha**. El área de texto: estirar por **los cuatro lados**, para que ocupe el espacio extra cuando la ventana crece. El botón: **abajo · derecha**. `counter` y `status`: **abajo**, para que queden bajo el área de texto. 5. **Pon el cursor en el primer campo al abrir.** Haz clic en un lugar vacío (queda seleccionado el formulario), abre **Eventos** y haz doble clic en `load`. Escribe: ```python def form_load(form, event): return form.your_name.focus() ``` Los manejadores pueden **devolver un comando**: `focus()` pone el cursor en ese campo. 6. **Cuenta caracteres mientras se escribe.** Selecciona el área de texto y haz doble clic en su evento `change`. `event.value` es el texto nuevo: ```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"` y `"text_muted"` son **colores del tema**: se ven bien en un tema oscuro y en uno claro. 7. **Revisa y envía.** Haz doble clic en el botón **Send** y escribe las comprobaciones. Cada una muestra un mensaje, lleva el cursor al campo y se detiene con `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. **Ejecútalo** con F5. Pulsa **Send** con los campos vacíos y luego rellénalos. El `print` va a la **Consola** de la parte de abajo del editor, junto con los eventos que manejó el formulario y cualquier error. **Menos código con enlace de datos:** Revisar los campos a mano está bien para unos pocos. En formularios más grandes, da a cada control **reglas** (obligatorio, un patrón, una longitud mínima) en el panel Propiedades y llama a `form.validate()`: mira [Enlace de datos y validación](https://albakore.github.io/pyzed/es/tutorials/intermediate/data-binding/). **Los errores no cierran la ventana:** Si un manejador lanza una excepción, la ventana en ejecución muestra un aviso rojo **Event handler error** y la consola recibe el traceback completo. Corrige el código, guarda, y la ventana toma la versión nueva. #### El código completo `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 ``` #### Eventos de este tutorial | Control | Evento | Handler | `event.value` | | --- | --- | --- | --- | | El formulario | `load` | `form_load` | `None`: pone el cursor en la primera caja. | | `message` (Área de texto) | `change` | `message_change` | El texto entero, en cada tecla. | | `send` (Botón) | `click` | `send_click` | `None`. | ##### Prueba más eventos ```python def email_submit(form, event): # Enter en la caja del email también envía return send_click(form, event) def topic_change(form, event): # un desplegable: event.value es la opción elegida form.status.color = "text_muted" form.status.text = f"Tema: {event.value}" def send_mouse_enter(form, event): # todo control tiene mouse_enter y mouse_leave form.status.text = "¿Listo para enviar?" def form_close(form, event): # devuelve False para dejar la ventana abierta if form.message.text.strip(): form.status.text = "Envía o borra el mensaje antes de cerrar." return False ``` #### Siguiente - [Bases del diseño de pantallas](https://albakore.github.io/pyzed/es/tutorials/basic/layout-basics/): filas, columnas y cuadrículas, en el diseñador y en código. - [Eventos y manejadores](https://albakore.github.io/pyzed/es/editor/events/): cada evento de cada control. - [Ejecución y consola](https://albakore.github.io/pyzed/es/editor/console-and-running/). ### Una app con varias vistas > Construye una app de Pyzed con varias páginas: una barra lateral que cambia de vista, pestañas dentro de una página, migas de pan y atajos de teclado, todo controlado por un atributo del estado. Source: https://albakore.github.io/pyzed/es/tutorials/intermediate/navigation/ Las apps reales tienen más de una pantalla: un inicio, una bandeja de entrada, ajustes. En Pyzed, "qué pantalla se ve" es solo **un dato del estado**, como cualquier otro. Lo cambias y `view()` dibuja la otra pantalla. No hay un router que aprender. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Una app con barra lateral, tres páginas y pestañas | 25 minutos | Intermedio · Python | [Bases del diseño de pantallas](https://albakore.github.io/pyzed/es/tutorials/basic/layout-basics/) | #### Qué vas a aprender - Guardar la página actual en `self.page` y elegir la vista a partir de ella. - Construir una **barra lateral** con filas `ui.list_item`, un estado seleccionado y un contador. - Usar `ui.segmented` como **pestañas** dentro de una página. - Mostrar **migas de pan** y saltar entre páginas con **atajos** (`on_key`). #### Pasos 1. **Un atributo decide la página.** ```python PAGES = {"Home": "house", "Inbox": "inbox", "Settings": "settings"} # nombre → icono def __init__(self): self.page = "Home" def go(self, page): self.page = page ``` 2. **La barra lateral.** Un `ui.list_item` por página. `selected=` resalta la actual y `end=` pone un contador a la derecha. El `name=name` del lambda guarda el nombre de cada página: ```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. **Elige la página en `view()`.** Cada página es un método que devuelve widgets; un dict relaciona nombres con métodos: ```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. **Pestañas dentro de una página.** Un segundo atributo, `self.tab`, y un control `ui.segmented` para cambiarlo. El contenido de abajo depende de él: ```python tabs = ui.segmented(["General", "Notifications", "About"], self.tab, lambda tab: setattr(self, "tab", tab)) ``` 5. **Atajos.** `on_key` recibe cada combinación de teclas como texto. Ctrl+1, Ctrl+2 y Ctrl+3 abren las páginas: ```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]) ``` **El estado sobrevive al cambiar de página:** Como las páginas se dibujan a partir de tu estado, no se pierde nada al salir de una: la bandeja recuerda qué mensajes leíste y Settings recuerda su pestaña. Para reiniciar una página, reinicia sus atributos en `go()`. **En Pyzed Design:** En el diseñador, un control **Pestañas** (*Tab bar*) lanza `change` con la pestaña elegida; muestra y oculta paneles en su manejador (`form.general.visible = event.value == "General"`), o abre otro formulario en su propia ventana con `form.open("settings")`. Mira [Eventos y manejadores](https://albakore.github.io/pyzed/es/editor/events/). #### Código completo `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) ``` #### Eventos de este tutorial | Widget | Evento | Handler | | --- | --- | --- | | `ui.list_item` de la barra lateral | `on_press` | `lambda name=name: self.go(name)` | | La app | `on_key` | `ctrl-1`, `ctrl-2`, `ctrl-3` van a una página | | `ui.breadcrumbs` | `on_press` | recibe el índice de la parte: `0` vuelve al inicio | | `ui.list_item` del buzón | `on_press` | marca el mensaje como leído | | `ui.segmented`, `ui.select`, `ui.switch` | `on_select`, `on_select`, `on_toggle` | la pestaña, el tema, sí/no | ##### Prueba más eventos Atrás y adelante con un historial, como un navegador: ```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() ``` Los ratones con botones de atrás y adelante los mandan como `mouse_down` con `event.button`: añade `.on(mouse_down=lambda e: ...)` a la página. #### Siguiente - [Listas y tablas con datos](https://albakore.github.io/pyzed/es/tutorials/intermediate/lists-tables/). - [Varias ventanas](https://albakore.github.io/pyzed/es/guides/windows/): varias ventanas reales que comparten estado. - El [Dashboard](https://albakore.github.io/pyzed/es/showcase/dashboards/#dashboard) y los [Ajustes](https://albakore.github.io/pyzed/es/showcase/forms/#ajustes) del Showcase usan la misma barra lateral. ### Bases del diseño de pantallas > Organiza una ventana de Pyzed con filas, columnas, relleno del espacio libre, separaciones y cuadrículas, en código Python y con los contenedores flexibles y los anclajes de Pyzed Design. Source: https://albakore.github.io/pyzed/es/tutorials/basic/layout-basics/ Cualquier pantalla, desde una caja de inicio de sesión hasta un IDE completo, se arma con cuatro ideas: **apilar** cosas en vertical, ponerlas **una al lado de otra**, decidir **quién ocupa el espacio libre** y dejar **aire** entre ellas. Apréndelas una vez y podrás leer cualquier layout de Pyzed. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Una página de perfil que se adapta bien al tamaño | 20 minutos | Básico · Python | Pyzed instalado ([Instalación](https://albakore.github.io/pyzed/es/getting-started/installation/)) | #### Qué vas a aprender - `pz.column` y `pz.row`: pilas vertical y horizontal. - `spacing` (la separación entre hijos) y `padding` (el margen interior del borde). - `width="fill"` / `height="fill"` y `pz.hspace()`: ocupar el espacio libre. - `align`: alinear los hijos a lo ancho de la pila. - `pz.grid`: celdas iguales. - Las mismas ideas en el diseñador: **Fila flexible**, **Columna flexible**, **Cuadrícula**, **Espaciador** y **anclajes**. #### Las cuatro ideas ```text pz.column([...], spacing=16) pz.row([...], spacing=12, align="center") ┌──────────────┐ ┌───────────────────────────────────────┐ │ header │ │ (AL) Ada Lovelace ⟷fill⟷ [Follow]│ │ ── 16 px ── │ └───────────────────────────────────────┘ │ stats │ pz.hspace() empuja el botón a la derecha │ ── 16 px ── │ │ about (fill) │ ← height="fill" ocupa lo que queda └──────────────┘ ``` | Valor de tamaño | Significa | | --- | --- | | un número (`width=200`) | Exactamente esa cantidad de píxeles. | | `"shrink"` (el valor por defecto de casi todos los widgets) | Tan pequeño como su contenido. | | `"fill"` | Todo el espacio libre; varios hijos `fill` lo reparten a partes iguales. | | `"fill:2"` | El doble de la parte de un hermano `fill` normal. | #### Pasos 1. **La página es una columna.** `view()` devuelve un widget; aquí un `pz.container` con `padding=24` alrededor de una columna de tres partes separadas por `spacing=16`: ```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. **La cabecera es una fila.** Un avatar, una columna con el nombre y una línea atenuada, luego `pz.hspace()`, una pieza vacía con `width="fill"`, y luego el botón. `align="center"` los centra en vertical: ```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. **Tres tarjetas iguales: una cuadrícula.** `columns=3` crea tres celdas del mismo ancho; `height` es el alto de cada fila: ```python pz.grid([ui.stat("Projects", "12", width="fill"), ...], columns=3, spacing=12, height=84) ``` 4. **La última parte rellena.** `height="fill"` en el recuadro About hace que ocupe cada píxel que dejan las otras partes, así que crece con la ventana. 5. **Ejecútalo y cambia el tamaño de la ventana.** La cabecera mantiene el botón en el borde derecho, las tarjetas siguen iguales y el recuadro About se estira. **Mira los cambios al guardar:** Ejecuta cualquier script con `pyzed dev examples/tutorial/layout_basics.py` (o `uv run pyzed dev ...`): la ventana se actualiza cada vez que guardas el archivo y conserva su estado. Ideal para jugar con `spacing` y `padding`. #### Lo mismo en Pyzed Design El diseñador tiene las mismas piezas en la categoría **Disposición** (*Layout*) de las Herramientas: | En código | En el diseñador | | --- | --- | | `pz.row([...], spacing=8)` | **Fila flexible** (*Flex row*), con **Separación** (*Gap*) 8 | | `pz.column([...])` | **Columna flexible** (*Flex column*) | | `pz.hspace()` | **Espaciador** (*Spacer*, un hijo que crece 1) | | `width="fill"` en un hijo | El campo **Crecer** (*Grow*) del hijo (sección Posición) en 1 | | `pz.grid([...], columns=3)` | **Cuadrícula** (*Grid*) con **Columnas** 3 | | `padding=24` | El **Relleno** (*Padding*) del contenedor | Los controles colocados libremente en el formulario (fuera de un contenedor flexible) usan **anclajes**: anclado a izquierda y derecha, un control se estira; anclado solo a la derecha, sigue al borde derecho. Selecciona controles y usa **Agrupar en una fila / columna / cuadrícula** (menú del clic derecho) para convertir una disposición libre en una flexible. Detalles en [Layouts y contenedores](https://albakore.github.io/pyzed/es/editor/layouts/). #### Código completo `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) ``` #### Eventos de este tutorial La página tiene un evento: el `on_press` del botón **Follow**, que llama a `toggle_follow` (sin argumentos). Los widgets de disposición no tienen eventos propios, pero cualquier widget acepta los [eventos comunes](https://albakore.github.io/pyzed/es/guides/events/) con `.on()`. ##### Prueba más eventos ```python def __init__(self): self.follows = False self.narrow = False # nuevo: la ventana es estrecha self.opened = None # nuevo: la cifra que se pulsó 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: una sola columna cuando la ventana mide menos de 560 px return page.on(resize=lambda e: setattr(self, "narrow", e.width < 560), double_click=lambda: setattr(self, "opened", None)) ``` #### Siguiente - [Cambia el tema](https://albakore.github.io/pyzed/es/tutorials/basic/themes/) de esta ventana. - [Guía de layout](https://albakore.github.io/pyzed/es/guides/layout/): cuadrículas por pistas (`"200 1fr auto"`), `pz.flex`, scroll y layouts anclados. - [Row](https://albakore.github.io/pyzed/es/library/row/), [Column](https://albakore.github.io/pyzed/es/library/column/) y [Grid](https://albakore.github.io/pyzed/es/library/grid/) en la biblioteca de componentes. ### Listas y tablas con datos > Muestra datos de Python en una app de Pyzed: una lista con búsqueda, una tabla ordenable con filas clicables y celdas propias, y un panel con los detalles de la fila elegida. Source: https://albakore.github.io/pyzed/es/tutorials/intermediate/lists-tables/ Casi todas las apps muestran **datos**: clientes, archivos, pedidos, libros. Este tutorial toma una lista normal de Python y la muestra de dos formas, como **tabla** ordenable y como **lista** simple, con un buscador y un panel con los detalles de la fila en la que haces clic. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Una biblioteca de libros con búsqueda, orden y detalles | 30 minutos | Intermedio · Python | [Una app con varias vistas](https://albakore.github.io/pyzed/es/tutorials/intermediate/navigation/) | #### Qué vas a aprender - Filtrar datos en `view()` a partir de un buscador. - Dibujar un `ui.table` con columnas, **orden** al hacer clic en un encabezado y filas clicables. - Convertir una celda en cualquier widget (una insignia de color) con `render=`. - Mostrar los mismos datos como una lista de filas `ui.list_item`. - Guardar el elemento **seleccionado** y mostrarlo en un panel de detalles; cambiarlo en el sitio. #### Pasos 1. **Los datos son solo Python.** Una lista de dicts (también sirven objetos o dataclasses): ```python BOOKS = [ {"title": "Dune", "author": "Frank Herbert", "year": 1965, "status": "Read"}, {"title": "Neuromancer", "author": "William Gibson", "year": 1984, "status": "Reading"}, # ... ] ``` 2. **Búsqueda.** Guarda el texto en `self.query` y filtra en cada redibujado. No hace falta "refrescar": escribir cambia el estado y `view()` se vuelve a ejecutar. ```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. **Las columnas.** Un `ui.Column` tiene un título, la clave que lee y un ancho (`("fill", 3)` toma tres partes del espacio libre). `render=` convierte el valor en un 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. **Orden.** Pasa a la tabla la columna por la que ordena y el sentido; al hacer clic en un encabezado se llama a `on_sort` con el título de esa columna, y tú inviertes el sentido si es la misma: ```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. **El panel de detalles** muestra `self.selected`, o un estado vacío. Cambiar el estado del libro escribe en el mismo dict, así que la tabla también lo muestra: ```python ui.segmented(list(TINTS), book["status"], lambda s: book.__setitem__("status", s)) ``` 6. **Una vista de lista.** Los mismos datos como filas con un icono y el año al final, alternando con un control segmentado: ```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"]))) ``` **Miles de filas:** `ui.table` solo construye las filas que se ven, así que una tabla de 100.000 filas se desplaza tan suave como una de diez. Mantén la lista ordenada en tu estado (o en una función `@pz.memo`) en lugar de ordenarla en cada redibujado. La demo de [contactos con SQLAlchemy](https://albakore.github.io/pyzed/es/integrations/sqlalchemy/) muestra 20.000 filas buscadas en SQL. #### Código completo `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) ``` #### Eventos de este tutorial | Widget | Evento | El handler recibe | | --- | --- | --- | | `ui.table` | `on_sort` | El título de la columna pulsada. | | `ui.table` | `on_row_press` | La fila: el dict del libro. | | `ui.list_item` | `on_press` | Nada (el libro se ata con `book=book`). | | `ui.search_input`, `ui.segmented` | `on_input`, `on_select` | La búsqueda; el modo. | ##### Prueba más eventos La tabla tiene dos eventos de fila más: el doble clic y el clic derecho. ```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), # abrir un editor on_row_context=lambda book: setattr(self, "menu_for", book), # un menú contextual striped=True, row_height=34, height="fill", ) def on_key(self, combo): # las flechas mueven la selección 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)))] ``` #### Siguiente - [Diálogos, notificaciones y validación](https://albakore.github.io/pyzed/es/tutorials/intermediate/dialogs-feedback/). - La [tabla de datos](https://albakore.github.io/pyzed/es/showcase/dashboards/#tabla-de-datos) del Showcase: filtros, selección, páginas y exportación. - [Table](https://albakore.github.io/pyzed/es/library/table/) y [List item](https://albakore.github.io/pyzed/es/library/list-item/) en la biblioteca de componentes. ### Un grafo de shader propio > Construye un grafo de shader de GPU nodo a nodo en Python con pyzed.shading: ruido fractal, bandas con smoothstep, un cielo en degradado, variables mutables controladas con deslizadores y un .pzshad que puedes abrir en Pyzed Design. Source: https://albakore.github.io/pyzed/es/tutorials/advanced/shader-graph/ En [el tutorial intermedio de shaders](https://albakore.github.io/pyzed/es/tutorials/intermediate/shader-effect/) editaste una plantilla en la vista de shader. Aquí construyes un shader desde cero, **en Python**, con el mismo grafo que edita la vista de shader: una aurora, cortinas de luz verde que flotan sobre un cielo nocturno, con deslizadores para su velocidad, su altura y su color. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Una aurora animada hecha de nodos, con controles en vivo | 40 minutos | Avanzado · Python | [Un efecto de shader](https://albakore.github.io/pyzed/es/tutorials/intermediate/shader-effect/) | #### Qué vas a aprender - Crear un `shading.Graph`, añadir nodos llamándolos y elegir sus salidas. - Usar aritmética sobre las salidas (`+`, `-`, `*`) para que se añadan nodos Math por ti. - Combinar **ruido fractal**, **smoothstep** y un **degradado** en una imagen. - Añadir **variables mutables** (uniforms) y cambiarlas en cada fotograma desde deslizadores. - Guardar el grafo como `.pzshad` y seguir editándolo visualmente en Pyzed Design. #### Cómo piensa un shader Un shader responde a una pregunta, para todos los píxeles a la vez: *¿de qué color es este píxel ahora mismo?* Sabe dónde está el píxel (**UV**: de 0,0 arriba a la izquierda a 1,1 abajo a la derecha) y el **tiempo**. Todo lo demás son operaciones sobre esas dos cosas. El grafo es esa matemática, dibujada como cajas y cables: ```mermaid flowchart LR UV[UV] --> P["Compose: x·3 + t, y·0.6 − t·0.3"] T["Time × speed"] --> P P --> N[Ruido fractal] N --> S1["Smoothstep 0.35 → 0.8"] UV --> B["Banda cerca de arriba (height)"] S1 --> L["luz = cortinas × banda"] B --> L UV --> SKY["Gradient: cielo nocturno"] SKY --> M["Mix cielo → tint según la luz"] L --> M M --> V["× Vignette"] --> O[Output] ``` #### Pasos 1. **Un grafo y sus variables.** `g.uniform` añade una variable *mutable* (la GPU la lee en cada fotograma, así que cambiarla no cuesta nada) y devuelve un nodo que la lee: ```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. **Entradas.** Llamar a un tipo de nodo sobre el grafo añade ese nodo. `node["nombre"]` elige una salida, y la aritmética añade nodos Math: ```python uv = g.uv() t = g.time()["time"] * speed # un nodo Multiply ``` 3. **Las cortinas.** Estira las coordenadas a lo ancho y muévelas con el tiempo, y luego toma **ruido fractal** (capas de ruido, cada una más fina que la anterior; el aspecto clásico de nubes y humo): ```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. **Mantenlas en una banda.** `smoothstep(a, b, x)` vale 0 por debajo de `a`, 1 por encima de `b` y sube suave entre medio: la versión de shader de "aparecer gradualmente". Dos de ellos forman una banda cerca de arriba, controlada por `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. **Dale color.** Un nodo **Gradient** hace el cielo; **Mix** pasa del cielo al tinte donde hay luz; **Vignette** oscurece las esquinas; **Output** es el color final: ```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() # ordena los nodos en columnas, como el botón Arrange de la vista de shader ``` 6. **Muéstralo.** `pz.shader` recibe el grafo, lo compila a WGSL una vez y lo ejecuta en la GPU. `uniforms=` fija las variables mutables por nombre, así que los deslizadores cambian la imagen sin recompilar nada: ```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. **Ábrelo en el diseñador.** Ejecuta el ejemplo con `--save` para escribir `examples/tutorial/shaders/aurora.pzshad`, y luego `pyzed design examples/tutorial/shaders/aurora.pzshad`: cada nodo que creaste en código está en el lienzo, con su vista previa en vivo, listo para ajustarlo con el mouse. **Cuando necesitas WGSL directo:** Un nodo **Expression** guarda tu propio WGSL (`g.add("expression", code="sin(a * 10.0) * b", a=..., b=...)`), así que nunca tienes que salir del grafo por una fórmula especial. Para un shader entero en WGSL, mira [Shaders](https://albakore.github.io/pyzed/es/shading/shaders/). **Los errores de compilación señalan el nodo:** `g.compile()` devuelve el WGSL y los problemas por nodo (`.errors`); un tipo que no encaja (una textura en un número) se rechaza con un `GraphError` en el momento de conectarlo. En la vista de shader el nodo con el problema se pone rojo. #### Código completo `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) ``` #### Siguiente - [Grafos de shader](https://albakore.github.io/pyzed/es/shading/shader-graphs/): toda la API de `pyzed.shading` y el formato `.pzshad`. - La sección [Shading](https://albakore.github.io/pyzed/es/shading/): cada nodo, con ejemplos. - [Conectar un backend](https://albakore.github.io/pyzed/es/tutorials/advanced/backend/). ### Cambia el tema > Dale otro aspecto a una app de Pyzed: elige un tema incluido, cambia el color de acento, sigue el modo claro u oscuro del sistema o genera tu propio tema a partir de tres colores, en código y en Pyzed Design. Source: https://albakore.github.io/pyzed/es/tutorials/basic/themes/ Un **tema** es el conjunto de colores (y algunos tamaños, como el radio de las esquinas) que lee cada componente al dibujarse. Cambias el tema y cambia toda la ventana a la vez: botones, campos, insignias, el editor de código... Nunca coloreas los componentes uno por uno. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Una ventana con un selector de tema | 15 minutos | Básico · Python, o sin código en el diseñador | [Bases del diseño de pantallas](https://albakore.github.io/pyzed/es/tutorials/basic/layout-basics/) | #### Qué vas a aprender - Elegir un tema en código (`theme = "One Light"`) y en el diseñador (**Propiedades → Aspecto → Tema**). - Cambiar solo el color de **acento**. - Seguir el modo **claro u oscuro** del sistema operativo. - Crear **tu propio tema** a partir de tres colores y darle un nombre. - Usar **colores del tema** (`"accent"`, `"text_muted"`) en lugar de valores `#hex` fijos. #### Pasos 1. **Elige un tema incluido.** `theme` es un atributo de clase de tu app. Sirve cualquier nombre de `pz.themes.names()`: las familias One, Ayu y Gruvbox, claras y oscuras. ```python class ThemeSwitcher(pz.App): theme = "One Dark" ``` 2. **Cámbialo mientras la app corre.** Asigna `self.theme`; el siguiente redibujado lo usa. Un desplegable con todos los temas es una línea: ```python ui.select(pz.themes.names(), self.base, lambda name: setattr(self, "theme", name)) ``` 3. **Cambia solo el acento.** El acento es el color de los botones principales, los anillos de foco, la selección y los enlaces: ```python self.theme = pz.themes.get("One Dark").with_accent("#e5c07b") ``` 4. **Sigue al sistema.** `pz.themes.auto(light, dark)` cambia solo cuando el usuario pasa su sistema operativo de modo claro a oscuro: ```python self.theme = pz.themes.auto("One Light", "One Dark") ``` 5. **Crea el tuyo.** `pz.Theme.generate` deduce cada superficie, borde, estado hover y texto atenuado a partir de tres colores; `pz.themes.register` lo añade a la lista con su nombre: ```python SUNSET = pz.themes.register( pz.Theme.generate("Sunset", background="#2b1d2f", text="#f5e6e8", accent="#ff8a65") ) ``` 6. **Lee el tema en tu vista.** `self.resolved_theme()` devuelve el `Theme` en uso, útil para un fondo o para decir "oscuro" o "claro": ```python t = self.resolved_theme() pz.container(..., bg=t.background) ui.muted(f"Using {t.name} ({'dark' if t.is_dark else 'light'})") ``` **Usa colores del tema, no #hex:** Donde un componente acepte un color, prefiere un **token** como `"accent"`, `"text_muted"`, `"success"` o `"error"` en lugar de un `"#3b82f6"` fijo. Los tokens siguen al tema, así tu app se lee bien en temas claros y oscuros. #### En Pyzed Design Selecciona un lugar vacío del lienzo para que el panel Propiedades muestre el **formulario** y abre la sección **Aspecto** (*Look*): - **Tema** (*Theme*): todos los temas registrados. - **Acento** (*Accent*): siete colores predefinidos, o un `#rrggbb` propio. - **Fondo**, **Radio de esquina** y **Tamaño de letra** para todo el formulario. **Ver → Vista previa con tema…** muestra el lienzo con otro tema sin cambiar el formulario, para revisar los dos aspectos. Cada control también puede cambiar sus propios colores en su sección **Estilo**. #### Código completo `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) ``` **Para ir más lejos:** Esto es el recorrido rápido. La sección [Temas y estilos](https://albakore.github.io/pyzed/es/theming/) explica cada token, cómo dar estilo a un widget con `.style(...)`, degradados, fuentes e iconos, y cómo guardar un tema en un archivo. #### Eventos de este tutorial | Widget | Evento | El handler recibe | | --- | --- | --- | | `ui.switch` (Seguir al sistema) | `on_toggle` | `True` o `False`. | | `ui.select` (Tema) | `on_select` | El nombre del tema. | | `ui.segmented` (Acento) | `on_select` | El nombre del acento. | | `ui.input` (Tu nombre) | `on_input` | El texto. | `self.choose("base")` devuelve el handler: una función pequeña que guarda lo que recibe en ese campo. Una sola fábrica sirve a los tres controles. ##### Prueba más eventos ```python def on_key(self, combo): # un atajo para todas las ventanas de la app if combo == "ctrl-t": # Ctrl+T: alterna claro y oscuro self.base = "One Light" if self.resolved_theme().is_dark else "One Dark" self.follow_system = False def on_resize(self, width, height): # el evento de tamaño de la propia ventana self.compact = width < 600 # p. ej. ocultar la vista previa en ventanas pequeñas ``` #### Siguiente - [Tutorial en 5 pasos](https://albakore.github.io/pyzed/es/tutorials/basic/step-by-step/): estado, layout, async y una app de notas en Python puro. - [Temas](https://albakore.github.io/pyzed/es/theming/themes/) y [Estilos](https://albakore.github.io/pyzed/es/theming/styles/) en la sección de temas. ### Conectar un backend > Conecta una app de escritorio de Pyzed a una base de datos: una clase repositorio con SQLite, consultas en un hilo en segundo plano con Command.perform, un spinner de carga y errores que se muestran en lugar de cerrar la app. Source: https://albakore.github.io/pyzed/es/tutorials/advanced/backend/ Pyzed es el **frontend**: dibuja ventanas y reacciona al usuario. Tus datos viven en otro sitio: una base de datos, una API REST, archivos. Este tutorial conecta las dos partes como lo hacen las apps grandes, con una **capa de backend** delgada con la que habla la ventana, y cada llamada lenta fuera del hilo de la interfaz para que la ventana nunca se congele. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Una app de contactos sobre SQLite | 40 minutos | Avanzado · Python | [Tareas en segundo plano](https://albakore.github.io/pyzed/es/tutorials/intermediate/background-tasks/) | #### Qué vas a aprender - Poner todo el acceso a datos en una clase **repositorio** sin interfaz, fácil de probar y de reemplazar. - Ejecutar consultas en un **hilo en segundo plano** con `Command.perform(work, then)`. - Mostrar un **spinner** mientras corre una consulta, y convertir las excepciones en un mensaje en lugar de un cierre. - Recargar la vista después de un cambio (añadir, borrar, buscar). #### La forma de la app ```mermaid flowchart LR UI["Contacts (pz.App)
estado + view()"] -- "Command.perform(work)" --> T["hilo en segundo plano"] T -- "work(): repo.search(...)" --> R["ContactsRepository"] R --> DB[("SQLite
(o una API, un ORM...)")] T -- "then(resultado) en el hilo de la interfaz" --> UI ``` #### Pasos 1. **El repositorio.** Python normal, sin importar Pyzed. Abre una conexión por llamada, así se puede usar desde cualquier hilo: ```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. **Nunca consultes en el hilo de la interfaz.** Una consulta de 300 ms congelaría la ventana 300 ms. `Command.perform(work, then)` ejecuta `work()` en un hilo y llama a `then(resultado)` de vuelta en el hilo de la interfaz, donde es seguro cambiar el estado: ```python def reload(self): return pz.Command.perform(lambda: self.repo.search(self.query), self.loaded) def loaded(self, contacts): self.contacts = contacts ``` 3. **Ocupado y errores.** Una pequeña función envuelve cada llamada: marca `self.busy` (la vista muestra un spinner) y atrapa las excepciones para que se conviertan en un aviso: ```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. **Carga al empezar, recarga después de cambios.** `on_start` se ejecuta una vez al abrir la ventana. Añadir o borrar recarga la lista al terminar: ```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. **Prueba el backend solo.** Como el repositorio no tiene interfaz, una prueba son tres líneas: ```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"] ``` **Bibliotecas async:** Si tu cliente es async (`httpx.AsyncClient`, `aiosqlite`, la `AsyncSession` de SQLAlchemy), pasa un `async def` a `Command.perform`, o devuelve una corrutina desde un callback: corre en el loop de asyncio de Pyzed. Mira [Async y suscripciones](https://albakore.github.io/pyzed/es/guides/async/). **No toques self desde el hilo:** `work()` corre en otro hilo: deja que devuelva un valor y cambia `self` en `then`. Cambiar el estado desde el hilo de trabajo puede chocar con el dibujado. #### Código completo `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) ``` #### Backends reales El mismo patrón con bibliotecas reales, cada uno un proyecto completo en [`examples/integrations`](https://github.com/albakore/pyzed/tree/main/examples/integrations): #### Eventos de este tutorial | Origen | Evento | Handler | | --- | --- | --- | | La app | `on_start` | Carga los contactos del backend en segundo plano. | | `ui.input` | `on_input` | Guarda el texto de cada campo. | | `ui.input` (email) | `on_submit` | `self.add`: manda el contacto nuevo al backend. | ##### Prueba más eventos ```python def row(self, contact): return ui.list_item(contact.name, None, icon="user", on_double_press=lambda: setattr(self, "editing", contact), # editarlo on_right_press=lambda: setattr(self, "deleting", contact)) # pedir borrarlo def on_key(self, combo): if combo == "f5": # recargar desde el backend return self.on_start() ``` #### Siguiente - [Empaqueta tu app](https://albakore.github.io/pyzed/es/tutorials/advanced/packaging/) para Windows, macOS y Linux. - [Integraciones](https://albakore.github.io/pyzed/es/integrations/overview/): dónde corre tu código y cómo los formularios muestran errores del servidor. ### Diálogos, notificaciones y validación > Comunícate con el usuario en una app de Pyzed: errores de campo calculados a partir del estado, un diálogo de confirmación sobre la página y notificaciones que desaparecen solas. Source: https://albakore.github.io/pyzed/es/tutorials/intermediate/dialogs-feedback/ Una buena app le cuenta al usuario qué pasa: qué está mal en el formulario, qué está a punto de pasar, qué acaba de pasar. Pyzed tiene una pieza lista para cada cosa: **errores de campo**, **diálogos** y **toasts** (pequeñas notificaciones en una esquina). | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un formulario de registro con errores, una confirmación y notificaciones | 30 minutos | Intermedio · Python | [Listas y tablas](https://albakore.github.io/pyzed/es/tutorials/intermediate/lists-tables/) | #### Qué vas a aprender - Calcular los errores de validación a partir de tu estado y mostrarlos con `ui.field(..., error=...)`. - Mostrarlos solo después del primer intento, no mientras el usuario todavía escribe. - Abrir un **diálogo de confirmación** sobre la página con `ui.dialog_layer` y `ui.confirm`. - Mostrar **toasts** con `ui.toasts` y quitarlos a los pocos segundos sin congelar la ventana. #### Pasos 1. **Los errores son una función del estado.** Un método devuelve `{campo: mensaje}` con lo que esté mal ahora mismo: ```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. **Muéstralos bajo cada campo**, pero solo cuando el usuario ya intentó enviar (`self.tried`). A nadie le gusta ver texto rojo antes de tener la oportunidad: ```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. **Un diálogo de confirmación.** `ui.dialog_layer(page, dialog)` dibuja `dialog` centrado sobre la página oscurecida; con `dialog=None` se ve solo la página. Así que "¿está abierto el diálogo?" es un atributo: ```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.** Guarda una lista de avisos, dibújalos con `ui.toasts` y quita cada uno a los 4 segundos. `Command.perform` espera en un hilo en segundo plano, así que la ventana sigue respondiendo: ```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)) # en 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. **Júntalo todo.** El botón marca `tried`, muestra un toast de error si algo está mal o abre el diálogo; confirmar vacía el formulario y muestra un toast de éxito. **Otros diálogos listos:** `ui.alert` (un botón), `ui.prompt` (pide una línea de texto), `ui.choice` (tus propios botones) y `ui.unsaved_changes` ("¿Guardar antes de cerrar?") funcionan igual. Míralos todos en `examples/dialogs.py` y en las páginas [Alert](https://albakore.github.io/pyzed/es/library/alert/) y [Confirm](https://albakore.github.io/pyzed/es/library/confirm/) de la biblioteca. **Validar con Pydantic:** Si tus datos ya tienen un modelo de Pydantic (o SQLModel, Django, FastAPI), deja que valide él y convierte sus errores en `{campo: mensaje}` con `pyzed.forms.validation_errors(error)`. Mira [Pydantic](https://albakore.github.io/pyzed/es/integrations/pydantic/). #### Código completo `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) ``` #### Eventos de este tutorial | Widget | Evento | Handler | | --- | --- | --- | | `ui.input` (tres campos) | `on_input` | `lambda v: setattr(self, "name", v)`… | | `ui.input` (contraseña) | `on_submit` | `self.submit` | | **Create account** | `on_press` | `self.submit` | | `ui.confirm` | `on_confirm`, `on_cancel` | `self.create`, cerrar el diálogo | | `ui.dialog_layer` | `on_dismiss` | cerrar el diálogo (un clic en el fondo) | | `ui.toast` | `on_dismiss` | `lambda i=i: self.dismiss(i)` | ##### Prueba más eventos ```python def on_key(self, combo): if combo == "escape" and self.confirming: # Escape cierra el diálogo self.confirming = False return [] # atendida: la tecla no sigue 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)) # un clic en la tarjeta también lo cierra for i, kind, title, text in self.notes] ``` #### Siguiente - [Un componente propio](https://albakore.github.io/pyzed/es/tutorials/intermediate/custom-component/) en la vista de componente del diseñador. - [Tareas en segundo plano](https://albakore.github.io/pyzed/es/tutorials/intermediate/background-tasks/): barras de progreso y toasts desde hilos. ### Empaqueta tu app > Del proyecto a la descarga: crea un proyecto de Pyzed con uv, genera un ejecutable independiente con pyzed build en Windows, macOS y Linux, añade un icono y archivos de datos, y compila cada plataforma en CI. Source: https://albakore.github.io/pyzed/es/tutorials/advanced/packaging/ Tu app funciona en tu máquina. Ahora quieres mandarla a alguien que no tiene Python, no sabe qué es `pip` y debería poder **abrirla con doble clic**. Este tutorial lleva una app desde una carpeta de proyecto hasta un archivo que puedes compartir, y luego a una compilación automática para los tres sistemas. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Una app que se abre con doble clic en Windows, macOS y Linux | 30 minutos (más el tiempo de compilación) | Avanzado · línea de comandos | [uv](https://docs.astral.sh/uv/getting-started/installation/) instalado, una cuenta de GitHub para la última parte | #### Qué vas a aprender - Crear (o convertir en) un **proyecto de Pyzed**: la estructura que espera `pyzed build`. - Generar un **ejecutable independiente** para el sistema en el que estás. - Añadir un **icono**, **archivos de datos** y un **nombre**, y leer los archivos incluidos desde tu código. - Compilar **todas las plataformas** a la vez con GitHub Actions, y por qué importa firmar. #### Pasos 1. **Parte de un proyecto.** `pyzed build` empaqueta un proyecto de [uv](https://docs.astral.sh/uv/) cuyo formulario o script principal está definido en `pyproject.toml`. Crea uno (o usa **Nuevo proyecto** en la ventana de bienvenida de Pyzed Design): **uv:** ```bash title="Windows, macOS y Linux" uvx --from pyzed-ui pyzed new my-app cd my-app uv run pyzed run # pruébala ``` **pip:** ```bash title="macOS y Linux" python -m pip install pyzed-ui pyzed new my-app # usa uv por debajo: instálalo primero 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="Con Pyzed ya instalado" python -m pyzed new my-app ``` ¿Ya tienes un proyecto de uv? Ejecuta `pyzed init` dentro. La interfaz vive en `ui/` (formularios y sus manejadores) y el backend en `src/my_app/`. 2. **Compila para este ordenador.** Desde la carpeta del proyecto: **uv:** ```bash title="Windows, macOS y Linux" uv run pyzed build ``` **pip:** ```bash title="macOS y Linux" source .venv/bin/activate pyzed build ``` ```powershell title="Windows (PowerShell)" .venv\Scripts\Activate.ps1 pyzed build ``` **python:** ```bash title="Dentro del entorno del proyecto" python -m pyzed build ``` La primera compilación tarda un minuto o dos. El resultado va a `dist/`: | Compilado en | Obtienes | Compártelo como | | --- | --- | --- | | Windows | `dist\my-app\my-app.exe` | Un zip de la carpeta, o un instalador (Inno Setup, WiX) | | macOS | `dist/my-app.app` | Un zip o un `.dmg`, firmado y notarizado | | Linux | `dist/my-app/my-app` | Un tarball, AppImage, `.deb` o Flatpak | 3. **Pruébala como lo haría un usuario.** Copia la carpeta a otro sitio (o a otra máquina sin Python) y abre el ejecutable. Si se cierra enseguida, compila con `--console` para ver el error. 4. **Nombre, icono y archivos de datos.** Añade una tabla `[tool.pyzed.build]` a `pyproject.toml`: ```toml title="pyproject.toml" [tool.pyzed.build] name = "Notes" # el nombre del ejecutable icon = "assets/icon.ico" # .ico en Windows, .icns en macOS include = ["assets", "data/help.md"] # archivos que tu app lee al ejecutarse ``` Dentro del paquete, busca esos archivos desde la raíz del paquete: ```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. **¿Un archivo o una carpeta?** `pyzed build --onefile` crea un único archivo, práctico para enviar, pero más lento al arrancar (se descomprime cada vez) y marcado más a menudo por los antivirus. Una carpeta más un instalador es la opción más segura para una versión real. 6. **Todas las plataformas desde CI.** Una compilación solo funciona en el sistema en el que se hizo, así que necesitas máquinas Windows, macOS y Linux. GitHub las tiene: ```bash uv run pyzed build --workflow # escribe .github/workflows/build.yml git add .github && git commit -m "Build on every platform" && git push git tag v1.0.0 && git push --tags # una etiqueta v* lanza la compilación ``` Cada ejecución guarda las tres carpetas `dist/` como artefactos para descargar. **Firma:** Las apps sin firmar funcionan, pero SmartScreen de Windows y Gatekeeper de macOS avisan a los usuarios (macOS puede negarse a abrirla). Para una versión pública firma el `.exe` de Windows con un certificado de firma de código, y firma y notariza la `.app` de macOS con un Apple Developer ID. Los comandos están en [Compilar y distribuir](https://albakore.github.io/pyzed/es/packaging/build/). **Escritorio hoy, más plataformas después:** Pyzed compila hoy apps de escritorio para Windows, macOS y Linux. Móvil y web están en la hoja de ruta; la sección [Distribuir](https://albakore.github.io/pyzed/es/packaging/) dirá cuándo estén listos. #### Siguiente - [Compilar y distribuir](https://albakore.github.io/pyzed/es/packaging/build/): qué se incluye, cada opción y solución de problemas. - [Construye una app con el agente de IA](https://albakore.github.io/pyzed/es/tutorials/advanced/ai-agent/). ### Tutorial en 5 pasos > Aprende Pyzed en cinco pasos: de una ventana vacía a una app de notas con Markdown, trabajo async y una segunda ventana, en Python puro. Source: https://albakore.github.io/pyzed/es/tutorials/basic/step-by-step/ Cinco archivos cortos en [`examples/tutorial/`](https://github.com/albakore/pyzed/tree/main/examples/tutorial). Cada uno se ejecuta solo y añade una idea al anterior, así que puedes parar después de cualquier paso y seguir teniendo un programa que funciona. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | De una ventana vacía a una app de notas, en Python puro | 45 minutos | Básico · Python | Pyzed instalado ([Instalación](https://albakore.github.io/pyzed/es/getting-started/installation/)) | #### Qué vas a aprender - Cómo funciona una app de Pyzed: una clase `App`, su estado y un `view()` que describe la ventana. - Dos formas de reaccionar al usuario: mensajes enviados a `update()` y callbacks normales. - Layout, componentes listos para usar y temas. - Trabajo lento con `async def` sin congelar la ventana. - Atajos de teclado, un panel dividido redimensionable y una segunda ventana. **Edita y mira:** Ejecuta un paso con `pyzed dev examples/tutorial/step1_hello.py` y la ventana se actualiza cada vez que guardas el archivo, sin perder su estado. #### 1. Una ventana Una app es una clase que hereda de `pz.App` y define `view()`. `view()` devuelve un árbol de widgets (`pz.text`, `pz.column`, `pz.row`, `pz.container`…). Pyzed la vuelve a llamar después de cada cambio, así que nunca modificas widgets: describes cómo se ve el estado actual. Atributos de clase como `title` y `size` configuran la ventana. (Image: Paso 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. Estado y mensajes El estado son atributos normales de tu objeto. Los widgets reciben qué hacer de dos formas: - **Un valor** (`ui.button("Greet", "greet")`): se envía a `update(message)`. - **Un callable** (`ui.button("Reset", self.reset)`): se llama directamente. Los callbacks que traen datos reciben el valor nuevo: `on_input=lambda text: ...`, `on_toggle=lambda checked: ...`, `on_change=lambda value: ...`. (Image: Paso 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, componentes y temas `pyzed.ui` trae componentes listos para usar (`button`, `input`, `select`, `number_field`, `settings_group`, `callout`…) que toman sus colores del tema activo. `theme` puede ser un `Theme`, el nombre de un tema incluido (`pz.themes.names()`) o `pz.themes.auto(...)` para seguir al sistema. `self.resolved_theme()` devuelve el `Theme` en uso. Más en [Temas](https://albakore.github.io/pyzed/es/theming/themes/). (Image: Paso 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 El trabajo lento no debe bloquear la ventana. `Command.perform` acepta funciones `async def`, que corren en el loop de asyncio de Pyzed; el resultado llega a tu callback en el hilo de la interfaz. Mientras carga, una suscripción `pz.every` redibuja la ventana para animar el spinner. También puedes devolver corrutinas o generadores desde `update` y los callbacks: lo explica la guía [Async y suscripciones](https://albakore.github.io/pyzed/es/guides/async/). (Image: Paso 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. Todo junto Una app de notas: lista lateral, editor con vista previa de Markdown en un `ui.split` redimensionable, un atajo (`on_key` recibe combinaciones como `"ctrl-n"`) y una segunda ventana (`pz.Window`) que se abre con `return About().open()` y comparte el estado de la app. Más en [Varias ventanas](https://albakore.github.io/pyzed/es/guides/windows/). (Image: Paso 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) ``` #### Eventos de este tutorial | Paso | Widget | Evento | El handler recibe | | --- | --- | --- | --- | | 2 | `ui.input` | `on_input` | El texto nuevo, en cada tecla. | | 2 | `ui.input`, `ui.button` | `on_submit`, `on_press` | Nada. Aquí son el *mensaje* `"greet"`, que va a `update`. | | 3 | `ui.number_field`, `ui.switch`, `ui.select` | `on_change`, `on_toggle`, `on_select` | El número, `True`/`False`, la opción. | | 4 | `subscriptions()` | un temporizador | `pz.every(segundos, mensaje)` manda el mensaje a `update`. | | 5 | `on_key(combo)` | cada tecla | La combinación, como `"ctrl-n"`. | | 5 | `ui.editor`, `ui.split` | `on_change`, `on_resize` | El texto; la parte del primer panel. | El paso 2 maneja sus eventos en dos estilos a la vez: la caja llama a una lambda que cambia `self` (imperativo) y el botón manda el mensaje `"greet"` a `update` (declarativo). El mismo saludador solo con mensajes: ```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="Tu nombre", on_submit="greet"), ui.button("Saludar", "greet", variant="solid"), ], spacing=12, padding=24) ``` Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/) para la tercera forma. #### Siguientes pasos - [Lista de tareas](https://albakore.github.io/pyzed/es/tutorials/basic/todo-app/) y el [mini editor de código](https://albakore.github.io/pyzed/es/tutorials/advanced/code-editor/), más detallados. - [Enlace de datos en el diseñador](https://albakore.github.io/pyzed/es/tutorials/intermediate/data-binding/), si prefieres dibujar tus formularios. - [Ejemplos](https://albakore.github.io/pyzed/es/examples/): recreaciones de las demos de Iced (game of life, reloj, cronómetro, multi-ventana, imágenes, QR, estilos, paneles…). ### Un componente propio > Dibuja una valoración con estrellas reutilizable en la vista de componente de Pyzed Design: una propiedad pública, enlaces, manejadores de eventos, un evento propio, y úsala en un formulario y en código. Source: https://albakore.github.io/pyzed/es/tutorials/intermediate/custom-component/ Cuando el mismo grupo de controles aparece una y otra vez (una valoración con estrellas, una tarjeta de usuario, una barra de búsqueda), conviértelo en un **componente**: lo dibujas una vez, le das propiedades y eventos, y lo colocas donde quieras como un control más. Es la idea de las escenas de Godot y de los widget blueprints de Unreal, y es como las apps grandes se mantienen ordenadas. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un `Rating` de cinco estrellas que puedes poner en cualquier formulario | 35 minutos | Intermedio · Python | [Enlace de datos](https://albakore.github.io/pyzed/es/tutorials/intermediate/data-binding/) | #### Qué vas a aprender - Crear un componente en la **Vista de componente** (un `.pzcomp` y su clase en un `.py`). - Añadir una **propiedad pública** (`value`) y un **estado privado** (`clicks`). - **Enlazar** las propiedades de los controles a las del componente (`"solid" if value >= 3 else "ghost"`). - Escribir **métodos** y lanzar un **evento propio** con `self.emit("change", ...)`. - Probarlo en la **ventana de vista previa**, ponerlo en un formulario y usarlo desde código Python. #### Pasos 1. **Crea el componente.** Ctrl+N → **Componente**, llámalo `rating` y pulsa **Crear componente**. El diseñador crea `rating.pzcomp` (el dibujo) y `rating.py` (la clase `Rating`) y los abre en la **Vista de componente**: un marco de color de acento en lugar de los botones de ventana, y una insignia **Componente**. 2. **Dibújalo.** Sin nada seleccionado, pon el **Ancho** del componente en 232 y el **Alto** en 28 (sección Disposición). Coloca cinco **Botones** en fila llamados `star1` … `star5`, cada uno con el icono `star`, sin texto y con variante `ghost`, y una **Etiqueta de texto** llamada `caption` al final. 3. **Añade las propiedades.** En la sección **Properties** del panel del componente pulsa **Public** y crea `value`, de tipo `int`, con valor por defecto `3`; luego `readonly`, de tipo `bool`. Pulsa **Private** para `clicks`, un `int` que solo ve el componente. La clase de `rating.py` los recibe como atributos anotados: ```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. **Enlaza las estrellas.** Selecciona `star3`, abre **Bindings**, elige **variant** y luego **Expression…**: ```python "solid" if value >= 3 else "ghost" ``` Haz lo mismo con las otras estrellas (cada una con su número) y enlaza `caption.text` a `f"{value} / 5"`. Cambia `value` en el panel: las estrellas se encienden en el lienzo. 5. **Añade el evento propio.** En **Custom events** pulsa **Event**, llámalo `change` y descríbelo: *A star was picked (event.value is the rating)*. 6. **Escribe la lógica.** Añade un método `pick` (escribe el nombre y pulsa **+ Method**) y haz doble clic en cada estrella para escribir su manejador de clic. En un componente los manejadores son métodos y los controles son `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. **Pruébalo solo.** Pulsa F5: el componente se abre en una **ventana de vista previa** que lista cada evento propio que lanza. Haz clic en las estrellas y mira llegar `change`. 8. **Úsalo en un formulario.** Guarda y abre (o crea) un formulario: `Rating` ya está en las **Herramientas**, en su categoría. Pon uno, añade una **Etiqueta de texto** llamada `status`, selecciona la valoración y, en **Eventos**, haz doble clic en `change`: ```python def rating1_change(form, event): form.status.text = f"You gave it {event.value} stars" # event.value es lo que envió emit() ``` Haz doble clic en un componente colocado para abrirlo en su propia pestaña; los cambios aparecen en cada formulario que lo usa. **La misma clase sirve en código:** Un componente diseñado **es** un `pz.Component`. Impórtalo y úsalo en cualquier app: `Rating(value=self.stars, on_change=lambda e: setattr(self, "stars", e.value))`. También puedes escribir componentes enteramente a mano: mira [Componentes](https://albakore.github.io/pyzed/es/guides/components/). #### Código completo Los componentes terminados (incluida una `ProfileCard` que contiene un `Rating`, y una `TeamCard` que hereda de ella) están en [`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 (un formulario que usa dos tarjetas)`: ```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" ``` Ejecuta la vista previa del componente, o el formulario que lo usa: #### Eventos de este tutorial `Rating` declara un evento propio, `change`, en `events` y lo lanza con `self.emit("change", stars)`. El padre lo escucha con `on_change=`, y su handler recibe un `pz.Event` cuyo `value` es la puntuación. Como todo componente, `Rating` lanza además `mount` y `unmount`, y acepta los eventos comunes (`on_right_click`, `on_mouse_enter`…). ##### Prueba más eventos Un segundo evento propio, que solo se lanza si alguien lo escucha: ```python class Rating(pz.Component): value: int = 0 events = { "change": "Se eligió una estrella (event.value es la puntuación).", "clear": "Se borró la puntuación con un clic derecho.", # nuevo } def render(self): stars = pz.row([...]) # como en el tutorial if self.handles("clear"): # solo si el padre pasó on_clear stars = stars.on(right_click=lambda: self.emit("clear")) return stars # El padre, en su view: Rating(value=self.stars, on_change=lambda e: setattr(self, "stars", e.value), on_clear=lambda: setattr(self, "stars", 0), # sin argumentos: se llama sin nada on_mount=lambda: print("la puntuación está en pantalla")) ``` Más en [crear tus propios eventos](https://albakore.github.io/pyzed/es/guides/classes/#crear-tus-propios-eventos). #### Siguiente - [Diseñador de componentes](https://albakore.github.io/pyzed/es/editor/component-designer/): cada sección del panel del componente, tipos de propiedad, anidación, componentes adaptables y herencia. - [Tareas en segundo plano](https://albakore.github.io/pyzed/es/tutorials/intermediate/background-tasks/). ### Construye una app con el agente de IA > Usa el agente de IA de Pyzed Design para construir una app de escritorio completa a partir de una descripción: conecta un modelo, escribe un buen pedido, sigue su plan, revisa sus cambios y sigue mejorando el resultado. Source: https://albakore.github.io/pyzed/es/tutorials/advanced/ai-agent/ Pyzed Design trae un **agente de IA**. Describe una app con palabras normales y el agente planifica el trabajo, crea el proyecto, dibuja los formularios, escribe el Python, mira el resultado, lo ejecuta y corrige lo que falla, con el mismo editor que usas tú. Tú sigues al mando: cada cambio se puede deshacer o rechazar con un clic. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un registro de gastos construido a partir de una descripción y mejorado a mano | 30 minutos | Avanzado · no hace falta código | Pyzed Design y un modelo de IA (Claude, o cualquier proveedor de la lista) | #### Qué vas a aprender - Abrir el **Panel del agente** y conectar un modelo. - Escribir un pedido que dé un buen resultado a la primera. - Seguir el **plan** del agente y **conservar** o **rechazar** sus cambios. - Señalar cosas con **@** (un control, el formulario, un archivo) y usar comandos **/**. - Seguir mejorando la app a mano, con el agente al lado. **Revisa lo que construye:** Un agente es una ayuda rápida, no una garantía. Lee el código que escribe, ejecuta la app y prueba tú mismo los caminos importantes antes de publicarla, sobre todo lo que toque archivos, dinero o datos personales. #### Pasos 1. **Abre el panel.** En Pyzed Design pulsa Ctrl+Alt+A (o **Ver → Panel del agente**). La primera vez te pide **conectar un modelo**: haz clic en un proveedor para iniciar sesión con el navegador, o en **Todos los proveedores…** para el resto. Si instalaste Pyzed con pip, añade antes los extras del agente: **uv:** ```bash title="Windows, macOS y Linux" uv tool install "pyzed-ui[designer,agents]" ``` **pip:** ```bash title="Windows, macOS y Linux (dentro de tu entorno)" python -m pip install "pyzed-ui[designer,agents]" ``` **python:** ```bash title="La app independiente Pyzed Design" # no hay nada que instalar: el agente viene incluido ``` 2. **Pide la app.** Escribe `/app` y descríbela. Di **para qué** es, **qué** hay en pantalla y **qué** debe pasar; deja el "cómo" al agente: > Crea una app para registrar mis gastos. Un formulario con fecha, importe, un desplegable de categoría (Comida, > Transporte, Casa, Ocio) y una nota. Guardar añade el gasto a una tabla debajo y a un archivo JSON junto a la > app. Muestra arriba el total del mes y valida que el importe sea un número positivo. 3. **Sigue el plan.** El agente escribe una tarjeta **Plan** encima de la caja de mensajes y va marcando los pasos: crear el proyecto, añadir dependencias, dibujar los formularios, conectar los eventos, escribir los manejadores, tomar una captura de cada formulario, hacer comprobaciones y, al final, ejecutar la app. Cada herramienta que usa es una línea de la conversación; haz clic en una para ver qué hizo. 4. **Revisa.** Cuando termine, ejecuta la app con F5 y pruébala. La barra **Cambios del agente** ofrece **Rechazar** (deja cada archivo que tocó como estaba) o **Conservar**. Ctrl+Z sigue deshaciendo un cambio cada vez. 5. **Mejórala señalando cosas.** Selecciona un control y pregunta por *esto*, o escribe **@** para adjuntar el formulario, su código, un control o un archivo: > @amount debería aceptar la coma como separador decimal, y la tabla debería mostrar primero el gasto más nuevo. 6. **Pule con comandos.** `/tidy` alinea los controles e iguala los márgenes, `/look` toma una captura del formulario y mejora su diseño, `/fix` revisa el código y corrige lo que encuentra, `/explain` te explica el formulario y sus manejadores. 7. **Toma el control.** Es tu proyecto: abre los archivos, cambia el diseño a mano, escribe código. El agente lee lo que cambiaste en el siguiente mensaje, así que puedes alternar entre hacerlo tú y pedirlo. **Pedidos que funcionan:** Nombra las pantallas y los campos, las reglas ("el importe debe ser positivo"), dónde se guardan los datos y un ejemplo del resultado. Pide un cambio cada vez cuando refines. Más consejos en [Cómo pedir](https://albakore.github.io/pyzed/es/ai/prompting/). **Perfiles:** **Escribir** (el perfil por defecto) edita formularios, código y archivos, y pregunta antes de crear archivos o ejecutar la app salvo que quites **Preguntar antes de escribir archivos**; **Preguntar** solo lee y responde; **Mínimo** es una charla normal, sin herramientas. Elige uno en el menú de perfil, abajo en el editor de mensajes. #### Siguiente - [Agentes de IA](https://albakore.github.io/pyzed/es/editor/ai-agents/) en el manual del diseñador: proveedores, servidores MCP, agentes externos e imágenes. - La sección [IA](https://albakore.github.io/pyzed/es/ai/): el servidor MCP y `llms.txt` para agentes fuera del editor (Claude Code, Cursor...). - [Empaqueta tu app](https://albakore.github.io/pyzed/es/tutorials/advanced/packaging/) cuando esté lista. ### Lista de tareas > Crea tu primera app de escritorio completa en Python con Pyzed: una lista de tareas con estado, formularios, listas, filtros y atajos de teclado. Source: https://albakore.github.io/pyzed/es/tutorials/basic/todo-app/ En este tutorial construimos una lista de tareas: la primera app clásica, y una buena, porque tiene todo lo que tiene una app real: datos, un formulario para añadirlos, una lista para mostrarlos y formas de cambiarlos. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Una lista de tareas con filtros y atajos | 30 minutos | Básico · Python | [Tutorial en 5 pasos](https://albakore.github.io/pyzed/es/tutorials/basic/step-by-step/) (pasos 1 y 2) | #### Qué vas a aprender - Guardar los datos de la app en objetos de Python normales (un dataclass). - Leer un campo de texto mientras el usuario escribe y enviarlo con Enter. - Dibujar una lista de filas, cada una con una casilla y un botón de borrar. - Filtrar lo que se muestra y añadir atajos de teclado. (Image: La app terminada) 1. **El modelo.** Una tarea es un dataclass. El estado de la app es la lista de tareas, el texto que se está escribiendo y el filtro activo: ```python @dataclass class Task: title: str done: bool = False class Todo(pz.App): title = "Tareas" size = (520, 560) def __init__(self): self.tasks = [Task("Crear mi primera app")] self.draft = "" self.filter = "Todas" ``` 2. **Añadir tareas.** El campo es *controlado*: muestra `self.draft` y cada pulsación lo actualiza. Enter (`on_submit`) y el botón llaman al mismo método: ```python def add(self): if self.draft.strip(): self.tasks.append(Task(self.draft.strip())) self.draft = "" return Command.focus("new-task") # devuelve el foco al campo # en view(): 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) ``` 3. **Dibujar la lista.** Cada tarea es un `ui.list_item` con una casilla al inicio y un botón de borrar al final. Las lambdas capturan `task` porque `task_row` recibe la tarea como argumento: ```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="Eliminar"), height=34, inset=True, ) ``` 4. **Filtrar.** `ui.segmented` cambia `self.filter`, y `view()` solo muestra las tareas visibles. Como `view()` se vuelve a llamar después de cada cambio, no hay que "refrescar" nada: ```python ui.segmented(["Todas", "Pendientes", "Hechas"], self.filter, lambda f: setattr(self, "filter", f)) ``` 5. **Atajos.** `on_key` recibe cada tecla como texto: ```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. **Foco inicial.** `on_start` se ejecuta una vez al abrir la ventana: ```python def on_start(self): return Command.focus("new-task") ``` **Tip:** Prueba a cambiar el tema con Ctrl+T: todos los componentes se restilizan porque leen sus colores del tema activo. #### Código completo `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) ``` #### Eventos de este tutorial | Widget | Evento | Handler | Recibe | | --- | --- | --- | --- | | `ui.input` | `on_input` | `lambda text: setattr(self, "draft", text)` | El texto. | | `ui.input`, **Añadir** | `on_submit`, `on_press` | `self.add` | Nada. | | `ui.segmented` | `on_select` | `lambda f: setattr(self, "filter", f)` | El filtro. | | `ui.list_item` | `on_press` | `lambda: self.toggle(task)` | Nada. | | `ui.checkbox` | `on_toggle` | `lambda _: self.toggle(task)` | `True`/`False` (se ignora: `_`). | | `ui.icon_button` | `on_press` | `lambda: self.remove(task)` | Nada. | | La app | `on_start`, `on_key` | enfoca el campo; `ctrl-l`, `ctrl-t` | —, la combinación. | ##### Prueba más eventos Renombra una tarea con doble clic y abre un menú con clic derecho: ```python def __init__(self): ... self.renaming = None # la tarea que se renombra self.menu_for = None # la tarea con el menú abierto 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), # doble clic on_right_press=lambda: setattr(self, "menu_for", task), # clic derecho height=34, inset=True, ) def view(self): page = ... # la página del tutorial dialog = None if self.renaming: task = self.renaming dialog = ui.prompt("Renombrar", task.title, lambda t: setattr(task, "title", t), lambda: setattr(self, "renaming", None), lambda: setattr(self, "renaming", None)) return ui.dialog_layer(page, dialog) ``` #### Siguiente - [Enlace de datos y validación](https://albakore.github.io/pyzed/es/tutorials/intermediate/data-binding/) en el diseñador. - [Listas y tablas](https://albakore.github.io/pyzed/es/tutorials/intermediate/lists-tables/): buscar, ordenar y seleccionar filas. - La [app Todos del Showcase](https://albakore.github.io/pyzed/es/showcase/apps/#todos), que guarda la lista en disco. ### Tareas en segundo plano > Tutorial: descargas con progreso, trabajo bloqueante y notificaciones en una app de escritorio de Pyzed sin congelar la interfaz de Python. Source: https://albakore.github.io/pyzed/es/tutorials/intermediate/background-tasks/ Una interfaz nunca debe bloquearse esperando a la red o al disco. Pyzed ofrece dos herramientas: - **`Command.perform(fn, then)`** ejecuta una función bloqueante en otro hilo y te entrega el resultado. - **`pz.post(mensaje)`** envía mensajes a la app desde cualquier hilo (progreso, eventos). | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Descargas con barras de progreso y notificaciones que nunca congelan la ventana | 30 minutos | Intermedio · Python | [Diálogos y notificaciones](https://albakore.github.io/pyzed/es/tutorials/intermediate/dialogs-feedback/) | #### Qué vas a aprender - Por qué el trabajo lento debe salir del hilo de la interfaz, y las dos formas de sacarlo. - Recibir un resultado con `Command.perform`. - Informar del progreso desde un hilo con `pz.post` y manejarlo en `update`. - Mostrar notificaciones que se cierran solas. **Tip:** Si prefieres `asyncio`, `update` y los callbacks también pueden ser `async def` o generadores `async`, y `pz.listen` convierte un stream en una suscripción. Lo cuenta la guía [Async y suscripciones](https://albakore.github.io/pyzed/es/guides/async/). (Image: Descargas con progreso y notificaciones) 1. **Un resultado: `Command.perform`.** Para "haz esto y avísame cuando termine": ```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) # por ejemplo, una petición HTTP return "0.2.0" def got_version(self, version): # se ejecuta en el hilo de la interfaz self.checking, self.latest = False, version ``` Mientras `self.checking` es `True`, el botón se dibuja deshabilitado (`on_press=None`). 2. **Muchos resultados: `pz.post`.** Un hilo propio informa del progreso con mensajes que llegan a `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} descargado", kind="success") ``` 3. **Notificaciones que se cierran solas.** Un `threading.Timer` publica el mensaje de cierre: ```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() # en view(): return ui.toasts(body, [ui.toast(t, m, kind=k, on_dismiss=("dismiss", i)) for i, t, m, k in self.toasts]) ``` **Note:** El estado solo se modifica en el hilo de la interfaz (en `update` y en los callbacks). Los hilos de trabajo no tocan `self`: envían mensajes con `pz.post`. #### Código completo `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) ``` #### Eventos de este tutorial Esta app es **declarativa**: los eventos de los hilos son mensajes. `pz.post(("progress", name, done))` desde un hilo y el `on_dismiss=("dismiss", tid)` de cada aviso llegan a `update(message)`, que desempaqueta la tupla. | Origen | Evento | Mensaje o handler | | --- | --- | --- | | Botón **Descargar** | `on_press` | `lambda: self.start(name)` | | El hilo de trabajo | progreso, terminado | `pz.post(("progress", name, done))`, `pz.post(("finished", name))` | | **Buscar actualizaciones** | `on_press` | `self.check_updates`, que devuelve `Command.perform(trabajo, hecho)` | | `Command.perform` | terminó el trabajo | `self.got_version(resultado)` | | Un aviso | `on_dismiss` | el mensaje `("dismiss", tid)` | ##### Prueba más eventos ```python def row(self, name): ... return pz.column([...]).on(double_click=lambda: self.start(name)) # doble clic: descargar otra vez def subscriptions(self): # un evento por segundo mientras algo se descarga busy = any(v < 1 for v in self.progress.values()) return [pz.every(1, ("tick", "", None))] if busy else [] ``` `update` recibe `("tick", "", None)` cada segundo mientras hay una descarga y nada cuando terminan: la suscripción que dejas de devolver se detiene. #### Siguiente - [Un efecto de shader](https://albakore.github.io/pyzed/es/tutorials/intermediate/shader-effect/) desde la vista de shader. - [Conectar un backend](https://albakore.github.io/pyzed/es/tutorials/advanced/backend/): una base de datos detrás de una clase repositorio. - [Async y suscripciones](https://albakore.github.io/pyzed/es/guides/async/): corrutinas, generadores async y streams. ### Un efecto de shader desde la vista de shader > Crea un fondo animado por GPU en la vista de shader de Pyzed Design a partir de una plantilla, ajusta sus nodos y variables, y ponlo detrás de un formulario cuyos botones lo cambian. Source: https://albakore.github.io/pyzed/es/tutorials/intermediate/shader-effect/ Un **shader** es un programa diminuto que la tarjeta gráfica ejecuta para cada píxel, en cada fotograma: así dibujan los videojuegos anillos luminosos, nubes y fuego sin ir lentos. En Pyzed no tienes que escribir ese programa. Conectas **nodos** en la vista de shader (como los visual shaders de Godot o el editor de materiales de Unreal) y el diseñador los convierte en código de GPU por ti. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un anillo de neón que late detrás de una tarjeta de bienvenida | 25 minutos | Intermedio · poco código | [Tu primera ventana](https://albakore.github.io/pyzed/es/tutorials/basic/first-window/) | #### Qué vas a aprender - Crear un shader desde una **plantilla** y moverte por las pestañas **Grafo**, **Vista previa** y **WGSL**. - Cambiar la entrada de un nodo, conectar un nodo nuevo y leer la **vista previa en cada nodo**. - Usar **variables**: valores que tu código Python cambia mientras el shader corre. - Dibujar el shader en un formulario con el control **Shader** y cambiar sus variables desde un botón. #### Pasos 1. **Crea el shader.** Ctrl+N → **Shader**, llámalo `glow`, elige la plantilla **Anillo luminoso** (*Glow ring*) y pulsa **Crear shader**. 2. **Echa un vistazo.** La pestaña **Grafo** muestra los nodos, de izquierda a derecha: **Time** y las variables alimentan algunas operaciones, una forma **Ring** da la distancia a un anillo, **Glow** la convierte en luz y **Output** es el color final. Cada nodo dibuja una pequeña vista previa de lo que produce, así que puedes seguir la imagen mientras se construye. 3. **Cambia un valor.** Las entradas que no están conectadas muestran su valor en el propio nodo. En el nodo **Glow** cambia **radius** de 0.12 a 0.3: el brillo crece, y la vista previa de cada nodo posterior se actualiza al instante. (El radio del anillo está conectado: viene de las operaciones del pulso, que es lo que lo hace respirar.) 4. **Conecta un nodo nuevo.** Cerca del final, un nodo **Multiply** mezcla la variable `color` con el brillo y alimenta al nodo **RGBA**. Desde la barra lateral **Shading** arrastra un nodo **Hue shift** (categoría *Color*) junto a ellos. Conecta el resultado de Multiply a su entrada **rgb** y su salida **rgb** al **rgb** de RGBA, y luego conecta **Time** a **shift**: el anillo ahora recorre todos los colores. Pulsa Ctrl+Z para volver atrás. 5. **Conoce las variables.** La **Estructura** lista las variables del shader: `color` (el color del neón) y `pulse` (latidos por segundo). Son **mutables**: tu app puede cambiarlas en cada fotograma sin coste. La pestaña **Vista previa** tiene un deslizador para cada una. 6. **Guarda** (Ctrl+S). El shader es el archivo `glow.pzshad`. Pulsa F5 para verlo llenar una ventana propia. 7. **Ponlo detrás de un formulario.** Crea un formulario `welcome` (560 × 380). Desde **Presentación** (*Display*) en las Herramientas arrastra un control **Shader**, haz que cubra todo el formulario y ánclalo por **los cuatro lados**; llámalo `backdrop` y pon en su **Archivo de shader** `glow.pzshad`. Luego añade una **Tarjeta** en el centro (sin anclajes, para que quede centrada) con un título, una etiqueta llamada `hint` y dos botones, `faster` y `color`. Da a la tarjeta un fondo translúcido en su sección **Estilo** (`#0f172acc`) para que se vea el brillo a través. 8. **Contrólalo desde Python.** La propiedad `variables` del control es un dict con las variables del shader. Haz doble clic en **faster** y **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. **Ejecuta** el formulario (F5) y pulsa los botones. **Rutas relativas:** Un **Archivo de shader** relativo (`glow.pzshad`) se lee desde la carpeta en la que ejecutas la app. En un proyecto es la carpeta del proyecto; para un formulario que puedes ejecutar desde cualquier sitio, pon la ruta en el manejador `load` del formulario a partir de `__file__`, como hace el ejemplo de abajo. **¿Sin GPU?:** En máquinas sin tarjeta gráfica (algunas máquinas virtuales y servidores de CI), un shader muestra en su lugar su color de **fallback**, que se define en las propiedades del shader. Todo lo demás de la ventana funciona igual. #### Código completo `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 ``` El mismo shader en Python puro, sin el diseñador, es un widget: ```python pz.stack([ pz.shader("glow.pzshad", uniforms={"pulse": 4, "color": "#a78bfa"}, width="fill", height="fill"), card, ]) ``` #### Eventos de este tutorial | Control | Evento | Handler | | --- | --- | --- | | El formulario | `load` | `form_load`: apunta el control Shader a `glow.pzshad`. | | `faster` (Botón) | `click` | `faster_click`: cambia la variable `pulse`. | | `color` (Botón) | `click` | `color_click`: el siguiente color neón. | ##### Prueba más eventos ```python def color_right_click(form, event): # todo control tiene right_click: vuelve al primer color variables = dict(form.backdrop.variables) variables["color"] = COLORS[0] form.backdrop.variables = variables def backdrop_mouse_enter(form, event): # el control Shader también tiene los eventos del ratón form.hint.text = "Aparta el puntero para calmarlo" def backdrop_mouse_leave(form, event): form.hint.text = "" ``` #### Siguiente - [Un grafo de shader propio](https://albakore.github.io/pyzed/es/tutorials/advanced/shader-graph/): construye un grafo en Python, nodo a nodo. - La sección [Shading](https://albakore.github.io/pyzed/es/shading/): cada nodo, la vista de shader y los shaders WGSL. - Los [shaders del Showcase](https://albakore.github.io/pyzed/es/showcase/shaders/): once archivos `.pzshad` para abrir y estudiar. ## Guías ### Arquitectura de una app > Arquitectura de una app de Pyzed: estado, view, update, mensajes y callables. La arquitectura Elm de Iced explicada para desarrolladores de Python. Source: https://albakore.github.io/pyzed/es/guides/architecture/ Pyzed sigue la **arquitectura Elm** de Iced: el estado vive en tu objeto, `view()` lo dibuja y los eventos lo cambian. Después de cada cambio, Pyzed vuelve a llamar a `view()`. ``` ┌──────────── view() ────────────┐ │ ▼ estado (self) árbol de widgets ──► Iced lo dibuja ▲ │ └── update(msg) / callable ◄─────┘ el usuario hace clic, escribe… ``` #### La clase App ```python import pyzed as pz class MiApp(pz.App): title = "Mi app" # o un método/propiedad que devuelva el título size = (900, 600) min_size = (480, 320) theme = "One Dark" # un Theme, un nombre o pz.themes.auto(...) def __init__(self): # el estado inicial self.items = [] def view(self): # obligatorio: devuelve un Widget ... def update(self, message): # opcional: recibe los mensajes ... pz.run(MiApp) # o MiApp().run() ``` Todos los atributos de clase están en la [referencia de App](https://albakore.github.io/pyzed/es/reference/app/#app). :::tip[`view()` debe ser barato y sin efectos] Pyzed llama a `view()` después de cada mensaje. Calcula ahí lo que vas a mostrar, pero no hagas E/S ni cambies el estado: eso va en `update` o en los callbacks. ::: Cada interacción recorre el mismo ciclo: Pyzed llama a `view()`, Iced dibuja el árbol y convierte cada evento en un mensaje, y el mensaje cambia tu estado, lo que provoca el siguiente `view()`. ```mermaid flowchart LR S[("estado: self.*")] --> V["view()"] V -->|árbol de widgets| R["Iced (Rust)"] R -->|clic, tecla, texto…| M{"mensaje"} M -->|un valor| U["update(message)"] M -->|un callable| C["se llama directamente"] U --> S C --> S U -.->|Command| E["efectos: tareas, foco, ventanas…"] E -.-> M ``` #### Mensajes Los widgets interactivos aceptan un **mensaje**: cualquier valor de Python. Al activarse, Pyzed lo envía a `update`: ```python ui.button("Borrar", ("borrar", item.id)) def update(self, message): match message: case ("borrar", item_id): self.items = [i for i in self.items if i.id != item_id] case "limpiar": self.items.clear() ``` Usa mensajes cuando quieras centralizar la lógica en un sitio, registrar acciones, deshacerlas o probar `update` sin interfaz. #### Callables Si el mensaje es un callable, Pyzed lo **llama directamente** y `update` no se entera: ```python ui.button("Guardar", self.guardar) ui.button("Borrar", lambda: self.borrar(item.id)) ``` Lo que devuelva el callable se trata así: | Devuelve | Qué pasa | | --- | --- | | `None` | Nada más: Pyzed vuelve a dibujar. | | un `Command` (o lista de ellos) | Se ejecuta el comando. | | una corrutina, un generador o un generador async | Se ejecuta en segundo plano y cada resultado va a `update`. Ver [async y suscripciones](https://albakore.github.io/pyzed/es/guides/async/). | | cualquier otro valor | Se envía a `update` como mensaje. | Los controles que producen un valor llaman al callable con ese valor: `on_input(texto)`, `on_toggle(bool)`, `on_change(valor)`, `on_select(id)`, `on_cursor(línea, columna)`. :::caution[Lambdas dentro de bucles] Captura la variable con un argumento por defecto, o todas las lambdas usarán el último valor: `lambda f=f: self.abrir(f)`. ::: #### Hooks opcionales | Método | Cuándo se llama | | --- | --- | | `on_start()` | Una vez, al arrancar. Puede devolver un `Command` o una corrutina (por ejemplo, cargar datos). | | `on_key(combo)` | En cada tecla pulsada. Ver [teclado y atajos](https://albakore.github.io/pyzed/es/guides/keyboard/). | | `subscriptions()` | Después de cada `update`. Devuelve los temporizadores y streams activos (`pz.every`, `pz.listen`). | | `on_open()`, `on_close()`, `on_resize(w, h)` | Eventos de la ventana. Ver [varias ventanas](https://albakore.github.io/pyzed/es/guides/windows/). | #### Efectos: `Command` `update`, `on_start`, `on_key` y los callbacks pueden devolver un [`Command`](https://albakore.github.io/pyzed/es/reference/command/) para pedir efectos: enfocar un campo, cerrar la app, hacer trabajo en segundo plano… Ver [comandos y tareas](https://albakore.github.io/pyzed/es/guides/commands/). #### Errores Si `view()` lanza una excepción, la ventana muestra el traceback en lugar de cerrarse. Corrige el código y la siguiente interacción vuelve a intentarlo. Las excepciones en callbacks y en `update` se imprimen en la consola. #### Organizar una app grande Como los componentes son funciones, divide `view()` en métodos y funciones normales: ```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("Proyecto"), ui.tree(self.nodes, expanded=self.expanded)]) ``` Para reutilizar piezas entre apps, escribe funciones que reciban datos y callbacks y devuelvan widgets, igual que las de `pyzed.ui`. Mira [`examples/zed_editor.py`](https://github.com/albakore/pyzed/blob/main/examples/zed_editor.py) como ejemplo de una app grande. ### Apps, ventanas, formularios y componentes > Los métodos que importan al escribir una pz.App, una pz.Window, un formulario diseñado o un pz.Component, cuándo los llama Pyzed y cómo crear tus propios eventos. Source: https://albakore.github.io/pyzed/es/guides/classes/ Todo lo que escribes en Pyzed es uno de cuatro tipos de clase (o archivo). Cada uno tiene unos pocos métodos que Pyzed llama por ti en el momento justo: esta página los lista, dice cuándo se ejecutan y muestra qué poner en ellos. | Escribes | Es | Pyzed llama a | | --- | --- | --- | | `class MiApp(pz.App)` | La app y su ventana principal. | `view()`, `update()`, `on_start()`, `on_key()`, `subscriptions()`… | | `class Ajustes(pz.Window)` | Otra ventana de la misma app. | `view()`, `on_open()`, `on_close()`, `on_resize()`… | | `login.pzform` + `login.py` | Un formulario dibujado en [Pyzed Design](https://albakore.github.io/pyzed/es/editor/), con sus handlers. | `form_load(form, event)`, `_(form, event)`… | | `class Rating(pz.Component)` | Una pieza de interfaz reutilizable con props, estado y eventos. | `setup()`, `render()`, `on_mount()`, `on_unmount()` | ```mermaid flowchart LR Start(["pz.run(MiApp)"]) --> Init["__init__()
estado inicial"] Init --> OnStart["on_start()"] OnStart --> View["view()"] View -->|árbol de widgets| Draw["Iced lo dibuja"] Draw -->|clic, tecla, temporizador…| Event{"evento"} Event -->|función| Call["tu método"] Event -->|mensaje| Update["update(mensaje)"] Call --> View Update --> View Draw -.->|se cierra la ventana| Close["on_close()"] ``` #### `pz.App`: la aplicación ```python import pyzed as pz from pyzed import ui class Notes(pz.App): title = "Notas" # las opciones de la ventana son atributos de clase size = (720, 480) theme = "One Dark" def __init__(self): # 1. el estado inicial: atributos normales self.notes = [] self.draft = "" self.saved_at = None def on_start(self): # 2. una vez, cuando la ventana está abierta return pz.Command.focus("draft") def view(self): # 3. después de cada cambio: describe la ventana return pz.column([ ui.input(self.draft, self.set_draft, id="draft", placeholder="Nota nueva", on_submit=self.add), pz.column([ui.list_item(n, icon="sticky-note") for n in self.notes]), ui.muted(f"Guardado a las {self.saved_at}" if self.saved_at else "Sin guardar"), ], spacing=8, padding=16) def set_draft(self, text): # 4. los callbacks cambian el estado self.draft = text def add(self): if self.draft.strip(): self.notes.append(self.draft.strip()) self.draft = "" def update(self, message): # 5. aquí llegan los mensajes que no son funciones if message == "save": self.saved_at = "ahora" def on_key(self, combo): # 6. cada tecla pulsada en la ventana if combo == "ctrl-s": return self.update("save") def subscriptions(self): # 7. temporizadores y flujos activos ahora mismo return [pz.every(60, "save")] if self.notes else [] pz.run(Notes) ``` ##### Los métodos | Método | Cuándo lo llama Pyzed | Qué poner | Puede devolver | | --- | --- | --- | --- | | `__init__()` | Una vez, al crear la app (`pz.run(MiApp)` la crea). | El estado inicial como atributos. Ni widgets ni E/S. | — | | `view()` | Al empezar y después de **cada** evento, mensaje o comando. | Construir y devolver el árbol de widgets a partir del estado. Nunca cambies el estado aquí. | Un `Widget` (obligatorio). | | `update(message)` | Cuando el handler de un widget es un valor (texto, tupla, dataclass…) o una función devuelve uno. | Cambiar el estado según el mensaje. | `None`, un `Command`, una lista, una corrutina u otro mensaje. | | `on_start()` | Una vez, después de abrir la ventana. | Cargar datos, enfocar un campo, empezar una tarea en segundo plano. | Un `Command`, una corrutina o un mensaje. | | `on_key(combo)` | En cada tecla de la ventana, antes que el widget enfocado. `combo` es como `"ctrl-shift-p"`. | Atajos globales. Devolver algo (aunque sea `[]`) evita que la tecla mueva el foco. | Como `update`. | | `on_key_up(key)` | Cuando se suelta una tecla (`"left"`, `"space"`). | Juegos y controles de mantener pulsado. | Como `update`. | | `subscriptions()` | Después de cada actualización. | Devolver los temporizadores, flujos y callbacks por fotograma que deben correr *ahora*: `pz.every`, `pz.listen`, `pz.frames`. La que dejas de devolver se detiene. | Una lista. | | `on_close()` | Cuando el usuario cierra la ventana. | Preguntar si guardar. Devuelve `False` para dejarla abierta. | `None` o `False`. | | `on_resize(width, height)` | Cuando la ventana cambia de tamaño. | Cambiar de diseño (mira también `self.window_size`). | Como `update`. | | `frame_menus()` | Cuando `frame = "custom"` dibuja la barra de título. | Los menús de su izquierda: `[ui.Menu("Archivo", [...])]`. | Una lista o `None`. | Y los que llamas tú: | Método | Qué hace | | --- | --- | | `run(reload=None)` | Abre la ventana y bloquea hasta que la app se cierra (igual que `pz.run(MiApp)`). `reload=True` activa la [recarga en caliente](https://albakore.github.io/pyzed/es/editor/console-and-running/). | | `screenshot(path, size=None)` | Dibuja el estado actual en un PNG sin pantalla (tests, documentación). | | `resolved_theme()` | El tema en uso ahora mismo, con `pz.set_style` aplicado: `self.resolved_theme().accent`. | Atributos útiles que el runtime mantiene al día: `self.window_size`, `self.window_focused`, `self.window_maximized`, `self.window_position` y `self.system_dark`. Las opciones de la ventana (`title`, `size`, `min_size`, `resizable`, `frame`, `transparent`, `icon`, `splash`, `tab_focus`…) están en la [referencia de App](https://albakore.github.io/pyzed/es/reference/app/#app). :::tip[¿Funciones o mensajes?] Un handler puede ser una **función** (`self.add`, `lambda: self.open(ruta)`), que Pyzed llama directamente, o un **mensaje** (`"save"`, `("delete", item.id)`, `Delete(item.id)`), que va a `update`. Las dos formas conviven en la misma app: mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/) para saber cuándo usar cada una. ::: #### `pz.Window`: más ventanas Una `Window` es una `App` sin las partes de toda la aplicación: tiene su propio `view()` y sus opciones de ventana, y manda sus mensajes al `update()` de la app. Se abre devolviendo un comando: ```python class Settings(pz.Window): title = "Ajustes" size = (420, 300) def on_open(self): # la ventana ya está en pantalla self.draft = self.app.prefs.copy() # self.app es la App que la abrió def view(self): return pz.column([ ui.switch(self.draft["dark"], self.set_dark, label="Modo oscuro"), ui.button("Guardar", 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() # un comando def on_close(self): # el usuario pulsó la X de la ventana return None if self.draft == self.app.prefs else False class Main(pz.App): prefs = {"dark": True} def view(self): return ui.button("Ajustes…", lambda: Settings().open()) ``` | Método | Cuándo | Notas | | --- | --- | --- | | `view()`, `on_key()`, `on_key_up()`, `on_resize()`, `subscriptions()` | Como en `App`. | — | | `on_open()` | La ventana se abrió (`self.app` ya está puesto). | Puede devolver un comando. | | `on_close()` | El usuario pidió cerrarla. | `False` la deja abierta. | | `update(message)` | Un mensaje producido dentro de la ventana. | Por defecto lo reenvía a `self.app.update`. | | `open()`, `close()`, `drag()`, `minimize()`, `toggle_maximize()` | Los llamas tú. | Cada uno devuelve un `Command`: devuélvelo desde un handler. | Mira [Varias ventanas](https://albakore.github.io/pyzed/es/guides/windows/) para posiciones, ventanas modales y marcos propios. #### Formularios diseñados: `.pzform` + handlers Un formulario que dibujas en Pyzed Design se guarda como `login.pzform` (los controles y sus propiedades) y `login.py` (tu código). El archivo de código no tiene clase: cada evento es una **función** llamada `_`, y los eventos del propio formulario son `form_`. Cada handler recibe el `form` en marcha y el `event`: ```python # login.py: los handlers de login.pzform def form_load(form, event): # se abrió la ventana form.status.text = "" return form.email.focus() # los handlers pueden devolver comandos def email_change(form, event): # cada tecla: event.value es el texto nuevo form.sign_in.enabled = "@" in event.value def password_submit(form, event): # Enter en la contraseña return sign_in_click(form, event) def sign_in_click(form, event): if not form.validate(): # aplica las reglas de validación del diseñador return form.progress.show() return form.open("welcome", data=form.email.text) def form_key(form, event): # cada tecla: event.value es la combinación if event.value == "escape": return form.close() def form_close(form, event): # devuelve False para dejar la ventana abierta if form.password.text: return False ``` | Handler | Cuándo | `event.value` | | --- | --- | --- | | `form_load` | Una vez, al abrirse la ventana. | `None` | | `form_close` | La ventana se está cerrando; `False` la deja abierta. | `None` | | `form_key` | Cada tecla pulsada. | La combinación, `"ctrl-s"` | | `form_resize` | La ventana cambió de tamaño. | `(ancho, alto)` | | `_` | El control lanzó el evento (`click`, `change`, `submit`, `tick`…). | Depende del evento: el texto nuevo, `True`/`False`, el elemento elegido… | El objeto `form` es tu acceso a la ventana: `form..` lee y escribe propiedades, `form..show()` / `.hide()` / `.focus()`, `form.title`, `form.data` (lo que pasó `form.open(..., data=)`), `form.validate()`, `form.close()` y `form.open("otro_formulario")`. Los eventos de cada control están en [Eventos y handlers](https://albakore.github.io/pyzed/es/editor/events/) y en cada página de la [biblioteca](https://albakore.github.io/pyzed/es/library/). Ejecuta un formulario desde Python, o ábrelo desde una app escrita en código: ```python from pyzed import designer designer.run("ui/login.pzform") # el formulario es la app (igual que `pyzed run`) class Main(pz.App): # o un formulario como una ventana más def view(self): return ui.button("Entrar…", lambda: designer.open_window("login", data="ada@example.com")) ``` #### `pz.Component`: piezas reutilizables Un componente es una clase con **props** (atributos de clase con anotación, que pasa el padre), **estado** (atributos que conserva entre dibujados) y **eventos** (que lanza con `self.emit`). Llamarlo devuelve un widget. ```python class Stepper(pz.Component): """Un número con botones − y +.""" value: int = 0 # props step: int = 1 min: int = 0 max: int = 10 events = {"change": "Cambió el valor (event.value es el número nuevo)."} pressed: int = pz.state(0) # estado, privado de cada instancia def setup(self): # una vez, antes del primer render self.pressed = 0 def on_mount(self): # después del primer 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) # llama al on_change del padre def render(self): # en cada render del padre 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): # ya no se dibuja print(f"pulsado {self.pressed} veces") # En cualquier view: Stepper(value=self.copies, max=99, on_change=lambda e: setattr(self, "copies", e.value)) ``` | Método | Cuándo | Notas | | --- | --- | --- | | `setup()` | Una vez, antes del primer render. Las props ya están puestas. | Inicializa el estado. | | `render()` | Cada vez que el `view()` del padre llama al componente. | Devuelve widgets; lee `self.` y el estado. | | `on_mount()` | Después del primer render. | Puede devolver un `Command` o un mensaje. El handler `on_mount=` del padre también se ejecuta. | | `on_unmount()` | Cuando el padre deja de dibujarlo. Su estado se descarta. | Para temporizadores, cierra archivos. | | `emit(name, value)` | Lo llamas tú. | Llama al `on_` del padre con un `pz.Event(value=..., source=self)`. Devuelve su resultado. | | `handles(name)` | Lo llamas tú. | Si el padre pasó `on_`: oculta un botón que nadie escucha. | | `provide()` | Antes de cada render. | Un dict compartido con todo lo que se dibuja dentro (`pz.use_context(nombre)`). | | `context(name)` | Lo llamas tú. | El valor más cercano compartido con `pz.provide` o `provide()`. | | `Rating.create(...)` | En tests. | Una instancia sin dibujarla: `Rating.create(value=3).render()`. | Todo componente acepta además los [eventos comunes](https://albakore.github.io/pyzed/es/guides/events/) (`on_click`, `on_drag`, `on_key_down`…), `style=`, `class_=`, `id=`, `animate=` y `key=`. Mira [Componentes propios](https://albakore.github.io/pyzed/es/guides/components/) para hijos, contexto, hojas de estilo y cómo aparecen los componentes en la caja de herramientas del diseñador. #### Crear tus propios eventos Un evento es un nombre más un valor que una pieza de la interfaz le da a quien la usa. Hay tres formas de crear uno, de la más estructurada a la más simple. ##### 1. En un componente: `events` y `emit` Declara los eventos y lánzalos con `self.emit(nombre, valor)`. El padre los escucha con `on_=`: ```python class ColorRow(pz.Component): colors: list = () selected: str = "" events = { "pick": "Se hizo clic en un color (event.value es el color).", "remove": "Se pulsó la × de un color (event.value es el 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` es un dict `{nombre: descripción}` (la descripción se ve en el diseñador y en la referencia) o una tupla de nombres. Las subclases suman eventos a los que heredan. - El handler recibe un `pz.Event` con `type` (el nombre), `value` y `source` (el componente). Un handler sin argumentos (`on_pick=self.refresh`) se llama sin nada. - `emit` de un nombre no declarado lanza un error, así que las erratas se ven enseguida. - Todo componente emite además `mount` y `unmount`. ##### 2. En una función: un parámetro callback Los componentes de `pyzed.ui` son funciones normales con parámetros `on_*`. Las tuyas también pueden serlo: recibe el callback como parámetro y dáselo a un widget, o envuélvelo para pasarle un valor. ```python def tag_list(tags, on_remove=None, on_add=None): """Etiquetas como chips. on_remove(tag) al pulsar la × de un chip, on_add() para el botón +.""" 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="Añadir etiqueta"), ], spacing=6) tag_list(self.tags, on_remove=self.tags.remove, on_add=self.new_tag) ``` ##### 3. Para toda la app: mensajes y `pz.post` Un mensaje es un evento para toda la app: cualquier valor que llega a `update`. Úsalo cuando lo mismo puede venir de varios sitios (un botón, un atajo, un menú, un temporizador, un hilo): ```python from dataclasses import dataclass @dataclass class Downloaded: # un mensaje con datos name: str size: int def worker(url): # se ejecuta en un hilo data = fetch(url) pz.post(Downloaded(url, len(data))) # cualquier hilo puede mandar un mensaje a update() class App(pz.App): def update(self, message): match message: case Downloaded(name, size): self.files.append((name, size)) ``` En Pyzed Design, los **eventos propios** de un componente diseñado se añaden en su tarjeta *Eventos* y se lanzan desde su código con `self.emit("change", valor)`. Mira [Diseñador de componentes](https://albakore.github.io/pyzed/es/editor/component-designer/). #### Siguiente - [Eventos](https://albakore.github.io/pyzed/es/guides/events/): los eventos comunes de todo widget y el objeto `pz.Event`. - [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/): cuántos eventos tiene cada componente, y cuáles. - [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/): la misma app escrita de tres formas. - [Arquitectura de una app](https://albakore.github.io/pyzed/es/guides/architecture/): el ciclo view → mensaje → update en detalle. ### Imperativo, declarativo y funcional > Tres formas de escribir una app de Pyzed: métodos que cambian el estado, mensajes que atiende un único update(), o un estado inmutable que dibujan funciones puras. La misma lista de tareas escrita de cada forma, y cómo manejar el estado en clases. Source: https://albakore.github.io/pyzed/es/guides/code-styles/ Pyzed no obliga a una forma de escribir una app. Un handler puede ser un método, un mensaje o una función que devuelve un estado nuevo, y las tres se mezclan en el mismo programa. Esta página escribe la misma lista de tareas en cada estilo, para que puedas compararlos línea a línea, y dice cuándo encaja cada uno. | Estilo | El estado vive en | Un evento | Encaja en | | --- | --- | --- | --- | | **Imperativo** | Atributos de la app (`self.tasks`). | Llama a un método que los cambia (`on_submit=self.add`). | Apps pequeñas, prototipos, formularios diseñados. | | **Declarativo** | Atributos de la app. | Produce un mensaje (`on_submit=Add()`); un único `update()` cambia el estado. | Apps con muchas formas de hacer lo mismo, registro de acciones, deshacer, tests sin ventana. | | **Funcional** | Un único valor inmutable (`State`). | Pide un estado nuevo (`set_state(draft="")`); la vista es una función pura. | Lógica que quieres probar como funciones normales, funciones de vista reutilizables, caché con `@pz.memo`. | En los tres la vista es **declarativa**: `view()` describe la ventana entera a partir del estado y Pyzed averigua qué cambió. Los estilos se diferencian en cómo llegan los eventos al estado. #### La misma app de tres formas **Imperativo:** `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) ``` **Declarativo:** `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) ``` **Funcional:** `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) ``` Ejecuta cualquiera con `python examples/code_styles/imperative.py` (o `declarative.py`, `functional.py`): las tres ventanas se ven y funcionan igual. #### Imperativo: los métodos cambian el estado Cada handler es un método o una lambda, y cambia `self`. Después, Pyzed vuelve a llamar a `view()`. ```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"]) ``` - Un handler que no recibe argumentos se llama sin nada (`on_press`, `on_submit`); el de un control que produce un valor lo recibe (`on_input(texto)`, `on_toggle(hecho)`, `on_select(elemento)`). - Para pasar tus propios datos, átalos con un argumento por defecto: `lambda t=t: self.remove(t)`. Sin `t=t`, todas las lambdas del bucle verían la última tarea. - Es el estilo más corto, y el que usan los handlers de un [formulario diseñado](https://albakore.github.io/pyzed/es/guides/classes/#formularios-diseñados-pzform--handlers): `form.status.text = "Guardado"`. #### Declarativo: mensajes y un único `update()` Los handlers son **valores**, los mensajes, que van a `update(message)`. Un mensaje dice *qué pasó*; `update` decide *qué significa*. ```python @dataclass class Type: text: str @dataclass class Toggle: index: int done: bool ui.input(self.draft, Type, on_submit=Add()) # Type(texto) se construye con el texto nuevo 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 ``` - **Un mensaje es cualquier valor que no se puede llamar**: un texto (`"save"`), una tupla (`("delete", 3)`) o, mejor, una dataclass pequeña, que se lee bien en `match`. - **Los controles con valor** llaman a lo que les das con ese valor. Una *clase* dataclass se puede llamar, así que `on_input=Type` construye `Type(texto)` y lo manda a `update`. Para eventos sin valor pasa una *instancia*: `on_submit=Add()`. - También vale una función que devuelve un mensaje: `lambda done, i=i: Toggle(i, done)`. - `update` puede devolver un `Command` (enfocar, cerrar, abrir una ventana) o lanzar trabajo asíncrono, como cualquier handler. - Como todos los cambios pasan por un método, puedes registrar los mensajes, guardar un historial para deshacer, repetirlos o probar la lógica sin abrir una ventana: ```python app = Todo() app.update(Type("Comprar leche")) app.update(Add()) assert app.tasks[-1]["title"] == "Comprar leche" ``` Los mensajes también llegan desde fuera de la vista: atajos (`on_key`), temporizadores (`pz.every(1, Tick())`), menús e hilos (`pz.post(Downloaded(...))`). Mira [Arquitectura de una app](https://albakore.github.io/pyzed/es/guides/architecture/#mensajes). #### Funcional: un estado inmutable y funciones puras El estado es **una única dataclass congelada**. La vista es una función `view(state, set_state)` que solo lo lee, y la lógica vive en funciones que reciben datos y devuelven datos nuevos: ```python @dataclass(frozen=True) class State: tasks: tuple = () draft: str = "" def added(tasks, title): # pura: sin self, sin efectos 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): # la única clase: guarda el estado actual state = State() def set_state(self, **changes): self.state = replace(self.state, **changes) def view(self): return view(self.state, self.set_state) ``` - Nada cambia en el sitio: `set_state(...)` construye un `State` nuevo con `dataclasses.replace`. Usa tuplas y `frozenset` en lugar de listas y conjuntos. - Las funciones puras se prueban sin esfuerzo: `assert added((), "Leche") == (Task("Leche"),)`. - Las funciones de vista son tus componentes: `task_row(task, on_toggle, on_remove)` recibe datos y callbacks y devuelve widgets, como las funciones de `pyzed.ui`. Como sus argumentos son valores simples, se pueden cachear con [`@pz.memo`](https://albakore.github.io/pyzed/es/guides/performance/), que solo las reconstruye cuando cambian. Para un *reductor* puro (al estilo de Elm o Redux), combínalo con mensajes: `update` sustituye el estado por lo que devuelve una función pura. ```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) # la vista solo manda mensajes ``` #### Manejar el estado en clases Viva donde viva el estado, las mismas reglas hacen que una app sea predecible: | Regla | Por qué | | --- | --- | | Pon el estado inicial en `__init__` (o en atributos de clase si son valores inmutables). | Un atributo de clase que es una lista o un dict lo comparten todas las instancias; `__init__` le da a cada app el suyo. | | No cambies nunca el estado dentro de `view()`. | `view()` se ejecuta tras cada evento; calcula ahí lo que vas a mostrar, nada más. | | Calcula en `view()` los valores derivados en lugar de guardarlos. | `shown = [t for t in self.tasks if ...]` nunca se queda desfasado. | | Guarda los datos en Python normal: dataclasses, dicts, listas. | Pyzed redibuja con lo que tengas; no hay observables que envolver. | | Sustituye las listas en lugar de modificarlas cuando una parte de la vista está cacheada. | `@pz.memo` compara argumentos; `self.tasks = [*self.tasks, t]` es un valor nuevo. | | El estado de una ventana va en su `Window`; el compartido, en la `App` (`self.app`). | Cada ventana tiene su `view()` y lee la app a través de `self.app`. | | El estado de una pieza reutilizable va en un `pz.Component` (`pz.state(...)`). | Cada instancia guarda el suyo, y el padre solo ve sus props y sus eventos. | Un componente tiene dentro las mismas tres opciones: métodos que cambian `self`, `self.emit(...)` para avisar al padre o un `render()` que solo lee sus props. Mira [Apps, ventanas, formularios y componentes](https://albakore.github.io/pyzed/es/guides/classes/). #### Mezclar estilos La mayoría de las apps reales los mezclan: handlers imperativos para el estado pequeño de la interfaz (un menú abierto, la fila bajo el puntero), mensajes para las acciones que importan (guardar, borrar, sincronizar) y funciones puras para la lógica y las partes reutilizables de la vista. ```python class Editor(pz.App): def __init__(self): self.menu_open = False # detalle de la interfaz: imperativo self.doc = Document() def update(self, message): # acciones: mensajes match message: case Save(): return save(self.doc) # función pura, devuelve un Command def view(self): return pz.column([ toolbar(self.doc.name, on_save=Save()), # una función de vista ui.icon_button("menu", lambda: setattr(self, "menu_open", True)), ]) ``` Cada página de la [biblioteca](https://albakore.github.io/pyzed/es/library/) muestra los eventos de su componente en los tres estilos, en **Eventos → Manejar los eventos**. ### Layout y estilo > Layout en Pyzed: filas, columnas, contenedores, tamaños, relleno, alineación y colores para organizar interfaces de escritorio nativas en Python. Source: https://albakore.github.io/pyzed/es/guides/layout/ #### Filas y columnas (Image: Widgets base: botones, campos, deslizadores, progreso y texto) ```python pz.column([a, b, c], spacing=8, padding=16) # vertical pz.row([a, b, c], spacing=8, align="center") # horizontal ``` - `children` acepta listas anidadas y omite `None` y `False`, así que puedes escribir `self.error and ui.label(self.error, color="error")`. - Un texto o un número como hijo se convierte en `pz.text`. - `align` alinea los hijos en el eje cruzado: `"start"`, `"center"`, `"end"`. #### Tamaños `width` y `height` aceptan: | Valor | Significado | | --- | --- | | `240` | Píxeles fijos. | | `"shrink"` | Lo justo para el contenido. | | `"fill"` | Todo el espacio disponible. | | `"fill:2"` | Una proporción del espacio: dos `"fill:1"` y un `"fill:2"` reparten 25/25/50. | ```python pz.row([ pz.container(sidebar, width=240), pz.container(contenido, width="fill"), ]) ``` Empuja elementos a los extremos con un hueco flexible: ```python pz.row([ui.label("Título"), pz.hspace(), ui.button("Cerrar", self.cerrar)]) ``` :::caution[Hijos que rellenan] Un hijo con `"fill"` (por ejemplo, `pz.rule()` horizontal) hace que su columna también rellene. Si quieres que la columna encoja, pon `width="shrink"` en la columna. ::: #### Relleno `padding` acepta `8` (todos los lados), `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. #### Contenedores `pz.container` envuelve un hijo para darle tamaño, alineación y estilo: ```python pz.container( contenido, 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), desenfoque) clip=True, ) ``` `pz.center(contenido)` lo centra en todo el espacio. `pz.container(None, width=8, height=8, bg=...)` es un rectángulo de color. #### Colores Los colores aceptan `"#rgb"`, `"#rrggbb"`, `"#rrggbbaa"`, tuplas `(r, g, b)` / `(r, g, b, a)` o `"transparent"`. En los componentes de `pyzed.ui` también puedes usar un token del tema (`"accent"`, `"text_muted"`…). Para colores derivados: ```python from pyzed.theme import alpha, mix alpha(self.resolved_theme().accent, 0.2) # 20 % de opacidad mix(self.resolved_theme().editor, "#ffffff", 0.05) # un 5 % más claro ``` #### Texto ```python pz.text("Hola", size=16, weight="semibold", color="#dce0e5") pz.text("código", font="mono") pz.text("una línea muy larga…", wrap=False) ``` Fuentes incluidas: IBM Plex Sans (`"sans"`, por defecto), IBM Plex Mono (`"mono"`) y Lucide (`"icons"`). Ver [iconos y fuentes](https://albakore.github.io/pyzed/es/theming/icons-fonts/). #### Superposiciones y scroll - `pz.stack([abajo, arriba])` superpone widgets; `pz.opaque(w)` evita que los clics lo atraviesen. - `pz.scrollable(contenido, direction="vertical" | "horizontal" | "both")` añade scroll. Con `anchor_bottom=True` se queda pegado al final. - `pz.hover(base, encima)` muestra `encima` solo al pasar el ratón. - `pz.mouse_area(w, on_right_press=..., on_double_click=..., cursor="pointer")` detecta eventos del ratón. #### Rejillas `pz.grid(hijos, columns=2)` sigue dando una rejilla uniforme. Si pasas **pistas**, se convierte en una rejilla al estilo CSS: cada pista es un tamaño en píxeles, una parte del espacio libre (`"1fr"`, `"2fr"`) o `"auto"` (tan grande como su contenido). Las pistas pueden ser una lista o un texto. (Image: Rejilla con pistas, filas flex y un dock) ```python pz.grid( [ pz.cell(cabecera, column=0, row=0, column_span=3), pz.cell(lateral, column=0, row=1, row_span=2), principal, detalles, pz.cell(pz.text("centrado"), align_x="center", align_y="center"), ], columns=[140, "1fr", "auto"], # o "140 1fr auto" rows="44 1fr 1fr", spacing=8, # row_spacing= para otro hueco vertical height=240, ) ``` `pz.cell` coloca un hijo en una columna/fila, hace que ocupe varias pistas y lo alinea dentro de su celda. Los hijos sin posición llenan las celdas libres fila a fila. #### Flex `pz.flex` coloca los hijos en un eje, como flexbox en CSS: ```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` es `"start"`, `"center"`, `"end"`, `"between"`, `"around"` o `"evenly"`. `pz.flex_item` indica cuánto espacio libre toma un hijo (`grow`) o un tamaño fijo (`basis`). `wrap=True` hace que una fila salte a nuevas líneas. #### Cajas `pz.box` es un contenedor para cualquier cosa, como un `div` de HTML. Coloca sus hijos y acepta todas las [propiedades de estilo](https://albakore.github.io/pyzed/es/theming/styles/), clases y un id, así que una pantalla entera se puede montar con cajas: ```python pz.box( pz.text("Planes").style(size=20, weight="bold"), pz.box([plan_card(p) for p in plans], display="grid", columns=3, gap=12), pz.box(ui.button("Cancelar", cancel), ui.button("Continuar", go), display="flex", justify="end", gap=8), ui.badge("Nuevo").style(position="absolute", top=8, right=8), class_="page", padding=24, gap=16, overflow="scroll", ) ``` - `display`: - `"block"` (por defecto) pone los hijos uno debajo de otro, tan anchos como la caja. - `"flex"` los coloca a lo largo de `direction` con `justify`, `align`, `gap` y `wrap`, como `pz.flex`. - `"grid"` usa `columns` y `rows`: un número de columnas iguales o pistas como `"200 1fr auto"`. - `"stack"` los pone uno encima de otro. - `align` acepta `"start"`, `"center"`, `"end"` o `"stretch"`. - `overflow` acepta `"visible"`, `"hidden"` (recorta el contenido) o `"scroll"` (también `"scroll_x"` o `"scroll_y"`). - Un hijo con `position="absolute"` sale del flujo. Se coloca con `top`/`right`/`bottom`/`left` dentro de la caja, por encima del resto, y el de mayor `z_index` queda arriba. - `class_` e `id` aplican las reglas de la [hoja de estilo](https://albakore.github.io/pyzed/es/theming/styles/#hojas-de-estilo) (una regla para `"box"` da estilo a todas las cajas). `style=` y las propiedades por palabra clave van encima, como un atributo `style` en línea. #### Disposiciones ancladas `pz.anchor_layout` coloca hijos libremente, como un formulario hecho en Pyzed Design, y cada hijo sigue a su **anclaje** cuando la ventana cambia de tamaño: mantiene su distancia a los bordes a los que está anclado, se estira entre bordes opuestos y se queda centrado en los demás casos. ```python pz.anchor_layout([ pz.anchor(ui.heading("Informe"), x=16, y=12, width=368, height=28, anchor="top_wide"), pz.anchor(cuerpo, x=16, y=48, width=368, height=192, anchor="full", max_width=720), pz.anchor(ui.button("Cerrar", on_close), x=284, y=252, width=100, height=32, anchor="bottom_right"), ], design=(400, 300)) ``` `x`, `y`, `width` y `height` están en el tamaño `design` (por defecto, el más pequeño que contiene a todos los hijos). `anchor` es uno de los 16 presets de `pyzed.anchors.PRESETS` (`"top_left"`, `"top_center"`, `"top_right"`, `"top_wide"`, `"center_left"`, `"center"`... `"bottom_wide"`, `"left_tall"`, `"center_tall"`, `"right_tall"` y `"full"`), palabras como `"left right bottom"` o una tupla de bordes. `min_width`, `min_height`, `max_width` y `max_height` limitan el tamaño cuando un hijo se estira; `pivot` (de `0` a `1` en cada eje) es el punto que queda fijo cuando un límite lo frena, y alrededor del que gira `rotate` (grados en sentido horario). `pyzed.anchors.place` hace la cuenta por su cuenta si colocas cajas tú mismo. #### Paneles acoplables y redimensionables `pz.resizable(contenido, edge="right", size=240)` da a un panel un borde que el usuario puede arrastrar. `min_size`/`max_size` lo limitan y `on_resize(tamaño)` avisa del nuevo tamaño al soltar. `ui.panel` y `ui.workspace` lo usan, así que sus paneles laterales se pueden redimensionar sin más, y `pz.split` conserva la proporción arrastrada. `pz.Dock` es un dock estilo editor: un área central con paneles a los lados. Arrastra una pestaña a una zona (o haz clic derecho) para mover el panel a otro lado, y arrastra su borde interior para cambiar su tamaño. ```python pz.Dock( center=editor, panels=[ pz.DockPanel("project", "Proyecto", arbol, icon="folder", side="left"), pz.DockPanel("outline", "Esquema", esquema, icon="list-tree", side="left"), pz.DockPanel("log", "Log", log, icon="terminal", side="bottom"), ], layout=self.layout, # restaura una disposición guardada on_layout=lambda e: setattr(self, "layout", e.value), ) ``` `on_layout` recibe un dict (posiciones, tamaños, pestañas activas y lados ocultos) que puedes guardar y devolver como `layout=`. Ver [`examples/layout.py`](https://github.com/albakore/pyzed/blob/main/examples/layout.py). ### Comandos y tareas > Comandos de Pyzed: enfoca campos, cierra la app, edita el editor de código, lanza trabajo en segundo plano y envía mensajes desde hilos. Source: https://albakore.github.io/pyzed/es/guides/commands/ Un [`Command`](https://albakore.github.io/pyzed/es/reference/command/) es un efecto que el runtime ejecuta por ti. Devuélvelo desde `update`, `on_start`, `on_key` o cualquier callback. ```python from pyzed import Command ``` | Comando | Efecto | | --- | --- | | `Command.focus(id)` | Enfoca el campo con ese `id`. | | `Command.exit()` | Cierra la aplicación. | | `Command.set_editor_text(id, texto)` | Reemplaza el texto de un editor. | | `Command.set_editor_cursor(id, línea, col)` | Mueve el cursor de un editor (desde 1). | | `Command.scroll_to_end(id)` | Desplaza un `scrollable` al final. | | `Command.scroll_to(id, x, y)` | Desplaza un `scrollable` a una posición en píxeles. | | `Command.copy(texto)` | Copia al portapapeles. | | `Command.paste(then)` | Lee el portapapeles y llama a `then(texto)`. | | `Command.paste_image(then)` | Lee una imagen del portapapeles y llama a `then(png)` con sus bytes PNG (`None` si no hay). | | `Command.pick_files(then, ...)` | Abre el diálogo de archivos del sistema y llama a `then(rutas)`; `filters=[("Imágenes", ["png", "jpg"])]`, `multiple=False`, `folder=True`. | | `Command.message(msg)` | Envía `msg` a `update` en el siguiente ciclo. | | `Command.perform(fn, then)` | Ejecuta `fn()` en segundo plano (un hilo, o el loop de asyncio si es `async def`) y entrega el resultado a `then`. | | `Command.batch([...])` | Varios a la vez (también vale devolver una lista). | | `Command.open_window(w)`, `close_window`, `drag_window`… | Controlan ventanas. Ver [varias ventanas](https://albakore.github.io/pyzed/es/guides/windows/#comandos-de-ventana). | #### Enfocar un campo ```python def abrir_buscador(self): self.buscando = True return Command.focus("buscar") def view(self): return ui.input(self.query, self.set_query, id="buscar") if self.buscando else ... ``` #### Trabajo en segundo plano `Command.perform` ejecuta una función lenta en segundo plano para no congelar la interfaz y, al terminar, llama a `then(resultado)` en el hilo de la interfaz. Si la función es `async def`, corre en el loop de asyncio de Pyzed; si devuelve un generador, `then` se llama con cada valor. Para código async también puedes devolver la corrutina directamente: ver [async y suscripciones](https://albakore.github.io/pyzed/es/guides/async/). ```python import json import urllib.request class Repos(pz.App): def __init__(self): self.repos, self.cargando = [], False def on_start(self): return self.cargar() def cargar(self): self.cargando = True return Command.perform(self.descargar, self.recibido) @staticmethod def descargar(): with urllib.request.urlopen("https://api.github.com/users/albakore/repos") as r: return json.load(r) def recibido(self, repos): self.repos, self.cargando = repos, False def view(self): if self.cargando: return pz.center(ui.muted("Cargando…")) return pz.column([ui.list_item(r["name"], icon="book") for r in self.repos], padding=8) ``` Si `fn` lanza una excepción, se imprime y `then` no se llama. Captúrala dentro de `fn` y devuelve un valor de error si quieres mostrarlo. #### Mensajes desde otros hilos `pz.post(mensaje)` envía un mensaje a la app desde **cualquier hilo**. Sirve para progreso, procesos largos, sockets o temporizadores: ```python import subprocess import threading class Build(pz.App): def __init__(self): self.lineas = [] def update(self, message): kind, value = message if kind == "linea": self.lineas.append(value) return Command.scroll_to_end("log") def compilar(self): def worker(): proc = subprocess.Popen(["cargo", "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) for linea in proc.stdout: pz.post(("linea", linea.rstrip())) threading.Thread(target=worker, daemon=True).start() def view(self): return pz.column([ ui.button("Compilar", self.compilar, variant="solid"), ui.terminal(self.lineas, id="log"), ], spacing=8, padding=12) ``` `pz.post` lanza `RuntimeError` si no hay ninguna app ejecutándose. El GIL se libera mientras corre el bucle de eventos, así que tus hilos avanzan con normalidad. #### Cerrar la app ```python def on_key(self, combo): if combo == "ctrl-q": return Command.exit() ``` ### Async y suscripciones > Async en Pyzed: ejecuta corrutinas, generadores, temporizadores y streams en apps de escritorio en Python sin bloquear ni congelar la interfaz. Source: https://albakore.github.io/pyzed/es/guides/async/ Pyzed ejecuta tus corrutinas en **un único loop de asyncio** que corre en un hilo en segundo plano. Así puedes usar `aiohttp`, `httpx`, `websockets` y cualquier librería async sin configurar nada, y la ventana nunca se bloquea. #### Qué puede devolver `update` o un callback ```mermaid sequenceDiagram participant UI as Hilo de la interfaz (Iced) participant App as update() participant Aio as loop de asyncio UI->>App: clic → mensaje App-->>Aio: return corrutina / generador Note over UI: la ventana sigue respondiendo Aio-->>App: resultado → update(resultado) App->>UI: view() Aio-->>App: cada valor producido → update (async generator) App->>UI: view() ``` | Valor devuelto | Qué hace Pyzed | | --- | --- | | `None` | Vuelve a dibujar. | | un `Command` o una lista de ellos | Los ejecuta. | | una **corrutina** (`async def`) | La ejecuta en el loop y envía su resultado a `update`. | | un **generador** | Lo recorre en un hilo y envía cada valor a `update`. | | un **generador async** | Lo recorre en el loop y envía cada valor a `update`. | | cualquier otro valor | Lo envía a `update` como mensaje. | Los valores `None` que produzca un generador se ignoran. #### Una petición: corrutina o `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): # devolver una corrutina la ejecuta self.loading = True async with httpx.AsyncClient() as client: r = await client.get("https://api.github.com/users/albakore/repos") return ("loaded", r.json()) # va a update() def update(self, message): kind, data = message if kind == "loaded": self.repos, self.loading = data, False ``` :::caution[Estado y corrutinas] El cuerpo de la corrutina corre en el loop de asyncio, no en el hilo de la interfaz. Cambiar `self` desde ahí funciona para cosas simples, pero lo más seguro es **devolver** el resultado y modificar el estado en `update` o en el callback `then`. ::: Con `Command.perform(funcion, then)` el resultado llega a un callback concreto en lugar de a `update`. `funcion` puede ser normal (corre en un hilo) o `async def` (corre en el 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): # en el hilo de la interfaz self.quote, self.loading = quote, False ``` `then` se llama aunque `funcion` no devuelva nada (con `None`), así que también sirve para "avísame cuando termine": `Command.perform(db.migrate, lambda _: self.reload())`. #### Progreso: generadores async Cada `yield` llega a `update` mientras el trabajo sigue en segundo plano: ```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) # devolver el generador lo arranca def update(self, message): index, event, value = message if event == "progress": self.downloads[index].progress = value ``` Para arrancar varios a la vez, envuélvelos en `Command.perform`: `return [pz.Command.perform(self.start(d)) for d in self.downloads]`. #### Suscripciones: temporizadores y streams `subscriptions()` devuelve las fuentes de mensajes que deben estar **activas ahora**. Pyzed la vuelve a evaluar después de cada `update`: una suscripción existe mientras la devuelves y se detiene en cuanto deja de aparecer. ```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 ``` | Función | Qué hace | | --- | --- | | `pz.every(segundos, mensaje)` | Envía `mensaje` cada `segundos` (como `time::every` de Iced). Si `mensaje` es un callable, se llama. | | `pz.listen(funcion)` | Ejecuta `funcion()` (un generador o generador async) mientras la suscripción esté activa y envía cada valor a `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 [] ``` Las suscripciones se identifican por una **clave** (por defecto, el mensaje o la función). Si necesitas dos iguales con parámetros distintos, pasa `key="..."`. :::tip[Animaciones] `ui.spinner()` y otros dibujos que dependen del reloj solo se mueven cuando la vista se redibuja. Mantén una suscripción `pz.every(1 / 30, lambda: None)` activa mientras se muestran. ::: #### Utilidades - `pz.spawn(corrutina)` programa una corrutina en el loop de Pyzed sin esperar su resultado (devuelve un `concurrent.futures.Future`). - `pz.asyncio_loop()` devuelve el loop, por si una librería lo necesita. - `pz.post(mensaje)` sigue funcionando desde cualquier hilo. ### Servicios > Temporizadores, atajos, vigilantes de archivos, procesos, tareas en segundo plano, peticiones web, portapapeles, ajustes, diálogos de archivos, notificaciones e iconos de bandeja para apps de escritorio en Python con Pyzed, con eventos en el hilo de la interfaz. Source: https://albakore.github.io/pyzed/es/guides/services/ Los **servicios** son las partes de una app que lanzan eventos pero no dibujan nada: `pz.Timer`, `pz.Shortcut`, `pz.FileWatcher`, `pz.Process`, `pz.Worker`, `pz.HttpRequest`, `pz.Clipboard`, `pz.Settings`, `pz.FileDialog`, `pz.Notification` y `pz.TrayIcon`. Son los mismos objetos que los [componentes no visuales](https://albakore.github.io/pyzed/es/editor/non-visual-components/) de Pyzed Design, y funcionan en un `pz.App`, en un formulario o en un script sin ninguna ventana. ```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): # corre en el hilo de la interfaz 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: Un Vigilante de archivos listando los archivos que se crean y se borran en una carpeta) #### Handlers e hilos Pasa los handlers como `on_=`, o más tarde con `service.on("evento", handler)` (`None` lo quita). Un handler con un parámetro recibe el valor del evento; uno sin parámetros simplemente se llama. Cada clase lista sus eventos en `Service.events`. Los vigilantes, los procesos, las tareas y las peticiones web trabajan en **hilos en segundo plano**, pero sus eventos llegan a los handlers en **el hilo de la app** (con `pz.post`), así que un handler puede cambiar el estado de la app y devolver comandos como cualquier callback. Sin una app en marcha, en un script o en un test, el handler se llama en el momento, en el hilo que lanzó el evento. `service.enabled = False` silencia todos sus eventos; `stop()` detiene lo que esté haciendo. #### Los servicios | Clase | Lanza | Qué saber | | --- | --- | --- | | `pz.Timer(interval=1000, repeat=True)` | `tick` (la cuenta) | Milisegundos, como en Visual Basic y Qt. Devuelve `timer.subscriptions()` desde `App.subscriptions`. `start()`, `stop()`, `running`. | | `pz.Shortcut("mod-s")` | `activate` (la combinación) | Llama a `shortcut.handle(combo)` desde `on_key`. Valen `"Ctrl+S"`, `"ctrl-s, cmd-s"` y `mod-` (Cmd en macOS, Ctrl en el resto). | | `pz.FileWatcher(path, pattern="*", recursive=False, interval=500)` | `created`, `modified`, `deleted` (una ruta), `change` (la lista) | Compara fechas y tamaños cada `interval` ms, así que funciona igual en todas partes, también en unidades de red. | | `pz.Process(command, directory=None, env=None, shell=False)` | `started`, `output`, `error_output` (una línea), `finished` (código de salida), `failed` | `start()`, `write(text)`, `kill()`, `wait()`, `running`, `exit_code`. Un comando en texto se separa como lo haría una shell. | | `pz.Worker(work)` | `progress`, `done` (el resultado), `failed` (la excepción) | `run(value)` llama a `work(value)` en un hilo; puede usar `report(progress)` y mirar `cancelled`. | | `pz.HttpRequest(url, method="GET", headers=None, body=None, timeout=30)` | `response` (un `HttpResponse`), `failed` | `send()`; `send(json={...})` envía JSON con POST. Los estados de error también son respuestas: mira `response.ok`. | | `pz.Clipboard(watch=False, interval=500)` | `change`, `paste` (el texto) | `copy(text)` y `paste()` devuelven comandos. Con `watch`, devuelve `subscriptions()` desde tu app. | | `pz.Settings(name, folder=None, defaults=None)` | `change` (`(clave, valor)`) | Un diccionario guardado como JSON al momento (`autosave`), por usuario, en la carpeta de ajustes de Pyzed. | | `pz.FileDialog(mode="open", filters=..., directory=None, file_name="")` | `chosen` (ruta o lista), `cancelled` | `show()` devuelve el comando que lo abre; modos `open`, `open_many`, `save`, `folder`. | | `pz.Notification(title, text)` | `shown`, `failed` | `show(text=None)` devuelve si se pudo mostrar. | | `pz.TrayIcon(icon=None, tooltip="", menu=())` | `click`, `double_click` (Windows), `menu` (el elemento), `failed` | `show()` devuelve si pudo; `hide()`. Los cambios de `icon`, `tooltip` y `menu` se ven al momento. | | `pz.SystemInfo(interval=0, disk_path=None)` | `update` (un diccionario) | `os`, `os_version`, `hostname`, `user`, `cpu_count`, `language`, `memory_total`, `memory_available`, `disk_free`; `folder("downloads")`, `env(name)`. | | `pz.Launcher(target="")` | `opened`, `failed` (el destino) | `open(target)` con el programa del sistema (archivos, carpetas, enlaces, `mailto:`), `reveal(path)`, `email(to, subject, body)`. | | `pz.NetworkMonitor(host="1.1.1.1", port=53, interval=5000)` | `change` (bool), `online`, `offline` | Prueba una conexión cada `interval` ms en segundo plano; `online` guarda la última respuesta. | | `pz.PowerMonitor(interval=30000, low_level=20)` | `change` (`(porcentaje, enchufada)`), `low`, `plugged`, `unplugged` | `percent` es `None` en ordenadores sin batería (`has_battery`). | ##### Un temporizador y un atajo ```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) ``` ##### Un comando y su salida ```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() ``` ##### Trabajo en segundo plano ```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) ``` ##### Una API web ```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"] ``` ##### Ajustes y diálogos ```python prefs = pz.Settings("my-app", defaults={"theme": "One Dark"}) prefs["theme"] = "One Light" # se guarda al momento prefs.get("theme") # "One Light", también la próxima vez dialog = pz.FileDialog("open", filters="Imágenes: png, jpg", on_chosen=self.load_image) def open_clicked(self): return dialog.show() # un comando: devuélvelo ``` ##### El sistema operativo Los servicios de `pyzed.system` (también en el nivel superior: `pz.SystemInfo`, `pz.Launcher`...) leen el ordenador y abren cosas con los programas que el usuario ya tiene: ```python info = pz.SystemInfo() f"{info.os} {info.os_version}, {info.cpu_count} núcleos" # "Windows 11, 8 núcleos" info.folder("downloads") # la carpeta Descargas del usuario info.disk_free // 2**30 # GB libres launcher = pz.Launcher(on_failed=lambda destino: print("No se pudo abrir", destino)) launcher.open("https://example.com") # el navegador por defecto launcher.open("informe.pdf") # el visor de PDF launcher.reveal("informe.pdf") # su carpeta, con el archivo seleccionado network = pz.NetworkMonitor(on_offline=lambda: print("Sin conexión"), on_online=lambda: print("De vuelta")) network.start() ``` Para archivos, `ui.file_input` es un campo de ruta con un botón que abre el diálogo del sistema, y `ui.DropZone` recibe archivos arrastrados desde el escritorio. Una ventana también recibe cada archivo soltado en `on_file_drop(path)` (`on_file_hover` y `on_file_leave` mientras se arrastran por encima). Mira las páginas de [Sistema](https://albakore.github.io/pyzed/es/library/drop-zone/) de la librería. ##### Un icono en la bandeja `pz.TrayIcon` pone un icono en el área de notificación de Windows, en la barra de menús de macOS y en el panel de Linux, con un texto al pasar el ratón y un menú. El menú es una lista, o un texto con un elemento por línea: `-` es un separador, `[x]` y `[ ]` crean casillas que cambian al hacer clic, y las líneas con sangría forman un submenú. ```python self.tray = pz.TrayIcon("icono.png", "Copias", menu=""" Abrir [x] Pausa Velocidad Lenta Rápida - Salir """, on_click=self.open, on_menu=self.menu) self.tray.show() def menu(self, item): if item == "Pausa": self.paused = self.tray.checked("Pausa") elif item == "Salir": return pz.Command.exit() ``` `pz.TrayItem(text, id=None, enabled=True, checked=None, items=[])` le da a un elemento un `id` (lo que recibe `on_menu`) o lo desactiva. Sin `icon` muestra un punto azul. En Linux el icono es un StatusNotifierItem, sin GTK: KDE, Cinnamon, XFCE, MATE, LXQt y la mayoría de paneles lo muestran, y GNOME también con la extensión AppIndicator. `pz.TrayIcon.supported()` dice si el escritorio muestra iconos de bandeja; donde no, `show()` devuelve `False` y `failed` dice por qué. #### Sin ventana Nada de esto necesita una app en marcha. Los scripts y los tests pueden esperar el resultado: ```python import pyzed as pz process = pz.Process(["git", "status", "--short"], on_output=print) process.start() process.wait() # espera a que termine; devuelve el código de salida request = pz.HttpRequest("https://example.com") request.send() print(request.wait(10).status) # 200 ``` `Process.wait()`, `Worker.wait()` y `HttpRequest.wait()` esperan a que termine el trabajo (con un `timeout` opcional) y devuelven el código de salida, el resultado o la respuesta. ### Varias ventanas > Varias ventanas en Pyzed: abre, cierra, mueve, redimensiona y controla ventanas de escritorio nativas desde Python con pz.Window, cada una con su tema. Source: https://albakore.github.io/pyzed/es/guides/windows/ Una app puede tener tantas ventanas como quieras. La ventana principal es tu `App`; las demás son subclases de `pz.Window`, cada una con su `view()`, título, tamaño y tema. (Image: Varias ventanas) ```python import pyzed as pz from pyzed import ui class Settings(pz.Window): title = "Ajustes" size = (420, 300) theme = "One Light" # opcional: por defecto usa el de la app def view(self): return pz.container( pz.column([ ui.heading("Ajustes", level=2), ui.field("Autoguardado", ui.switch(self.app.autosave, self.app.set_autosave)), ui.button("Cerrar", 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("Abrir ajustes", self.open_settings, icon="settings")) pz.run(Main) ``` #### Cómo funciona ```mermaid flowchart LR subgraph APP["App (estado compartido)"] ST[("self.*")] end M["Ventana principal"] -->|view| ST W1["Window A"] -->|"view / self.app"| ST W2["Window B"] -->|"view / self.app"| ST M -->|"return Window().open()"| W1 W1 -->|"eventos"| U["update()"] W2 -->|"eventos"| U M -->|"eventos"| U U --> ST ``` - `ventana.open()` (o `Command.open_window(ventana)`) devuelve el comando que la abre. - `self.app` apunta a la app dentro de cada ventana, así que **comparten estado**: los callbacks como `self.app.guardar` funcionan directamente. - Los mensajes que se producen en una ventana van a su `update()`, que por defecto los reenvía a `app.update()`. - `ventana.is_open` indica si está abierta. Cerrar la ventana principal termina la app. #### Opciones Atributos de clase o de instancia: | Atributo | Descripción | | --- | --- | | `title` | Texto o callable. | | `size`, `min_size`, `max_size` | Tamaños en píxeles lógicos. | | `position` | `"center"` o `(x, y)`. | | `resizable`, `decorations` | `decorations=False` quita el marco del sistema por completo. | | `frame` | `"custom"` dibuja la barra de título, los botones de ventana y los menús en Pyzed (ver [Marcos de ventana](#marcos-de-ventana-tu-propia-barra-de-título)). | | `transparent`, `maximized`, `fullscreen` | Estado inicial. | | `scale` | Escala de la interfaz. | | `theme` | Un `Theme`, un nombre o `themes.auto(...)`. `None` usa el de la app. | | `transition`, `exit_transition` | Cómo se anima al abrirse y al cerrarse: `"fade"`, `"zoom"`, `"pop"`, `"slide_up"`... un preset o una `pz.Animation`. Ver [Animación](https://albakore.github.io/pyzed/es/animation/transitions/#transiciones-de-ventana). | `window_size` contiene el tamaño actual y `window_position` dónde está la ventana en la pantalla (los actualiza el runtime; la posición es `None` en Wayland). `window_maximized` y `window_focused` dicen si está maximizada y si tiene el foco del teclado. #### Ganchos | Método | Cuándo se llama | | --- | --- | | `on_open()` | Al abrirse. | | `on_close()` | Cuando el usuario pide cerrarla. Devuelve `False` para mantenerla abierta (por ejemplo, para preguntar si guardar). | | `on_resize(ancho, alto)` | Al cambiar de tamaño. | | `on_key(combo)` | Teclas pulsadas con esa ventana enfocada. | #### Comandos de ventana Todos aceptan la ventana como último argumento; sin él actúan sobre la ventana que produjo el mensaje. | Comando | Efecto | | --- | --- | | `Command.open_window(w)` / `w.open()` | Abre la ventana. | | `Command.close_window(w)` / `w.close()` | La cierra. | | `Command.drag_window(w)` / `w.drag()` | Empieza a moverla con el ratón (para barras de título propias). | | `Command.minimize_window(w)` / `w.minimize()` | La minimiza. | | `Command.toggle_maximize_window(w)` / `w.toggle_maximize()` | Maximiza o restaura. | | `Command.maximize_window(True, w)` | La maximiza (`False` la restaura). | | `Command.drag_resize_window("se", w)` | Empieza a cambiar su tamaño desde un borde o una esquina (para marcos propios). | | `Command.show_window_menu(w)` | Muestra el menú de ventana del sistema (solo Windows). | | `Command.set_fullscreen(True, w)` | Pantalla completa. | | `Command.resize_window(ancho, alto, w)` | Cambia el tamaño. | | `Command.move_window(x, y, w)` | La mueve. | | `Command.focus_window(w)` | La trae al frente. | ##### Marcos de ventana: tu propia barra de título `frame = "custom"` cambia el marco del sistema por uno dibujado por Pyzed: una barra de título que mueve la ventana (doble clic la maximiza), los botones de minimizar, maximizar y cerrar con el estilo del sistema, menús a la izquierda y bordes que cambian el tamaño de la ventana. `frame_menus()` devuelve los menús. (Image: Marcos de ventana de cada plataforma) ```python class Notas(pz.App): title = "Notas" frame = "custom" # o "macos", "windows", "gnome", "ubuntu", "kde" def frame_menus(self): return [ ui.Menu("Archivo", [ui.MenuItem("Abrir…", self.abrir, keys="ctrl-o"), ui.SEPARATOR, ui.MenuItem("Salir", self.close)]), ui.Menu("Editar", [ui.MenuItem("Deshacer", self.deshacer, keys="ctrl-z")]), ] def view(self): return self.editor() ``` Los menús quedan tras un botón ☰: un clic abre el primero y muestra el nombre de todos, y al pasar el puntero a otro nombre se abre ese. Para poner más cosas en la barra (un selector de proyecto, un botón Ejecutar), llama tú a `ui.window_frame` en `view()`; así la ventana no se enmarca dos veces: ```python def view(self): return ui.window_frame( self.editor(), menus=self.frame_menus(), start=[ui.title_button("pyzed", chevron=True)], end=[ui.button("Ejecutar", self.ejecutar, icon="play", size="sm")], ) ``` | Valor de `frame` | Qué obtienes | | --- | --- | | `None` (por defecto) o `"system"` | El marco del sistema. | | `"custom"` o `True` | La barra de título de Pyzed con los botones del sistema. En macOS el sistema sigue dibujando sus semáforos sobre la barra. | | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"`, `"kde"` | Los botones de esa plataforma en cualquier sistema. | `window_maximized` y `window_focused` dicen si la ventana está maximizada y tiene el foco (los actualiza el runtime), así que el botón de maximizar muestra el símbolo de restaurar y la barra se atenúa en segundo plano. Maximizada, la ventana no tiene borde para cambiar su tamaño. Las piezas también funcionan por separado: `ui.window_title_bar` es la barra sola (sin callbacks solo se muestra, que es como Pyzed Design previsualiza los formularios), `ui.window_controls` los tres botones y `pz.window_edges` los bordes que cambian el tamaño de una ventana sin decoraciones. Para una barra hecha desde cero, usa los comandos `Command.drag_window`, `Command.drag_resize_window`, `Command.toggle_maximize_window` y `Command.show_window_menu`. **Notas por plataforma.** En Windows la ventana conserva la sombra y las esquinas redondeadas del sistema. Wayland mueve y redimensiona la ventana igual que X11 (lo hace el compositor). En WSL Pyzed usa X11 y `"custom"` dibuja botones de Windows, el escritorio donde aparece la ventana. `ui.system_platform()` dice qué estilo elige `"custom"`. ##### Pestañas arrastrables y ventanas desprendibles Las pestañas de `ui.tab_bar` se pueden arrastrar. `on_drag(id)` avisa de que empezó un arrastre; mientras `dragging` nombra la pestaña, `on_drag_over(index)` informa del punto de inserción bajo el puntero (o `None` fuera de la fila) y `drop_index` dibuja la marca ahí. `on_drop(id, event)` llega al soltarla, con el puntero en la ventana (`event.window_x`, `event.window_y`, fuera de ella si se soltó fuera) y en la pantalla (`event.screen`). El `window_position` y el `window_size` de cada ventana dicen dónde está, así que puedes abrir la pestaña en una ventana nueva o apilarla en la ventana que hay bajo el puntero: ```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: # soltada en la fila: reordenar 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 # soltada dentro de la ventana, fuera de la fila 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) ``` Para apilar una pestaña en otra de tus ventanas, compara `event.screen` con el `window_position` y el `window_size` de esa ventana. Wayland no les dice a las apps dónde están sus ventanas: ahí `event.screen` y `window_position` son `None`, así que ofrece también un comando de menú (Pyzed Design tiene **Mover la pestaña a una ventana nueva** y **Mover la pestaña a la ventana principal**). :::note `pz.run()` solo puede llamarse una vez por proceso (limitación de winit). Para más ventanas, usa `pz.Window`. ::: ### Dibujo con canvas > Dibuja gráficos propios en apps de escritorio en Python con pz.canvas y pyzed.draw: formas, texto, transformaciones y eventos del ratón, en Rust. Source: https://albakore.github.io/pyzed/es/guides/canvas/ `pz.canvas` es una superficie de dibujo 2D. Las formas se crean con `pyzed.draw` y son diccionarios simples, así que construirlas en cada frame es barato. examples/game_of_life.py: miles de celdas dibujadas en un solo canvas, avanzadas con 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, "Hola", color="#dce0e5", align_x="center"), ], width=200, height=200) ``` Los ángulos van en grados, `y` crece hacia abajo y los colores aceptan lo mismo que `pz.text`. #### Formas (Image: Formas dibujadas con pyzed.draw) | Función | Forma | | --- | --- | | `draw.background(color)` | Rellena todo el canvas. | | `draw.rect(x, y, w, h, fill=, stroke=, width=, radius=, dash=)` | Rectángulo (redondeado con `radius`). | | `draw.rects([(x, y, w, h), ...], fill=)` | Muchos rectángulos de un color en una sola forma (rápido para rejillas). | | `draw.circle(x, y, r, ...)` | Círculo. | | `draw.line(x1, y1, x2, y2, stroke=, width=, dash=, cap=)` | Línea; `cap="round"` redondea los extremos. | | `draw.polyline(puntos, closed=False)` / `draw.polygon(puntos)` | Líneas que unen puntos / figura cerrada. | | `draw.arc(x, y, r, inicio, fin, pie=False)` | Arco (0° = las 3 en punto, sentido horario); `pie=True` lo cierra por el centro. | | `draw.path(ops)` | Trazado libre con operaciones tipo SVG: `("M", x, y)`, `("L", x, y)`, `("Q", cx, cy, x, y)`, `("C", ...)`, `("A", cx, cy, r, inicio, fin)`, `("Z",)`. | | `draw.text(x, y, texto, color=, size=, font=, weight=, align_x=, align_y=)` | Texto anclado en `(x, y)`. | | `draw.image(source, x, y, w, h, rotation=, opacity=, pixelated=)` | Un mapa de bits (sprite), decodificado una vez y en caché. Ver [Crear videojuegos](https://albakore.github.io/pyzed/es/games/). | #### Transformaciones `draw.translate(x, y)`, `draw.rotate(grados)` y `draw.scale(x, y)` afectan a las formas que vienen **después**. Delimítalas con `draw.save()` / `draw.restore()`, o usa `draw.group`: ```python def manecilla(angulo, largo, color): return draw.group( [draw.line(0, 0, 0, -largo, stroke=color, width=3, cap="round")], x=100, y=100, rotation=angulo, ) pz.canvas([draw.circle(100, 100, 90, stroke="#5c6370", width=2), *manecilla(self.minutos * 6, 70, "#dce0e5"), *manecilla(self.segundos * 6, 85, "#d07277")], width=200, height=200) ``` #### Eventos del ratón `pz.canvas` acepta callbacks que reciben coordenadas relativas al canvas: ```python pz.canvas(formas, width=400, height=300, on_press=lambda x, y: self.pintar(x, y), on_move=lambda x, y: self.mover(x, y), cursor="crosshair") ``` Consulta la [referencia de `canvas`](https://albakore.github.io/pyzed/es/reference/widgets/#canvas) para la lista completa. #### Rendimiento Dale un `id` al canvas para cachear la geometría entre frames: solo se vuelve a teselar cuando cambian las formas. Para miles de celdas, usa `draw.rects` en lugar de un `draw.rect` por celda. Los gráficos de `pyzed.ui` (`sparkline`, `bar_chart`, `donut`, `spinner`) están hechos con `canvas`: mira [`python/pyzed/ui/display.py`](https://github.com/albakore/pyzed/blob/main/python/pyzed/ui/display.py) como ejemplo. ### Eventos > Los eventos comunes de cualquier widget de Pyzed (clic, doble clic, arrastre, hover, foco, teclas, cambio de tamaño) y cómo los reciben tus handlers. Source: https://albakore.github.io/pyzed/es/guides/events/ Cualquier widget puede reaccionar a los eventos que suelen ofrecer los editores gráficos: clics, dobles clics, hover, arrastre, foco, teclas y cambios de tamaño. Se añaden con `.on(...)`: ```python import pyzed as pz tarjeta = pz.container(pz.text("Arrástrame"), padding=12, bg="#2c313a").on( click=self.seleccionar, # sin argumentos: solo se llama double_click=lambda e: self.renombrar(e.x), # con uno: recibe un pz.Event drag=self.mover, # e.dx, e.dy desde el último drag key_down=self.atajo, # después de que un clic le dé el foco cursor="grab", ) ``` `pz.on(widget, ...)` hace lo mismo. Llamar otra vez a `.on()` combina los handlers, y `None` quita uno. Un handler también puede ser un mensaje (cualquier valor que no sea invocable), que va a `update` como el resto de callbacks de Pyzed. (Image: Eventos y componentes) #### Los eventos | Grupo | Evento | Cuándo | | --- | --- | --- | | Ratón | `click` | Se pulsó y soltó el botón izquierdo. | | | `double_click` | Dos clics seguidos (después del segundo `click`). | | | `right_click`, `middle_click` | Se pulsó el botón derecho o el central. | | | `mouse_down`, `mouse_up` | Cualquier botón (`event.button` es `"left"`, `"right"`...). | | | `mouse_enter`, `mouse_leave` | El puntero entró o salió del widget. | | | `mouse_move` | El puntero se movió sobre el widget. | | | `scroll` | Giró la rueda (`event.dx`, `event.dy` en líneas). | | Arrastre | `drag_start`, `drag`, `drag_end` | Botón izquierdo pulsado y movido unos píxeles; `drag` da el movimiento desde el anterior. | | Foco | `focus`, `blur` | Un clic en el widget le da el foco; un clic fuera se lo quita. | | Teclado | `key_down`, `key_up` | Teclas mientras tiene el foco (`event.key`, por ejemplo `"ctrl-s"`). | | Layout | `resize` | El widget tiene un tamaño nuevo, incluido el primero. | `pz.events.CATALOG` tiene esta lista con sus descripciones (el diseñador las muestra). #### El objeto `Event` | Campo | Significado | | --- | --- | | `type` | El nombre del evento. | | `x`, `y` | Posición del puntero respecto a la esquina superior izquierda del widget (`position` da las dos). | | `dx`, `dy` | Cantidad de scroll, o movimiento del arrastre desde el `drag` anterior. | | `width`, `height` | El tamaño del widget (`size` da los dos). | | `button` | `"left"`, `"right"`, `"middle"`... para los botones del ratón. | | `key` | La combinación de teclas en los eventos de teclado. | | `shift`, `ctrl`, `alt`, `logo` | Modificadores pulsados. | | `value`, `source` | En eventos personalizados: el valor y el componente que lo emitió. | Un handler recibe el evento solo si tiene un parámetro posicional obligatorio (o `*args`). `lambda: ...` y `lambda i=i: ...` se llaman sin nada, así que la forma habitual de capturar la variable de un bucle sigue funcionando. #### Eventos propios y eventos comunes Todo widget reacciona a los eventos en dos capas: | | Eventos propios | Eventos comunes | | --- | --- | --- | | Qué son | Los parámetros `on_*` de un componente: `on_press`, `on_input`, `on_select`, `on_close`… | Los 18 eventos de esta página: `click`, `mouse_enter`, `drag`, `key_down`, `resize`… | | Qué widgets | Cada componente los suyos (un botón tiene 1, una barra de pestañas 7, un área de ratón 10). | Todos, sean lo que sean. | | Cómo | `ui.input(texto, self.escrito, on_submit=self.enviar)` | `widget.on(double_click=self.renombrar)` | | El handler recibe | El valor del componente: el texto, si está marcado, el elemento, la fila… | Nada, o un `pz.Event`. | Los dos se mezclan sin problema en el mismo widget: ```python ui.list_item("main.py", self.abrir, icon="file").on( # on_press: su evento propio right_click=lambda e: self.menu(e.x, e.y), # un evento común mouse_enter=lambda: setattr(self, "encima", "main.py"), ) ``` [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/) lista los eventos propios de cada componente, qué recibe su handler y cuántos eventos puede tener en total. Cada página de la [biblioteca](https://albakore.github.io/pyzed/es/library/) tiene una sección **Eventos** con ellos, escritos en tres estilos. #### Recetas ##### Pasar el puntero y hacer clic ```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 ("Entrada", "Borradores", "Enviados")], spacing=8) ``` :::tip Para cambiar solo *colores* al pasar el puntero no hacen falta eventos: `pz.Style(hover={"background": "..."})` y el `hover_bg` de los botones los cambian sin llamar a Python. Usa `mouse_enter` / `mouse_leave` cuando el estado le importa a tu app. ::: ##### Un menú contextual donde está el puntero ```python class Board(pz.App): def __init__(self): self.menu_at = None def view(self): board = pz.container(ui.muted("Clic derecho en cualquier sitio"), width="fill", height="fill").on( right_click=lambda e: setattr(self, "menu_at", (e.x, e.y)), # dónde, dentro del tablero ) x, y = self.menu_at or (0, 0) menu = ui.menu([ui.MenuItem("Nota nueva", 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)) ``` ##### Arrastrar para mover ```python class Desk(pz.App): def __init__(self): self.x, self.y = 40, 40 def move(self, e): # e.dx, e.dy: lo que se movió desde el último drag self.x += e.dx self.y += e.dy def view(self): note = pz.container(pz.text("Arrástrame"), 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") ``` ##### Doble clic para renombrar ```python class Rename(pz.App): def __init__(self): self.name, self.editing = "notas.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") # un handler puede devolver un comando ``` ##### Ctrl + clic para elegir varios ```python class Files(pz.App): def __init__(self): self.selected = set() def pick(self, name, e): if e.ctrl or e.logo: # Ctrl (Cmd en macOS) suma a la selección 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) ``` ##### Cambiar el diseño según el tamaño ```python class Gallery(pz.App): def __init__(self): self.narrow = False def view(self): items = [ui.card(ui.muted(f"Foto {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 + rueda para hacer zoom ```python class Zoom(pz.App): def __init__(self): self.zoom = 1.0 def view(self): return pz.container(pz.text("Hazme zoom", 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), ) ``` ##### Foco y teclas en cualquier 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 "Haz clic y pulsa teclas"), 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), ) ``` #### En los tres estilos Un handler de `.on(...)` puede ser una función o un mensaje, como cualquier otro handler, así que los eventos comunes encajan en los [tres estilos](https://albakore.github.io/pyzed/es/guides/code-styles/): ```python # Imperativo: el handler cambia self ui.card(ui.muted("Pasa por encima"), selected=self.hovered).on( mouse_enter=lambda: setattr(self, "hovered", True), mouse_leave=lambda: setattr(self, "hovered", False)) # Declarativo: los handlers son mensajes y update() decide @dataclass class Hover: on: bool ui.card(ui.muted("Pasa por encima"), selected=self.hovered).on(mouse_enter=Hover(True), mouse_leave=Hover(False)) def update(self, message): match message: case Hover(on): self.hovered = on # Funcional: una vista pura pide un estado nuevo def view(state, set_state): return ui.card(ui.muted("Pasa por encima"), selected=state.hovered).on( mouse_enter=lambda: set_state(hovered=True), mouse_leave=lambda: set_state(hovered=False)) ``` Un mensaje que le das a `.on()` llega a `update` tal cual, sin el `pz.Event`. Si necesitas los datos del evento, construye el mensaje en una lambda: `drag=lambda e: Moved(e.dx, e.dy)`. #### Cómo funciona Los eventos están implementados una sola vez, en un widget nativo que envuelve al tuyo (`src/events.rs`), en lugar de en cada componente. Ese envoltorio solo observa: nunca detiene un evento, así que un botón dentro de una tarjeta sigue recibiendo su clic y la tarjeta también. ```mermaid sequenceDiagram participant OS as Sistema de ventanas participant Area as Envoltorio de eventos (Rust) participant Child as Widget envuelto participant Py as Tu handler OS->>Area: eventos de ratón, teclado y redibujado Area->>Child: le pasa primero cada evento Area->>Area: detecta clic, doble clic, arrastre, foco, tamaño Area->>Py: pz.Event(type, x, y, ...) Py-->>Area: None, un Command o un mensaje Note over Py: la ventana se redibuja después de cada handler ``` Los componentes hechos con [`pz.Component`](https://albakore.github.io/pyzed/es/guides/components/) también aceptan estos eventos (`on_click=...`) y añaden los suyos: mira [crear tus propios eventos](https://albakore.github.io/pyzed/es/guides/classes/#crear-tus-propios-eventos). En un [formulario diseñado](https://albakore.github.io/pyzed/es/editor/events/), los eventos de cada control se conectan en el panel Eventos. ### Componentes propios > Crea componentes reutilizables de Pyzed con sus props, estado, lógica y eventos, como en React, y úsalos en el diseñador visual de formularios. Source: https://albakore.github.io/pyzed/es/guides/components/ Un componente es una clase que agrupa widgets con su lógica, como un componente de React: - **Props**: atributos de clase anotados, con su valor por defecto. El padre los pasa en cada render. - **Estado**: cualquier otro atributo que asignes. Se conserva entre renders. - `render()` devuelve los widgets del componente. - `events` declara los eventos propios que lanza con `self.emit(nombre, valor)`. ```python import pyzed as pz class Rating(pz.Component): """Estrellas que se pueden pulsar.""" value: int = 0 # props max: int = 5 events = {"change": "Se eligió una estrella (event.value es la puntuación)."} icon = "star" # cómo aparece en la biblioteca del diseñador def setup(self): # una vez, antes del primer render self.hover = None # estado def render(self): visibles = self.hover or self.value return pz.row([ pz.text("★" if i <= visibles 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) ``` Llamar a un componente devuelve un widget, así que va en cualquier sitio donde iría un widget: ```python class Opinion(pz.App): estrellas = 3 def view(self): return pz.column([ Rating(value=self.estrellas, on_change=lambda e: setattr(self, "estrellas", e.value)), pz.text(f"{self.estrellas} de 5"), ]) ``` (Image: Eventos y componentes) [`examples/components.py`](https://github.com/albakore/pyzed/blob/main/examples/components.py) tiene una puntuación con estrellas, un editor de etiquetas y una nota que se puede arrastrar. #### Eventos - **Eventos propios**: se declaran en `events`, como tupla de nombres o como diccionario `{nombre: descripción}`. El padre los atiende con `on_=`. - `self.emit(nombre, valor)` llama al handler del padre con un `pz.Event` que lleva `value` y `source`. Devuelve su resultado desde tu callback, para que un `Command` o un mensaje lleguen al runtime. - **Eventos comunes**, como `on_click`, `on_drag` y `on_key_down`, funcionan en cualquier componente sin escribir nada. Ver [Eventos](https://albakore.github.io/pyzed/es/guides/events/). - **Eventos de ciclo de vida**: `on_mount` y `on_unmount` avisan al padre cuando el componente aparece y desaparece. - **`style=`** da estilo al componente como a cualquier widget (`Rating(value=3, style="card")`), salvo que declare su propia prop `style`. Ver [Estilos](https://albakore.github.io/pyzed/es/theming/styles/). #### Estado y claves El estado también se puede declarar en la clase con `pz.state(valor)`. A diferencia de una prop, el padre no puede pasarlo, cada instancia empieza con su propia copia del valor (así que listas y diccionarios son seguros) y el diseñador lo muestra junto a las props: ```python class Counter(pz.Component): step: int = 1 # prop count: int = pz.state(0) # estado def render(self): return ui.button(f"{self.count}", lambda: setattr(self, "count", self.count + self.step)) ``` El estado se guarda por posición dentro del `view()` del padre: el segundo `Rating` de una vista conserva su propio estado mientras no cambie el orden. Si los componentes salen de una lista cuyos elementos se mueven, dale a cada uno una `key` estable: ```python pz.column([TodoItem(key=tarea.id, todo=tarea) for tarea in self.tareas]) ``` Un componente que deja de renderizarse se desmonta y su estado se descarta. ```mermaid flowchart LR View["App.view()"] -->|"Rating(value=3)"| Slot{"¿Hay instancia en esta
posición o key?"} Slot -->|no| New["instancia nueva
setup()"] Slot -->|sí| Same["la misma instancia
(conserva el estado)"] New --> Props["se asignan los props"] Same --> Props Props --> Changed{"¿cambiaron props,
estado o contexto?"} Changed -->|sí| Render["render() → widgets"] Changed -->|no| Last["último dibujo, reutilizado"] Render -->|la primera vez| Mount["on_mount()"] View -.->|no se vuelve a renderizar| Unmount["on_unmount()"] ``` `render()` solo se ejecuta cuando cambió algo de lo que depende: sus props, su estado, el contexto que lee, el tema o un componente que lleva dentro. Si no, se reutiliza el último dibujo, así que una vista con cientos de componentes sigue siendo rápida. Si `render()` lee otra cosa (la hora, una variable global), llama a `self.refresh()` cuando cambie o pon `cache = False` en la clase. Ver [componentes que solo se dibujan cuando cambian](https://albakore.github.io/pyzed/es/guides/performance/#componentes-que-solo-se-dibujan-cuando-cambian). #### Hijos, contexto y estilos Los componentes se anidan como los elementos de HTML y los componentes de React: ```python class Card(pz.Component): title: str = "" children: list = () # los argumentos posicionales, como los children de JSX stylesheet = { # reglas tipo CSS de este componente (& es el componente) "&": {"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("Tres mensajes nuevos"), ui.button("Abrir", self.open), title="Bandeja", class_="wide", animate="fade_in_up") ``` - Una prop llamada `children` recibe los argumentos posicionales. Las listas se aplanan y `None` y `False` se ignoran. - `stylesheet` añade reglas a [`pz.stylesheet`](https://albakore.github.io/pyzed/es/theming/styles/#hojas-de-estilo) al definir la clase. `&` representa al componente: `"&"`, `"&:hover"`, `"&.wide"`. - Todo componente acepta `style=`, `class_=`, `id=` y `animate=` (presets de [`widget.animate`](https://albakore.github.io/pyzed/es/animation/)), salvo que declare props con esos nombres. - El contexto comparte valores con todos los componentes que se dibujan dentro, sin pasar props nivel a nivel: ```python with pz.provide(user=self.user): page = Dashboard() # cualquier componente de dentro lo lee class Avatar(pz.Component): def render(self): return ui.avatar(pz.use_context("user").name) # o self.context("user") ``` Un componente también puede compartir valores con lo que dibuja devolviéndolos desde un método `provide()`. Los hijos que recibe como argumentos se dibujan antes que el componente, así que ven el contexto de alrededor, no el suyo. En [`examples/web_layout.py`](https://github.com/albakore/pyzed/blob/main/examples/web_layout.py) hay una página hecha así. #### Ciclo de vida | Hook | Cuándo | | --- | --- | | `setup()` | Una vez, antes del primer render. Los props ya están asignados. Inicializa aquí el estado. | | `render()` | En cada render del padre. | | `on_mount()` | Después del primer render. Puede devolver un `Command` o un mensaje. | | `on_unmount()` | Cuando el componente deja de renderizarse. | Los hooks se ejecutan después de dibujar la vista, nunca en medio. #### En el diseñador Cada subclase de `pz.Component` aparece en la biblioteca del [diseñador](https://albakore.github.io/pyzed/es/editor/): - **Props**: se convierten en propiedades. `bool` muestra un interruptor, `int` y `float` un campo numérico, `Literal[...]` un desplegable, `list` un elemento por línea, y un prop llamado `color` o `icon` tiene su propio selector. Las uniones, opcionales, diccionarios, sets y tuplas (`int | None`, `dict[str, int]`...) tienen un editor construido a partir del tipo, con una fila por elemento. Las anotaciones de `pyzed` eligen un editor más completo, como los export hints de Godot: | Anotación | Editor | | --- | --- | | `pz.Color` | Un selector de color (token del tema o `#rrggbb`). | | `pz.Icon` | Un selector de icono. | | `pz.Multiline` | Un área de texto. | | `Annotated[int, pz.Range(0, 10)]` | Un campo numérico limitado de 0 a 10 (`step` es opcional). | En ejecución son `str` e `int` normales: `tint: pz.Color = "accent"`. - **Eventos propios**: se convierten en eventos a los que puedes asignar código. Estos atributos de clase controlan cómo aparece: | Atributo | Por defecto | | --- | --- | | `label` | El nombre de la clase, separado en palabras. | | `icon` | `"component"` (cualquier icono de Lucide). | | `category` | `"Custom"`. | | `size` | `(160, 40)`, el tamaño al colocarlo en un formulario. | | `designer` | `True`; con `False` no aparece en la biblioteca. | | `type_name` | El nombre de la clase en snake case; el id que se guarda en los formularios. | El primer párrafo del docstring se muestra como descripción. Para crear un componente dibujándolo en vez de escribir `render()`, usa el [modo componente](https://albakore.github.io/pyzed/es/editor/component-designer/) del diseñador. ### Teclado y atajos > Atajos de teclado en apps de Pyzed: combinaciones con on_key y cómo mostrar los atajos en menús, botones y el resto de la interfaz. Source: https://albakore.github.io/pyzed/es/guides/keyboard/ Define `on_key(self, combo)` en tu app para recibir **todas** las teclas pulsadas, como un keymap global. `combo` es un texto como `ctrl-shift-p` (el mismo formato que usan los keymaps de Zed): | Tecla | `combo` | | --- | --- | | Ctrl+Shift+P | `"ctrl-shift-p"` | | Cmd+S (macOS) / Win+S | `"cmd-s"` | | Alt+Z | `"alt-z"` | | Esc | `"escape"` | | Flechas | `"up"`, `"down"`, `"left"`, `"right"` | | Otras | `"enter"`, `"tab"`, `"space"`, `"backspace"`, `"delete"`, `"home"`, `"end"`, `"pageup"`, `"pagedown"`, `"f1"`… | Los modificadores van siempre en este orden: `ctrl-`, `alt-`, `shift-`, `cmd-`. Las letras llegan en minúscula. ```python KEYMAP = { "ctrl-shift-p": "abrir_paleta", "ctrl-p": "abrir_archivos", "ctrl-b": "toggle_left", "ctrl-s": "guardar", } class App(pz.App): def on_key(self, combo): # admite Cmd en macOS y Ctrl en el resto con la misma tabla accion = KEYMAP.get(combo.replace("cmd-", "ctrl-")) if accion: return getattr(self, accion)() ``` `on_key` puede devolver lo mismo que un callback: `None`, un `Command` o un mensaje para `update`. :::note `on_key` recibe también las teclas que ya procesó un campo con foco (por ejemplo, las letras que escribes en un `input`). Responde solo a los combos que te interesen, y comprueba el estado si un atajo solo tiene sentido en cierto contexto (`if self.palette_open and combo == "escape"`). Las teclas que usa un desplegable, interruptor, casilla, deslizador, segmentado o grupo de radios con foco (las flechas, Espacio…) **no** llegan a `on_key`; ver [Controles con el foco del teclado](https://albakore.github.io/pyzed/es/guides/accessibility/#controles-con-el-foco-del-teclado). Tab y Shift+Tab además mueven el foco entre campos cuando ningún widget las usó; ver [Accesibilidad](https://albakore.github.io/pyzed/es/guides/accessibility/#moverse-entre-campos-con-tab). ::: #### Teclas mantenidas y teclas soltadas `on_key` se llama una vez por pulsación. Para teclas que actúan *mientras* se mantienen, define `on_key_up(self, key)`, que recibe cada tecla al soltarla por su nombre (`"left"`, `"space"`, `"a"`, sin modificadores), o pregunta en cualquier momento qué teclas están pulsadas: - `pz.pressed()` devuelve las teclas pulsadas ahora mismo como un `frozenset` de nombres. - `pz.is_pressed("left", "a")` es `True` mientras se mantenga cualquiera de ellas. ```python class Juego(pz.App): def on_key_up(self, key): if key == "space": self.cargando = False def tick(self, dt): if pz.is_pressed("left", "a"): self.x -= 200 * dt ``` Las pulsaciones y las teclas soltadas solo se convierten en mensajes cuando la app o alguna de sus ventanas define `on_key` u `on_key_up`, así que una app sin ellos no se redibuja con cada tecla. `pz.pressed()` funciona en ambos casos. La [sección de juegos](https://albakore.github.io/pyzed/es/games/#entrada) los muestra en un bucle de fotogramas, junto con ayudas de `pyzed.game` como `axis` y `vector`. #### Mostrar atajos - `ui.kbd("ctrl-shift-p")` dibuja las teclas. En macOS usa símbolos (⌃⇧P) y en el resto palabras. - `ui.button(..., keys="ctrl-s")`, `ui.icon_button(..., tooltip="Guardar", keys="ctrl-s")`, `ui.MenuItem(..., keys=...)`, `ui.PaletteItem(..., keys=...)` y `ui.tooltip(..., keys=...)` muestran el atajo junto a la acción, dibujado como teclas. Pasa `keys=` en vez de escribir las teclas en el texto: `f"Guardar ({keys})"` queda desfasado cuando el usuario cambia el atajo. - `ui.keys_text("ctrl-shift-p")` da `"Ctrl+Shift+P"` para una etiqueta o línea de estado en texto plano. - Fija el estilo con `pyzed.ui.base.KEY_STYLE = "symbols"` o `"words"`. ### Idiomas y diálogos > Traduce una app de escritorio de Pyzed a varios idiomas, cambia el idioma mientras corre y usa los diálogos listos para usar ya traducidos. Source: https://albakore.github.io/pyzed/es/guides/i18n/ #### Idiomas `pyzed.i18n` guarda un catálogo de textos por idioma. Las vistas buscan los textos con `pz.t`, así que cambiar el idioma desde un handler vuelve a dibujar toda la app en el nuevo. ```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()) # o "es", "pt_BR"... class Hola(pz.App): def view(self): return pz.column([ pz.text(pz.t("greeting", name="Kev")), pz.text(pz.t("files", count=3)), pz.button("English", on_press=lambda: i18n.set_language("en")), ]) ``` - Los marcadores como `{name}` se rellenan con los argumentos por nombre. - Un texto con formas `one`/`other` (y opcionalmente `zero`) es un plural; `count=` elige la forma. - Las tablas anidadas se vuelven claves con puntos: `{"menu": {"open": "Abrir"}}` es `pz.t("menu.open")`. - Si falta un texto se prueba de `"es_AR"` a `"es"`, luego el idioma de respaldo (`"en"`, ver `i18n.set_fallback`) y por último se devuelve la clave. Los catálogos pueden estar en archivos, uno por idioma, junto a tu app: ```text locales/ ├── en.json {"greeting": "Hello, {name}!"} └── es.toml greeting = "¡Hola, {name}!" ``` ```python i18n.load("locales") # ["en", "es"] ``` Los componentes de Pyzed (los diálogos de abajo, el menú del dock) también se traducen. Inglés y español vienen incluidos; sus claves empiezan por `pyzed.` (`pyzed.ok`, `pyzed.cancel`...), así que `i18n.add("fr", {"pyzed": {"ok": "OK", "cancel": "Annuler"}})` añade francés. #### Diálogos `pyzed.ui` trae diálogos listos. Guarda en tu estado cuál está abierto y envuelve la vista con `ui.dialog_layer`, que oscurece la ventana y centra el diálogo: (Image: Un diálogo de confirmación) ```python from pyzed import ui class Archivos(pz.App): preguntando = False def eliminar(self): ... self.preguntando = False def view(self): dialogo = None if self.preguntando: dialogo = ui.confirm("¿Eliminar 3 archivos?", self.eliminar, lambda: setattr(self, "preguntando", False), danger=True) return ui.dialog_layer(self.vista_principal(), dialogo, on_dismiss=lambda: setattr(self, "preguntando", False)) ``` | Diálogo | Qué hace | | --- | --- | | `ui.alert(mensaje, on_close, kind=)` | Un mensaje con un botón Aceptar; `kind` añade un icono de info, éxito, aviso o error. | | `ui.confirm(mensaje, on_confirm, on_cancel, danger=)` | Una pregunta de sí o no; `danger=True` hace el botón un Eliminar rojo. | | `ui.prompt(titulo, valor, on_input, on_submit, on_cancel)` | Pide una línea de texto (Enter acepta). | | `ui.choice(mensaje, [(texto, on_press), ...], on_cancel)` | Tus propios botones; el último es el principal. | | `ui.unsaved_changes(nombre, on_save, on_discard, on_cancel)` | «¿Guardar los cambios antes de cerrar?» | Para cualquier otra cosa, `ui.modal(contenido, title=, footer=[...])` es la misma superficie con tu propio contenido. Ver [`examples/dialogs.py`](https://github.com/albakore/pyzed/blob/main/examples/dialogs.py). ### Tests y capturas > Prueba apps de Pyzed sin ventana: renderiza tu GUI de Python en modo headless para tests unitarios, capturas de documentación e integración continua. Source: https://albakore.github.io/pyzed/es/guides/testing/ `app.screenshot(ruta)` dibuja el estado actual de la app **fuera de pantalla** y guarda un PNG. No necesita ventana, display ni GPU (usa el renderizador por software), así que funciona en CI. ```python app = MiApp() app.items = ["uno", "dos"] # prepara el estado que quieras mostrar app.screenshot("docs/lista.png", size=(480, 320), scale=2) ``` - `size`: tamaño lógico; por defecto `App.size`. - `scale`: densidad del PNG (2 = retina). - `wait`: segundos a esperar antes de capturar, por si `on_start` lanzó trabajo. Todas las imágenes de esta documentación se generan así: #### Probar la lógica Como el estado vive en tu objeto, `update` y los callbacks se prueban como Python normal: ```python def test_counter(): app = Counter() app.update("increment") app.update("increment") assert app.count == 2 ``` #### Probar que la vista se dibuja `view()` debe devolver un `Widget` sin lanzar excepciones. Una captura es una buena prueba de humo: ```python def test_view_renders(tmp_path): app = MiApp() app.screenshot(tmp_path / "ui.png", size=(800, 600)) assert (tmp_path / "ui.png").stat().st_size > 0 ``` Si `view()` lanza, la captura muestra el traceback en lugar de la interfaz, igual que la ventana. ### Cómo funciona por dentro > Cómo funciona Pyzed: el puente entre Python e Iced (Rust), el árbol de widgets inmutable, el estado del editor y por qué la interfaz es tan rápida. Source: https://albakore.github.io/pyzed/es/guides/how-it-works/ ``` Python Rust (pyzed._core, PyO3) Iced 0.14 App.view() ──► árbol de Widget ──► render.rs: Widget → iced::Element ──► layout + wgpu/tiny-skia App.update() ◄── mensajes ◄────── app.rs: Message, Command → Task ◄──── eventos (ratón, teclado) ``` #### Piezas ```mermaid flowchart TB subgraph PY["Python (tu código)"] A["App / Window"] --> W["view() → pz.* / ui.*"] U["update()"] L["loop de asyncio + hilos"] end subgraph RS["Rust (extensión nativa, PyO3)"] B["construye elementos de Iced"] --> I["Iced 0.14"] I --> G["wgpu / tiny-skia"] end W -->|"_core.Widget"| B I -->|"eventos → mensajes"| U L -->|"pz.post / resultados"| U U --> W ``` | Archivo | Responsabilidad | | --- | --- | | `src/widget.rs` | La clase `Widget` (inmutable) y las funciones que la construyen (`text`, `row`…). | | `src/render.rs` | Convierte el árbol de `Widget` en `iced::Element` en cada frame. | | `src/app.rs` | El runtime: `Message`, `Command`, despacho a Python, teclado, `post`, `run` y `screenshot`. | | `src/style.rs` | Conversión de colores, tamaños, relleno y fuentes desde valores de Python. | | `src/highlighter.rs` | Resaltado de sintaxis con syntect (gramáticas de two-face) y los colores del tema. | | `src/fonts.rs` | Fuentes incluidas (IBM Plex, Lucide) y `load_font`. | | `python/pyzed/` | La clase `App`, temas, iconos, CLI y los componentes de `pyzed.ui`. | #### Rendimiento - **Los widgets son datos de Rust.** Construir el árbol desde Python crea objetos pequeños e inmutables; no hay objetos de Iced vivos en Python. - **El trabajo pesado está en Rust.** Layout, medición de texto, resaltado, scroll y dibujado ocurren en Rust; Python solo se ejecuta cuando hay un mensaje. - **El GIL se libera** mientras Iced espera eventos, así que los hilos de Python siguen corriendo. - **Estado nativo para el editor.** El buffer de cada editor (texto, cursor, historial) vive en Rust y se identifica por `id`, así que escribir no reconstruye el texto en Python. #### Mensajes Cada widget interactivo guarda una referencia al valor de Python que le diste. Cuando Iced produce un evento, `app.rs` lo convierte en una llamada: 1. Si es un callable, lo llama (con el valor del control si lo hay). 2. Si devuelve un `Command`, lo traduce a una `Task` de Iced. 3. Si devuelve otro valor (o el mensaje no era callable), llama a `app.update(valor)`. 4. Vuelve a pedir `view()` y Iced dibuja la diferencia. #### Contribuir ```bash maturin develop --release pytest tests cargo fmt --check && cargo clippy -- -D warnings ``` La documentación vive en [`docs/`](https://github.com/albakore/pyzed/tree/main/docs). Ver [cómo editar la documentación](https://albakore.github.io/pyzed/es/contributing/docs/). ### Recarga en caliente > Edita una app de Pyzed mientras corre: métodos nuevos, atributos nuevos y nuevos valores iniciales llegan a la ventana abierta al guardar, sin perder los datos que ya tiene la app. Source: https://albakore.github.io/pyzed/es/guides/hot-reload/ La recarga en caliente (hot reload) actualiza la ventana abierta cada vez que guardas un archivo del proyecto. La app no se reinicia: los mismos objetos conservan sus datos y pasan a ejecutar el código nuevo. Desde código, usa `pz.run(MiApp, reload=True)` o define la variable de entorno `PYZED_RELOAD=1`. Los formularios también se actualizan al guardar (ver [Ejecutar y la consola](https://albakore.github.io/pyzed/es/editor/console-and-running/#actualización-en-vivo-al-guardar)); esta página trata de las apps escritas en Python. #### Qué cambia al guardar | Cambias | Qué pasa en la ventana abierta | | --- | --- | | `view()`, un método, un módulo auxiliar, un estilo | El código nuevo se usa desde el siguiente redibujado. | | Un método que otra cosa guarda (`self.al_terminar = self.guardar`, un temporizador, un hilo, `functools.partial`) | Esa referencia también ejecuta el código nuevo. | | Un atributo nuevo en `__init__` (`self.etiquetas = []`) | El objeto vivo lo recibe al momento; nada más cambia. | | El valor inicial de un atributo (`self.paso = 1` → `self.paso = 5`) | Toma el valor nuevo si la app no lo cambió mientras corría; si lo cambió, se queda el de la app. | | Un `pz.state(...)` nuevo o un atributo nuevo en el `setup()` de un componente | Todas las instancias del componente lo reciben. | | Un atributo de clase (`size = (400, 300)`, `title`) | Sale de la clase nueva. | | Código que no se puede importar, o un `view()` que lanza un error | El error se ve en la ventana hasta que guardes la corrección. | Cada recarga escribe una línea por clase que cambió, para que veas qué se aplicó: ```text pyzed: reloaded Notas: methods changed: agregar, view; new state: etiquetas pyzed: reloaded Notas: updated state: paso ``` #### Un ejemplo Abre esta app con `pyzed dev notas.py`, escribe algunas notas y luego haz los cambios de abajo de a uno, guardando cada vez: ```python import pyzed as pz from pyzed import ui class Notas(pz.App): title = "Notas" size = (420, 320) def __init__(self): self.notas = [] self.borrador = "" def escribir(self, texto): self.borrador = texto def agregar(self): if self.borrador.strip(): self.notas.append(self.borrador.strip()) self.borrador = "" def view(self): return pz.column([ pz.row([ui.input(self.borrador, self.escribir, on_submit=self.agregar, placeholder="Nota nueva"), ui.button("Agregar", self.agregar)], spacing=8), *[pz.text(nota) for nota in self.notas], ], spacing=8, padding=16) if __name__ == "__main__": pz.run(Notas) ``` 1. **Un atributo nuevo.** Agrega `self.favoritas = set()` en `__init__` y un botón de estrella por nota en `view()`. Las notas que escribiste siguen ahí y `self.favoritas` existe al instante. 2. **Un método nuevo.** Agrega `def limpiar(self): self.notas.clear()` y un botón que lo llame. Funciona en el siguiente clic. 3. **Un valor inicial nuevo.** Agrega `self.limite = 10` en `__init__`, guarda y cámbialo a `5`: la app ve `5`. Si la app hubiera cambiado `limite` por su cuenta, se conservaría su valor. #### Cómo se actualiza el estado Cuando una clase cambió, Pyzed vuelve a ejecutar su `__init__` sobre una **copia de prueba**, con los argumentos con los que se creó el objeto (en un componente, `setup()` con sus props actuales), y la compara con el objeto vivo: - los atributos que el objeto vivo no tiene se copian; - los valores simples (números, texto, booleanos y listas, tuplas, conjuntos y diccionarios pequeños de ellos) cuyo valor inicial cambió en el código se actualizan, pero solo si el valor vivo sigue siendo el inicial anterior; - todo lo demás se deja como está. Después la copia se descarta, y lo que su `__init__` imprimió no se muestra. Las clases cuyo código no cambió no se tocan, así que guardar otro archivo nunca ejecuta tu `__init__`. Los objetos de tus propias clases que Pyzed no creó (un `Tarea` dentro de una lista, por ejemplo) reciben los atributos nuevos que solo usan `self`, globales o funciones integradas: `self.hecha = False` funciona, `self.etiqueta = texto.upper()` necesita el argumento `texto` y se omite. #### Cuando `__init__` tiene efectos secundarios Si `__init__` abre un archivo, un dispositivo, un socket o arranca un hilo, ejecutarlo otra vez sobre una copia suele ser inofensivo, pero puedes desactivarlo en esa clase: ```python class Reproductor(pz.App): reload_init = False # la recarga deja el estado como está: sin copia de prueba def __init__(self): self.salida = abrir_dispositivo_de_audio() ``` Mejor aún, mueve ese trabajo a `on_start()`, que se ejecuta una vez al abrir la ventana. Si el `__init__` de la copia falla, la recarga se aplica igual y la consola explica por qué no se actualizó el estado. #### Migrar el estado a mano Para cambios que Pyzed no puede adivinar (renombrar un atributo, cambiar su tipo), define `on_reload()` en la app o en una ventana. Se ejecuta después de cada recarga y puede devolver un `Command`: ```python class Notas(pz.App): def on_reload(self): if isinstance(self.notas, list) and self.notas and isinstance(self.notas[0], str): self.notas = [{"texto": n, "hecha": False} for n in self.notas] ``` #### Ver qué hizo una recarga `pz.hot_reload.current()` devuelve el `Reloader` en marcha. Su lista `changes` tiene un `Change` por clase que cambió en la última recarga (métodos `changed`, `added` y `removed`, `new_state` y `updated_state`), `count` dice cuántas recargas salieron bien y `error` guarda el traceback de una que falló. Con `verbose = False` deja de escribir en la consola. #### Límites - Una tarea `async` o un generador en marcha sigue con el código con el que empezó hasta que termina. - Quitar un atributo de `__init__` no lo borra del objeto vivo. - El código que ya se ejecutó (el bloque `if __name__ == "__main__":`, `on_start()`) no se vuelve a ejecutar. - Una clase cuyos `__slots__` cambian no puede pasar a la versión nueva; en ese caso reinicia la app. ### Diseñador visual > Crea ventanas de escritorio en Python arrastrando componentes, conecta sus eventos a Python y ejecútalas: un diseñador como Visual Basic o Qt Designer. Source: https://albakore.github.io/pyzed/es/guides/designer/ Pyzed Design es un editor de formularios al estilo de Visual Basic, el diseñador de WinForms de Visual Studio y Qt Designer, hecho con el propio Pyzed. Dibujas la ventana con el ratón, ajustas propiedades en una grilla, haces doble clic en un control para escribir el código de su evento y pulsas **F5** para ejecutarla con toda su lógica. (Image: El diseñador con el ejemplo de inicio de sesión) Todo sobre el editor está ahora en su propio manual, **[Pyzed Design](https://albakore.github.io/pyzed/es/editor/)**: | Tema | Página | | --- | --- | | La ventana y sus zonas | [Introducción](https://albakore.github.io/pyzed/es/editor/) | | Colocar, mover, alinear, snap, la estructura | [Herramientas y lienzo](https://albakore.github.io/pyzed/es/editor/toolbox-and-canvas/) | | La grilla de propiedades, listas y tablas | [Propiedades](https://albakore.github.io/pyzed/es/editor/properties/) | | Paneles, filas y columnas flex, grillas, anclajes | [Disposición y contenedores](https://albakore.github.io/pyzed/es/editor/layouts/) | | La sección Estilo, el selector de color, los temas | [Estilos y temas](https://albakore.github.io/pyzed/es/editor/styles-and-themes/) | | Eventos y cómo escribir manejadores | [Eventos y manejadores](https://albakore.github.io/pyzed/es/editor/events/) | | Autocompletado, diagnósticos, servidores de lenguaje | [Editor de código](https://albakore.github.io/pyzed/es/editor/code-editor/) | | F5, la consola de depuración, recarga en caliente | [Ejecutar y la consola](https://albakore.github.io/pyzed/es/editor/console-and-running/) | | Proyectos de uv, dependencias, ruff, ty, git | [Proyectos](https://albakore.github.io/pyzed/es/editor/projects/) | | Componentes reutilizables | [Diseñador de componentes](https://albakore.github.io/pyzed/es/editor/component-designer/) | | Formularios nuevos, plantillas, archivos `.pzform`, scripting | [Archivos, plantillas y scripting](https://albakore.github.io/pyzed/es/editor/files-and-scripting/) | | Menús, la paleta de comandos, docks | [Menús y comandos](https://albakore.github.io/pyzed/es/editor/menus-and-commands/) | | Todos los atajos | [Atajos de teclado](https://albakore.github.io/pyzed/es/editor/keyboard-shortcuts/) | | Preferencias | [Ajustes](https://albakore.github.io/pyzed/es/editor/settings/) | #### Consola de depuración Ver [Ejecutar y la consola](https://albakore.github.io/pyzed/es/editor/console-and-running/#la-consola). #### Paneles Ver [Paneles y docks](https://albakore.github.io/pyzed/es/editor/menus-and-commands/#paneles-y-docks). #### Editor de código Ver [Editor de código](https://albakore.github.io/pyzed/es/editor/code-editor/): autocompletado, diagnósticos, code lens, plegado y los servidores de lenguaje ty y ruff. #### Telemetría Ver [Ajustes → Telemetría](https://albakore.github.io/pyzed/es/editor/settings/#telemetría). #### Componentes **Comunes**: Label, Heading, Button, Text box, Text area, Check box, Switch, Radio group, Drop-down, Slider, Number box, List box, Link y Search box. **Contenedores**: Panel, Group box, Card. **Disposición**: Flex row, Flex column, Grid, Spacer. **Navegación**: Tab bar, Segmented. **Presentación**: Progress bar, Badge, Chip, Callout, Avatar, Key binding, Stat, Chart, Table, Image, Icon, Markdown, Spinner, Divider y Timer. (Image: Todos los componentes incluidos) Para añadir los tuyos, ver [Archivos, plantillas y scripting](https://albakore.github.io/pyzed/es/editor/files-and-scripting/#añadir-tus-propios-componentes) y el [diseñador de componentes](https://albakore.github.io/pyzed/es/editor/component-designer/). #### Cómo está hecho Ver [Archivos, plantillas y scripting → Cómo está hecho](https://albakore.github.io/pyzed/es/editor/files-and-scripting/#cómo-está-hecho). ### Diseñador de componentes > Dibuja componentes reutilizables de Pyzed de forma visual, con propiedades, estado, métodos y eventos propios, y anídalos como escenas de Godot. Source: https://albakore.github.io/pyzed/es/guides/component-designer/ Pyzed Design tiene un segundo modo para crear **componentes** en vez de ventanas: dibujas el componente como un formulario, declaras sus propiedades y eventos en un panel, escribes la lógica de sus métodos y lo colocas en formularios o dentro de otros componentes, como las escenas de Godot. Un componente diseñado **es** un [`pz.Component`](https://albakore.github.io/pyzed/es/guides/components/). (Image: Un componente ProfileCard que contiene un componente Rating) La guía completa está en el manual de Pyzed Design: **[Diseñador de componentes](https://albakore.github.io/pyzed/es/editor/component-designer/)**. #### Tipos de propiedad Ver [Tipos de propiedad](https://albakore.github.io/pyzed/es/editor/component-designer/#tipos-de-propiedad). #### Herencia Ver [Herencia](https://albakore.github.io/pyzed/es/editor/component-designer/#herencia). ### Proyectos con uv > Crea y gestiona apps de escritorio de Pyzed como proyectos estándar de uv, con la UI en su propia carpeta y ruff, ty y git listos desde el principio. Source: https://albakore.github.io/pyzed/es/guides/projects/ Un proyecto de Pyzed es un proyecto de [uv](https://docs.astral.sh/uv/) normal. La interfaz vive en su propia carpeta (`ui/`) y el resto del código queda donde lo pone uv, así el frontend y el backend están separados pero pueden importarse entre sí. Las dependencias las gestiona uv, [ruff](https://docs.astral.sh/ruff/) revisa y da formato, [ty](https://docs.astral.sh/ty/) comprueba los tipos y el proyecto nace como repositorio git. #### Crear un proyecto ``` tienda/ ├── pyproject.toml # proyecto de uv + [tool.pyzed] ├── uv.lock ├── .venv/ # lo crea uv sync ├── .python-version ├── .gitignore ├── src/tienda/ # backend: Python normal, sin código de UI │ └── __init__.py └── ui/ # frontend: forms y sus handlers ├── main.pzform └── main.py ``` El form inicial llama a una función del backend, para que veas funcionar el import entre carpetas: ```python # ui/main.py from tienda import greeting def greet_click(form, event): form.message.text = greeting(form.name_input.text or "world") ``` | Opción | Efecto | | --- | --- | | `--app` | Sin `src/`: los módulos del backend van junto a `pyproject.toml` (`uv init --app`). | | `--python 3.12` | Versión de Python del proyecto (uv la descarga si hace falta). | | `--no-git` | No crea un repositorio git. | | `--no-sync` | Solo escribe `pyproject.toml`; no resuelve ni instala nada (sin conexión). | | `--script` | La app de un solo archivo de antes (`main.py` + `requirements.txt`), sin uv. | #### Añadir Pyzed a un proyecto de uv existente Si empiezas tus proyectos con `uv init`, sigue haciéndolo y añade la UI después: ```bash uv init --package tienda && cd tienda uvx --from pyzed-ui pyzed init # o `pyzed init` si tienes pyzed instalado ``` `pyzed init` no toca tu código: añade la tabla `[tool.pyzed]`, crea `ui/` con un form inicial y añade `pyzed-ui` (y `ruff`/`ty` como dependencias de desarrollo) con `uv add`. Si lo ejecutas en una carpeta sin `pyproject.toml`, primero ejecuta `uv init`. ```toml [tool.pyzed] ui = "ui" # la carpeta de la UI main = "ui/main.pzform" # lo abren `pyzed design`/`pyzed run` y lo arranca `pyzed build` ``` #### Importar código de fuera de la carpeta de UI Los forms y sus handlers pueden importar cualquier cosa del proyecto. `pyzed run`, `pyzed design` y `pyzed build` *activan* antes el proyecto, lo que añade estas carpetas a `sys.path`: 1. `ui/`: módulos auxiliares junto a los handlers (`from helpers import money`). 2. `src/`: el paquete del backend, incluso antes de que `uv sync` lo instale. 3. La raíz del proyecto: módulos junto a `pyproject.toml` (estructura `--app`). 4. El site-packages de `.venv`, cuando el diseñador corre en otro intérprete de la misma versión de Python, para que también se resuelvan todas las dependencias instaladas con `uv add`. Cualquier otra cosa (un repositorio hermano, una librería compartida) es una dependencia más: añádela con su ruta y uv la instala en modo editable. ```bash pyzed add --editable ../facturacion # uv add --editable ../facturacion ``` Desde tus propios scripts, haz lo mismo con `pyzed.project`: ```python from pyzed import project project.find().activate() # el proyecto que contiene la carpeta actual ``` #### Dependencias Cada comando ejecuta uv en la carpeta del proyecto y muestra su salida. | Comando | Ejecuta | | --- | --- | | `pyzed add httpx` | `uv add httpx` | | `pyzed add "httpx==0.27.2"` | Cambia la restricción de un paquete que ya está. | | `pyzed add --dev pytest` | `uv add --dev pytest` (`--group docs` para otros grupos). | | `pyzed remove httpx` | `uv remove httpx` (encuentra el grupo solo). | | `pyzed upgrade` | `uv lock --upgrade` + `uv sync` (todos los paquetes). | | `pyzed upgrade httpx` | `uv lock --upgrade-package httpx` + `uv sync`. | | `pyzed sync` | `uv sync`: deja `.venv` igual que `uv.lock`. | | `pyzed deps [--outdated]` | Lista las dependencias directas con su versión fijada (y la más nueva). | | `pyzed check [--fix]` | `ruff check` y `ty check` con el `.venv` del proyecto. | Los comandos de `uv` funcionan igual de bien; Pyzed solo lee `pyproject.toml` y `uv.lock`. #### La pestaña Project del diseñador El diseñador tiene una pestaña **Project** junto a la caja de herramientas. Abre el proyecto al que pertenece el form o, si no hay ninguno, ofrece **New project** y **Open…** (un explorador de carpetas con tus proyectos recientes). (Image: La pestaña Project: los archivos y carpetas del proyecto) - **Project** muestra los archivos del proyecto en árbol, como en un editor de código, coloreados según git. Busca archivos y carpetas desde su barra o con Ctrl+P; con clic derecho tienes nuevo archivo, nueva carpeta, renombrar, borrar, copiar la ruta y mostrar en el gestor de archivos. Los forms y componentes se abren en el diseñador; los demás archivos, con la app del sistema. - **Dependencies**, una pestaña propia al lado: añade un paquete (marca *dev* para el grupo de desarrollo), cambia su versión (lápiz), actualízalo o quítalo. Su barra sincroniza el entorno, busca actualizaciones (se ven en verde) y actualiza todo. Debajo, **Tools** muestra las versiones de uv, git, ruff y ty; **Lint**, **Format** y **Types** ejecutan ruff y ty y muestran su salida. - **Create repository** (en la cabecera) ejecuta `git init` y escribe un `.gitignore`. Cuando ya hay repositorio, la cabecera muestra su rama. Los comandos corren en segundo plano, así el diseñador sigue respondiendo mientras uv descarga paquetes. Cuando el diseñador no corre dentro del entorno del proyecto (por ejemplo Pyzed Design compilado, o `pyzed design` instalado con `uv tool`), **F5** guarda el form y lo arranca con `uv run pyzed run --reload`, así tiene todas las dependencias del proyecto y se actualiza al guardar. #### Pyzed Design como app independiente Cada [release](https://github.com/albakore/pyzed/releases) incluye **Pyzed Design** para Linux, Windows y macOS (`pyzed-design-*.zip`): el diseñador compilado con su propio Python y el binario de `uv`. Descomprímelo y ejecútalo; no hace falta instalar nada más. Los proyectos que crees desde él son proyectos de uv normales con su propio `.venv`. Para compilarlo tú mismo en la plataforma actual: -.zip `} pip={` {venv} {activate} pip install pyzed-ui pyinstaller uv python scripts/build_designer.py # dist/Pyzed Design/ y dist/pyzed-design--.zip `} python={` {py} -m pip install pyzed-ui pyinstaller uv {py} scripts/build_designer.py # dist/Pyzed Design/ y dist/pyzed-design--.zip `} /> ##### Actualizaciones Pyzed Design consulta GitHub por si hay una versión nueva al abrirse (una vez al día) y cada pocas horas mientras está abierto (se desactiva en **Ayuda → Buscar actualizaciones automáticamente**, o se consulta ya con **Ayuda → Buscar actualizaciones**). La descarga de tu plataforma empieza sola, se verifica con el SHA-256 de la release y se descomprime en segundo plano; luego aparece arriba a la derecha de la barra de título un botón **Reiniciar para actualizar** con un parpadeo suave. Cierra la app, cambia su carpeta (el `.app` en macOS) por la nueva y la vuelve a abrir. Tras actualizar, la pestaña Novedades se abre sola con las notas de la versión nueva, y **Ayuda → Novedades** las muestra cuando quieras. **Omitir esta versión** oculta una actualización hasta que salga otra más nueva. **Ayuda → Recibir versiones preliminares** cambia al canal preliminar, que también recibe las pre-releases (`X.Y.Z-rc.N`). Los instaladores ([Descargar](https://albakore.github.io/pyzed/es/download/#cómo-se-instala)) dejan la app en una carpeta tuya, así que puede reemplazarse a sí misma; si no, descarga otra vez el instalador o el zip nuevo. Cuando el diseñador se ejecuta desde Python (`pip` o `uv`), la pestaña muestra el comando que actualiza el paquete. Tus propias apps compiladas pueden usar el mismo actualizador, `pyzed.updater`: ```python from pyzed import updater release = updater.latest("yo/mi-app") # la GitHub Release más nueva if updater.is_newer(release.version, "1.0.0"): asset = release.asset_for(updater.asset_name("mi-app")) # mi-app--.zip app = updater.extract(updater.download(asset)) updater.replace_on_exit(app) # y cierra; arranca la versión nueva ``` #### Desde Python ```python from pyzed import project tienda = project.init("tienda", git=True) # devuelve un Project tienda.add("httpx") # cada comando devuelve un Result(code, output) tienda.upgrade("httpx") print([(d.name, d.locked) for d in tienda.dependencies()]) print(tienda.check().output, tienda.typecheck().output) tienda.git_init() ``` Para empaquetar la app en cada plataforma, consulta [Compilar y distribuir](https://albakore.github.io/pyzed/es/packaging/build/). ### Servidor MCP para agentes de IA > Conecta agentes de IA (Cursor, VS Code, Zed y otros clientes MCP) al servidor MCP de Pyzed para que conozcan su API y generen y comprueben código. Source: https://albakore.github.io/pyzed/es/guides/mcp/ Pyzed incluye un servidor [MCP](https://modelcontextprotocol.io). Conéctalo a tu agente de IA (Claude Code, Claude Desktop, Cursor, VS Code, Zed...) y el agente deja de adivinar: consulta la firma exacta de cada widget y componente, lee esta documentación, sigue las buenas prácticas de Pyzed, genera apps, componentes y formularios del diseñador, comprueba el código que escribe e incluso mira una captura del resultado. #### Instalación El servidor es un extra opcional del paquete (necesita Python 3.10 o superior): Casi nunca lo ejecutas tú: lo arranca el agente. Con [uv](https://docs.astral.sh/uv/) el agente puede ejecutarlo sin instalar nada antes, con `uvx --from "pyzed-ui[mcp]" pyzed mcp`. `pyzed mcp --transport streamable-http` lo sirve por HTTP en `127.0.0.1`. Ahí cualquier programa de tu equipo puede usarlo sin iniciar sesión, así que `screenshot` y las herramientas de proyecto, que leen y escriben archivos y ejecutan código, se omiten salvo que añadas `--allow-code`. Las herramientas de proyecto trabajan en la carpeta donde arrancó el servidor; `pyzed mcp --root RUTA` elige otra. Nunca salen de ella ni tocan archivos que puedan guardar secretos (`.env`, claves, `.netrc`...). #### Conecta tu agente ##### Claude Code ```bash claude mcp add pyzed -- uvx --from "pyzed-ui[mcp]" pyzed mcp ``` Dentro de un proyecto creado con `pyzed new`, usa el entorno del propio proyecto para que el servidor vea la misma versión de Pyzed y tus componentes: `claude mcp add pyzed -- uv run pyzed mcp`. ##### Claude Desktop, Cursor y otros clientes La mayoría de clientes leen un JSON con la misma forma (`claude_desktop_config.json`, `.cursor/mcp.json`...): ```json { "mcpServers": { "pyzed": { "command": "uvx", "args": ["--from", "pyzed-ui[mcp]", "pyzed", "mcp"] } } } ``` ##### VS Code En `.vscode/mcp.json`: ```json { "servers": { "pyzed": { "type": "stdio", "command": "uvx", "args": ["--from", "pyzed-ui[mcp]", "pyzed", "mcp"] } } } ``` Si Pyzed ya está instalado en un entorno, `"command": "pyzed", "args": ["mcp"]` funciona en todos. #### Qué recibe el agente ##### Herramientas | Herramienta | Qué hace | | --- | --- | | `plan_app` | Por dónde empezar una app o pantalla: los bloques y ejemplos más parecidos, los componentes y temas de guía que necesita, la documentación que leer y los pasos. Los agentes la llaman primero. | | `get_guide` | Cómo funciona Pyzed (`"overview"`), sus reglas (`"best-practices"`), recetas ejecutables (`"cookbook"`: estructura de una app, trabajo en segundo plano, formularios, listas virtuales, animación, shaders, juegos, marcos de ventana propios, layouts con anclas) o cualquier tema de `list_guides` (`"games"`, `"shaders"`, `"performance"`, `"integrations"`...), en inglés o en español. | | `list_guides` | Todos los temas que acepta `get_guide`. | | `list_components` | Cada widget (`pz.*`), componente listo (`pz.ui.*`), tipo de la caja de herramientas del diseñador y clase `pz.Component`, con un resumen de una línea. Filtra por `kind`, `query`, `category` o `role` (el rol de color de los tipos del diseñador: `layout`, `input`, `action`, `navigation`, `display`, `media`, `system`, `custom`). | | `describe_component` | La firma, la documentación y los parámetros de un widget o componente (`"pz.column"`, `"ui.button"`), o las propiedades y eventos de un tipo del diseñador (`"designer:button"`). | | `search_reference`, `read_reference` | Buscan y leen la [referencia](https://albakore.github.io/pyzed/es/reference/) de cada módulo, clase y función (`pyzed.game`, `pyzed.updater`, `ui.virtual_list`...). | | `editor_keymap` | Los atajos por defecto del editor de código y cómo cambiarlos. | | `search_icons` | Nombres de iconos Lucide para `icon=` y `pz.icon`. | | `list_themes` | Los temas incluidos y los tokens de color que aceptan los estilos (`"accent"`, `"panel"`...). | | `list_docs`, `read_docs` | Las páginas de esta documentación, en inglés o en español. | | `list_examples`, `read_example` | El código de los ejemplos, por `kind`: apps, bloques (pantallas completas para copiar), juegos, showcase (shaders, efectos), integraciones con backend (FastAPI, SQLAlchemy, SQLModel, Django), tutorial y diseñador. | | `new_app` | Un script `pz.App` ejecutable: un punto de partida simple o un workspace estilo editor. | | `new_component` | El esqueleto de un `pz.Component` con sus props, estado y eventos. | | `new_form` | Un formulario del diseñador visual: el `.pzform` y su módulo de handlers. | | `validate_code` | Revisa el código sin ejecutarlo: nombres que no existen en `pz` o `pz.ui`, argumentos que una función no acepta, apps sin `view()`, componentes sin `render()`, efectos secundarios en `view()`, lambdas en bucles que no capturan la variable y las comprobaciones de pyflakes de ruff si está instalado. | | `validate_form` | Revisa un `.pzform` (tipos, propiedades, eventos, nombres, contenedores) y que su módulo de handlers defina cada handler. | | `screenshot` | Dibuja el código fuera de pantalla (sin ventana ni GPU) y devuelve un PNG, para que el agente vea lo que construyó. | | `animation_properties`, `new_animation`, `check_animation` | [Animaciones en línea de tiempo](https://albakore.github.io/pyzed/es/animation/timeline/): qué puede animar una pista, una biblioteca `.pzanim` nueva a partir de clips y pistas, y su revisión. | | `list_shader_nodes`, `new_shader`, `check_shader` | Los nodos de los [grafos de shaders](https://albakore.github.io/pyzed/es/shading/shader-graphs/), un `.pzshad` nuevo desde una plantilla y su compilación a WGSL con los errores por nodo. | ##### Herramientas de proyecto Con ellas el agente construye una app completa en una carpeta, desde el proyecto hasta una revisión limpia: | Herramienta | Qué hace | | --- | --- | | `project_info` | El proyecto: nombre, versión, entrada (su `script` o, si no, el formulario principal), formularios, componentes, shaders, paquete, dependencias, archivos y entorno (el `.venv` donde corre, el sistema, WSL). | | `list_files`, `read_file` | Listan y leen los archivos de la carpeta. | | `write_files` | Crea o reemplaza varios archivos a la vez y revisa cada archivo Python, formulario y grafo de shader que escribió. | | `create_project` | Un proyecto uv nuevo, como `pyzed new` (pyzed-ui, ruff, ty, `ui/main.pzform`, `src/`, git). | | `add_dependency` | `uv add` en el proyecto. | | `check_project` | Las comprobaciones de Pyzed en cada archivo, más ruff y ty. | | `run_tests` | `uv run pytest` en el proyecto. | | `screenshot_project` | Una captura de un script, formulario, componente o grafo de shader, en el entorno del propio proyecto; por defecto la entrada. | | `project_settings`, `set_project_settings` | Los ajustes propios del proyecto: su [renderizador](https://albakore.github.io/pyzed/es/editor/settings/#renderizado) (GPU o CPU, API gráfica, GPU, vsync, en `[tool.pyzed.rendering]`) y las preferencias del editor y del diseñador que comparte en `.pyzed/settings.json` (tabulación, cuadrícula...). | | `start_live_preview`, `live_preview_status`, `stop_live_preview` | Abren la app en una ventana real con hot reload mientras el agente la construye: cada archivo que escribe actualiza la ventana al guardarse, así ves cómo toma forma. El estado devuelve lo que la app imprimió, trazas de error incluidas. | ##### Recursos y prompts Los clientes que admiten recursos pueden adjuntar `pyzed://guide`, `pyzed://best-practices`, `pyzed://cookbook`, `pyzed://reference/{name}`, `pyzed://components`, `pyzed://themes`, `pyzed://docs`, `pyzed://component/{name}` y `pyzed://docs/{slug}` (con `--` en lugar de `/`, como `guides--layout`). Los prompts `create_app`, `create_component` y `create_form` empiezan una tarea con los pasos correctos: leer las guías, buscar componentes existentes, generar, validar y previsualizar. `build_project` construye una app completa en la carpeta: lee las guías y ejemplos, crea el proyecto, lo escribe en pasos pequeños y luego lo revisa, lo prueba y lo mira hasta que todo esté limpio. #### Ejemplo de sesión > Haz un componente de Pyzed que muestre una valoración con estrellas y deje cambiarla. Una ejecución típica del agente con el servidor conectado: 1. `get_guide("best-practices")` y luego `list_components(query="rating")` para comprobar que no existe nada parecido. 2. `new_component("star rating", props=[{"name": "value", "type": "int", "default": 0}], events={"change": "Se eligió una estrella."}, icon="star")`. 3. Completa `render()` con `pz.row`, `pz.text(...).on(click=...)` y `self.emit("change", i)`, consultando `describe_component("pz.text")` para los parámetros. 4. `validate_code(code)` y `screenshot(code + "def preview():\n return StarRating(value=3)")` para verlo. #### Cómo funciona El servidor vive en el paquete (`pyzed.mcp`), así que lo que sabe coincide siempre con la versión instalada: las firmas y docstrings salen del propio código, las descripciones de esta referencia y la caja de herramientas del diseñador de su registro, incluidas tus propias clases `pz.Component`. Las páginas de la documentación y los ejemplos se leen del repositorio si trabajas desde el código fuente y, si no, se descargan de GitHub en el tag de la versión instalada. `screenshot` ejecuta el código en un proceso aparte con un límite de 60 segundos y sustituye `pz.run`, así que no se abre ninguna ventana. Usa el mismo renderizador fuera de pantalla que [tests y capturas](https://albakore.github.io/pyzed/es/guides/testing/). `pyzed.mcp.knowledge`, `pyzed.mcp.projects`, `pyzed.mcp.scaffold` y `pyzed.mcp.validate` son Python normal y funcionan sin el SDK de MCP, por ejemplo desde tus propios scripts o tests. #### Más sobre agentes de IA La sección [Agentes de IA](https://albakore.github.io/pyzed/es/ai/) reúne el resto: la [guía de prompts](https://albakore.github.io/pyzed/es/ai/prompting/) con una sesión MCP real, [llms.txt y reglas para editores](https://albakore.github.io/pyzed/es/ai/llms-txt/) con plantillas de `AGENTS.md`, Cursor y Copilot, y [Crear apps con IA](https://albakore.github.io/pyzed/es/ai/ai-apps/). ### Agentes de IA en tu app > pyzed.agents: añade IA a apps de escritorio en Python con Anthropic, OpenAI, Gemini, OpenRouter u Ollama, da herramientas al modelo y transmite respuestas. Source: https://albakore.github.io/pyzed/es/guides/agents/ `pyzed.agents` es el motor del [panel del agente](https://albakore.github.io/pyzed/es/editor/ai-agents/) del diseñador, y funciona en cualquier app de Pyzed: una sola API para varios proveedores de modelos, herramientas escritas como funciones de Python normales, eventos en streaming para la UI, conversaciones guardadas y claves en el llavero del sistema operativo. #### Un primer agente ```python from pyzed import agents agents.set_key("anthropic", "sk-ant-…") # una vez: se guarda en el llavero del sistema @agents.tool def stock(product: str) -> int: """Cuántas unidades de un producto hay en stock. product: el nombre del producto. """ return {"chair": 4, "desk": 0}.get(product, 0) agent = agents.Agent("anthropic:claude-opus-5-5", tools=[stock], system="Ayudas al personal de la tienda.") print(agent.run("¿Puedo vender dos escritorios hoy?")) ``` `run` llama al modelo, ejecuta las herramientas que pide, le devuelve los resultados y repite hasta que el modelo responde. Devuelve la última respuesta y nunca lanza excepciones: los fallos (una clave mala, sin red) llegan como un evento `"error"` y como el texto devuelto. `agent.messages` guarda la conversación, así que el siguiente `run` la continúa. #### Modelos y proveedores Un modelo se nombra `":"`: | Proveedor | Ejemplo | Credenciales | | --- | --- | --- | | `anthropic` | `anthropic:claude-opus-5-5` | `set_key`, `ANTHROPIC_API_KEY` (también sirve un perfil de `ant auth login`) o [federación de identidades](#claude-sin-clave-de-api) | | `openai` | `openai:` | `set_key` u `OPENAI_API_KEY` | | `google` | `google:` | `set_key` o `GEMINI_API_KEY` | | `openrouter` | `openrouter:anthropic/claude-opus-5-5` | `set_key`, `OPENROUTER_API_KEY` o `agents.login.openrouter_login()` | | `xai`, `mistral`, `deepseek`, `groq` | `groq:` | `set_key` o `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()` (flujo de dispositivo), un token detallado con el permiso Models o `GITHUB_TOKEN` | | `vertex` | `vertex:google/gemini-2.5-pro` | Gemini en Vertex AI: `agents.login.google_login()` o las credenciales de gcloud del equipo; `settings.provider("vertex").project` | | `ollama`, `lmstudio` | `ollama:qwen3:8b` | ninguna (local) | | `custom` | `custom:` | la URL del servidor en los ajustes y una clave si hace falta | `agents.list_models("openai")` pide a un proveedor sus modelos. Claude va por el SDK oficial `anthropic` con pensamiento adaptativo, el ajuste `effort`, caché de prompts y el respaldo del servidor ante rechazos; el resto de proveedores, por la API Chat Completions compatible con OpenAI. #### Ajustes ```python settings = agents.AgentSettings(model="ollama:qwen3:8b", effort="medium", profile="ask", rules="Responde en español.") settings.provider("ollama").base_url = "http://gpu-box:11434/v1" agent = agents.Agent(tools=[stock], settings=settings) ``` | Campo | Significado | | --- | --- | | `model` | Modelo predeterminado. | | `effort` | `low`, `medium`, `high`, `xhigh` o `max` (Claude). | | `fast` | Modo rápido en los modelos de `providers.FAST_MODELS` (Claude Opus 5.5 y Opus 5, solo la API de Claude): respuestas más rápidas a mayor precio. | | `profile` | `"write"` (todas las herramientas), `"ask"` (solo las que no tienen `writes=True`) o `"minimal"` (ninguna). | | `mode` | El modo de permisos del panel del agente, un atajo sobre `profile` y `confirm_writes`: `"ask"`, `"bypass"`, `"read"` o `"chat"`. | | `confirm_writes` | Llama a `confirm(tool, args)` antes de una herramienta con `writes=True` (activo por defecto). Sin un callback `confirm` esas herramientas se rechazan; desactívalo para que se ejecuten sin preguntar. | | `rules` | Texto añadido al prompt de sistema. | | `keep_history` | Si Pyzed Design guarda las conversaciones en este equipo (activo por defecto). | | `providers[id].base_url` / `.models` | URL del servidor y modelos ofrecidos para un proveedor. | `agents.providers.load()` y `save()` leen y escriben `agents.json` en la carpeta de ajustes de Pyzed (los ajustes del diseñador); las claves nunca forman parte de él. #### Herramientas `@agents.tool` construye el esquema JSON a partir de la firma: `str`, `int`, `float`, `bool`, `list`, `dict` y `Optional[...]`; los argumentos con valor por defecto son opcionales. El primer párrafo del docstring describe la herramienta y las líneas `nombre: texto` describen los argumentos. Devuelve un texto o cualquier cosa que JSON pueda codificar. ```python @agents.tool(writes=True, title="Haciendo un pedido") def order(product: str, units: int) -> dict: """Pide unidades de un producto al proveedor.""" ... ``` | Opción | Significado | | --- | --- | | `writes=True` | Cambia algo: se omite en el perfil `"ask"` y se confirma antes cuando `confirm_writes` está activo. | | `ui=True` | Se ejecuta a través del `executor` del agente (ver abajo). | | `title` | Texto corto para tu UI mientras corre. | Los argumentos del modelo se comprueban contra el esquema antes de ejecutar la función; un argumento erróneo o ausente, o una excepción, vuelve al modelo como error para que corrija la llamada. Herramientas listas: `agents.pyzed_tools()` (componentes, iconos, temas, documentación, generadores y comprobaciones de Pyzed, lo mismo que el [servidor MCP](https://albakore.github.io/pyzed/es/guides/mcp/)) y `agents.file_tools(carpeta)` (listar, leer, buscar y escribir archivos, nunca fuera de `carpeta`). Las herramientas de archivos nunca abren claves ni credenciales (`.env`, `*.pem`, `*.key`, claves SSH...; `agents.tools.SECRET_FILES`), así que una inyección de prompt en un archivo del proyecto no puede enviarlas al proveedor; pasa `hidden=` para cambiar la lista. #### Imágenes Los modelos que admiten imágenes pueden verlas en un mensaje, y las herramientas pueden devolverlas: ```python image = agents.Image.open("maqueta.png") # PNG, JPEG, GIF o WebP, hasta agents.agent.IMAGE_LIMIT agent.run([{"type": "text", "text": "Construye esta pantalla."}, agents.image_block(image)]) @agents.tool def chart() -> list: """Dibuja el gráfico de ventas.""" return ["El gráfico:", agents.Image(render_png())] # el texto y las imágenes vuelven al modelo ``` `image_block` también acepta los bytes o una ruta. Una herramienta que devuelve un `Image`, o una lista con texto e imágenes, responde con ellos; los proveedores que no admiten imágenes en los resultados de herramientas las reciben en el mensaje siguiente. #### Servidores MCP `agents.mcp_client` se conecta a servidores del [Model Context Protocol](https://modelcontextprotocol.io), locales (stdio) o remotos (Streamable HTTP), y convierte sus herramientas en herramientas del agente: ```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 …"}) # en el llavero servers = mcp_client.Servers([fetch, github], cwd=".") agent = agents.Agent("anthropic:claude-opus-5-5", tools=servers.tools()) ... servers.close() ``` Las herramientas se llaman `mcp__` y las que el servidor no marca con `readOnlyHint` son `writes=True`, así que se confirman antes. `secret_names` lista las variables de entorno (stdio) o cabeceras (HTTP) cuyos valores `set_secrets` guarda en el llavero. Un servidor que falla se omite; pasa `on_error(servidor, mensaje)` a `tools()` para enterarte. `mcp_client.MCPClient` habla directamente con un servidor (`list_tools`, `call`). #### Agentes externos `agents.acp` ejecuta agentes que hablan el [Agent Client Protocol](https://agentclientprotocol.com) (Claude Code, Gemini CLI, Codex…) como su propio programa y transmite su trabajo con los mismos eventos que `Agent`: ```python from pyzed.agents import acp config = acp.preset("claude-code") # o "gemini", "codex", o providers.ACPAgentConfig(nombre, comando, args) own = acp.pyzed_mcp_server() # el servidor MCP de Pyzed, o None sin el paquete mcp agent = acp.ACPAgent(config, cwd="mi_app", confirm=lambda tool, args: True, mcp_servers=[own] if own else []) print(agent.run("Añade un tema oscuro a la app", on_event=print)) agent.close() ``` El agente lee y escribe archivos a través de Pyzed, solo dentro de `cwd` y nunca los de `SECRET_FILES`. Sus peticiones de permiso siguen `settings.mode`: `"bypass"` las permite, `"read"` solo permite lecturas, `"chat"` las rechaza y `"ask"` llama a `confirm`. `acp.installed(config)` dice si su programa está en el `PATH`, y los ajustes los guardan en `AgentSettings.acp_agents` (y los servidores MCP en `mcp_servers`; `acp.mcp_entries(settings.mcp_servers)` también se los pasa al agente). Tus propias herramientas también pueden ir al agente externo: `mcp_host.ToolServer` sirve las herramientas de un `Agent` por MCP en `127.0.0.1` (una dirección de un solo uso y un token), y así es como Pyzed Design deja que Claude Code controle el editor: ```python from pyzed.agents import mcp_host server = mcp_host.ToolServer(agents.Agent(tools=[mi_herramienta]), name="mi_app", instructions="Cómo usarlas") agent = acp.ACPAgent(config, cwd="mi_app", mcp_servers=[server.acp_entry(), server.bridge_entry()]) ... server.close() ``` Con las dos, el agente recibe la entrada HTTP si la admite y el puente por stdio si no. `acp.claude_signed_in()` dice si Claude Code tiene la sesión iniciada en este equipo, y `acp.claude_login_command()` es el comando que lo abre para iniciar sesión con una suscripción de Claude. #### Streaming en la UI `run` bloquea, así que en una app llámalo desde un hilo y pasa los eventos a la UI. Las vistas de Pyzed corren en el hilo de la UI, así que pon los eventos en una cola y vacíala desde una suscripción `pz.every`: ```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) ``` Eventos: `text` y `thinking` (un trozo nuevo de la respuesta o del resumen del razonamiento), `tool_call` y `tool_result` (`tool`, `args`, `call_id`, `result`, `error`), `turn` (terminó una respuesta del modelo, con `usage`), `error` y `done`. `agent.stop()` cancela desde cualquier hilo. Las herramientas que tocan el estado de tu app también deben correr en el hilo de la UI: pasa `executor=` (una función que ejecuta un callable allí y devuelve su resultado) y márcalas con `ui=True`. El diseñador hace exactamente eso, y por eso las ediciones del agente son pasos normales de deshacer. #### Claves e inicio de sesión ```python agents.set_key("openai", "sk-…") # True: en el llavero; False: sin llavero, solo para esta sesión agents.get_key("openai") # la clave guardada o la variable de entorno agents.delete_key("openai") agents.secrets.store().backend_name # backend de keyring en uso, o "memory (this session only)" key = agents.login.openrouter_login() # OpenRouter: navegador + PKCE, devuelve una clave agents.set_key("openrouter", key) agents.login.key_page("anthropic") # los demás: abre la página de claves de API del proveedor... agents.providers.looks_like_key("anthropic", texto_copiado) # ...y dice si un texto copiado es su clave def check(key): # por qué el proveedor rechaza la clave, o None try: agents.list_models("anthropic", key=key) except agents.AgentError as err: return str(err) # ...o una página de inicio de sesión servida en 127.0.0.1: enlaza a la página de claves y devuelve la clave pegada key = agents.login.key_portal("anthropic", check) ``` GitHub y Google también inician sesión sin clave, con apps OAuth que registras tú (sus ID van en los ajustes del proveedor, o en `PYZED_GITHUB_CLIENT_ID` / `PYZED_GOOGLE_CLIENT_ID`): ```python code = agents.login.github_device_code(client_id) # muestra code.user_code y abre code.verification_uri token = agents.login.github_device_token(client_id, code) # espera hasta que se aprueba agents.providers.save_token("github", token) # en el llavero; se renueva cuando caduca token = agents.login.google_login(client_id, client_secret) # navegador + PKCE, ámbito cloud-platform agents.providers.save_token("vertex", token) ``` Sin iniciar sesión, `vertex` usa las credenciales de gcloud del equipo (`gcloud auth application-default login`) y `github` acepta como clave un token detallado con el permiso Models. Aparte de estos, solo OpenRouter ofrece OAuth a otras apps. Para el resto, Pyzed Design abre la página de claves y vigila el portapapeles unos minutos; cuando se copia un texto que cumple `looks_like_key`, lo comprueba con `list_models(key=...)` y lo guarda con `set_key`. Tu app puede hacer lo mismo. Las claves viven en el llavero del sistema (`keyring`, servicio `pyzed`); `PYZED_KEYRING=0` lo desactiva y `agents.secrets.use(SecretStore(mi_backend))` conecta tu propia bóveda (cualquier objeto con `get_password` / `set_password` / `delete_password` de keyring). El orden de búsqueda, la variable de entorno de cada proveedor y cómo mantener las claves seguras están en [Claves de API y credenciales](https://albakore.github.io/pyzed/es/ai/api-keys/). ##### Claude sin clave de API Con la federación de identidad de cargas de trabajo, un token de tu proveedor de identidad (GitHub Actions, Google Cloud, Azure, Okta, Kubernetes…) se cambia por un token de Claude de corta duración, así que no se guarda ninguna clave. Crea el emisor y la regla en la Claude Console y luego: ```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` es `"file"` (`token_file`, o `ANTHROPIC_IDENTITY_TOKEN_FILE`), `"command"` (imprime un token nuevo) o `"env"` (`token_env`, `ANTHROPIC_IDENTITY_TOKEN` por defecto). Una clave guardada tiene prioridad sobre la federación. `providers.credential_source("anthropic", settings=settings)` dice de dónde salen las credenciales. #### Conversaciones ```python store = agents.ThreadStore() # agents/threads en la carpeta de ajustes de Pyzed thread = agents.Thread(model="ollama:qwen3:8b") agent = agents.Agent(thread.model, messages=thread.messages) agent.run("Hola") store.save(thread) # JSON que solo tú puedes leer; sin claves store.list() # la más reciente primero ``` #### Pruebas Pasa `stream=` a `Agent` para cambiar el modelo por un guion, así las pruebas corren sin red y siempre igual: ```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 quedan escritorios."}]) agent = agents.Agent("x", tools=[stock], stream=fake, settings=agents.AgentSettings()) assert agent.run("¿Escritorios?") == "No quedan escritorios." ``` #### Una app de chat completa [Crear apps con IA](https://albakore.github.io/pyzed/es/ai/ai-apps/) construye un chat completo con streaming sobre una API compatible con OpenAI, con detener, errores y tests sin red, y muestra cómo usar `pyzed.agents` desde `pz.Command.perform` y `pz.post`. ### Accesibilidad > Accesibilidad en apps de escritorio con Pyzed: foco del teclado con Tab, el ajuste de reducir movimiento del sistema y el estado de los lectores de pantalla. Source: https://albakore.github.io/pyzed/es/guides/accessibility/ Las apps de Pyzed se pueden usar con el teclado y respetan la preferencia de movimiento del sistema. Los lectores de pantalla todavía no están soportados: en [Lectores de pantalla](#lectores-de-pantalla) se explica por qué y qué viene después. #### Moverse entre campos con Tab Tab mueve el foco del teclado al siguiente campo de la ventana y Shift+Tab al anterior, en el orden en que aparecen en `view()`. Tras el último vuelve al primero. Los campos son los `text_input`, `text_editor` y `code_editor` (y los componentes construidos con ellos, como `ui.input` o `ui.search`), los desplegables (`pz.pick_list` y `ui.select`) y los controles envueltos en `pz.focusable`: `ui.switch`, `ui.checkbox`, `ui.slider`, `ui.segmented` y `ui.radio_group` de la librería. Cada ventana recorre solo sus propios campos. Un `code_editor` con `tab_size` se queda con Tab para indentar, como un editor de código. Los editores sin él dejan pasar la tecla, así que Tab sale de ellos. `on_key` ve la tecla primero (`"tab"` o `"shift-tab"`). Si devuelve algo (un mensaje o un `Command`), la app la ha gestionado y el foco no se mueve; devolver `None` deja que se mueva. Desactívalo para toda una app o ventana con `tab_focus = False`: ```python class App(pz.App): tab_focus = False # Tab no hace nada salvo que on_key lo gestione def on_key(self, combo): if combo == "tab" and self.completion_open: return self.accept_completion() ``` `Command.focus_next()` y `Command.focus_previous()` hacen lo mismo desde código, y `Command.focus(id)` salta a un campo concreto (pasa `id=` al control). #### Controles con el foco del teclado Un clic o Tab le dan el foco a un control, y lo mantiene después de usarlo, como un campo de texto. Al usar las teclas aparece a su alrededor un anillo del color de foco del tema (un clic solo no lo dibuja, como el `:focus-visible` de un navegador); un desplegable ilumina su borde. | Control | Teclas mientras tiene el foco | | --- | --- | | `ui.select`, `pz.pick_list` | ↓/↑ (y →/←) eligen la opción siguiente o la anterior **sin abrir el menú**, Inicio/Fin la primera y la última, y las letras saltan a la opción que empieza por ellas. Enter, Espacio, Alt+↓ o F4 abren el menú; ahí ↑/↓ se mueven, Enter elige y Esc cierra. | | `ui.switch` | Espacio o Enter lo cambian. | | `ui.checkbox` | Espacio la marca o desmarca (una mixta queda marcada). | | `ui.segmented`, `ui.radio_group` | Las flechas eligen la opción anterior o la siguiente, saltando las desactivadas; Inicio/Fin la primera y la última. | | `ui.slider` | Las flechas lo mueven un `step`, con Shift (o Re Pág/Av Pág) diez; Inicio/Fin van a `min`/`max`. | | `ui.number_field` | ↑/↓ lo cambian un paso (diez con Shift), Inicio/Fin van a `min`/`max`. | Las teclas que usa un control con foco no llegan al `on_key` de la app, así que ↓ en un desplegable no desplaza también una lista ni mueve otra selección. El resto de teclas (atajos como Ctrl+S) siguen llegando. Tus propios controles tienen lo mismo con `pz.focusable(contenido, on_key, keys=[...])`: mientras tiene el foco, las combinaciones de `keys` llaman a `on_key(combo)` y nadie más las ve. ```python def estrellas(self): t = self.theme fila = pz.row([pz.icon("star", color=t.warning if i < self.puntos else t.text_muted) for i in range(5)], spacing=2) def puntuar(combo): self.puntos = max(0, min(5, self.puntos + (1 if combo == "right" else -1))) return pz.focusable(fila, puntuar, keys=["left", "right"], id="puntos", ring_color=self.theme.border_focused) ``` :::note Los botones normales aún no toman el foco: úsalos con el ratón o dale un atajo a las acciones importantes (mira [Teclado y atajos](https://albakore.github.io/pyzed/es/guides/keyboard/)). ::: #### Reducir movimiento Cuando el sistema pide a las apps reducir el movimiento, Pyzed apaga sus animaciones: cada `animate()`, `transition()`, `presence()`, transición de ventana y scroll animado muestra al instante su estado final. El ajuste se lee al arrancar la app: | Sistema | Ajuste | | --- | --- | | macOS | Ajustes del Sistema → Accesibilidad → Pantalla → Reducir movimiento | | Windows | Configuración → Accesibilidad → Efectos visuales → Efectos de animación (desactivado) | | Linux (GNOME) | Configuración → Accesibilidad → Reducir animación (`enable-animations`) | | Linux (KDE) | Preferencias del sistema → Velocidad de animación en *Instantánea* | `pz.system_reduced_motion()` devuelve lo que dice el sistema (`None` si no lo sabe). Tu app puede decidir igualmente: ```python pz.set_reduced_motion(True) # siempre apagado, p. ej. desde los ajustes de tu app pz.set_reduced_motion(False) # siempre encendido pz.set_reduced_motion(None) # volver a seguir al sistema (por defecto) ``` La variable de entorno `PYZED_REDUCED_MOTION` (`1` o `0`) se impone a ambos. Las capturas muestran siempre el estado de reposo. Ver [Animación](https://albakore.github.io/pyzed/es/animation/motion-and-tests/). #### Lectores de pantalla Pyzed dibuja su interfaz con Iced, que pinta cada widget en la GPU en vez de usar los controles nativos del sistema. Los lectores de pantalla (VoiceOver, Narrador, NVDA, Orca) necesitan un *árbol de accesibilidad* que describa esos widgets, e Iced 0.14 no lo genera. La forma habitual de darlo en Rust es [AccessKit](https://accesskit.dev/), y evaluamos añadirlo a Pyzed para la 1.1: - **Iced 0.14 no soporta AccessKit.** Ni `iced` ni `iced_winit` dependen de él, y winit 0.30 deja la accesibilidad a adaptadores como `accesskit_winit`. - **La ventana es de Iced.** `accesskit_winit` debe crearse antes de mostrar la ventana y recibir todos sus eventos; el bucle de eventos de Iced no da sitio para ninguna de las dos cosas. Los adaptadores de cada plataforma (`accesskit_windows` y `accesskit_macos` subclasifican la ventana nativa, `accesskit_unix` habla con AT-SPI) se podrían enganchar desde el handle de la ventana, pero son tres integraciones distintas, cada una probada con un lector de pantalla real. - **Los widgets no se describen a sí mismos.** Las operaciones de widgets de Iced informan de texto, campos de texto, scrollables y foco, pero no de roles (botón, casilla, slider), estados ni acciones ("pulsar"), así que Pyzed tendría que construir el árbol desde sus propias descripciones de widgets y sincronizarlo con el layout en cada frame. Eso es un proyecto en sí mismo, no algo para publicar a medias en una versión menor. La accesibilidad está en la hoja de ruta de Iced y algunos forks de Iced ya incluyen AccessKit, así que el plan es: 1. Seguir a Iced y adoptar su soporte de accesibilidad cuando llegue; los widgets de Pyzed corresponden uno a uno con los de Iced, así que lo ganarían con poco trabajo extra. 2. Mientras tanto, completar el uso con teclado (foco para botones y otros controles cuando Iced lo permita) y mantener las etiquetas en el texto de la interfaz, no solo en iconos. Si el soporte de lectores de pantalla es importante para tu app, dilo en los [issues](https://github.com/albakore/pyzed/issues): ayuda a decidir prioridades. ### Muchos datos y rendimiento > Rendimiento de GUIs en Python con Pyzed: muestra cientos de miles de filas con listas y tablas virtuales y evita reconstruir vistas con @pz.memo. Source: https://albakore.github.io/pyzed/es/guides/performance/ Pyzed llama a `view()` después de cada mensaje y convierte el árbol de widgets que devuelve en widgets de Iced. En la mayoría de apps eso tarda mucho menos de un milisegundo. Dos cosas lo hacen lento: construir **muchas filas** que ni siquiera están en pantalla y reconstruir **subárboles grandes** que no cambiaron. Pyzed tiene una herramienta para cada una. #### Rápido por defecto No hace falta activar nada de esto. Pyzed lo hace en todas las apps, siguiendo lo que hacen los editores nativos rápidos para mantener ágil su interfaz: empezar a dibujar cuanto antes y no repetir trabajo que nadie pidió. - **Arranque rápido.** `import pyzed` carga solo lo que una app necesita para abrir una ventana. El diseñador, la referencia de la API, asyncio y los metadatos del paquete se cargan la primera vez que los usas. - **Fuentes en paralelo.** La búsqueda de las fuentes del sistema empieza en cuanto se importa Pyzed, en su propio hilo, mientras Python ejecuta el resto de tus imports. - **Primero las APIs nativas de la GPU.** El renderizador de GPU arranca Vulkan, Metal o DirectX 12. El backend de OpenGL, más lento de arrancar y con más memoria, solo se arranca si ninguno de ellos funciona. - **Árboles de widgets baratos.** Construir contenedores, botones y colores del tema cuesta menos, y el núcleo nativo reserva memoria con [mimalloc](https://github.com/microsoft/mimalloc), mucho más rápido que el asignador del sistema con los muchos objetos pequeños que crea cada `view()`. - **Piezas compartidas de los componentes.** `ui.button`, `ui.icon_button`, `ui.kbd` y los tooltips reutilizan los widgets que solo dependen del tema y de su texto, icono o teclas. Un widget no cambia una vez construido, así que una sola copia sirve para todos los botones que dicen "Abrir". Una vista de 1.000 botones y etiquetas se construye en un tercio menos de tiempo. - **El renderizador de CPU cuando no hay una GPU de verdad.** En una máquina virtual, un escritorio remoto o un equipo sin driver gráfico, la GPU que ofrecen Vulkan, DirectX u OpenGL es una emulación que corre en la CPU (llvmpipe y lavapipe de Mesa, WARP de Windows). Ahí Pyzed dibuja con su renderizador de CPU (tiny-skia): es más rápido y usa unos 100 MB menos de memoria. - **Sin reconstrucciones por teclas que nadie escucha.** Las pulsaciones y liberaciones de teclas solo llegan a tu app (y reconstruyen la vista) cuando una ventana define `on_key` u `on_key_up`. Escribir en un campo reconstruye la vista una vez por tecla en lugar de tres. - **Pasar el ratón no cuesta nada.** Los botones, las filas de las listas y todo lo que tiene un estilo `hover=` se iluminan bajo el puntero por sí solos, sin mensaje ni un `view()` nuevo. Los callbacks del puntero (`on_move`, `on_enter`, `on_exit`, `mouse_move`...) se ejecutan en cuanto se mueve el ratón, y cuando uno devuelve [`pz.UNCHANGED`](#callbacks-que-no-cambian-nada-en-pantalla) no se reconstruye ni se redibuja nada. Los componentes de la librería y Pyzed Design lo hacen siempre que un movimiento no cambia nada. - **Estilos resueltos una vez.** `widget.style(...)` guarda lo que resuelve según sus propiedades y el tema, así que el mismo color de hover en todas las filas de una lista se calcula una vez, no una vez por fila en cada `view()`. - **Markdown analizado una vez.** Un chat o documento largo en bloques `pz.markdown` se analiza la primera vez que se dibuja y se conserva mientras está en pantalla, tenga los bloques que tenga. - **Las listas largas construyen lo visible.** `ui.table`, `ui.virtual_list` y el List box de Pyzed Design (desde 50 elementos) solo construyen las filas que se ven. - **Los componentes se dibujan cuando cambian.** Un [`pz.Component`](#componentes-que-solo-se-dibujan-cuando-cambian) cuyas props, estado y contexto son los mismos que la vez anterior devuelve su último dibujo en vez de volver a ejecutar `render()`. Una vista de 500 filas de componentes se reconstruye en 4 ms en lugar de 26 ms cuando cambia una. - **Y los controles de los formularios.** Un formulario hecho en Pyzed Design, en marcha o en el lienzo del diseñador, solo vuelve a construir los controles cuyas propiedades cambiaron. El diseñador hace lo mismo con su consola, su árbol y su caja de herramientas: su vista pasó de 21 ms a 4,5 ms con un formulario de 85 controles y 400 líneas de log. - **Los archivos se listan fuera del hilo de la interfaz.** El diálogo Abrir, la lista `@` del agente y el selector de archivos de los shaders leen la carpeta del proyecto una vez y luego la actualizan en un hilo aparte, así que escribir en ellos nunca espera al disco. Medido con la [app del benchmark](https://albakore.github.io/pyzed/es/why/benchmarks/) (ventana de 640×480, una etiqueta y un botón por fila, Xvfb sin GPU, medianas de 5 arranques) en la misma máquina: | | Antes | Ahora | | --- | --- | --- | | `import pyzed` | 178 ms | 78 ms | | Primer frame, 10 filas | 454 ms | 323 ms | | Actualizar 1.000 filas | 40,7 ms | 34,9 ms | | Memoria, 10 filas (RSS) | 174 MB | 146 MB | :::note `WGPU_BACKEND=gl` (o `vulkan`, `metal`, `dx12`) sigue eligiendo el backend de GPU a mano, `ICED_BACKEND=wgpu` mantiene el renderizador de GPU aunque la GPU sea emulada (lo que necesitan los [shaders](https://albakore.github.io/pyzed/es/shading/shaders/)), e `ICED_BACKEND=tiny-skia` usa siempre el renderizador de CPU. Un proyecto puede fijarlos en `[tool.pyzed.rendering]` de su `pyproject.toml`, o en **Ajustes → Proyecto → Renderizado** ([Renderizado](https://albakore.github.io/pyzed/es/editor/settings/#renderizado)). ::: #### Listas y tablas virtuales Una lista virtual solo construye las filas visibles. Las filas se construyen cuando entran en la vista y se descartan cuando salen, así que una lista de un millón de elementos cuesta lo mismo que una de cincuenta. ```python import pyzed as pz from pyzed import ui class Logs(pz.App): def __init__(self): self.lines = [f"[{i:06}] petición atendida" 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/es/library/virtual-list/) es una lista con scroll y con el tema: `render(elemento)` devuelve el widget de un elemento. - [`ui.table`](https://albakore.github.io/pyzed/es/library/table/) también está virtualizada: pásale todas tus filas. - [`pz.virtual_column(count, item, item_height=...)`](https://albakore.github.io/pyzed/es/reference/widgets/#virtual_column) es la pieza básica: `item(índice)` devuelve la fila `índice`. Ponla dentro de cualquier `pz.scrollable`. Todas las filas tienen la misma altura (`item_height`), y por eso Pyzed sabe dónde va cada fila sin construirla. Para ir a la fila `i`, desplaza el `id` de la lista hasta `i * (item_height + spacing)`: ```python pz.Command.scroll_to("logs", 0, 1_000 * 20) # fila 1000 ``` Medido con una tabla de 200 000 filas, construir la vista pasó de 3,7 s a 0,2 ms. :::tip `items` y `rows` deberían ser listas (u otras secuencias): otros iterables se copian a una lista en cada `view()`. Ordenar una tabla enorme en cada `view()` es el coste que queda; guarda las filas ya ordenadas en tu estado u ordénalas en una función `@pz.memo`. ::: #### Callbacks que no cambian nada en pantalla Un mensaje hace que Pyzed vuelva a llamar a `view()` y a calcular el layout y dibujar la ventana. Eso es lo que quieres tras un clic, pero un puntero que se mueve sobre una vista grande manda un mensaje por movimiento. Cuando un callback solo recuerda algo que no se dibuja, devuelve `pz.UNCHANGED`: el mensaje termina ahí, sin `view()`, sin layout y sin redibujar. ```python class Lienzo(pz.App): def __init__(self): self.puntero = (0, 0) # se usa al abrir el menú, no se dibuja self.resaltado = None # se dibuja resaltado def movido(self, x, y): self.puntero = (x, y) resaltado = self.elemento_en(x, y) if resaltado == self.resaltado: return pz.UNCHANGED # nada en pantalla depende del movimiento self.resaltado = resaltado # esto sí se dibuja: se reconstruye la vista def view(self): return pz.mouse_area(self.tablero(), on_move=self.movido) ``` Sirve en los callbacks del puntero (`on_move`, `on_enter`, `on_exit` de `pz.mouse_area` y los eventos `mouse_move`, `mouse_enter`, `mouse_leave`... de `.on(...)`), en los mensajes enviados con `pz.post` y en lo que devuelva tu `update()`. Devuélvelo solo cuando no cambió nada de lo que leen `view()`, `title` o `subscriptions()`: la ventana sigue mostrando lo que mostraba. :::tip Para resaltar algo bajo el puntero no hace falta ningún callback: `.style(hover={"background": "ghost_hover"})` lo dibuja el propio widget nativo. ::: #### Componentes que solo se dibujan cuando cambian Cada [componente](https://albakore.github.io/pyzed/es/guides/components/) guarda su último dibujo. En el siguiente `view()` se reutiliza, sin llamar a `render()`, mientras todo esto siga igual: - sus **props** y su **estado** (cualquier atributo que pongas en `self`). Las listas, dicts, sets y objetos normales se comparan por su contenido, así que `self.items.append(x)` se nota; - los valores que lee con `pz.use_context` (o `self.context`); - el tema, el idioma, el tamaño de la ventana y la hoja de estilos; - los componentes que dibuja dentro: cuando un hijo cambia su estado, se vuelven a dibujar también todos los componentes que lo contienen. Los callbacks que pasas como props o creas en `render()` no estorban: se comparan por su código y los valores que capturaron, así que un `lambda: self.pick(i)` nuevo con el mismo `i` cuenta como el mismo callback. ```python class Row(pz.Component): name: str = "" stars: int = 0 def render(self): # solo se ejecuta si cambian name, stars o el Rating de dentro return pz.row([ui.label(self.name), Rating(value=self.stars)]) ``` Dos casos necesitan ayuda, porque el componente no puede ver qué cambió: - `render()` lee algo que no es ni prop, ni estado, ni contexto: la hora, una variable global, datos que cambia otro objeto. Llama a `self.refresh()` cuando cambie, o pon `cache = False` en la clase para dibujarlo en cada `view()` (los medidores en vivo lo hacen). - Un valor cuyos cambios no se pueden comparar (un array de NumPy, un recurso nativo, un grafo de objetos enorme) desactiva la caché de ese componente por sí solo: se dibuja siempre, como antes. Un componente que recibe widgets como `children` se dibuja cada vez que el padre los vuelve a construir, porque son widgets nuevos; no cuesta más que dibujarlo. #### Hilos Tu `view()`, tu `update()` y tus callbacks se ejecutan en el hilo de la interfaz, de uno en uno, como en cualquier toolkit nativo. Python ejecuta un solo hilo a la vez de todos modos (el GIL), así que la interfaz más rápida es la que hace menos en ese hilo, y para eso están las cachés de arriba. Lo que Pyzed hace en otros hilos: - Iced calcula el layout y renderiza el frame en Rust, fuera del GIL, y la GPU lo dibuja. - Las fuentes del sistema se buscan, los archivos del proyecto se listan y los agentes, servidores de lenguaje y builds del diseñador corren en hilos aparte. - Tu trabajo lento también va ahí: [tareas async](https://albakore.github.io/pyzed/es/guides/async/) o un `threading.Thread` que envía su resultado con `pz.post(...)`. #### Subárboles memoizados `@pz.memo` guarda lo que devuelve una función según sus argumentos. Mientras los argumentos sean iguales, el siguiente `view()` reutiliza el mismo árbol de widgets en vez de construirlo otra vez: ```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()]) ``` Cómo se comparan los argumentos: | Argumento | Se compara por | | --- | --- | | Números, textos, tuplas, dataclasses congeladas... | Valor (su hash). | | Listas, dicts y sets | Su contenido. | | Cualquier otra cosa (tus objetos, `self`) | Identidad: modificarlo sin reemplazarlo no refresca la caché. | El tema y el idioma actuales siempre forman parte de la clave, así que al cambiarlos se reconstruyen todos los subárboles memoizados. :::caution Una función memoizada solo ve sus argumentos. Si lee `self.algo` o una variable global, el subárbol guardado conserva el valor antiguo hasta que cambien los argumentos. Pasa todo lo que usa, o un número de versión que incrementes cuando cambien los datos. ::: - `@pz.memo(maxsize=256)` guarda más resultados (64 por defecto). Es útil en una función que se llama una vez por fila, como el `render` de una lista virtual. - `funcion.cache_clear()` vacía la caché de una función y `pz.clear_memos()` la de todas. - `pz.memo_stats()` devuelve `{"hits": ..., "misses": ...}` para comprobar que un subárbol se reutiliza. #### Otros consejos - Deja `view()` sin trabajo lento: carga archivos, consulta bases de datos y ordena datos en los handlers o en [tareas async](https://albakore.github.io/pyzed/es/guides/async/), y guarda los resultados en tu estado. - Dibuja miles de formas con un solo [canvas](https://albakore.github.io/pyzed/es/guides/canvas/) en vez de miles de widgets. - Las suscripciones `pz.every` llaman a `view()` en cada tic: detenlas cuando nada se mueve. ### Estabilidad y limitaciones conocidas > Qué promete Pyzed 1.x sobre su API de Python, cómo se retiran funciones de una versión a otra y qué no hace todavía el framework. Source: https://albakore.github.io/pyzed/es/guides/stability/ #### Versiones Desde la 1.0.0, Pyzed sigue el [versionado semántico](https://semver.org/lang/es/): - Las versiones **patch** (`1.0.x`) solo corrigen errores. - Las versiones **minor** (`1.x.0`) añaden funciones y nunca rompen el código que usa la API pública. - Las versiones **major** (`2.0.0`) son las únicas que pueden quitar o cambiar API pública. La API pública es todo lo documentado en la [referencia de la API](https://albakore.github.io/pyzed/es/reference/app/) y en estas guías. Los nombres que empiezan con guion bajo y todo lo que está en `pyzed.designer` son internos y pueden cambiar en cualquier versión. `pyzed.agents`, `pyzed.mcp` y la API de animaciones son **provisionales** en 1.x: siguen las mismas reglas, pero una versión minor todavía puede ajustar detalles que resulten equivocados, y el cambio siempre aparece en *Breaking Changes* en las [notas de versión](https://albakore.github.io/pyzed/es/changelog/). #### Deprecaciones Antes de quitar API pública, sigue funcionando al menos durante una versión minor y avisa con un `DeprecationWarning` que nombra su reemplazo. Se quita en la siguiente versión major. Los archivos de Pyzed Design (`.pzform` y componentes) llevan una versión de formato. Pyzed abre los de su versión y anteriores, y rechaza un archivo guardado por un Pyzed más nuevo en lugar de perder en silencio lo que no conoce. #### Limitaciones conocidas | Área | Limitación | | --- | --- | | Accesibilidad | Todavía no hay soporte para lectores de pantalla, y el foco solo pasa de un campo a otro desde el código (`Command.focus`), no con Tab. | | Efectos | `glass`, `bloom` y `glow` se dibujan con degradados y sombras: no se desenfoca lo que hay detrás de un panel. | | Animaciones | Se pueden animar 11 propiedades (opacidad, posición, escala, colores, radio, grosor del borde y glow); el tamaño, la rotación y el padding no. | | Muchos datos | Las listas y tablas virtuales necesitan filas de la misma altura. Cada actualización sigue llamando a `view()`; [`@pz.memo`](https://albakore.github.io/pyzed/es/guides/performance/) se salta los subárboles que no cambiaron. | | Plataformas | En Linux los wheels necesitan glibc 2.28 o posterior, o musl (Alpine), donde el motor de audio no tiene dispositivos de sonido. Las builds de Pyzed Design todavía no están firmadas. | | Python | Python free-threaded (3.13t, 3.14t) no está soportado. Pyzed necesita Python 3.10 o posterior. | ## Animación ### Animación > Cómo funciona la animación en Pyzed: anima cualquier widget como en la web (presets, keyframes, transiciones), mueve muchos widgets en una línea de tiempo como el AnimationPlayer de Godot y edita clips en Pyzed Design. Source: https://albakore.github.io/pyzed/es/animation/ Cualquier widget de Pyzed puede moverse, aparecer, crecer, girar, desenfocarse, brillar o cambiar de color. Esta sección explica cada parte del sistema de animación, con un ejemplo animado de cada una. (Image: Un panel que entra: el encabezado baja, las tarjetas suben una tras otra, una insignia late y un indicador de carga gira) ```python pz.text("Hola de nuevo").animate("fade_in_down") # al aparecer pz.row(pz.stagger(tarjetas, "fade_in_up", step=80)) # una tras otra insignia.animate("pulse", repeat="infinite") # sin parar ui.button("Grabando", self.parar).animate("glow_pulse") # un brillo en bucle pz.icon("loader-circle").animate("spin") # un indicador de carga ``` :::tip[Empieza aquí] ¿Primera vez con animaciones? [Empieza aquí: aplicar y reproducir](https://albakore.github.io/pyzed/es/animation/start/) muestra dónde va `.animate()`, todas las formas de iniciar una animación (al aparecer un widget, con el puntero, desde tu código con `pz.Trigger`, desde una línea de tiempo) y cómo se hace lo mismo en Pyzed Design. ::: #### Tres formas de animar Elige la que encaje con lo que quieres hacer. Funcionan juntas, así que una app suele usar las tres. | Quieres... | Usa | Página | | --- | --- | --- | | Darle a un widget su propio movimiento: aparecer, reaccionar al puntero, llamar la atención, repetirse. | `widget.animate(...)` | [animate() y disparadores](https://albakore.github.io/pyzed/es/animation/animate/) | | Mover un widget con suavidad cada vez que cambia uno de sus valores (abrir un panel, seleccionar una fila). | `widget.transition(...)` y estados de estilo | [Transiciones y estados](https://albakore.github.io/pyzed/es/animation/transitions/) | | Reproducir una escena en la que cambian a la vez muchos widgets y valores de tu app, con reproducir, pausar y buscar. | `pz.AnimationPlayer` y `pz.Clip` | [Animaciones con línea de tiempo](https://albakore.github.io/pyzed/es/animation/timeline/) | Y en [Pyzed Design](https://albakore.github.io/pyzed/es/editor/animations-and-effects/) puedes hacer lo mismo sin código: una sección **Animación** en cada control y un panel **Animation** con una línea de tiempo para los clips. #### Cómo se reproduce Python describe una animación una vez, en `view()`. La parte de Rust la reproduce a la frecuencia de la pantalla, así que **una animación en marcha nunca llama a tu código** y no ralentiza tu app. Cuando algo cambia (un clic, datos nuevos), `view()` se vuelve a ejecutar como siempre; las animaciones que no cambiaron siguen donde estaban. - Los **presets** son animaciones listas con un nombre (`"fade_in_up"`, `"lift"`, `"spin"`). Mira la [galería de presets](https://albakore.github.io/pyzed/es/animation/presets/). - Los **keyframes** describen tu propia animación paso a paso, como `@keyframes` en CSS. Mira [Keyframes y curvas](https://albakore.github.io/pyzed/es/animation/keyframes/). - Las **propiedades** son lo que una animación puede cambiar: posición, tamaño, rotación, colores, desenfoque, brillo... Mira [Propiedades animables](https://albakore.github.io/pyzed/es/animation/properties/). - Los **efectos** como glass, bloom y glow son propiedades de estilo que las animaciones también pueden cambiar. Mira [Efectos](https://albakore.github.io/pyzed/es/animation/effects/). :::tip[Pruébalas] Ejecuta [`examples/animations.py`](https://github.com/albakore/pyzed/blob/main/examples/animations.py) para las animaciones de widgets y [`examples/timeline.py`](https://github.com/albakore/pyzed/blob/main/examples/timeline.py) para una línea de tiempo. Los dos son cortos y están comentados. ::: #### En esta sección | Página | Qué explica | | --- | --- | | [Empieza aquí: aplicar y reproducir](https://albakore.github.io/pyzed/es/animation/start/) | Dónde va `.animate()`, qué inicia una animación, cómo se nombran y se personalizan, en cada estilo de código y en el editor. | | [animate() y disparadores](https://albakore.github.io/pyzed/es/animation/animate/) | Qué reproduce una animación (aparecer, hover, press, focus, clic, un indicador, un `pz.Trigger`) y sus opciones de tiempo. | | [Propiedades animables](https://albakore.github.io/pyzed/es/animation/properties/) | Todas las propiedades que puede cambiar una animación y cómo funcionan los valores. | | [Keyframes y curvas](https://albakore.github.io/pyzed/es/animation/keyframes/) | Tus propias animaciones con `pz.Animation` y las curvas que dan forma al movimiento. | | [Transiciones y estados](https://albakore.github.io/pyzed/es/animation/transitions/) | `transition()`, estilos de hover y pressed, cambios de página con `presence`, listas con `stagger` y transiciones de ventana. | | [Galería de presets](https://albakore.github.io/pyzed/es/animation/presets/) | Todas las animaciones con nombre, en movimiento. | | [Efectos](https://albakore.github.io/pyzed/es/animation/effects/) | Glass, glow, bloom, desenfoque, rotación y opacidad. | | [Animaciones con línea de tiempo](https://albakore.github.io/pyzed/es/animation/timeline/) | Clips, pistas, keys, bucles y llamadas a métodos en una línea de tiempo. | | [El reproductor de animaciones](https://albakore.github.io/pyzed/es/animation/player/) | Reproducir, pausar, buscar y encolar clips, y guardarlos en archivos `.pzanim`. | | [Movimiento reducido, tests y límites](https://albakore.github.io/pyzed/es/animation/motion-and-tests/) | Respetar "reducir movimiento", probar vistas animadas y lo que el sistema no puede hacer. | ### Empieza aquí: aplicar y reproducir > Las dos cosas que necesita toda animación de Pyzed: qué widget mueve (animate() sobre el widget) y qué la inicia (un disparador, tu código, una línea de tiempo). Ejemplos declarativos e imperativos, en clases, componentes y funciones, y lo mismo en Pyzed Design. Source: https://albakore.github.io/pyzed/es/animation/start/ Toda animación responde dos preguntas: 1. **¿Qué widget se mueve?** Le pones la animación a ese widget con `.animate(...)`. 2. **¿Cuándo se reproduce?** Lo decide un **disparador**: que el widget aparezca, el puntero, un clic, un valor de tu estado o una línea de tu código. Esta página explica las dos cosas, con el mismo ejemplo escrito en los distintos estilos que admite Pyzed. Las demás páginas de la sección profundizan en cada parte. (Image: Una tarjeta de inicio de sesión: la tarjeta sube al aparecer, el botón se eleva bajo el puntero, una contraseña incorrecta hace temblar el campo y una correcta hace aparecer una marca de verificación) #### 1. Aplícala: `.animate()` sobre el widget Una animación vive **en el widget que mueve**. Llama a `.animate()` donde construyes el widget en `view()` y usa el widget que devuelve: ```python def view(self): return pz.column([ pz.text("Hola de nuevo").animate("fade_in_down"), # este texto se mueve ui.button("Entrar", self.entrar).animate("lift", on="hover"), ]) ``` `.animate()` funciona en **todos** los widgets: textos, botones, campos, contenedores, filas, iconos, imágenes y tus propios componentes (que además aceptan `animate="fade_in"` como argumento). En un contenedor, todo lo que tiene dentro se mueve con él. :::caution[Usa lo que devuelve `.animate()`] Los widgets no cambian en el sitio: `.animate()` devuelve un widget nuevo con la animación. Escribir `titulo.animate("fade_in")` en su propia línea no hace nada si luego devuelves el `titulo` antiguo. ```python titulo = pz.text("Hola") titulo.animate("fade_in") # ✗ el resultado se pierde titulo = titulo.animate("fade_in") # ✓ guárdalo (o encadénalo donde construyes el widget) ``` ::: Puedes llamar a `.animate()` varias veces: cada llamada añade una animación con su propio disparador. ```python ui.button("Comprar", self.comprar).animate("fade_in_up").animate("lift", on="hover").animate("shrink", on="press") ``` #### 2. Iníciala: el disparador La opción `on=` dice qué reproduce la animación. Eliges uno en cada llamada a `.animate()`: | Quieres que se reproduzca... | Escribe | Se reproduce | | --- | --- | --- | | cuando el widget aparece | `.animate("fade_in")` (`on="mount"` es el valor por defecto) | una vez, cada vez que el widget aparece | | mientras el puntero está encima | `.animate("lift", on="hover")` | avanza al entrar y vuelve al salir | | mientras está pulsado | `.animate("shrink", on="press")` | avanza mientras el botón está abajo | | mientras tiene el foco | `.animate("glow", on="focus")` | avanza mientras tiene el foco | | en cada clic sobre él | `.animate("tada", on="click")` | desde el principio, en cada clic | | mientras un valor de tu estado es verdadero | `.animate("glow_pulse", active=self.grabando)` | avanza con `True` y vuelve con `False` | | **cuando tu código lo diga** | `.animate("shake", on=self.error)` + `self.error.fire()` | desde el principio, en cada `fire()` | | cuando cambia un valor de tu estado | `.animate("pulse", on="manual", key=self.contador)` | desde el principio, con cada `key` nueva | Los cinco primeros no necesitan nada más: Pyzed vigila el puntero y el teclado por ti. Los tres últimos son la forma de que **tu código** inicie una animación. ##### Desde tu código: `pz.Trigger` Un `pz.Trigger` es como un botón que pulsa tu código. Guarda uno en la app, pásalo a `on=` y llama a `fire()` en cualquier manejador: ```python class Login(pz.App): def __init__(self): self.clave = "" self.error = pz.Trigger() # 1. créalo una vez def view(self): return pz.column([ ui.input(self.clave, self.poner_clave, secure=True) .animate("shake", on=self.error), # 2. ponlo: todavía no se reproduce nada ui.button("Entrar", self.entrar), ], spacing=12) def poner_clave(self, texto): self.clave = texto def entrar(self): if self.clave != "secreto": self.error.fire() # 3. reprodúcela ahora (otra vez desde el principio si seguía) ``` No se reproduce nada cuando el widget aparece: solo `fire()` la inicia. `fire` ignora sus argumentos, así que puede ser un callback directamente: `ui.button("Temblar", self.error.fire)`. `on="manual"` con una `key` hace lo mismo sin objeto: la animación se reproduce cada vez que cambia la key y nunca por sí sola. Es práctico con un contador que ya tienes: ```python ui.badge(str(self.sin_leer)).animate("pulse", on="manual", key=self.sin_leer) # late con cada mensaje nuevo ``` :::note[`key` con o sin `on="manual"`] Una `key` sola (`.animate("shake", key=self.errores)`) también se repite cuando cambia la key, pero al ser una animación **mount** se reproduce además cuando el widget aparece. Añade `on="manual"` (o usa un `pz.Trigger`) cuando solo deba reproducirse con un cambio. ::: ##### Parar un bucle Una animación en bucle (`repeat="infinite"`, o un preset de bucle como `spin`) sigue mientras el widget la tenga. Para pararla, deja de pedirla en `view()`: ```python icono = pz.icon("loader-circle") if self.cargando: icono = icono.animate("spin") ``` O usa `active=`, que además la saca con suavidad: `.animate("glow_pulse", active=self.cargando)`. #### 3. Nómbrala: qué puedes pasarle a `.animate()` El primer argumento dice **qué** animación se reproduce. Puede ser: | Pasas | Ejemplo | De dónde sale | | --- | --- | --- | | El nombre de un preset | `"fade_in_up"`, `"lift"`, `"spin"` | La [galería de presets](https://albakore.github.io/pyzed/es/animation/presets/): más de 50 animaciones listas, en cinco grupos (entrada, salida, atención, interacción, bucle). | | Un nombre que definiste | `"meneo"` | `pz.animation.define_animation("meneo", ...)`, una vez, en cualquier sitio antes de que se ejecute `view()`. | | Un `pz.Animation` | `pz.Animation([{"x": 0}, {"x": 8}])` | Tus propios fotogramas clave, usados en el sitio. Ver [Fotogramas clave y curvas](https://albakore.github.io/pyzed/es/animation/keyframes/). | | Valores de propiedades | `.animate(opacity=0, y=12)` | Una animación rápida desde (o hacia) esos valores. Ver [Propiedades animables](https://albakore.github.io/pyzed/es/animation/properties/). | | Un objetivo de línea de tiempo | `self.player["tarjeta"]` | Una parte de un clip que mueve muchos widgets. Ver [Animaciones con línea de tiempo](https://albakore.github.io/pyzed/es/animation/timeline/). | Los nombres de los presets son palabras en inglés y minúsculas unidas por `_`, y dicen lo que hacen: `fade_in_left` (aparece desde la izquierda), `slide_out_down` (sale deslizándose hacia abajo), `rubber_band` (se estira como una goma). Para listarlos desde Python: ```python from pyzed import animation print(list(animation.presets())) # todos los nombres print(list(animation.presets("entrance"))) # un grupo print(animation.animation("pop_in").description, animation.animation("pop_in").duration) ``` #### 4. Personalízala Cada preset trae su propio tiempo; las opciones de `.animate()` lo cambian solo para ese widget: ```python tarjeta.animate("fade_in_up", duration=600, delay=200, easing="ease_out_back") insignia.animate("pulse", repeat=3) flecha.animate("nudge_right", on="hover", duration=120) ``` Cuando el cambio deba valer **en todas partes**, define tu animación una vez y usa su nombre: ```python from pyzed.animation import define_animation define_animation("subida_lenta", [{"opacity": 0, "y": 40}, {"opacity": 1, "y": 0}], duration=900, easing="ease_out_cubic") define_animation("fade_in_up", pz.animation.animation("fade_in_up").with_options(duration=500)) # tu valor por defecto pz.text("Hola").animate("subida_lenta") ``` La lista completa de opciones (`duration`, `delay`, `easing`, `repeat`, `direction`, `fill`, `on_start`, `on_end`...) está en [animate() y disparadores](https://albakore.github.io/pyzed/es/animation/animate/#opciones-de-tiempo), y los fotogramas clave, las curvas y la curva por fotograma en [Fotogramas clave y curvas](https://albakore.github.io/pyzed/es/animation/keyframes/). #### La misma animación en distintos estilos Pyzed te deja organizar el código de varias formas. Las animaciones funcionan igual en todas, porque siempre acaban en un widget que devuelve una vista. ##### Declarativo: la vista sigue a tu estado Guardas valores en tu estado y `view()` decide qué animaciones tienen los widgets. Nunca "inicias" nada: cambias el estado y la pantalla lo sigue. Es el estilo por el que empezar. ```python class Reproductor(pz.App): def __init__(self): self.sonando = False self.me_gusta = 0 def view(self): return pz.row([ ui.icon_button("pause" if self.sonando else "play", self.alternar) .animate("glow_pulse", active=self.sonando), # brilla mientras suena ui.icon_button("heart", self.gustar) .animate("heartbeat", on="manual", key=self.me_gusta), # late con cada me gusta pz.text(f"{self.me_gusta} me gusta").transition(opacity=1 if self.me_gusta else 0.4), ], spacing=8, align="center") def alternar(self): self.sonando = not self.sonando def gustar(self): self.me_gusta += 1 ``` ##### Imperativo: tu código la inicia Guardas "mandos" de reproducción y los llamas desde los manejadores, como `boton.play()` en otros toolkits. En Pyzed esos mandos son `pz.Trigger` (la animación de un widget) y `pz.AnimationPlayer` (un clip entero): ```python class Pago(pz.App): def __init__(self): self.error = pz.Trigger() self.hecho = pz.Trigger() self.player = pz.AnimationPlayer("pago.pzanim") # clips guardados desde Pyzed Design o código def view(self): return pz.column([ ui.input(self.tarjeta, self.poner_tarjeta).animate("shake", on=self.error), pz.icon("circle-check", size=32).animate("pop_in", on=self.hecho), pz.text("¡Gracias!").animate(self.player["gracias"]), ui.button("Pagar", self.pagar), ]) def pagar(self): if not self.valida(): self.error.fire() return self.hecho.fire() self.player.play("celebrar") ``` ##### En un componente Un `pz.Component` guarda su propio estado, así que sus disparadores viven con él (créalos en `setup()`): ```python class BotonCopiar(pz.Component): texto: str = "" def setup(self): self.copiado = pz.Trigger() def copiar(self): self.copiado.fire() return pz.Command.copy(self.texto) def render(self): return pz.row([ ui.button("Copiar", self.copiar, icon="copy"), pz.text("¡Copiado!").animate("fade_in_up", on=self.copiado), ], spacing=8, align="center") ``` Todos los componentes aceptan además `animate=` al colocarlos: `BotonCopiar(texto=enlace, animate="fade_in")`. ##### Con funciones Las piezas de una vista pueden ser funciones normales que reciben valores y devuelven widgets. Pásales el estado (y los disparadores) que necesiten: la animación va en el widget que devuelven. ```python def punto_estado(conectado: bool) -> pz.Widget: punto = pz.container(width=10, height=10).style(background="success" if conectado else "text_muted", radius=5) return punto.animate("breathe", active=conectado) def aviso(texto: str, indice: int) -> pz.Widget: return ui.callout("Nuevo", texto).animate("fade_in_left", delay=indice * 60) class Bandeja(pz.App): def view(self): return pz.column([ pz.row([punto_estado(self.conectado), pz.text("Bandeja")], spacing=6, align="center"), *[aviso(texto, i) for i, texto in enumerate(self.mensajes)], ], spacing=8) ``` #### En Pyzed Design El editor crea las mismas animaciones sin código, y te muestra la línea a llamar cuando el código deba iniciarlas. ##### Un control: la sección Animación Selecciona un control y abre **Propiedades → Animación**. **Añadir animación** añade una fila; haz clic en ella para elegir **Cuándo** se reproduce y **qué** animación (los presets de arriba): | Cuándo (en el editor) | Lo mismo en código | | --- | --- | | Al aparecer | `on="mount"` | | Con el ratón encima / Mientras se pulsa / Mientras tiene el foco | `on="hover"` / `"press"` / `"focus"` | | Al hacer clic | `on="click"` | | **Al reproducirla desde el código** | `on=pz.Trigger()` | Una fila con **Al reproducirla desde el código** espera a tus manejadores. La fila muestra la línea exacta, con un botón para copiarla: ```python def sign_in_click(form, event): if not comprobar(form.password.text): form.password.play_animation() # reproduce sus animaciones "desde el código" ``` `play_animation()` también acepta cualquier preset, `pz.Animation` o valores, incluso en un control sin filas, y `stop_animation()` para lo que inició (un bucle, por ejemplo): ```python form.badge.play_animation("pulse", repeat=3) form.card.play_animation(scale=1.05, duration=200) # hasta esos valores y de vuelta form.spinner.play_animation("spin") form.spinner.stop_animation() ``` ##### Muchos controles a la vez: clips El panel **Animación** (**Ver → Animación**) edita **clips**: líneas de tiempo que mueven varios controles juntos. Un clip empieza de una de estas dos formas, y el panel dice cuál se aplica: - activa **⚡ Reproducir al abrir el formulario** (autoplay), o - llámalo desde un manejador: `form.animations.play("intro")` (el panel muestra esa línea con un botón para copiarla). Consulta [Animaciones y efectos en Pyzed Design](https://albakore.github.io/pyzed/es/editor/animations-and-effects/) para cada campo del editor, y [El reproductor de animaciones](https://albakore.github.io/pyzed/es/animation/player/) para `play`, `pause`, `seek` y compañía. #### Chuleta | Quiero... | Librería | Pyzed Design | | --- | --- | --- | | Animar un widget al aparecer | `w.animate("fade_in_up")` | Animación → Al aparecer | | Reaccionar al puntero | `w.animate("lift", on="hover")` | Animación → Con el ratón encima | | Reproducirla al hacer clic en él | `w.animate("tada", on="click")` | Animación → Al hacer clic | | Reproducirla desde un manejador | `on=self.t` + `self.t.fire()` | Al reproducirla desde el código + `form.x.play_animation()` | | Reproducirla mientras algo está activo | `w.animate("glow_pulse", active=valor)` | Un bucle «desde el código» + `play_animation()` / `stop_animation()` | | Cambiar un valor con suavidad | `w.transition(opacity=...)` | Estados de estilo (hover, pulsado) | | Mover muchos widgets juntos | `pz.AnimationPlayer` + `player["objetivo"]` | Panel Animación (clips) | | Iniciar un clip | `player.play("intro")` | ⚡ autoplay o `form.animations.play("intro")` | | Crear mi propia animación | `pz.Animation([...])`, `define_animation` | Un clip con pistas y claves | ### animate() y disparadores > widget.animate() en Pyzed: reproduce un preset o tu propia animación cuando un widget aparece, en hover, press, focus o clic, o mientras un indicador está activo, con duración, retraso, repeticiones, dirección, fill y curva. Source: https://albakore.github.io/pyzed/es/animation/animate/ `widget.animate(...)` le da a un widget su propio movimiento. Acepta nombres de presets, objetos `pz.Animation` (o listas de ellos), [valores de propiedades](https://albakore.github.io/pyzed/es/animation/properties/) y opciones: ```python pz.text("Guardado").animate("fade_in_up") # al aparecer tarjeta.animate("lift", on="hover") # avanza en hover y vuelve al salir icono.animate("pulse", repeat="infinite") # en bucle campo.animate("shake", key=self.errores) # se repite cuando cambia key campo.animate("shake", on=self.error) # se reproduce con self.error.fire() boton.animate(on="press", scale=0.96, duration=90) # tus propios valores ``` :::tip[¿Primera vez con animaciones?] [Empieza aquí: aplicar y reproducir](https://albakore.github.io/pyzed/es/animation/start/) explica en una página dónde va `.animate()` y todas las formas de iniciar una animación, con ejemplos en cada estilo de código. ::: `animate()` devuelve el widget, así que puedes encadenarlo después de `style()` o llamarlo varias veces. Cada llamada añade una animación que conserva su propio disparador: ```python ui.button("Comprar", self.comprar).animate("fade_in_up").animate("lift", on="hover").animate("shrink", on="press") ``` #### Disparadores La opción `on` dice **qué reproduce la animación**: (Image: Seis tarjetas, una por disparador: mount se reproduce al aparecer, hover sube bajo el puntero, press se encoge al pulsar, focus ilumina el borde, click reproduce tada y active enciende un brillo) | `on` | Se reproduce | Uso típico | | --- | --- | --- | | `"mount"` (por defecto) | Una vez, cuando el widget aparece en pantalla. | Entradas: una página, un aviso, un elemento de lista. | | `"hover"` | Hacia delante mientras el puntero está encima y hacia atrás cuando sale. | Tarjetas y botones que reaccionan. | | `"press"` | Hacia delante mientras el botón del ratón está pulsado sobre él y hacia atrás al soltar. | Un botón que se hunde. | | `"focus"` | Hacia delante mientras tiene el foco del teclado y hacia atrás cuando lo pierde. | Campos que se iluminan. | | `"click"` | Desde el principio en cada clic. | Un "me gusta" que salta, una celebración. | | `"active"` | Hacia delante mientras `active=True` y hacia atrás cuando pasa a `False`. | Una luz de grabación, un estado seleccionado. | | `"manual"` | Desde el principio cada vez que cambia `key`, nunca por sí sola. | Un latido cuando sube un contador. | | un `pz.Trigger` | Desde el principio cada vez que tu código llama a `fire()`. | Un temblor con una contraseña incorrecta, una marca de "guardado". | ```python ui.button("Grabar", self.alternar).animate("glow_pulse", active=self.grabando) ``` :::tip[Hover, press y focus vuelven con suavidad] Estos disparadores no saltan hacia atrás: si el puntero sale a mitad de camino, la animación vuelve desde donde estaba. Por eso encajan con presets cortos de "interacción" como `lift`, `grow` y `shrink`. ::: ##### Reproducirla desde tu código con `pz.Trigger` Un `pz.Trigger` inicia una animación desde un manejador. Créalo una vez (en `__init__`, o en el `setup()` de un componente), pásalo a `on=` y llama a `fire()` cada vez que deba reproducirse: ```python class Login(pz.App): def __init__(self): self.error = pz.Trigger() def view(self): return ui.input(self.clave, self.poner_clave).animate("shake", on=self.error) def enviar(self): if not self.comprobar(): self.error.fire() # tiembla ahora; otra vez desde el principio si seguía temblando ``` El widget no se mueve al aparecer, solo con `fire()`. `trigger.count` dice cuántas veces se disparó, y `fire` ignora sus argumentos, así que `ui.button("Otra vez", self.error.fire)` funciona. ##### Repetir con `key` Una animación de `mount` se reproduce una vez. Dale un `key` y se vuelve a reproducir **cada vez que cambia el key**: ```python contrasena.animate("shake", key=self.intentos_fallidos) # se sacude en cada contraseña incorrecta ui.button("Copiar", self.copiar).animate("pulse", key=self.copias) ``` Al ser una animación de `mount` también se reproduce cuando el widget aparece por primera vez. Con `on="manual"` **solo** se reproduce cuando cambia el key (es lo que hace `pz.Trigger` por debajo): ```python ui.badge(str(self.sin_leer)).animate("pulse", on="manual", key=self.sin_leer) ``` #### Opciones de tiempo Estas opciones sustituyen el tiempo que trae una animación (o un preset): (Image: Seis bolas en seis carriles, cada una con una opción: duration, delay, repeat, alternate, reverse y fill none) | Opción | Valores | Qué hace | | --- | --- | --- | | `duration` | Milisegundos, o `"0.3s"` / `"300ms"`. | Cuánto dura una pasada. | | `delay` | Las mismas unidades. | Cuánto espera antes de empezar. | | `easing` | Un nombre de curva, una Bézier `(x1, y1, x2, y2)` o `"steps(4)"`. | La forma del movimiento: mira las [curvas](https://albakore.github.io/pyzed/es/animation/keyframes/#curvas). | | `repeat` | Un número de veces o `"infinite"`. | Cuántas pasadas seguidas. | | `direction` | `"normal"`, `"reverse"`, `"alternate"`, `"alternate_reverse"`. | `reverse` la reproduce al revés; `alternate` va y vuelve en pasadas alternas. | | `fill` | `"both"` (por defecto), `"forwards"`, `"backwards"`, `"none"`. | Qué se ve antes del retraso y después del final, como en CSS. Con `"none"` el widget vuelve a su aspecto propio al terminar. | | `key` | Cualquier valor. | Repite una animación de `mount` o `manual` cuando cambia. | | `active` | `True` o `False`. | Controla una animación `"active"`. | | `on_start`, `on_end` | Un callable o un mensaje. | Se envía cuando la animación empieza y cuando termina. | ```python insignia.animate("pulse", repeat=3, delay=400) flecha.animate(pz.Animation({0: {"x": 0}, 1: {"x": 6}}, duration=600), direction="alternate", repeat="infinite") aviso.animate("fade_in_up", on_end=self.aviso_mostrado) ``` #### Listas o a medida - Un **nombre de preset** es lo más rápido: mira la [galería de presets](https://albakore.github.io/pyzed/es/animation/presets/). - Los **valores de propiedades** describen una animación sencilla ahí mismo: `animate(opacity=0, y=12)` aparece desde abajo. Mira [Propiedades animables](https://albakore.github.io/pyzed/es/animation/properties/). - Una **`pz.Animation`** te da keyframes, curvas por paso y un nombre que puedes reutilizar en toda la app. Mira [Keyframes y curvas](https://albakore.github.io/pyzed/es/animation/keyframes/). - Un **objetivo de línea de tiempo** de un reproductor (`self.player["card"]`) permite que un clip mueva muchos widgets. Mira [Animaciones con línea de tiempo](https://albakore.github.io/pyzed/es/animation/timeline/). :::note[Las animaciones vuelven a empezar cuando un widget regresa] Si un widget sale de la vista y vuelve (un `if` en `view()`, un elemento nuevo de una lista), su animación de `mount` se reproduce otra vez. Usa un `key` estable, o [`pz.presence`](https://albakore.github.io/pyzed/es/animation/transitions/#presence-y-stagger), para decidir cuándo se repite. ::: ### Propiedades animables > Todas las propiedades que puede cambiar una animación de Pyzed: opacidad, posición, escala, rotación, desenfoque, colores, radio, borde, brillo, tamaño, padding y tamaño de letra, con sus unidades y cómo se animan los valores. Source: https://albakore.github.io/pyzed/es/animation/properties/ Una animación cambia una o varias **propiedades** de un widget. Los mismos nombres sirven en todas partes: en `animate()`, `transition()`, los keyframes de `pz.Animation` y las pistas de una línea de tiempo. (Image: Doce tarjetas, cada una moviendo una propiedad de ida y vuelta: opacity, x, y, scale, rotate, blur, background, border color, radius, glow, width y font size) | Propiedad | Valores | Notas | | --- | --- | --- | | `opacity` | De `0` (invisible) a `1`. | Desvanece el widget y todo lo que contiene. | | `x`, `y` | Píxeles, o `"50%"` del tamaño del propio widget. | Lo mueve sin mover a sus vecinos. | | `scale` | Un número: `1` es su tamaño, `0.5` la mitad, `1.2` una quinta parte más grande. | Alrededor de su centro. | | `rotate` | Grados en sentido horario, o `"0.25turn"`, `"1.5rad"`, `"90deg"`. | Alrededor de su centro. | | `blur` | Píxeles. | Desenfoca el widget y todo lo que contiene. | | `background`, `border_color`, `color`, `glow_color` | `"#rrggbb"`, `"#rrggbbaa"` o un token del tema (`"accent"`, `"elevated"`...). | Los colores se mezclan con suavidad. `color` cambia el texto de dentro. | | `radius`, `border_width`, `glow` | Píxeles. | `glow` es el tamaño del halo alrededor de la caja. | | `width`, `height` | Píxeles. | **Cambian el layout**: los widgets de alrededor se mueven. | | `padding` | Píxeles. | Dentro de un contenedor o botón (los cuatro lados lo siguen); en otros widgets añade espacio alrededor. Cambia el layout. | | `font_size` | Píxeles. | Todos los textos de dentro. Cambia el layout. | `pz.animation.PROPERTIES` las enumera en código. :::tip[Prefiere x, y, scale y opacity en vistas grandes] `width`, `height`, `padding` y `font_size` vuelven a calcular el layout de la vista en cada fotograma mientras se mueven. Va bien para una barra lateral o un título, pero en una página grande `x`, `y`, `scale` y `opacity` cuestan menos y se ven igual. ::: #### Valores de propiedades como animación Sin preset, los valores que pasas **son** la animación. Lo que significan depende del disparador: | Disparador | El widget... | | --- | --- | | `mount` | **parte** de esos valores y termina en su aspecto propio: `animate(opacity=0, y=12)` aparece desde abajo. | | `hover`, `press`, `focus`, `active` | **va hacia** esos valores mientras se cumple la condición, y vuelve después. | | `click` | va hacia esos valores y vuelve, en cada clic. | ```python pz.text("Hola").animate(opacity=0, y=12, duration=250) # aparece desde abajo tarjeta.animate(on="hover", scale=1.03, background="elevated") # crece y se aclara en hover boton.animate(on="press", scale=0.96, duration=90) # se hunde al pulsar icono.animate(on="click", rotate=360, duration=500) # una vuelta completa en cada clic ``` La duración por defecto es de 300 ms para `mount` y `click`, y de 150 ms para `hover`, `press` y `focus`. #### Valores que vienen del widget Una propiedad que no das al principio o al final de una animación va **desde o hacia el valor propio del widget**. Así, una animación puede cambiar el fondo de cualquier botón sin saber su color: ```python destello = pz.Animation({"50%": {"background": "#fbbf24"}}, duration=400) # a amarillo y vuelta ``` Por eso una animación de `mount` puede terminar en "el aspecto propio del widget": el último fotograma deja fuera la propiedad. #### Más ejemplos ```python icono.animate("spin") # un indicador de carga tarjeta.animate(on="hover", rotate=-3, scale=1.02) # se inclina en hover lateral.transition(width=240 if self.abierto else 56, duration=200) # el contenido de al lado se recoloca titulo.transition(font_size=28 if self.grande else 16) estado.animate(on="active", active=self.en_linea, background="#14532d", border_color="#22c55e") ``` ### Keyframes y curvas > Escribe tus propias animaciones de Pyzed con keyframes de pz.Animation (como @keyframes en CSS), dale a cada paso su curva, nómbralas y guárdalas, y elige curvas: ease, cubic, back, bounce, spring, Bézier y steps. Source: https://albakore.github.io/pyzed/es/animation/keyframes/ Cuando ningún preset hace lo que quieres, describe la animación tú mismo con `pz.Animation`. Funciona como una regla `@keyframes` de CSS con su tiempo. #### Keyframes Un **keyframe** dice cómo se ve el widget en un momento de la animación. Pyzed rellena los momentos entre keyframes. (Image: Un corazón que late y una tarjeta que se sacude, cada uno con una barra de sus keyframes y un cabezal que los recorre) ```python latido = pz.Animation({0: {"scale": 1}, "30%": {"scale": 1.25, "easing": "ease_out"}, "45%": {"scale": 1}, "60%": {"scale": 1.15}, 1: {"scale": 1}}, duration=1000, repeat="infinite") corazon.animate(latido) sacudida = pz.Animation([{"x": 0}, {"x": -10}, {"x": 10}, {"x": -6}, {"x": 0}], duration=1000) tarjeta.animate(sacudida, key=self.errores) ``` Los fotogramas se escriben de dos formas: - **Una lista:** los fotogramas se reparten por igual, del principio al final. - **Un dict:** la clave dice dónde va el fotograma, de `0` (principio) a `1` (final). También puede ser `"50%"`, `"from"` o `"to"`. Cada fotograma es un dict de [propiedades](https://albakore.github.io/pyzed/es/animation/properties/). Un fotograma también puede fijar su propio `"easing"`: la curva **desde ese fotograma hasta el siguiente**. `pz.Animation` acepta el mismo tiempo que `animate()`: `duration`, `delay`, `easing`, `repeat`, `direction`, `fill` y `on`. Las opciones que le das a `animate()` lo sustituyen. :::note[Los valores que faltan vienen del widget] Una propiedad que falta al principio o al final va desde o hacia el valor propio del widget, así que `{"50%": {"background": "#fbbf24"}}` hace destellar en amarillo a cualquier widget y vuelve. ::: #### Nombrar y guardar animaciones `pz.define_animation(nombre, animacion)` le da un nombre a una animación, para que `animate(nombre)` funcione en toda tu app, como un preset: ```python pz.define_animation("sacudida", sacudida) ui.button("Reintentar", self.reintentar).animate("sacudida", key=self.fallos) ``` `Animation.to_dict()` y `Animation.from_dict()` convierten una animación en datos aptos para JSON y al revés, para guardarla en un archivo o en un ajuste. Para guardar **muchas** animaciones que mueven varios widgets a la vez, usa un [clip de línea de tiempo y un archivo `.pzanim`](https://albakore.github.io/pyzed/es/animation/player/#bibliotecas-y-archivos-pzanim). #### Curvas La **curva** (easing) es la forma del movimiento: si empieza despacio, termina suave, se pasa o rebota. El mismo movimiento con cada curva: (Image: Nueve bolas que recorren la misma distancia, cada una con una curva distinta dibujada al lado: linear, ease, ease in, ease out cubic, ease in out, ease out back, ease out bounce, spring y steps) | Curva | Se siente | Va bien para | | --- | --- | --- | | `"linear"` | Una velocidad constante. | Indicadores de carga, progreso, ciclos de color. | | `"ease"` | Un inicio y un final suaves (la de CSS por defecto). | Casi todo. | | `"ease_in"` | Empieza despacio y termina rápido. | Cosas que salen de la pantalla. | | `"ease_out"`, `"ease_out_cubic"` | Empieza rápido y termina suave. | Cosas que llegan: menús, avisos, páginas. | | `"ease_in_out"` | Lento, rápido, lento. | Moverse entre dos sitios de la pantalla. | | `"ease_out_back"` | Se pasa un poco del final y se asienta. | Botones, entradas con gracia. | | `"ease_out_bounce"` | Rebota al final. | Caídas, juegos. | | `"spring"` | Se pasa y oscila como un muelle. | Interruptores, tarjetas que encajan en su sitio. | | `"steps(5)"` | Salta en 5 pasos, sin movimiento entre ellos. | Fotogramas de sprites, relojes, texto que se escribe. | `pz.animation.EASINGS` enumera todos los nombres: `linear`, `ease`, `ease_in`, `ease_out`, `ease_in_out`, las familias `sine`, `quad`, `cubic`, `quart`, `expo`, `circ` y `back` (cada una con `ease_in_*`, `ease_out_*` y `ease_in_out_*`), `ease_in_elastic`, `ease_out_elastic`, `ease_in_bounce`, `ease_out_bounce` y `spring`. También puedes dar: - **Una Bézier cúbica** `(x1, y1, x2, y2)`, como `cubic-bezier()` en CSS: `easing=(0.2, 0.8, 0.2, 1)`. - **Pasos** `"steps(n)"`: `n` saltos en lugar de un movimiento suave. `pz.animation.ease(curva, t)` evalúa cualquier curva en `t` (de 0 a 1). Úsala para animar tus propios dibujos en un [canvas](https://albakore.github.io/pyzed/es/guides/canvas/) con las mismas curvas: ```python from pyzed import animation y = 300 - animation.ease("ease_out_bounce", self.t) * 240 # una bola que cae en un canvas ``` ### Transiciones y estados > Anima cambios en Pyzed como en CSS: widget.transition() va hacia los valores nuevos, los estados de estilo hover y pressed se animan, pz.presence cambia de página con animaciones de entrada y salida, pz.stagger escalona listas y las ventanas se abren con transiciones. Source: https://albakore.github.io/pyzed/es/animation/transitions/ Una animación se reproduce sola. Una **transición** se reproduce cuando algo cambia: se abre un panel, se selecciona una fila, cambia la página. Tú solo pones el valor nuevo y Pyzed anima el camino. #### transition() `widget.transition(**props)` fija valores de propiedades y **anima cada cambio** hacia ellos, como un `transition` de CSS. Cambia los valores en `view()` y el widget se mueve hacia ellos: (Image: Una barra lateral se estrecha hasta dejar solo los iconos y se vuelve a ensanchar al pulsar el botón Menu, las tarjetas de al lado se recolocan y el color de la fila seleccionada pasa a otra fila) ```python def view(self): lateral = pz.column(elementos).transition(width=220 if self.abierto else 64, duration=220, easing="ease_out_cubic") etiqueta.transition(opacity=1 if self.abierto else 0) fila.transition(background="accent" if seleccionada else "panel", duration=260) ``` - `duration` (200 ms por defecto), `delay` y `easing` (`"ease_out"` por defecto) se aplican a todas las propiedades. - Una propiedad puede tener su propio tiempo: `transition(scale={"value": 1.1, "duration": 90, "easing": "spring"})`. - La primera vez que aparece el widget muestra los valores al momento; solo se animan los cambios posteriores. :::tip[¿transition() o animate()?] Usa `transition()` para un valor que sigue el estado de tu app (abierto o cerrado, seleccionado o no). Usa `animate()` para un movimiento que se reproduce solo (una entrada, un efecto de hover, un bucle). ::: #### Estados de estilo, como en CSS Un [estilo](https://albakore.github.io/pyzed/es/theming/styles/) puede decir cómo se ve un widget mientras el puntero está encima (`hover`), mientras está pulsado (`pressed`) y mientras tiene el foco del teclado (`focus`). Añade `transition` y el cambio se anima, como `:hover` con `transition` en CSS: (Image: Tres widgets que reaccionan al puntero: un botón cambia de color y sube, un botón con borde se vuelve rojo y una tarjeta crece un poco con un borde de color) ```python ui.button("Guardar", self.guardar).style( background="accent", radius=6, hover={"background": "#4f8cff", "translate": (0, -1)}, pressed={"scale": 0.97}, transition=120, # milisegundos, "0.2s", True o {"duration", "easing", "delay"} ) ``` Un estado puede cambiar `background`, `color`, `border_color`, `border_width`, `radius`, `opacity`, `translate`, `scale`, `rotate` y `blur`. Ninguna mueve a los widgets de alrededor, así que el hover nunca descoloca el layout. Mira [Estilos](https://albakore.github.io/pyzed/es/theming/styles/#estados-y-transiciones) para las hojas de estilo y los estilos con nombre. #### Presence y stagger `pz.presence(contenido, key)` anima el contenido **al entrar y al salir** cuando cambia `key`. El contenido viejo se queda en pantalla mientras sale, así que las páginas, las pestañas y los pasos de un asistente pueden cruzarse: (Image: Tres pestañas: al pulsar una, la página vieja sale por la izquierda mientras la nueva entra por la derecha y sus filas aparecen escalonadas) ```python pz.presence(self.vista_pagina(), key=self.pagina, enter="fade_in_right", exit="fade_out_left") ``` `enter` y `exit` aceptan presets u objetos `pz.Animation`. Dale un `id` cuando una misma línea de código crea varias presencias (por defecto usa el archivo y la línea desde donde se llama). `pz.stagger(elementos, animacion, step=40)` le da a cada elemento la misma animación, cada uno `step` milisegundos después del anterior, para que una lista entre en cascada: ```python pz.column(pz.stagger([fila(e) for e in self.elementos], "fade_in_up", step=30)) ``` #### Transiciones de ventana Una ventana se anima al abrirse y al cerrarse. Pon `transition` (un nombre de `pz.animation.WINDOW_TRANSITIONS`, un preset o una `pz.Animation`) y, si quieres, `exit_transition`: (Image: Tres ventanas que se abren con zoom, pop y slide up, y luego se cierran) ```python class Ajustes(pz.Window): transition = "zoom" # fade, zoom, pop, slide_up, slide_down, slide_left, slide_right exit_transition = "fade_out" ``` La ventana sigue abierta hasta que termina su animación de salida. La ventana principal de la app acepta los mismos atributos. Mira [Ventanas](https://albakore.github.io/pyzed/es/guides/windows/). ### Galería de presets > Todos los presets de animación de Pyzed, en movimiento: entradas, salidas, avisos, respuesta a hover y press, y bucles, con su duración y cuándo usarlos. Source: https://albakore.github.io/pyzed/es/animation/presets/ Un **preset** es una animación lista con un nombre. Pasa el nombre a `animate()`: ```python dialogo.animate("pop_in") tarjeta.animate("lift", on="hover") pz.icon("loader-circle").animate("spin") ``` Las opciones cambian el tiempo de un preset sin cambiar el preset: `animate("fade_in_up", duration=600, delay=200)`. `pz.animation.presets(categoria)` los enumera en código, cada uno con su `description`, y `pz.define_animation` añade [los tuyos](https://albakore.github.io/pyzed/es/animation/keyframes/#nombrar-y-guardar-animaciones). #### Entrada Se reproducen una vez cuando aparece el widget (`on="mount"`). Úsalos en páginas, diálogos, avisos y elementos de lista. (Image: Quince tarjetas que aparecen, cada una con un preset de entrada) | Preset | Qué hace | Duración | | --- | --- | --- | | `fade_in` | Aparece desvaneciéndose. | 280 ms | | `fade_in_up` | Aparece mientras sube. | 280 ms | | `fade_in_down` | Aparece mientras baja. | 280 ms | | `fade_in_left` | Aparece desde la izquierda. | 280 ms | | `fade_in_right` | Aparece desde la derecha. | 280 ms | | `slide_in_up` | Entra deslizándose desde abajo, una vez su altura. | 280 ms | | `slide_in_down` | Entra deslizándose desde arriba. | 280 ms | | `slide_in_left` | Entra deslizándose desde la izquierda. | 280 ms | | `slide_in_right` | Entra deslizándose desde la derecha. | 280 ms | | `zoom_in` | Crece desde un poco más pequeño. | 280 ms | | `pop_in` | Aparece de golpe y se pasa un poco. | 320 ms | | `bounce_in` | Entra rebotando desde pequeño. | 600 ms | | `drop_in` | Cae a su sitio con un rebote. | 520 ms | | `blur_in` | Se enfoca, como una lente. | 360 ms | | `rotate_in` | Entra girando desde un cuarto de vuelta. | 420 ms | #### Salida Lo contrario: el widget se va. Úsalos como `exit=` de [`pz.presence`](https://albakore.github.io/pyzed/es/animation/transitions/#presence-y-stagger), como `exit_transition` de una ventana o antes de quitar algo. (Image: Trece tarjetas que se van, cada una con un preset de salida) | Preset | Qué hace | Duración | | --- | --- | --- | | `fade_out` | Se desvanece. | 220 ms | | `fade_out_up` | Se desvanece mientras sube. | 220 ms | | `fade_out_down` | Se desvanece mientras baja. | 220 ms | | `fade_out_left` | Se desvanece hacia la izquierda. | 220 ms | | `fade_out_right` | Se desvanece hacia la derecha. | 220 ms | | `slide_out_up` | Sale deslizándose hacia arriba. | 220 ms | | `slide_out_down` | Sale deslizándose hacia abajo. | 220 ms | | `slide_out_left` | Sale deslizándose hacia la izquierda. | 220 ms | | `slide_out_right` | Sale deslizándose hacia la derecha. | 220 ms | | `zoom_out` | Se encoge hasta desaparecer. | 220 ms | | `pop_out` | Se hincha un poco y desaparece. | 260 ms | | `blur_out` | Se desenfoca. | 260 ms | | `rotate_out` | Se va girando un cuarto de vuelta. | 320 ms | #### Atención Un pequeño aviso para mirar aquí. Repítelos con `key` (`animate("shake", key=self.errores)`) o con `on="click"`. (Image: Diez tarjetas, cada una con un preset de atención) | Preset | Qué hace | Duración | | --- | --- | --- | | `pulse` | Crece y se encoge una vez. | 600 ms | | `heartbeat` | Dos latidos rápidos. | 1000 ms | | `shake` | Se sacude de lado a lado (un dato incorrecto). | 450 ms | | `wobble` | Se tambalea y se asienta. | 800 ms | | `bounce` | Salta dos veces. | 900 ms | | `flash` | Parpadea dos veces. | 800 ms | | `rubber_band` | Se estira y vuelve como una goma. | 800 ms | | `tada` | Un salto de celebración con un meneo. | 900 ms | | `swing` | Se balancea como un cartel colgado. | 900 ms | | `glow_flash` | Un destello de brillo alrededor de la caja. | 700 ms | #### Interacción Respuesta a hover, press y focus: avanzan mientras el puntero está sobre el widget y vuelven después. Su disparador por defecto está en la tabla. (Image: Ocho tarjetas que pasan a su aspecto de hover o press y vuelven) | Preset | Qué hace | Duración | Disparador | | --- | --- | --- | --- | | `lift` | Sube con un brillo suave (tarjetas y botones). | 160 ms | `hover` | | `grow` | Crece un poco. | 160 ms | `hover` | | `shrink` | Se hunde (para press). | 90 ms | `press` | | `dim` | Se apaga un poco. | 160 ms | `hover` | | `glow` | Brilla alrededor de la caja. | 200 ms | `hover` | | `bloom` | Un brillo fuerte y amplio que sale de la caja. | 260 ms | `hover` | | `nudge_right` | Se mueve un poco a la derecha (enlaces, flechas). | 140 ms | `hover` | | `tilt` | Se inclina un poco (pegatinas, tarjetas). | 180 ms | `hover` | #### Bucle Movimiento que no se detiene (`repeat="infinite"`): luces de estado, indicadores de carga, cosas que flotan. (Image: Siete tarjetas con presets en bucle: float, breathe, blink, glow pulse, throb, drift y spin) | Preset | Qué hace | Duración | | --- | --- | --- | | `float` | Flota arriba y abajo. | 2400 ms | | `breathe` | Se apaga y se encoge despacio, como al respirar. | 3000 ms | | `blink` | Parpadea (luces de estado, cursores). | 1000 ms | | `glow_pulse` | El brillo crece y se apaga (grabando, en directo). | 1600 ms | | `throb` | Late sin parar (notificaciones). | 1200 ms | | `drift` | Se desplaza de lado a lado. | 3000 ms | | `spin` | Gira sin parar (indicadores de carga). | 1000 ms | ### Efectos > Efectos visuales en Pyzed: opacidad, rotación, desenfoque, desenfoque de fondo, glow, bloom, glass, translate y scale como propiedades de estilo, y cuáles pueden cambiar las animaciones. Source: https://albakore.github.io/pyzed/es/animation/effects/ Los efectos son propiedades de [estilo](https://albakore.github.io/pyzed/es/theming/styles/), así que funcionan con `style()`, `pz.Style` y los estilos con nombre. Varios también son [propiedades animables](https://albakore.github.io/pyzed/es/animation/properties/), así que una animación puede cambiarlos con el tiempo. (Image: Sobre un degradado de colores: una tarjeta que se desenfoca y se enfoca, otra que se inclina, otra que brilla, otra que se desvanece y un panel de cristal que pasa sobre círculos de colores) | Propiedad | Qué hace | Animable | | --- | --- | --- | | `opacity` | De 0 a 1. Desvanece el widget y todo lo que contiene. | Sí | | `rotate` | Grados en sentido horario (o `"0.25turn"`, `"1rad"`): dibuja el widget girado alrededor de su centro. | Sí | | `blur` | Píxeles: desenfoca el widget y todo lo que contiene. | Sí | | `backdrop_blur` | Píxeles: desenfoca lo que hay detrás de la caja, dentro de sus esquinas redondeadas. | No | | `glow` | Un halo suave alrededor de la caja: un color, un tamaño (color de acento) o `(color, tamaño)`. | Sí (`glow` y `glow_color`) | | `bloom` | Luz que sale de las partes claras del widget: un color o `(color, intensidad)` con una intensidad de 0 a 1. | No | | `glass` | Un panel translúcido que desenfoca lo que hay detrás, con un borde de luz: `True`, un color de tinte o `{"tint": ..., "strength": 0.08, "blur": 20}`. | No (muévelo con `x`, `y`) | | `translate` | `(x, y)` en píxeles o `"50%"`: dibuja el widget desplazado, sin cambiar el layout. | Sí (como `x`, `y`) | | `scale` | Dibuja el widget más grande o más pequeño alrededor de su centro. | Sí | ```python pz.container(tarjeta).style(glass=True, radius=12, padding=16) ui.button("Ir", self.ir).style(glow=("#22d3ee", 18)) insignia.style(bloom=("accent", 0.8), opacity=0.9) pegatina.style(rotate=-8) ``` (Image: Rotación, glass, bloom y desenfoque sobre un degradado) #### Cómo se dibujan La rotación, el desenfoque, el desenfoque de fondo y el bloom los dibuja el renderizador: el widget se dibuja en su propia capa, que después se gira, se desenfoca o se ilumina antes de llegar a la ventana. - En una caja (un widget con fondo o glass), el bloom también añade un halo suave y un borde iluminado; en un texto o icono solo, solo brilla. - Glass añade un degradado con tinte, un borde y una sombra; los que fijes tú mandan. Se ve mejor sobre un fondo de colores (un degradado o una imagen). - Los presets `glow`, `bloom`, `glow_pulse` y `glow_flash` animan `glow` y `glow_color`. :::caution[Efectos y clics] Un widget girado o desenfocado recibe los clics donde está su caja en el layout, no donde se dibuja. Un widget con rotación, desenfoque, desenfoque de fondo o bloom se dibuja encima de sus vecinos, así que su bloom puede iluminar a los widgets de al lado. ::: ### Animaciones con línea de tiempo > Anima muchos widgets y el estado de tu app en una línea de tiempo con clips, pistas, keys y llamadas a métodos, como el AnimationPlayer de Godot: interpolación, curva por key, bucles y pistas de atributos. Source: https://albakore.github.io/pyzed/es/animation/timeline/ `animate()` le da a un widget su propio movimiento. Cuando **varios widgets** tienen que moverse a la vez (una pantalla de título, una escena, un HUD que reacciona a un golpe, un recorrido guiado), ponlos en una **línea de tiempo**. Un **clip** guarda esa línea de tiempo y un **reproductor de animaciones** la reproduce, con reproducir, pausar, buscar, velocidad y colas. El AnimationPlayer de Godot funciona igual. (Image: Una pantalla de nivel completado: la tarjeta sube, el título salta, tres estrellas entran girando una tras otra, la puntuación sube y el botón Siguiente nivel aparece y después sigue respirando) ```python import pyzed as pz INTRO = pz.Clip("intro", length=2.2, tracks=[ pz.Track("card:opacity", {0: 0, 0.35: 1}), pz.Track("card:y", [(0, 40, "ease_out_back"), (0.5, 0)]), pz.Track("title:scale", [(0.3, 0.6, "ease_out_back"), (0.7, 1)]), pz.Track(":puntos", [(0.7, 0, "ease_out_cubic"), (1.9, 12_480)]), ], calls=[pz.Call(1.6, "campana")]) class Juego(pz.App): def __init__(self): self.puntos = 0 self.player = pz.AnimationPlayer(INTRO, autoplay="intro") def campana(self): ... def view(self): return pz.column([ pz.text("NIVEL COMPLETADO").animate(self.player["title"]), pz.text(f"{self.puntos:,.0f}"), ]).animate(self.player["card"]) ``` El ejemplo completo es [`examples/timeline.py`](https://github.com/albakore/pyzed/blob/main/examples/timeline.py). Para hacer clips sin código, usa el [panel Animation](https://albakore.github.io/pyzed/es/editor/animations-and-effects/#animaciones-con-línea-de-tiempo) de Pyzed Design. #### Las partes de una línea de tiempo (Image: Una tarjeta a la izquierda y su clip a la derecha: un carril por pista con sus keys, un carril de llamadas y un cabezal que va de 0 a 2 segundos mientras la tarjeta aparece, sube, el título crece y la puntuación sube) | Parte | Qué es | | --- | --- | | **Clip** (`pz.Clip`) | Una línea de tiempo con nombre: una duración `length` en segundos, pistas, llamadas y un modo de bucle. | | **Pista** (`pz.Track`) | Una propiedad que cambia con el tiempo, como la `y` de `card`. | | **Key** (`pz.Key`) | Un valor en un momento de una pista. Pyzed rellena el tiempo entre keys. | | **Llamada** (`pz.Call`) | Un método de tu app que se ejecuta cuando el cabezal pasa por su momento. | | **Objetivo** | Un nombre, como `"card"`, al que se unen los widgets con `.animate(self.player["card"])`. | | **Cabezal** | Dónde está el reproductor dentro del clip (`player.position`). | Un clip sin `length` dura hasta su última key o llamada. ##### Rutas de las pistas La ruta de una pista dice qué anima: | Ruta | Anima | | --- | --- | | `"card:opacity"` | Una propiedad de todos los widgets que reproducen el objetivo `card`. | | `":puntos"` | Un atributo de la app. | | `":hud.vida"` | Un atributo de un atributo (sigue los puntos). | Las **pistas de widget** usan las [propiedades animables](https://albakore.github.io/pyzed/es/animation/properties/) (`opacity`, `x`, `y`, `scale`, `rotate`, `blur`, colores, `radius`, `glow`, `width`, `font_size`...). Se reproducen de forma nativa a la frecuencia de la pantalla y tu código no se ejecuta mientras tanto. Las **pistas de atributo** (empiezan por `:`) cambian valores de tu propia app, así que pueden controlar **cualquier parámetro de cualquier widget**: el texto de una etiqueta, el valor de una barra de progreso, un tamaño, un icono. Los números, los colores (`"#rrggbb"`) y las tuplas de números se mezclan entre keys; los demás valores (textos, booleanos) cambian en cada key. Mira [pistas de atributo y llamadas](#pistas-de-atributo-y-llamadas) más abajo. ##### Escribir keys Las keys se pueden escribir de varias formas: ```python pz.Track("card:x", {0: -40, 0.5: 0}) # {tiempo: valor} pz.Track("card:x", [(0, -40, "ease_out_back"), (0.5, 0)]) # (tiempo, valor, curva hasta la siguiente key) pz.Track("card:x", [pz.Key(0, -40), pz.Key("500ms", 0)]) # objetos Key; los tiempos también como "500ms" ``` #### Entre keys (Image: Tres pistas con las mismas keys, cada una con su curva y una bola: linear se mueve a velocidad constante, la curva por key se pasa y luego se suaviza, discrete salta de valor en valor) - **Curva por key.** El `easing` de una key es la curva **hasta la siguiente key**. Sirve cualquier nombre de [`pz.animation.EASINGS`](https://albakore.github.io/pyzed/es/animation/keyframes/#curvas), además de Bézier cúbicas `(x1, y1, x2, y2)` y `"steps(n)"`. El `easing` de la pista (`"linear"` por defecto) se usa en las keys que no fijan uno. - **Discreta.** `interpolation="discrete"` mantiene cada valor hasta la siguiente key, sin movimiento entre ellas. Va bien para fotogramas de sprites, textos e interruptores. - **Silenciar.** `enabled=False` deja la pista en el clip sin reproducirla. - **Antes y después de las keys.** Un widget mantiene el valor de la primera key antes de ella y el de la última key después, como en Godot. #### Bucles (Image: Tres cabezales: once se detiene al final, loop vuelve a empezar, pingpong va y vuelve) | `loop=` | Se reproduce | | --- | --- | | `"none"` (por defecto) | Una vez, y se queda en el último fotograma. | | `"loop"` | Vuelve a empezar al llegar al final, sin parar. | | `"pingpong"` | Va hasta el final y vuelve, sin parar. | ```python REPOSO = pz.Clip("reposo", length=1.2, loop="pingpong", tracks=[ pz.Track("button:scale", [(0, 1, "ease_in_out_sine"), (1.2, 1.06)]), ]) ``` #### Pistas de atributo y llamadas (Image: Una tarjeta de subida cuyo texto de estado, barra de progreso y número de archivos vienen de pistas de atributo, mientras el carril de llamadas dispara halfway() y finished() cuando el cabezal pasa por ellas) ```python SUBIDA = pz.Clip("subida", length=2.0, tracks=[ pz.Track(":progreso", [(0, 0, "ease_in_out"), (1.8, 1)]), pz.Track(":estado", {0: "Subiendo…", 0.9: "Casi listo", 1.8: "Hecho"}), # los textos cambian en cada key pz.Track(":archivos", [(0, 0), (1.8, 24)]), ], calls=[pz.Call(0.9, "a_medias"), pz.Call(1.8, "terminado", "informe.pdf")]) class Subida(pz.App): def __init__(self): self.progreso, self.estado, self.archivos = 0.0, "", 0 self.player = pz.AnimationPlayer(SUBIDA, autoplay="subida") def a_medias(self): ... def terminado(self, nombre): ... def view(self): return pz.column([pz.text(self.estado), ui.progress(self.progreso), pz.text(f"{self.archivos:.0f} / 24")]) ``` `pz.Call(tiempo, metodo, *args)` llama a un método de la app cuando el clip pasa por ese momento. `metodo` también puede ser cualquier función. Lo que devuelve se trata como lo que devuelve un callback (un `Command`, un mensaje). Las llamadas se ejecutan al reproducir hacia delante, hacia atrás y en cada vuelta de un bucle, pero no cuando saltas con `seek`. :::note[El dueño] Las pistas de atributo y las llamadas actúan sobre el **dueño** del reproductor: la app (o ventana) que guarda el reproductor en un atributo. Pyzed lo encuentra solo; pasa `owner=` a `pz.AnimationPlayer` para elegir otro objeto. ::: Siguiente: [el reproductor de animaciones](https://albakore.github.io/pyzed/es/animation/player/) reproduce, pausa, busca y encola clips, y los guarda en archivos `.pzanim`. ### El reproductor de animaciones > pz.AnimationPlayer reproduce clips de línea de tiempo de Pyzed: reproducir, pausar, reanudar, detener, buscar, reproducir al revés, velocidad, colas y eventos, y guarda clips reutilizables en bibliotecas .pzanim. Source: https://albakore.github.io/pyzed/es/animation/player/ `pz.AnimationPlayer` reproduce los [clips](https://albakore.github.io/pyzed/es/animation/timeline/) que le das, uno cada vez, y le da a cada widget su parte con `player["objetivo"]`. ```python self.player = pz.AnimationPlayer(INTRO, REPOSO, "ui.pzanim", autoplay="intro", on_started=..., on_finished=...) ``` - **Fuentes:** clips, listas de clips, [bibliotecas](#bibliotecas-y-archivos-pzanim) o rutas de archivos `.pzanim`. - **Dónde guardarlo:** en un atributo de la app (o de una ventana). Ahí encuentra a su **dueño**, el objeto cuyos atributos y métodos usan las pistas y las llamadas, y se mantiene en marcha. - **Objetivos:** `widget.animate(self.player["card"])` le da a un widget el objetivo `card`. Todos los widgets que usan el mismo objetivo lo siguen. #### Controles (Image: La pantalla de nivel completado controlada por el reproductor: play, pause, seek a 0.5 s, reproducir al revés y después a doble velocidad, con la llamada arriba y los controles siguiéndola) | Llamada | Qué hace | | --- | --- | | `play(nombre, speed=1, backwards=False, restart=False)` | Reproduce un clip o sigue con el actual. Un clip que ya se está reproduciendo sigue, salvo con `restart`; uno terminado vuelve a empezar. | | `play_backwards(nombre)` | Lo reproduce al revés desde el final. | | `pause()`, `resume()`, `toggle()` | Detiene el tiempo y lo vuelve a poner en marcha. | | `stop(reset=False)` | Vuelve al primer fotograma y vacía la cola. Con `reset=True` los widgets recuperan sus valores propios. | | `seek(segundos)` | Salta a un momento. Las llamadas de en medio no se ejecutan. | | `advance(segundos)` | Avanza el tiempo a mano y ejecuta las llamadas del camino (útil en tests). | | `queue(nombre)`, `clear_queue()` | Reproduce un clip cuando termina el actual. | | `speed` | La velocidad de todos los clips (`2` es el doble de rápido). `play(speed=...)` la multiplica. | ```python def repetir(self): self.player.play("intro", restart=True) self.player.queue("reposo") # en bucle después de la intro def poner_velocidad(self, etiqueta): self.player.speed = float(etiqueta.rstrip("×")) ``` #### Estado y eventos | Lee | Da | | --- | --- | | `current` | El nombre del clip que se reproduce o está en pausa (`None` si está detenido). | | `position`, `length` | Dónde está el cabezal y cuánto dura el clip, en segundos. | | `playing`, `paused`, `finished` | Qué está haciendo el reproductor. | | `queued` | Los clips que esperan su turno. | | `names`, `has(nombre)`, `clip(nombre)` | Los clips que conoce. | | `value(ruta)` | El valor que el clip actual le da a una ruta en este momento: `player.value(":puntos")`. | `on_started(nombre)` y `on_finished(nombre)` se llaman cuando un clip empieza y cuando termina (los clips en bucle nunca terminan). Un slider unido a `position` y `seek` hace de barra de búsqueda: ```python ui.slider(p.position, p.seek, min=0, max=p.length, step=0.01) ``` :::tip[Solo despierta a tu app cuando hace falta] Las pistas de widget se reproducen de forma nativa y nunca necesitan tu código. Un clip con pistas de atributo las actualiza hasta `fps` veces por segundo (60 por defecto, `pz.AnimationPlayer(..., fps=30)` para menos). Sin ellas, el reproductor solo despierta para la siguiente llamada o el final del clip. ::: #### Bibliotecas y archivos .pzanim Los clips son valores inmutables, así que puedes compartirlos entre reproductores y apps. Una `AnimationLibrary` los agrupa y los guarda en un archivo `.pzanim` (JSON) que puedes tener en tu proyecto y cargar en cualquier app: ```python biblioteca = pz.AnimationLibrary([INTRO, REPOSO], name="ui") biblioteca.save("ui.pzanim") player = pz.AnimationPlayer("ui.pzanim") # clips llamados "ui/intro", "ui/reposo" player.play("ui/intro") biblioteca = pz.load_animations("ui.pzanim") ``` Una biblioteca también tiene `add`, `remove`, `rename` y `names`. Los formularios hechos en Pyzed Design guardan sus clips dentro del `.pzform`; mira el [panel Animation](https://albakore.github.io/pyzed/es/editor/animations-and-effects/#animaciones-con-línea-de-tiempo). #### Reutilizar clips (Image: El mismo clip de rebote en tres widgets: en su propio objetivo, movido a otro widget y convertido en una pz.Animation) Los métodos de un clip devuelven un clip **nuevo**, así que el original no cambia: - `clip.retarget({"card": "panel"})` mueve las pistas del clip a otros objetivos. - `clip.animation("card")` convierte las pistas de un objetivo en una `pz.Animation`. Reprodúcela con `animate()` sin reproductor, o dale un nombre con `pz.define_animation`. - `clip.with_key(ruta, tiempo, valor)`, `without_key`, `with_track` y `without_track` lo editan. - `clip.to_dict()` y `pz.Clip.from_dict()` lo convierten en datos aptos para JSON y al revés. ### Movimiento reducido, tests y límites > Respeta el ajuste de reducir movimiento del sistema en Pyzed, prueba vistas animadas y clips de línea de tiempo, haz capturas estables y conoce los límites del sistema de animación. Source: https://albakore.github.io/pyzed/es/animation/motion-and-tests/ #### Movimiento reducido A algunas personas el movimiento las marea o las distrae, y activan "reducir movimiento" en su sistema. Pyzed sigue ese ajuste, que lee al arrancar la app: entonces cada animación **muestra su estado de reposo al momento**, sin moverse. - `pz.set_reduced_motion(True)` quita el movimiento, `False` lo activa y `None` vuelve a seguir al sistema. Ofrécelo como un ajuste de tu app. - La variable de entorno `PYZED_REDUCED_MOTION` (`1` o `0`) manda sobre ambos. Mira [Accesibilidad](https://albakore.github.io/pyzed/es/guides/accessibility/#reducir-movimiento). :::tip No escondas información solo en el movimiento. Si un campo se sacude con una contraseña incorrecta, muestra también el mensaje de error. ::: #### Tests y capturas Las capturas (`App.screenshot`, `pz.screenshot`) siempre muestran el **estado de reposo**: en lo que termina una animación de `mount`. Así los tests y las imágenes de la documentación son estables, sea cual sea el tiempo. Para comprobar que un widget se anima, mira el árbol de widgets: `repr(widget)` termina en `.animate(2 animation(s), 1 transition(s))`. Para probar una línea de tiempo, controla el reproductor a mano. `advance` ejecuta las llamadas y actualiza las pistas de atributo; `seek` salta sin ejecutar llamadas: ```python def test_intro_suena(): app = Juego() app.player.owner = app # una app en marcha lo fija; un test lo fija a mano app.player.pause() app.player.advance(1.7) # pasa la llamada campana() en 1.6 s assert app.mensaje == "¡Nuevo récord!" app.player.seek(0.5) assert app.player.value("card:y") < 40 ``` Un reproductor en pausa muestra su clip en el cabezal, incluso en una captura, así que puedes sacar una foto de cualquier momento de un clip: `app.player.pause(); app.player.seek(0.8); app.screenshot("intro.png")`. #### Límites - Las transformaciones son traslación, escala uniforme y rotación; no hay inclinación (skew). - Un widget girado o desenfocado recibe los clics donde está su caja en el layout, no donde se dibuja. - Un widget con rotación, desenfoque, desenfoque de fondo o bloom se dibuja encima de sus vecinos, así que su bloom puede iluminar a los widgets de al lado. - `backdrop_blur` desenfoca lo que la ventana dibujó antes del widget, dentro de la misma capa padre (los contenedores con scroll y otros widgets que recortan empiezan una capa nueva). - `width`, `height`, `padding` y `font_size` vuelven a calcular el layout en cada fotograma mientras se mueven, así que en vistas grandes prefiere `x`, `y` y `scale`. - Las animaciones vuelven a empezar cuando un widget sale del árbol y regresa. Dale un `key` estable, o usa [`presence`](https://albakore.github.io/pyzed/es/animation/transitions/#presence-y-stagger), para decidir cuándo se repiten. - Un reproductor reproduce un clip cada vez. Para dos escenas a la vez, usa dos reproductores. ## Por qué Pyzed ### ¿Por qué Pyzed? Interfaces Python modernas para una nueva generación > Por qué elegir Pyzed para apps de escritorio en Python: aspecto moderno sin esfuerzo, renderizado nativo por GPU desde Rust, API pythónica, diseñador visual, recarga en caliente y herramientas listas para IA. Una alternativa moderna a tkinter, Kivy y PyQt. Source: https://albakore.github.io/pyzed/es/why/ Python es el primer lenguaje de toda una generación de desarrolladores, pero sus interfaces de escritorio todavía parecen de 2005: widgets grises, diseños escritos a mano, reiniciar la app para ver un cambio y un empaquetado que es un proyecto en sí mismo. **Pyzed** es nuestra respuesta para quienes crecieron con VS Code, Figma, el modo oscuro y los asistentes de IA, y esperan que sus herramientas se vean y se sientan así. (Image: Un espacio de trabajo estilo editor hecho con Pyzed, con el tema One Dark) #### Lo que tienes desde el primer día **Aspecto moderno, sin trabajo de estilos** Decenas de componentes inspirados en el editor [Zed](https://zed.dev): botones, campos, pestañas, árboles, paletas de comandos, menús, avisos, un editor de código y paneles acoplables. Los temas (One, Ayu, Gruvbox), el modo oscuro y seguir la apariencia del sistema vienen incluidos. **Velocidad nativa, dibujada por Rust** El diseño, el texto y el dibujo se ejecutan en Rust con [Iced](https://iced.rs) y [wgpu](https://wgpu.rs) (Vulkan, Metal, DirectX 12 u OpenGL), con un renderizador por CPU de respaldo. Python solo describe la interfaz. **Una API pythónica** El estado vive en una clase normal. `view()` devuelve lo que se muestra y los callbacks son métodos o lambdas. Sin señales ni slots, sin archivos `.kv`, sin IDs de widgets que buscar. **Un diseñador visual** [Pyzed Design](https://albakore.github.io/pyzed/es/editor/) te deja dibujar formularios, editar su código con autocompletado y diagnósticos, y ejecutarlos, todo en una sola app. **Recarga en caliente que conserva el estado** `pyzed dev main.py`: guardas un archivo y la ventana cambia, con tus datos todavía ahí. **Lista para asistentes de IA** Un [servidor MCP](https://albakore.github.io/pyzed/es/guides/mcp/) da a los agentes de código la API exacta, la documentación y capturas de lo que construyen, y [`pyzed.agents`](https://albakore.github.io/pyzed/es/guides/agents/) pone modelos dentro de tu app. #### Todo el flujo, en unos pocos comandos Los proyectos son proyectos [uv](https://docs.astral.sh/uv/) normales, así que las dependencias, los lock files, el linter ([ruff](https://docs.astral.sh/ruff/)) y la comprobación de tipos ([ty](https://docs.astral.sh/ty/)) funcionan como en el resto del Python moderno. Mira [Crear un proyecto](https://albakore.github.io/pyzed/es/getting-started/new-project/) y [Compilar y distribuir](https://albakore.github.io/pyzed/es/packaging/build/). #### Código que se lee como la interfaz Un contador, completo: ```python title="contador.py" import pyzed as pz from pyzed import ui class Contador(pz.App): title = "Contador" 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(Contador) ``` No hay ningún widget que buscar y actualizar cuando cambia la cuenta: cambias `self.count` y Pyzed vuelve a llamar a `view()`. Es la [arquitectura Elm](https://guide.elm-lang.org/architecture/) que usa Iced, la misma idea que React o SwiftUI, así que te resultará familiar si vienes de la web o del móvil. (Image: Componentes de Pyzed con el tema One Dark) #### Hecho para cómo se desarrolla hoy - **Editar, guardar, ver.** La recarga en caliente conserva el estado de la app y, si el código nuevo falla, el error se muestra en la ventana hasta que guardes una corrección. (Image: Una recarga fallida muestra el error dentro de la ventana) - **Tu editor conoce la API.** El paquete incluye información de tipos (`py.typed` y stubs del núcleo en Rust), así que el autocompletado y la comprobación de tipos funcionan en VS Code, PyCharm, Zed y con ty o mypy. - **Capturas sin pantalla.** `app.screenshot("vista.png")` renderiza fuera de pantalla, sin ventana ni GPU, para tests, CI y documentación. Todas las imágenes de este sitio se hicieron así. - **Agentes que no adivinan.** Conecta el [servidor MCP](https://albakore.github.io/pyzed/es/guides/mcp/) a Claude Code, Cursor, VS Code o Zed y tu asistente consulta firmas reales, lee esta documentación y comprueba el código que escribe. La documentación también se publica como texto plano para modelos de lenguaje en [`llms.txt`](https://albakore.github.io/pyzed/llms.txt). - **Async cuando lo necesitas.** Los callbacks pueden ser funciones `async def` o generadores; Pyzed los ejecuta y envía sus resultados a `update`. Mira [Tareas en segundo plano](https://albakore.github.io/pyzed/es/guides/async/). #### Límites, con honestidad Pyzed es joven: su primera versión salió en 2026, mientras que tkinter, Qt y wxPython tienen décadas detrás. Hoy apunta **solo al escritorio** (Windows, macOS y Linux): no genera apps para Android, iOS ni el navegador, algo que sí ofrecen [Kivy](https://albakore.github.io/pyzed/es/why/vs-kivy/) y [Flet](https://albakore.github.io/pyzed/es/why/vs-flet/). **Los lectores de pantalla todavía no están soportados** porque Iced no expone información de accesibilidad ([detalles](https://albakore.github.io/pyzed/es/guides/accessibility/#lectores-de-pantalla)), y su ecosistema de widgets de terceros es pequeño. La [comparativa](https://albakore.github.io/pyzed/es/why/comparison/) explica cuándo otras bibliotecas son mejor opción. #### Comparar - [Comparativa de funciones](https://albakore.github.io/pyzed/es/why/comparison/): Pyzed junto a tkinter, Qt, Kivy, Dear PyGui, Flet, Toga, NiceGUI, wxPython y CustomTkinter. - [Benchmarks](https://albakore.github.io/pyzed/es/why/benchmarks/): Arranque, memoria, velocidad de actualización, tamaño instalado y líneas de código, medidos y reproducibles. - [Experiencia de desarrollo](https://albakore.github.io/pyzed/es/why/developer-experience/): De la instalación a la app empaquetada: cómo es el día a día. - [Pyzed vs tkinter](https://albakore.github.io/pyzed/es/why/vs-tkinter/): La biblioteca estándar, lado a lado. - [Pyzed vs PyQt / PySide](https://albakore.github.io/pyzed/es/why/vs-pyqt-pyside/): El toolkit más completo, y cuándo compensa su peso. - [Pyzed vs Kivy](https://albakore.github.io/pyzed/es/why/vs-kivy/): Widgets dibujados por GPU en escritorio y móvil. - [Pyzed vs Flet](https://albakore.github.io/pyzed/es/why/vs-flet/): Flutter desde Python, para escritorio, web y móvil. - [Pyzed vs Dear PyGui](https://albakore.github.io/pyzed/es/why/vs-dear-pygui/): Interfaces en modo inmediato para herramientas y gráficas. ### Comparativa de frameworks GUI para Python (2026) > Pyzed comparado con tkinter, PyQt/PySide (Qt), Kivy, Dear PyGui, Flet, Toga, NiceGUI, wxPython y CustomTkinter: licencia, renderizado, aspecto, diseñador, recarga en caliente, async, móvil y web, empaquetado, tipos y herramientas de IA. Source: https://albakore.github.io/pyzed/es/why/comparison/ Cada toolkit GUI de Python hace concesiones distintas. Esta página las pone lado a lado, incluidos los casos en los que Pyzed **no** es la mejor elección. Para números medidos, mira los [Benchmarks](https://albakore.github.io/pyzed/es/why/benchmarks/). :::note[Cómo leer las tablas] Los datos se comprobaron con la documentación de cada proyecto en octubre de 2026. «De terceros» significa que existe pero fuera del proyecto; «–» significa que el proyecto no lo ofrece (hasta donde pudimos encontrar). Si algo está desactualizado, [abre un issue](https://github.com/albakore/pyzed/issues) y lo corregimos. ::: #### Los contendientes | Toolkit | Qué es | Desde | | --- | --- | --- | | **[Pyzed](https://github.com/albakore/pyzed)** | API de Python sobre [Iced](https://iced.rs) (Rust), con componentes modernos listos para usar | 2026 | | **[tkinter](https://docs.python.org/es/3/library/tkinter.html)** | El binding estándar de Python a Tcl/Tk | años 90 | | **[CustomTkinter](https://customtkinter.tomschimansky.com)** | Widgets de aspecto moderno dibujados sobre tkinter | 2021 | | **[PyQt](https://www.riverbankcomputing.com/software/pyqt/) / [PySide](https://doc.qt.io/qtforpython-6/)** | Bindings al framework Qt (C++) | 1998 / 2009 | | **[wxPython](https://wxpython.org)** | Bindings a wxWidgets, que envuelve los widgets nativos | 1998 | | **[Kivy](https://kivy.org)** | Widgets dibujados con OpenGL, con su propio lenguaje KV, para escritorio y móvil | 2011 | | **[Dear PyGui](https://github.com/hoffstadt/DearPyGui)** | Bindings a Dear ImGui, un toolkit en modo inmediato por GPU | 2020 | | **[Flet](https://flet.dev)** | Python controlando una interfaz Flutter | 2022 | | **[Toga](https://toga.readthedocs.io)** | El toolkit de BeeWare sobre los widgets nativos de cada plataforma | 2014 | | **[NiceGUI](https://nicegui.io)** | Interfaz web (Vue + Quasar) servida desde Python, en un navegador o un webview | 2021 | #### Renderizado, aspecto y plataformas | Toolkit | Licencia | Cómo dibuja | Aspecto por defecto | Escritorio | Móvil | Web | | --- | --- | --- | --- | --- | --- | --- | | **Pyzed** | MIT | Rust: wgpu (GPU), tiny-skia (CPU de respaldo) | Estilo de editor de código moderno, temas, modo oscuro | Win, macOS, Linux | – | – | | tkinter | PSF + Tcl/Tk (estilo BSD) | Widgets propios de Tk; `ttk` usa temas nativos en Windows y macOS | Clásico; ttk más cercano a lo nativo | Win, macOS, Linux | – | – | | CustomTkinter | MIT | Dibujo en canvas de Tk | Moderno, claro y oscuro | Win, macOS, Linux | – | – | | PyQt / PySide | GPL o comercial / LGPL o comercial | Qt Widgets (raster por CPU) o Qt Quick (grafo de escena por GPU) | Estilos casi nativos, totalmente personalizables | Win, macOS, Linux | PySide: Android (vista previa técnica) | – | | wxPython | wxWindows Library Licence | Widgets nativos de cada sistema | Nativo | Win, macOS, Linux | – | – | | Kivy | MIT | OpenGL (ES 2) | Propio de Kivy; Material con KivyMD | Win, macOS, Linux | Android, iOS | – | | Dear PyGui | MIT | Dear ImGui sobre DirectX 11, Metal u OpenGL | Estilo de herramienta ImGui | Win, macOS, Linux | – | – | | Flet | Apache 2.0 | Flutter (Skia / Impeller) en un proceso cliente aparte | Material o Cupertino | Win, macOS, Linux | Android, iOS | Sí | | Toga | BSD 3-Clause | Widgets nativos (GTK, Cocoa, WinForms, Android, iOS) | Nativo | Win, macOS, Linux | Android, iOS | Experimental | | NiceGUI | MIT | El navegador (HTML, CSS, JavaScript) | Quasar (Material) | En navegador o webview | A través del navegador | Sí | #### Herramientas y experiencia de desarrollo | Toolkit | Diseñador visual | Recarga en caliente | `async` | Tipos | Empaquetado | | --- | --- | --- | --- | --- | --- | | **Pyzed** | **[Pyzed Design](https://albakore.github.io/pyzed/es/editor/)**, incluido | **Sí, conserva el estado** (`pyzed dev`) | Sí (corrutinas y generadores async en callbacks) | Sí (`py.typed`, stubs del núcleo) | `pyzed build` (PyInstaller) | | tkinter | De terceros (PAGE, Pygubu) | – | – (integración manual del bucle) | Stubs en typeshed | PyInstaller, Nuitka… | | CustomTkinter | – | – | – | Parcial | PyInstaller, Nuitka… | | PyQt / PySide | Qt Designer, Qt Creator, Qt Design Studio | Vista previa de QML en las herramientas de Qt; no para código Widgets | PySide: QtAsyncio; `qasync` de terceros | Sí (stubs incluidos) | `pyside6-deploy` (Nuitka), PyInstaller | | wxPython | De terceros (wxFormBuilder, wxGlade) | – | De terceros (`wxasync`) | Parcial | PyInstaller… | | Kivy | – | De terceros | Sí (bucles de asyncio y trio) | Parcial | Buildozer, python-for-android, kivy-ios, PyInstaller | | Dear PyGui | – | – | – (bucle de render propio) | Sí (API anotada) | PyInstaller… | | Flet | – | Sí (`flet run` reinicia la app al guardar) | Sí | Sí | `flet build` (requiere el SDK de Flutter) | | Toga | – | – | Sí (asyncio) | Parcial | [Briefcase](https://briefcase.readthedocs.io) | | NiceGUI | – | Sí (recarga del servidor, el estado de la página se reinicia) | Sí | Sí | Despliegue web; `nicegui-pack` para escritorio | #### Asistentes de IA, accesibilidad y madurez | Toolkit | Herramientas de IA del proyecto | Lectores de pantalla | Madurez y ecosistema | | --- | --- | --- | --- | | **Pyzed** | **[Servidor MCP](https://albakore.github.io/pyzed/es/guides/mcp/), [`llms.txt`](https://albakore.github.io/pyzed/llms.txt), [`pyzed.agents`](https://albakore.github.io/pyzed/es/guides/agents/), panel de agente en el diseñador** | **Todavía no** ([por qué](https://albakore.github.io/pyzed/es/guides/accessibility/#lectores-de-pantalla)) | **Nuevo (2026)**, ecosistema pequeño | | tkinter | – | Limitado | Décadas de respuestas, tutoriales y libros; en toda instalación de Python | | CustomTkinter | – | Limitado (como tkinter) | Popular, un mantenedor principal | | PyQt / PySide | Qt AI Assistant en Qt Creator (comercial, orientado a C++/QML) | Sí | El toolkit más completo: gráficas, multimedia, motor web, 3D… | | wxPython | – | Sí (widgets nativos) | Longevo, comunidad más pequeña hoy | | Kivy | – | – | Maduro, fuerte en táctil y móvil | | Dear PyGui | – | – | Excelente para herramientas y gráficas; modelo en modo inmediato | | Flet | [Servidor MCP](https://flet.dev/docs/cookbook/flet-mcp) | Sí (semántica de Flutter) | Crece rápido, gran conjunto de widgets de Flutter | | Toga | – | Sí (widgets nativos) | Parte de BeeWare; todavía antes de la 1.0 | | NiceGUI | Servidores MCP de la comunidad (de terceros) | Accesibilidad del navegador | Activo; lo mejor para paneles web | #### ¿Cuál debería usar? - **Necesitas Android o iOS:** Kivy, Flet o Toga. Pyzed es solo para escritorio. - **Necesitas una app web:** NiceGUI o Flet (o un framework web). Pyzed dibuja ventanas nativas. - **El soporte de lectores de pantalla es un requisito hoy:** Qt, wxPython, Toga o Flet. - **Necesitas un conjunto enorme de widgets** (gráficas, vídeo, un navegador embebido, 3D) y te encajan su tamaño y su licencia: Qt. Mira [Pyzed vs PyQt/PySide](https://albakore.github.io/pyzed/es/why/vs-pyqt-pyside/). - **Un script diminuto que debe funcionar sin instalar nada:** tkinter, que viene con Python. Mira [Pyzed vs tkinter](https://albakore.github.io/pyzed/es/why/vs-tkinter/). - **Gráficas en tiempo real y herramientas de depuración:** Dear PyGui es excelente. Mira [Pyzed vs Dear PyGui](https://albakore.github.io/pyzed/es/why/vs-dear-pygui/). - **Una app de escritorio moderna** (una herramienta interna, un editor, un panel, un lanzador, un cliente para tu API) que se vea bien sin trabajo de diseño, arranque rápido, se construya visualmente y con ayuda de IA, y se distribuya como un único ejecutable: **para eso está Pyzed.** Empieza con el [inicio rápido](https://albakore.github.io/pyzed/es/getting-started/quickstart/). ### Benchmarks de GUI en Python — Pyzed vs tkinter, Qt, Kivy, Flet… > Benchmarks medidos y reproducibles de toolkits GUI de Python: tiempo de importación, tiempo hasta el primer fotograma, memoria, latencia al actualizar 1.000 filas, tamaño instalado y líneas de código de Pyzed, tkinter, CustomTkinter, PySide6, Kivy, Dear PyGui, Flet y Toga. Source: https://albakore.github.io/pyzed/es/why/benchmarks/ Estos números se **midieron**, no se estimaron: cada gráfica de esta página se dibuja a partir del archivo JSON que escribió el script de benchmarks, y puedes ejecutar el mismo script en tu máquina. Vienen de una sola máquina sin GPU, así que léelos como una comparación entre toolkits en las mismas condiciones, no como números absolutos para tu ordenador. :::caution[Renderizado por software] La máquina no tiene GPU. Las ventanas se mostraron en un servidor X virtual (Xvfb) y todos los toolkits que usan OpenGL o Vulkan (Pyzed, Kivy, Dear PyGui, Flet) renderizaron en la CPU con los drivers llvmpipe y lavapipe de Mesa. Es el peor caso para los toolkits por GPU y el caso normal para tkinter, Qt Widgets y GTK, que dibujan en la CPU de todos modos. En un ordenador con GPU, la parte de dibujo de los toolkits por GPU sale de la CPU. ::: #### Conclusiones principales - **El arranque es rápido en todos salvo en Flet y, con muchos widgets, en Kivy y Toga.** Pyzed muestra una ventana de 10 filas en unos 0,16 s desde el lanzamiento y una de 1.000 filas en unos 0,24 s: construir y distribuir 2.000 widgets más le cuesta unos 80 ms. tkinter, Dear PyGui y Qt arrancan antes; Kivy, Toga y Flet necesitan casi un segundo para 1.000 filas. - **Actualizar 1.000 filas le lleva a Pyzed unos 18 ms** desde el cambio de estado hasta la pantalla, aunque reconstruye toda la vista desde Python cada vez. Es algo más rápido que Qt Widgets, unas 3 veces más rápido que Kivy y 9 veces más que Flet; tkinter y Dear PyGui son aún más rápidos. Toga (GTK) necesitó unos 37 s por actualización en esta prueba, porque cada cambio de etiqueta provoca un nuevo cálculo del diseño. - **La memoria depende del renderizador.** Con el driver Vulkan por software usado aquí, el renderizador por GPU de Pyzed ocupa unos 151 MB; la misma app con el renderizador por CPU de Pyzed (tiny-skia, activado con `ICED_BACKEND=tiny-skia`) ocupa unos 48 MB, entre tkinter (25 MB) y Qt (72 MB). Los demás toolkits por GPU pagan un coste de driver parecido aquí (Dear PyGui 123 MB, Kivy 146 MB, Flet 268 MB). - **Tamaño instalado:** unos 30 MB para Pyzed, frente a 238 MB de lo esencial de Qt, 83 MB de Kivy y 51 MB de Flet con su cliente. tkinter, Dear PyGui, Toga (que usa el GTK del sistema) y CustomTkinter (que usa tkinter) son más pequeños. - **Código:** la lista de tareas ocupa 31 líneas en Pyzed, la mitad que en PySide6 (62) y muy por debajo de Kivy (81). La de Flet es la más corta (22). #### Arranque **Importar** es el tiempo de hacer `import` del toolkit en un proceso de Python nuevo. **Primer fotograma** es el tiempo desde que se lanza el proceso de la app hasta que su ventana está en pantalla con su contenido: el script vigila la pantalla buscando una barra de color en la parte inferior de la ventana. #### Memoria Memoria residente (RSS) de los procesos de la app un segundo después del primer fotograma. En Flet incluye el proceso cliente de Flutter que dibuja la ventana. #### Actualizar 1.000 filas Cada 250 ms la app cambia el texto de las 1.000 etiquetas y el color de la barra inferior en la misma actualización. La latencia es el tiempo desde que la app cambia su estado hasta que el color nuevo está en pantalla; 10 actualizaciones por lanzamiento, 7 lanzamientos. #### Tamaño instalado Cuánto crece un entorno virtual nuevo al instalar el toolkit con sus dependencias. tkinter cuenta los archivos de Tcl/Tk que vienen con Python. Flet incluye el cliente de escritorio que descarga en la primera ejecución. Toga no incluye GTK, que viene del sistema operativo. NiceGUI aparece solo como referencia: dibuja en un navegador web, así que no formó parte de los benchmarks de ventanas. #### Líneas de código Las mismas apps de contador y lista de tareas escritas en cada toolkit ([código](https://github.com/albakore/pyzed/tree/main/benchmarks/apps)), contando las líneas que no son vacías, comentarios ni docstrings. Mira [Experiencia de desarrollo](https://albakore.github.io/pyzed/es/why/developer-experience/#cuánto-código) para las gráficas y qué hacen esas líneas. #### Números en bruto Mediana en negrita; debajo, el mínimo–máximo y el número de muestras. Los resultados completos, con cada muestra, están en [`benchmarks/results/`](https://github.com/albakore/pyzed/tree/main/benchmarks/results). #### Máquina y versiones
  • Fecha: {bench.date}
  • Sistema: {m.os}
  • CPU: {m.cpu}, {m.cores} núcleos; {m.ram_gb} GB de RAM (un contenedor en la nube)
  • Pantalla: Xvfb (framebuffer virtual de X, sin GPU); OpenGL/Vulkan con llvmpipe/lavapipe de Mesa {m.mesa}
  • Python {m.python} (python-build-standalone, con uv); Tk {m.tk}
  • Toolkits:{' '} {versionsLine()}
#### Método - **La misma app en todos.** El `bench.py` de cada toolkit construye una ventana de 640×480 con una lista desplazable de N filas (una etiqueta y un botón «Open» cada una) y una barra de 40 px abajo pintada con un color centinela. Las listas se construyen de la forma sencilla que documenta cada toolkit, con un widget por fila: sin `RecycleView` en Kivy, sin `ListView` perezoso en Flet, sin `virtual_list` en Pyzed. - **El primer fotograma** se mide desde fuera: el harness lanza el proceso y captura la pantalla más o menos cada milisegundo (con [mss](https://github.com/BoboTiG/python-mss)) hasta que hay 8.000 píxeles visibles del color centinela. - **La memoria** es el RSS del proceso y sus hijos ([psutil](https://github.com/giampaolo/psutil)), un segundo después del primer fotograma. - **Las actualizaciones** se miden entre la marca de tiempo de la propia app (`time.monotonic()`, impresa justo antes de cambiar su estado) y el momento en que el harness ve el nuevo color de la barra. - **Repeticiones:** 7 lanzamientos por toolkit y tamaño, 10 actualizaciones por lanzamiento, 10 importaciones; las gráficas muestran la mediana y una línea fina del mínimo al máximo. - **Pyzed (CPU renderer)** es la misma app de Pyzed arrancada con `ICED_BACKEND=tiny-skia`, que hace que Iced use su renderizador por CPU en vez de wgpu: lo que Pyzed usa en máquinas sin un driver de GPU que funcione. - **Tamaño instalado:** un `uv venv` nuevo, `uv pip install --link-mode=copy `, diferencia de tamaño. #### Reprodúcelo Sin pantalla, arranca Xvfb por su cuenta; en un escritorio, deja las ventanas del benchmark a la vista mientras se ejecuta. Mira [`benchmarks/README.md`](https://github.com/albakore/pyzed/blob/main/benchmarks/README.md) para las opciones y los paquetes del sistema que necesitan algunos toolkits. Los resultados en otras máquinas son muy bienvenidos en un [issue](https://github.com/albakore/pyzed/issues). #### Advertencias - **Una máquina, sin GPU** (ver arriba). Los resultados en Windows y macOS, y con una GPU real, serán distintos. - **El harness comparte la CPU** con la app: capturar la pantalla cuesta más o menos un núcleo mientras sondea. - **Tk dibuja widget a widget**, así que en tkinter la barra puede aparecer un momento antes de que se hayan dibujado todas las filas; los toolkits por GPU presentan fotogramas completos. - **Dear PyGui redibuja continuamente**, así que su latencia de actualización incluye esperar a su siguiente fotograma; además usa CPU en reposo, algo que aquí no se mide. - **CustomTkinter no tiene resultados con 1.000 filas**: su app murió en Xvfb con un error de X (BadAlloc o BadValue en `CreatePixmap`) cuando su marco desplazable tenía más de unas 100 filas. - **No medidos:** wxPython no tiene wheels para Linux en PyPI y compilarlo desde el código fuente no era práctico aquí; NiceGUI se ejecuta en un navegador web, así que medir una ventana de escritorio mediría el navegador. - **Las líneas de código** dependen del estilo; las apps siguen las formas documentadas de cada toolkit, y las mejoras son bienvenidas. ### Experiencia de desarrollo con Pyzed > Cómo es construir una app de escritorio en Python con Pyzed en el día a día: instalación, primera ventana, recarga en caliente, diseñador visual, autocompletado y tipos, mensajes de error, empaquetado y asistentes de IA, comparado con otros toolkits GUI de Python. Source: https://albakore.github.io/pyzed/es/why/developer-experience/ Los números de velocidad importan menos que lo que se siente con un toolkit a las once de la noche cuando intentas terminar algo. Esta página recorre un proyecto Pyzed desde el primer comando hasta la app empaquetada, y compara cada paso con lo que te piden los demás toolkits. Donde decimos «medimos» o «nos encontramos», ocurrió al preparar los [benchmarks](https://albakore.github.io/pyzed/es/why/benchmarks/). #### 1. Instalar Un solo wheel con el núcleo en Rust ya compilado, para Windows, macOS y Linux, y sin dependencias en tiempo de ejecución. Compara con lo que nos encontramos en un contenedor Ubuntu 24.04 limpio: | Toolkit | Qué necesitó `pip install` allí | | --- | --- | | Pyzed | Nada más (un Linux de escritorio ya tiene la biblioteca `libxkbcommon-x11` que carga; nuestro contenedor mínimo no) | | tkinter | No se instala con pip: viene con Python, o con un paquete del sistema `python3-tk` en Debian y Ubuntu | | PySide6 | Nada más, pero una descarga grande (mira el [tamaño instalado](https://albakore.github.io/pyzed/es/why/benchmarks/#tamaño-instalado)) | | Kivy, Dear PyGui | Nada más | | Flet | `flet[desktop]`; el cliente de escritorio se descarga la primera vez que se ejecuta una app, y en Linux necesitó los paquetes del sistema `libsecret` y `libgles2` | | Toga | Falló hasta instalar `libgirepository-2.0-dev` y `libcairo2-dev` con apt, porque PyGObject se compiló desde el código fuente | | wxPython | Sin wheels para Linux en PyPI: pip compila wxWidgets desde el código fuente (hay wheels para algunas distribuciones en wxpython.org) | #### 2. Primera ventana o un [proyecto uv](https://albakore.github.io/pyzed/es/guides/projects/) completo con linter, tipos y git con `pyzed new hola`. La app es una clase con un `view()`: ```python import pyzed as pz from pyzed import ui class Hola(pz.App): title = "Hola" nombre = "" def view(self): return pz.center(pz.column([ ui.input(self.nombre, lambda texto: setattr(self, "nombre", texto), placeholder="Tu nombre"), ui.heading(f"¡Hola, {self.nombre or 'mundo'}!"), ], spacing=12, max_width=320)) pz.run(Hola) ``` Ya tiene tema oscuro, anillos de foco, estados al pasar el ratón y texto bien renderizado; nada que estilizar. ##### ¿Cuánto código? Escribimos las mismas dos apps en cada toolkit, con el estilo de la documentación de cada proyecto ([código](https://github.com/albakore/pyzed/tree/main/benchmarks/apps)), y contamos las líneas que no son vacías, comentarios ni docstrings. El número de líneas depende del estilo, así que tómalo como guía aproximada. La diferencia mayor es *qué* hacen esas líneas: en tkinter, Qt, Flet y Dear PyGui parte del código copia datos a los widgets y los mantiene sincronizados; en Pyzed, `view()` es el único lugar que convierte datos en widgets. #### 3. Recarga en caliente Edita `view()`, un módulo auxiliar o un estilo y guarda: la ventana se actualiza en el sitio, **con tus datos todavía ahí** (el objeto de la app se conserva y recibe la clase nueva). Los métodos nuevos y los atributos nuevos de `__init__` aparecen sin reiniciar, y un valor inicial cambiado se aplica si la app no lo modificó (ver [Recarga en caliente](https://albakore.github.io/pyzed/es/guides/hot-reload/)). Si el código nuevo falla, el error aparece en la ventana, y el siguiente guardado que lo corrige devuelve la app. (Image: Antes de guardar) (Image: Después de guardar: mismos datos, nuevo diseño) Flet reinicia la app al guardar y NiceGUI recarga su servidor, así que el estado en memoria empieza de cero. Qt ofrece una vista previa en vivo para QML en sus herramientas, no para código Widgets en Python. tkinter, Kivy, Dear PyGui y Toga necesitan reiniciar (o una herramienta de terceros). #### 4. Un diseñador visual que escribe Python `pyzed design` abre [Pyzed Design](https://albakore.github.io/pyzed/es/editor/): arrastra componentes a un formulario, ajusta sus propiedades, conecta eventos a manejadores en Python, edita código con autocompletado y diagnósticos (ty y ruff) y ejecuta el formulario. (Image: Pyzed Design) Qt tiene los diseñadores más potentes (Qt Designer, Qt Creator, Qt Design Studio). tkinter y wxPython tienen diseñadores de terceros; Kivy, Dear PyGui, Flet, Toga y NiceGUI no tienen. #### 5. Autocompletado y comprobación de tipos El paquete tiene tipos (`py.typed`, con stubs del núcleo en Rust), así que los editores completan los argumentos de los widgets y los comprobadores de tipos detectan errores antes de ejecutar nada: ```python ui.button("Guardar", self.guardar, variant="solid", size="sm", keys="ctrl-s") # ^ el autocompletado lista cada opción, con su documentación ``` Los proyectos nuevos ejecutan [ty](https://docs.astral.sh/ty/) y [ruff](https://docs.astral.sh/ruff/) con `pyzed check`. #### 6. Mensajes de error que dicen qué arreglar Los errores se detectan al crear el widget, con el valor que estaba mal: ```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' ``` Una excepción dentro de `view()` o de un callback aparece en la ventana (con recarga en caliente) o como un traceback normal de Python que apunta a tu línea, no al código C++ o Rust de debajo. #### 7. Capturas y tests sin pantalla ```python app = Todo() app.tasks = [["Escribir la documentación", False], ["Publicar", True]] app.screenshot("tareas.png", size=(420, 320)) ``` `screenshot` renderiza fuera de pantalla con la CPU, así que funciona en CI y en tests; mira [Tests y capturas](https://albakore.github.io/pyzed/es/guides/testing/). Otros toolkits necesitan una pantalla virtual (Xvfb) o un capturador de pantalla para lo mismo. #### 8. Empaquetado `pyzed build` envuelve PyInstaller y añade lo que los formularios cargan en tiempo de ejecución. En otros: Qt tiene `pyside6-deploy` (Nuitka), `flet build` de Flet necesita el SDK de Flutter, Toga usa Briefcase (muy bueno, y además apunta a móviles), Kivy usa Buildozer para Android y PyInstaller para escritorio. Mira [Compilar y distribuir](https://albakore.github.io/pyzed/es/packaging/build/). #### 9. Trabajar con asistentes de IA Los modelos de lenguaje escriben mejor código para APIs que pueden consultar. Pyzed les da: - un [servidor MCP](https://albakore.github.io/pyzed/es/guides/mcp/) con la firma exacta de cada widget, la documentación, comprobaciones de código y capturas del resultado, para que un agente en Claude Code, Cursor, VS Code o Zed vea lo que ha construido; - esta documentación como texto plano en [`llms.txt`](https://albakore.github.io/pyzed/llms.txt); - el [panel de agente](https://albakore.github.io/pyzed/es/editor/ai-agents/) del diseñador, que edita formularios contigo; - [`pyzed.agents`](https://albakore.github.io/pyzed/es/guides/agents/) para poner modelos con herramientas dentro de tu propia app. Flet también publica un [servidor MCP](https://flet.dev/docs/cookbook/flet-mcp); Qt ofrece Qt AI Assistant en Qt Creator (comercial, orientado a C++ y QML). ### Pyzed vs tkinter — una alternativa moderna a tkinter > Pyzed vs tkinter (y CustomTkinter) para apps de escritorio en Python: las mismas apps lado a lado, arranque, memoria y velocidad medidos, equivalencias de conceptos y cómo migrar de tkinter a Pyzed. Source: https://albakore.github.io/pyzed/es/why/vs-tkinter/ [tkinter](https://docs.python.org/es/3/library/tkinter.html) es el toolkit gráfico que viene con Python. Está en todas partes, es pequeño y arranca rápido, por eso casi todo el mundo empieza con él. También se le nota la edad: widgets con aspecto de los 2000, un modelo de diseño repartido entre `pack`, `grid` y `place`, y nada de modo oscuro, recarga en caliente ni diseñador visual. [CustomTkinter](https://customtkinter.tomschimansky.com) moderniza el aspecto sobre tkinter, y también aparece en los números de abajo. #### En resumen | | Pyzed | tkinter | | --- | --- | --- | | Instalación | `pip install pyzed-ui` | Ya está (en Linux, a veces un paquete `python3-tk`) | | Aspecto | Componentes modernos listos, temas, modo oscuro | Tk clásico, o temas `ttk` | | Modelo | Estado + `view()`: la interfaz se reconstruye desde tus datos | Widgets que creas, guardas y modificas | | Diseño | Filas, columnas, rejillas, pilas, docks, anclas | `pack`, `grid`, `place` | | Dibujo | GPU (wgpu), CPU de respaldo | Tk, en la CPU | | Diseñador, recarga | Pyzed Design, `pyzed dev` | Diseñadores de terceros; sin recarga en caliente | | Widgets avanzados | Editor de código, paleta de comandos, árbol de archivos, pestañas, avisos, markdown… | Text, Canvas, Treeview; el resto lo haces tú | | Accesibilidad | Teclado; sin lectores de pantalla todavía | Limitada | #### El mismo contador Ambos archivos son las apps de referencia de [los benchmarks](https://albakore.github.io/pyzed/es/why/benchmarks/), contadas línea a línea. #### La misma lista de tareas Añadir con el botón o con Enter, marcar tareas, borrarlas y mostrar cuántas quedan. En tkinter, mantener la pantalla sincronizada con los datos es cosa tuya (`refresh()` destruye y vuelve a crear las filas); en Pyzed, `view()` siempre describe los datos actuales. #### Medido Medianas de [los benchmarks](https://albakore.github.io/pyzed/es/why/benchmarks/) (Linux, sin GPU: renderizado por software, que penaliza al renderizador por GPU de Pyzed y favorece el dibujo por CPU de tkinter). tkinter gana en arranque y tamaño: son unos pocos megas de C que ya vienen con Python, y dibuja con la CPU, justo lo que una máquina sin GPU hace bien. Pyzed trae un renderizador por GPU, un motor de texto y decenas de componentes, y aun así abre una ventana con 1.000 filas en una fracción de segundo. #### De los conceptos de tkinter a Pyzed | tkinter | Pyzed | | --- | --- | | `root = tk.Tk()` … `root.mainloop()` | `class MiApp(pz.App)` … `pz.run(MiApp)` | | `root.title(...)`, `root.geometry("400x300")` | atributos de clase `title = ...`, `size = (400, 300)` | | `tk.StringVar`, `IntVar` | atributos normales: `self.nombre = ""` | | `widget.configure(text=...)` | cambia el atributo; `view()` lo muestra | | `ttk.Label`, `ttk.Button`, `ttk.Entry` | `ui.label`, `ui.button`, `ui.input` | | `command=callback` | `on_press=callback` (o un mensaje para `update()`) | | `pack(side="left")` / `grid(row, column)` | `pz.row([...])`, `pz.column([...])`, `pz.grid(...)` | | `Canvas` + `Scrollbar` + `Frame` interior | `pz.scrollable(...)` | | `ttk.Treeview` | [`ui.tree`](https://albakore.github.io/pyzed/es/library/tree/), [`ui.table`](https://albakore.github.io/pyzed/es/library/table/) | | `root.after(ms, fn)` | `pz.every(segundos, mensaje)` en `subscriptions()`, o un callback `async` | | `messagebox` | [`ui.confirm`](https://albakore.github.io/pyzed/es/library/confirm/), [`ui.prompt`](https://albakore.github.io/pyzed/es/library/prompt/), [`ui.modal`](https://albakore.github.io/pyzed/es/library/modal/) | | `filedialog` | Todavía no hay diálogo de archivos nativo: usa un [`ui.tree`](https://albakore.github.io/pyzed/es/library/tree/) de archivos o una biblioteca auxiliar | | `ttk.Style` | [temas](https://albakore.github.io/pyzed/es/theming/themes/) y [estilos](https://albakore.github.io/pyzed/es/theming/styles/) | #### Consejos de migración 1. **Saca el estado de los widgets.** Cada `StringVar`, `IntVar` y cada valor que lees con `.get()` pasa a ser un atributo de tu `App`. Ese es casi todo el trabajo. 2. **Sustituye el código de actualización por `view()`.** Las funciones que destruyen y reconstruyen frames o hacen `configure` sobre etiquetas desaparecen: escribe `view()` una vez a partir de los atributos. 3. **Traduce los diseños uno a uno.** Un `Frame` empaquetado en horizontal es un `pz.row`, en vertical un `pz.column`; `grid` pasa a `pz.grid`; el espaciado y el relleno son argumentos en vez de `padx`/`pady` en cada widget. 4. **Temporizadores e hilos.** Los bucles con `root.after` pasan a `subscriptions()`; el trabajo largo va en un callback `async def` o en un hilo, y su resultado vuelve como mensaje. 5. **Deja tkinter donde encaja.** Un diálogo de cinco líneas en un script que debe funcionar con un Python pelado sigue siendo un buen caso para tkinter. #### Cuándo elegir cada uno - **Elige tkinter** para utilidades diminutas y material didáctico que debe funcionar sin instalar nada, o cuando tus usuarios necesitan lectores de pantalla en plataformas donde Tk los soporta. - **Elige Pyzed** cuando la app deba verse actual, crecer más allá de unas pocas ventanas, diseñarse visualmente, recargarse mientras trabajas y distribuirse como un ejecutable pulido. ### Pyzed vs PyQt y PySide (Qt para Python) > Pyzed vs PyQt6 y PySide6: licencia, tamaño, aspecto, modelo de programación, diseñador y recarga en caliente, con las mismas apps lado a lado, números medidos y una guía para migrar código Qt Widgets a Pyzed. Source: https://albakore.github.io/pyzed/es/why/vs-pyqt-pyside/ [Qt](https://www.qt.io) es el framework gráfico más completo que puedes usar desde Python, mediante [PySide6](https://doc.qt.io/qtforpython-6/) (de The Qt Company, LGPL) o [PyQt6](https://www.riverbankcomputing.com/software/pyqt/) (de Riverbank, GPL o comercial). Lo tiene todo: gráficas, multimedia, un Chromium embebido, 3D, impresión, accesibilidad y treinta años en producción. Esa amplitud tiene un peso: una descarga grande, un modelo de objetos de C++ (señales, slots, padres, layouts) y una licencia en la que pensar. #### En resumen | | Pyzed | PyQt6 / PySide6 | | --- | --- | --- | | Licencia | MIT | PyQt: GPL v3 o comercial · PySide: LGPL v3 o comercial | | Modelo | Estado + `view()`; callbacks de Python normales | Árbol de objetos, señales y slots, o QML declarativo | | Aspecto | Componentes modernos y temas, listos | Estilos casi nativos; aspecto moderno con QSS o QML | | Diseñador | Pyzed Design, genera Python | Qt Designer / Creator (archivos `.ui`), Qt Design Studio (QML) | | Recarga en caliente | `pyzed dev`, conserva el estado | Vista previa de QML en las herramientas de Qt; el código Widgets necesita reiniciar | | Amplitud | Lo esencial de una app de escritorio + componentes de IDE | Casi cualquier cosa, con decenas de módulos | | Móvil | – | PySide6 en Android (vista previa técnica) | | Accesibilidad | Teclado; sin lectores de pantalla todavía | Soporte completo de lectores de pantalla | #### El mismo contador #### La misma lista de tareas En Qt Widgets construyes los widgets una vez y los mantienes sincronizados con tus datos a mano (`refresh()` borra y vuelve a crear las filas). En Pyzed solo describes las filas para los datos actuales. #### Medido Medianas de [los benchmarks](https://albakore.github.io/pyzed/es/why/benchmarks/) (Linux, sin GPU: renderizado por software). El tamaño instalado de Qt es solo `pyside6-essentials`; el paquete `pyside6` completo con add-ons es varias veces mayor. #### De los conceptos de Qt a Pyzed | Qt | Pyzed | | --- | --- | | `QApplication` + `QMainWindow` + `app.exec()` | `class MiApp(pz.App)` + `pz.run(MiApp)` | | `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=metodo` | | señales propias | mensajes a `update()`, o `pz.post(mensaje)` desde un hilo | | `QTimer` | `pz.every(...)` en `subscriptions()` | | `QThread` / `QRunnable` | callbacks `async def`, generadores o hilos normales | | `QDockWidget`, `QTabWidget`, `QSplitter` | [`ui.workspace`](https://albakore.github.io/pyzed/es/library/workspace/), [`ui.tab_bar`](https://albakore.github.io/pyzed/es/library/tab-bar/), [`ui.split`](https://albakore.github.io/pyzed/es/library/split/) | | `QTreeView` / `QTableView` + modelos | [`ui.tree`](https://albakore.github.io/pyzed/es/library/tree/), [`ui.table`](https://albakore.github.io/pyzed/es/library/table/), [`ui.virtual_list`](https://albakore.github.io/pyzed/es/library/virtual-list/) desde listas de Python | | Hojas de estilo QSS | [temas](https://albakore.github.io/pyzed/es/theming/themes/) y [`pz.Style`](https://albakore.github.io/pyzed/es/theming/styles/) | | `QShortcut`, `QAction` | `on_key(combo)`, [comandos](https://albakore.github.io/pyzed/es/guides/commands/) y `keys="ctrl-s"` en botones | | Archivos `.ui` de Qt Designer | Archivos `.pzform` de [Pyzed Design](https://albakore.github.io/pyzed/es/editor/) con manejadores en Python | #### Consejos de migración 1. **Empieza por los datos, no por los widgets.** Haz una lista de lo que guardan tus subclases de `QWidget` (`self.items`, `self.actual`…) y conviértelo en atributos de tu `App`. Todo lo que los copia a los widgets (`setText`, `setChecked`, reinicios de modelos) desaparece. 2. **Convierte los slots en métodos.** Un slot conectado con `clicked.connect(self.guardar)` pasa a ser `on_press=self.guardar`. 3. **Sustituye los modelos por listas.** Un `QAbstractListModel` sobre una lista de Python pasa a ser la propia lista, más `ui.virtual_list` cuando es larga. 4. **Quédate con Qt donde dependas de él.** Si la app necesita `QtWebEngine`, `QtMultimedia`, impresión o 3D, Qt sigue siendo la herramienta adecuada para esa app. #### Cuándo elegir cada uno - **Elige Qt** cuando necesites su amplitud, una app accesible con lectores de pantalla hoy, o una estabilidad probada durante décadas en un equipo grande, y su tamaño y su licencia encajen con tu proyecto. - **Elige Pyzed** cuando quieras una app de escritorio con aspecto moderno, con mucho menos código y ceremonia, licencia MIT permisiva, una instalación mucho más pequeña, recarga en caliente y ayuda de IA integrada en el flujo de trabajo. ### Pyzed vs Kivy para apps de escritorio en Python > Pyzed vs Kivy: los dos dibujan sus propios widgets por GPU, pero difieren en plataformas, aspecto, modelo de programación y herramientas. Las mismas apps lado a lado, números medidos y cómo pasar de Kivy y KV a Pyzed. Source: https://albakore.github.io/pyzed/es/why/vs-kivy/ [Kivy](https://kivy.org) y Pyzed comparten la idea básica: dibujar cada widget por su cuenta en la GPU en vez de pedir widgets nativos al sistema operativo. Kivy nació para pantallas táctiles y **funciona en Android e iOS**, algo que Pyzed no hace. Pyzed está hecho para **apps de productividad de escritorio** y trae los componentes, temas y herramientas para ellas. #### En resumen | | Pyzed | Kivy | | --- | --- | --- | | Plataformas | Windows, macOS, Linux | Windows, macOS, Linux, **Android, iOS** | | Dibujo | wgpu (Vulkan, Metal, DX12, GL), CPU de respaldo | OpenGL ES 2 | | Aspecto | Componentes de escritorio modernos, temas, modo oscuro | Estilo mínimo propio; Material con [KivyMD](https://kivymd.readthedocs.io) | | Descripción de la interfaz | Solo Python | Python más el lenguaje KV | | Estado | Atributos normales, `view()` se reconstruye al cambiar | Propiedades y bindings de Kivy | | Detalles de escritorio | Menús, atajos, pestañas, árboles, docks, editor de código, varias ventanas | Pensado para táctil; las convenciones de escritorio son cosa tuya | | Diseñador, recarga | Pyzed Design, `pyzed dev` | Herramientas de terceros | | Accesibilidad | Teclado; sin lectores de pantalla todavía | Sin lectores de pantalla | #### El mismo contador #### La misma lista de tareas La versión de Kivy usa un `RecycleView`, la forma recomendada por Kivy para mostrar listas, con el diseño de la fila en KV. #### Medido Medianas de [los benchmarks](https://albakore.github.io/pyzed/es/why/benchmarks/) (Linux, sin GPU: los dos toolkits renderizan con el OpenGL/Vulkan por software de Mesa, así que entre ellos la comparación es justa). #### De los conceptos de Kivy a Pyzed | Kivy | Pyzed | | --- | --- | | `App.build()` devuelve el widget raíz | `App.view()` devuelve el árbol, cada vez que cambia el estado | | `NumericProperty`, `StringProperty`, `ListProperty` | atributos normales | | Reglas KV (`:`) | funciones de Python que devuelven widgets, o [componentes](https://albakore.github.io/pyzed/es/guides/components/) | | `BoxLayout(orientation=...)` | `pz.row` / `pz.column` | | `GridLayout`, `AnchorLayout`, `FloatLayout` | `pz.grid`, `pz.center` / alineación, `pz.stack` + [`pz.anchor_layout`](https://albakore.github.io/pyzed/es/guides/layout/) | | `ScrollView`, `RecycleView` | `pz.scrollable`, [`ui.virtual_list`](https://albakore.github.io/pyzed/es/library/virtual-list/) | | `on_press: app.hacer()` | `on_press=self.hacer` | | `Clock.schedule_interval` | `pz.every(...)` en `subscriptions()` | | `Animation(...)` | [`pz.Animation` y transiciones](https://albakore.github.io/pyzed/es/animation/) | | `Window.clearcolor`, temas de KivyMD | [temas](https://albakore.github.io/pyzed/es/theming/themes/) | #### Consejos de migración 1. **Pasa el KV a `view()`.** Cada regla KV se convierte en una función que recibe datos y devuelve widgets. Los bindings como `text: str(app.count)` pasan a ser expresiones normales: `ui.heading(str(self.count))`. 2. **Olvídate de las propiedades.** Las propiedades de Kivy existen para que los bindings observen cambios; Pyzed vuelve a ejecutar `view()` después de cada evento, así que bastan atributos normales. 3. **Repiensa los patrones solo táctiles.** En escritorio se esperan menús, atajos de teclado y estados al pasar el ratón, y los componentes de Pyzed los traen. #### Cuándo elegir cada uno - **Elige Kivy** cuando la misma app deba funcionar en móviles y tablets, o para quioscos táctiles y apps multimedia. - **Elige Pyzed** para apps de escritorio que deban verse y comportarse como software de escritorio moderno, con diseñador, recarga en caliente y herramientas de IA, y sin un segundo lenguaje que aprender. ### Pyzed vs Flet para apps de escritorio en Python > Pyzed vs Flet (Flutter para Python): arquitectura, plataformas, aspecto, herramientas y arranque y memoria medidos, con las mismas apps lado a lado y cómo pasar una app de escritorio de Flet a Pyzed. Source: https://albakore.github.io/pyzed/es/why/vs-flet/ [Flet](https://flet.dev) te deja construir apps [Flutter](https://flutter.dev) en Python. Tu código Python se ejecuta en un proceso y envía el árbol de controles a un cliente Flutter que lo dibuja, en el escritorio, en la web o en un móvil. Ese alcance es la gran fortaleza de Flet. Pyzed dibuja en el mismo proceso que tu código Python, con un renderizador en Rust, y se centra en el escritorio. #### En resumen | | Pyzed | Flet | | --- | --- | --- | | Arquitectura | Un proceso: Python + renderizador en Rust (Iced) | Proceso Python + proceso cliente Flutter, comunicados por un protocolo | | Plataformas | Windows, macOS, Linux | Windows, macOS, Linux, **web, Android, iOS** | | Aspecto | Estilo de editor de código moderno, denso, pensado para escritorio | Material 3 o Cupertino, pensado para móvil | | Modelo | Estado + `view()` reconstruido desde los datos | Controles mutables + `page.update()`; Flet 1.0 añade componentes declarativos con hooks | | Empaquetado | `pyzed build` (PyInstaller), sin SDK extra | `flet build` (requiere el SDK de Flutter) | | Diseñador | Pyzed Design | – | | Recarga en caliente | Conserva el estado de la app | `flet run` reinicia la app al guardar | | Accesibilidad | Teclado; sin lectores de pantalla todavía | Lectores de pantalla mediante la semántica de Flutter | #### El mismo contador #### La misma lista de tareas #### Medido Medianas de [los benchmarks](https://albakore.github.io/pyzed/es/why/benchmarks/) (Linux, sin GPU: renderizado por software en ambos). La memoria de Flet incluye su proceso cliente de Flutter, que es donde vive la ventana. Su tamaño instalado cuenta los paquetes de Python más el cliente de escritorio que `flet` descarga en la primera ejecución. #### De los conceptos de Flet a Pyzed | Flet | Pyzed | | --- | --- | | `ft.run(main)` con `main(page)` | `pz.run(MiApp)` con una clase `pz.App` | | `page.add(...)`, `page.update()` | devuelve el árbol desde `view()`; sin actualizar a mano | | `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=manejador(e)` | `on_press=metodo` (sin argumento de evento) | | `page.theme_mode = ft.ThemeMode.DARK` | `theme = "One Dark"` o `pz.themes.auto(...)` | | `page.run_task(corrutina)` | devuelve una corrutina desde un callback | | `ft.ListView` | [`ui.virtual_list`](https://albakore.github.io/pyzed/es/library/virtual-list/) | | `ft.NavigationRail`, `ft.Tabs` | paneles de [`ui.workspace`](https://albakore.github.io/pyzed/es/library/workspace/), [`ui.tab_bar`](https://albakore.github.io/pyzed/es/library/tab-bar/) | #### Consejos de migración 1. **Lleva el estado de los controles a la app.** Los valores que lees de los controles (`campo.value`, `casilla.value`) pasan a ser atributos, actualizados por los callbacks de los controles. 2. **Borra las llamadas a `page.update()`.** Pyzed redibuja después de cada callback. 3. **Espera una interfaz más densa.** El espaciado de Material está pensado para el tacto; los componentes de Pyzed tienen el tamaño de un editor de escritorio. Usa variantes `size="lg"` o [estilos](https://albakore.github.io/pyzed/es/theming/styles/) si quieres más aire. #### Cuándo elegir cada uno - **Elige Flet** cuando un mismo código deba funcionar en la web y en móviles, o quieras los widgets Material de Flutter. - **Elige Pyzed** para apps pensadas para escritorio: un solo proceso, sin SDK de Flutter para empaquetar, un diseñador visual, recarga en caliente que conserva el estado y un aspecto denso y profesional. ### Pyzed vs Dear PyGui > Pyzed vs Dear PyGui para interfaces de escritorio en Python: interfaz retenida al estilo Elm frente a ImGui en modo inmediato, aspecto, herramientas y números medidos, con las mismas apps lado a lado y cuándo encaja cada uno. Source: https://albakore.github.io/pyzed/es/why/vs-dear-pygui/ [Dear PyGui](https://github.com/hoffstadt/DearPyGui) envuelve [Dear ImGui](https://github.com/ocornut/imgui), el toolkit en modo inmediato que usan los motores de videojuegos para sus herramientas de depuración. Se dibuja por GPU y es muy rápido, y brilla en gráficas en tiempo real, editores de nodos y herramientas internas. Su aspecto es el de «herramienta» de ImGui, y redibuja continuamente, algo que encaja mejor en un panel que cambia en cada fotograma que en un formulario que cambia cuando haces clic. #### En resumen | | Pyzed | Dear PyGui | | --- | --- | --- | | Modelo | Estado + `view()` (arquitectura Elm) | Elementos creados una vez con etiquetas, cambiados con `set_value` / `configure_item` | | Redibujado | Cuando algo cambia | En cada fotograma, por defecto | | Aspecto | Componentes modernos listos, temas, modo oscuro | Estilo de herramienta ImGui, configurable con temas | | Texto | Shaping completo, cualquier escritura, emoji, texto enriquecido y markdown | Fuentes y rangos de glifos que cargas tú | | Gráficas | [Sparklines, barras y donuts](https://albakore.github.io/pyzed/es/library/bar-chart/), un canvas | Excelentes gráficas integradas (ImPlot) | | Escritorio | Menús, atajos, varias ventanas, docks, editor de código | Docking y viewports de ImGui | | Diseñador, recarga | Pyzed Design, `pyzed dev` | – | | Accesibilidad | Teclado; sin lectores de pantalla todavía | Sin lectores de pantalla | #### El mismo contador #### La misma lista de tareas #### Medido Medianas de [los benchmarks](https://albakore.github.io/pyzed/es/why/benchmarks/) (Linux, sin GPU: ambos renderizan con los drivers por software de Mesa). #### De los conceptos de Dear PyGui a Pyzed | Dear PyGui | Pyzed | | --- | --- | | `create_context`, `create_viewport`, `start_dearpygui` | `pz.run(MiApp)` | | `with dpg.window(...)` | la raíz que devuelve `view()` | | `dpg.add_text(tag="x")` + `dpg.set_value("x", ...)` | `ui.label(self.x)`: cambia `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` | [temas](https://albakore.github.io/pyzed/es/theming/themes/) y [estilos](https://albakore.github.io/pyzed/es/theming/styles/) | | `dpg.add_plot` | [`ui.bar_chart`](https://albakore.github.io/pyzed/es/library/bar-chart/), [`ui.sparkline`](https://albakore.github.io/pyzed/es/library/sparkline/), o [`pz.canvas`](https://albakore.github.io/pyzed/es/guides/canvas/) para dibujo propio | #### Cuándo elegir cada uno - **Elige Dear PyGui** para gráficas en tiempo real, editores de nodos, paneles de instrumentos y herramientas de depuración donde el aspecto de ImGui está bien. - **Elige Pyzed** para apps de usuario final: formularios, listas, editores y paneles que deban verse pulidos, con texto de calidad, un diseñador, recarga en caliente y empaquetado. ## Biblioteca de componentes ### Componentes > Más de 90 componentes de UI de Pyzed para GUIs de escritorio en Python: botones, tablas, árboles, editores, gráficos y diálogos, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/ Todos los componentes de `pyzed.ui`, más los widgets de disposición y contenido de `pyzed`, con una vista previa, el código que la dibuja, ejemplos y su API. Los componentes son **funciones que devuelven widgets**, así que se combinan libremente entre sí y con los [widgets base](https://albakore.github.io/pyzed/es/reference/widgets/). ```python import pyzed as pz from pyzed import ui ui.button("Guardar", self.guardar, icon="save", variant="solid", keys="ctrl-s") ``` - **Colores del tema.** Todos los componentes leen el tema actual (`App.theme`, o el de la ventana en apps [multiventana](https://albakore.github.io/pyzed/es/guides/windows/)) y aceptan `theme=` para usar otro. - **Tokens o hex.** Los parámetros de color (`color`, `icon_color`, `tint`…) aceptan un token del tema como `"accent"`, `"text_muted"` o `"error"`, o un color `"#rrggbb"`. - **Mensajes o funciones.** `on_press`, `on_select`, `on_toggle`… aceptan un mensaje para `update` o una función; los que producen un valor (texto, `bool`, id) la llaman con ese valor. - **Iconos.** `icon=` acepta cualquier nombre de [Lucide](https://lucide.dev) (`pyzed icons` los lista). Para verlos todos funcionando, abre la galería: `python examples/gallery.py`. ¿Buscas pantallas completas hechas con ellos (login, dashboard, ajustes, chat, kanban...)? Mira los [Bloques](https://albakore.github.io/pyzed/es/blocks/). :::note[Qué hay en cada página] Una explicación en pocas palabras, una vista previa con su código, cada variante, tamaño y estado capturados en claro y oscuro con el fragmento que los dibuja, ejemplos, consejos y la tabla de la API. Las páginas de componentes interactivos, como [Botón](https://albakore.github.io/pyzed/es/library/button/) y [Campo de texto](https://albakore.github.io/pyzed/es/library/input/), también muestran una grabación corta de su uso. ::: :::tip[Inspiración] El aspecto de estos componentes se inspira en la interfaz del editor [Zed](https://zed.dev); la idea de construir herramientas de escritorio con piezas listas para usar viene de Godot, Unreal Engine, Visual Basic y Qt Designer. pyzed es un proyecto independiente. ::: #### Acciones Botones y controles pequeños que ejecutan algo. #### Visualización Texto, insignias, teclas, avatares y superficies. #### Formularios Controles controlados: muestran tu estado y te avisan de cada cambio. #### Fechas y horas Calendarios y campos para fechas, rangos y horas, en el formato que elijas. #### Estado y avisos Progreso, carga, avisos y estados vacíos. #### Datos Tablas, cifras y gráficos pequeños con los colores del tema. #### Listas y menús Filas, árboles de archivos, menús contextuales y secciones plegables. #### Navegación Pestañas, migas de pan y barras de herramientas. #### Capas flotantes Lo que se dibuja encima de la interfaz: capas, modales, paletas y notificaciones. #### Diálogos Diálogos listos para usar con los botones traducidos. #### Sistema Piezas que hablan con el sistema operativo: sus diálogos de archivos y los archivos soltados desde el escritorio. #### Espacio de trabajo El marco de una ventana estilo editor: docks, barra de título, barra de estado y paneles. #### Código y herramientas El editor de código, una vista de terminal, una consola de registro y cajas de ayuda. #### Agentes de IA Las piezas del chat y los ajustes de un agente de IA: llamadas a herramientas, diferencias, planes, historial e inicio de sesión. #### Disposición Los widgets de disposición de `pyzed` (`pz.*`) con los que se construye todo. #### Contenido Widgets base para texto, iconos, imágenes, Markdown, códigos QR y dibujo. ### Eventos de cada componente > Todos los eventos de todos los componentes de Pyzed: qué avisa cada uno, qué recibe el handler y cuántos eventos puede tener cada componente. Source: https://albakore.github.io/pyzed/es/library/events/ Los widgets de Pyzed reaccionan a eventos en dos capas: - **Sus eventos propios**: los parámetros `on_*` de cada componente, como `on_press` de un botón o `on_select` de un árbol. Avisan de lo que significa para el componente (se cerró una pestaña, se abrió una fila) y suelen pasar un valor (el texto, el elemento, el estado nuevo). - **Los eventos comunes**: 18 eventos que tiene cualquier widget, sea cual sea, y que se conectan con `.on(...)`: clics, paso del puntero, arrastre, foco, teclas y tamaño. Un `pz.Component` además lanza `on_mount` y `on_unmount`. Así que un componente puede tener sus eventos propios más los 18 comunes. Entre los 114 componentes de la biblioteca hay **160 eventos propios**; los que más tienen aparecen primero en la tabla del final. ```python ui.list_item("main.py", self.abrir, icon="file").on( # on_press es su evento propio right_click=self.mostrar_menu, # los comunes: cualquier widget mouse_enter=lambda: setattr(self, "encima", "main.py"), drag=lambda e: self.mover(e.dx, e.dy), # recibe el pz.Event ) ``` #### Los eventos comunes Los tiene todo widget. Un handler sin argumentos se llama sin nada; uno con un argumento recibe un `pz.Event`. | Grupo | Evento | Cuándo | | --- | --- | --- | | Ratón | `click` | Se hizo clic en el widget (se pulsó y soltó el botón izquierdo). | | Ratón | `double_click` | Dos clics seguidos. | | Ratón | `right_click` | Se pulsó el botón derecho (úsalo para menús contextuales). | | Ratón | `middle_click` | Se pulsó el botón central (la rueda). | | Ratón | `mouse_down` | Se pulsó cualquier botón sobre el widget (`event.button`). | | Ratón | `mouse_up` | Se soltó cualquier botón sobre el widget (`event.button`). | | Ratón | `mouse_enter` | El puntero entró en el widget. | | Ratón | `mouse_leave` | El puntero salió del widget. | | Ratón | `mouse_move` | El puntero se movió sobre el widget (`event.x`, `event.y`). | | Ratón | `scroll` | La rueda giró sobre el widget (`event.dx`, `event.dy` en líneas). | | Arrastre | `drag_start` | Se pulsó el botón izquierdo y el puntero se movió unos píxeles. | | Arrastre | `drag` | El puntero se movió mientras se arrastra (`event.dx`, `event.dy` desde el anterior). | | Arrastre | `drag_end` | Se soltó el botón después de arrastrar. | | Foco | `focus` | Se hizo clic en el widget y ahora recibe las teclas. | | Foco | `blur` | Un clic en otro sitio le quitó el foco. | | Teclado | `key_down` | Se pulsó una tecla con el foco (`event.key`, por ejemplo `"ctrl-s"`). | | Teclado | `key_up` | Se soltó una tecla con el foco (`event.key`). | | Tamaño | `resize` | El widget cambió de tamaño, también la primera vez (`event.width`, `event.height`). | ##### Campos de `pz.Event` | Propiedad | Descripción | | --- | --- | | `type` | El nombre del evento (`"click"`, o un evento propio de un componente). | | `x`, `y` | Posición del puntero relativa al widget (`position` da las dos). | | `window_x`, `window_y` | Posición del puntero en la ventana. | | `screen_x`, `screen_y` | En la pantalla (`None` donde el sistema no lo dice, como Wayland). | | `dx`, `dy` | Cantidad de desplazamiento, o movimiento del arrastre desde el `drag` anterior. | | `width`, `height` | El tamaño del widget (`size` da los dos). | | `button` | `"left"`, `"right"`, `"middle"`… | | `key` | La combinación de teclas, como `"ctrl-s"`. | | `shift`, `ctrl`, `alt`, `logo` | Modificadores pulsados. | | `value`, `source` | En los eventos propios de un componente: el dato y el componente. | #### Qué reciben los handlers Los controles que producen un valor llaman a tu función con él. Las formas más habituales: | Handler | Descripción | Eventos | | --- | --- | --- | | `()` | Nada: el evento ya es la noticia (una pulsación, un cierre, Enter). | `ui.alert(on_close=)`, `pz.mouse_area(on_press=)`, `pz.mouse_area(on_release=)`, `pz.mouse_area(on_right_press=)`… | | `(event)` | Un `pz.Event`: su `value` trae el dato y `source` el componente. | `ui.AuthPrompt(on_pick=)`, `ui.AuthPrompt(on_cancel=)`, `ui.AuthPrompt(on_open_url=)`, `ui.AuthPrompt(on_copy=)`… | | `(text)` | El texto nuevo, en cada pulsación de tecla. | `ui.shortcuts(on_query=)`, `ui.picker(on_query=)`, `ui.search_input(on_input=)`, `ui.input(on_input=)`… | | `(x, y)` | La posición del puntero, relativa al widget. | `pz.mouse_area(on_move=)`, `pz.canvas(on_press=)`, `pz.canvas(on_release=)`, `pz.canvas(on_right_press=)`… | | `(value)` | El valor nuevo: un número, un color, una opción… | `ui.number_field(on_change=)`, `ui.slider(on_change=)`, `ui.toggle_group(on_select=)`, `ui.radio_group(on_select=)`… | | `(info)` | Un dict (desplazamiento y tamaños) o un `ui.ErrorInfo`. | `pz.scrollable(on_scroll=)`, `ui.editor(on_scroll=)`, `ui.error_boundary(on_error=)`, `ui.error_boundary(on_details=)`… | | `(id)` | El id de la fila o pestaña (en un árbol, su ruta). | `ui.tree(on_toggle=)`, `ui.tree(on_select=)`, `ui.tree(on_open=)`, `ui.tree(on_context=)` | | `(tab_id)` | El id de la pestaña. | `ui.tab_bar(on_select=)`, `ui.tab_bar(on_close=)`, `ui.tab_bar(on_drag=)`, `ui.tab_bar(on_context=)` | | `(row)` | La fila (el dict u objeto que pasaste en `rows`). | `ui.table(on_row_press=)`, `ui.table(on_row_double_press=)`, `ui.table(on_row_context=)` | | `(dx, dy)` | Cuánto giró la rueda, en líneas. | `pz.mouse_area(on_scroll=)`, `pz.canvas(on_scroll=)` | | `(checked)` | El estado nuevo, `True` o `False`. | `ui.checkbox(on_toggle=)`, `ui.switch(on_toggle=)` | | `(line)` | La línea del registro (un `ui.LogEntry`). | `ui.console(on_context=)`, `ui.console(on_open=)` | | `(name)` | Un nombre: del icono o de la acción del editor. | `ui.editor(on_action=)`, `ui.icon_picker_dialog(on_pick=)` | | `(url)` | La URL del enlace. | `ui.markdown(on_link=)`, `ui.reference_browser(on_open_url=)` | | `(size)` | El tamaño nuevo en píxeles, al terminar de arrastrar. | `ui.panel(on_resize=)`, `pz.resizable(on_resize=)` | | `(item)` | La opción elegida. | `ui.segmented(on_select=)`, `ui.select(on_select=)` | #### Eventos de cada componente **Propios** cuenta los eventos `on_*` del componente; **Total** suma los 18 comunes (y `mount` / `unmount` en las clases `pz.Component`). | Componente | Propios | Total | Sus eventos | | --- | --- | --- | --- | | [Área de ratón](https://albakore.github.io/pyzed/es/library/mouse-area/#eventos) | 10 | 28 | `on_press`, `on_release`, `on_right_press`, `on_double_click`, `on_enter`, `on_exit`, `on_move`, `on_scroll`, `on_middle_press`, `on_middle_release` | | [Consola](https://albakore.github.io/pyzed/es/library/console/#eventos) | 8 | 26 | `on_query`, `on_toggle_level`, `on_clear`, `on_copy`, `on_context`, `on_open`, `on_source`, `on_write` | | [Barra de pestañas](https://albakore.github.io/pyzed/es/library/tab-bar/#eventos) | 7 | 25 | `on_select`, `on_close`, `on_drag`, `on_drop`, `on_drag_over`, `on_context`, `on_new` | | [Editor](https://albakore.github.io/pyzed/es/library/editor/#eventos) | 6 | 24 | `on_change`, `on_cursor`, `on_scroll`, `on_zoom`, `on_action`, `on_hover` | | [Barra de cambios](https://albakore.github.io/pyzed/es/library/edits-bar/#eventos) | 5 | 25 | `on_keep`, `on_reject`, `on_keep_file`, `on_reject_file`, `on_open` | | [Lienzo](https://albakore.github.io/pyzed/es/library/canvas/#eventos) | 5 | 23 | `on_press`, `on_release`, `on_right_press`, `on_move`, `on_scroll` | | [Marco de ventana](https://albakore.github.io/pyzed/es/library/window-frame/#eventos) | 5 | 23 | `on_drag`, `on_minimize`, `on_maximize`, `on_close`, `on_menu` | | [Zona para soltar](https://albakore.github.io/pyzed/es/library/drop-zone/#eventos) | 5 | 25 | `on_drop`, `on_files`, `on_reject`, `on_enter`, `on_leave` | | [Árbol](https://albakore.github.io/pyzed/es/library/tree/#eventos) | 4 | 22 | `on_toggle`, `on_select`, `on_open`, `on_context` | | [Aviso de inicio de sesión](https://albakore.github.io/pyzed/es/library/auth-prompt/#eventos) | 4 | 24 | `on_pick`, `on_cancel`, `on_open_url`, `on_copy` | | [Selector de fecha](https://albakore.github.io/pyzed/es/library/date-picker/#eventos) | 4 | 24 | `on_change`, `on_open`, `on_close`, `on_dismiss` | | [Tabla](https://albakore.github.io/pyzed/es/library/table/#eventos) | 4 | 22 | `on_sort`, `on_row_press`, `on_row_double_press`, `on_row_context` | | [Cambios sin guardar](https://albakore.github.io/pyzed/es/library/unsaved-changes/#eventos) | 3 | 21 | `on_save`, `on_discard`, `on_cancel` | | [Campo de texto](https://albakore.github.io/pyzed/es/library/input/#eventos) | 3 | 21 | `on_input`, `on_submit`, `on_clear` | | [Diálogo de error](https://albakore.github.io/pyzed/es/library/error-dialog/#eventos) | 3 | 21 | `on_close`, `on_go_to`, `on_logs` | | [Fila de lista](https://albakore.github.io/pyzed/es/library/list-item/#eventos) | 3 | 21 | `on_press`, `on_right_press`, `on_double_press` | | [Llamada a herramienta](https://albakore.github.io/pyzed/es/library/tool-call/#eventos) | 3 | 23 | `on_allow`, `on_allow_always`, `on_deny` | | [Paleta de comandos](https://albakore.github.io/pyzed/es/library/command-palette/#eventos) | 3 | 21 | `on_query`, `on_submit`, `on_press` | | [Pregunta](https://albakore.github.io/pyzed/es/library/prompt/#eventos) | 3 | 21 | `on_input`, `on_submit`, `on_cancel` | | [Select](https://albakore.github.io/pyzed/es/library/select/#eventos) | 3 | 21 | `on_select`, `on_open`, `on_close` | | [Selector de hora](https://albakore.github.io/pyzed/es/library/time-picker/#eventos) | 3 | 23 | `on_change`, `on_open`, `on_close` | | [Selector de iconos](https://albakore.github.io/pyzed/es/library/icon-picker/#eventos) | 3 | 23 | `on_change`, `on_pick`, `on_close` | | [Selector de rango de fechas](https://albakore.github.io/pyzed/es/library/date-range-picker/#eventos) | 3 | 23 | `on_change`, `on_open`, `on_close` | | [Botón dividido](https://albakore.github.io/pyzed/es/library/split-button/#eventos) | 2 | 20 | `on_press`, `on_menu` | | [Buscador](https://albakore.github.io/pyzed/es/library/picker/#eventos) | 2 | 20 | `on_query`, `on_submit` | | [Calendario](https://albakore.github.io/pyzed/es/library/calendar/#eventos) | 2 | 22 | `on_change`, `on_month` | | [Campo numérico](https://albakore.github.io/pyzed/es/library/number-field/#eventos) | 2 | 20 | `on_change`, `on_submit` | | [Chip](https://albakore.github.io/pyzed/es/library/chip/#eventos) | 2 | 20 | `on_press`, `on_remove` | | [Confirmación](https://albakore.github.io/pyzed/es/library/confirm/#eventos) | 2 | 20 | `on_confirm`, `on_cancel` | | [Deslizador](https://albakore.github.io/pyzed/es/library/slider/#eventos) | 2 | 20 | `on_change`, `on_release` | | [Fila de servicio](https://albakore.github.io/pyzed/es/library/service-row/#eventos) | 2 | 22 | `on_toggle`, `on_primary` | | [Historial de hilos](https://albakore.github.io/pyzed/es/library/thread-history/#eventos) | 2 | 22 | `on_open`, `on_delete` | | [Límite de errores](https://albakore.github.io/pyzed/es/library/error-boundary/#eventos) | 2 | 20 | `on_error`, `on_details` | | [Migas de pan](https://albakore.github.io/pyzed/es/library/breadcrumbs/#eventos) | 2 | 20 | `on_press`, `on_more` | | [Notificación](https://albakore.github.io/pyzed/es/library/toast/#eventos) | 2 | 20 | `on_dismiss`, `on_press` | | [Selector de color](https://albakore.github.io/pyzed/es/library/color-picker/#eventos) | 2 | 22 | `on_change`, `on_press` | | [Acordeón](https://albakore.github.io/pyzed/es/library/accordion/#eventos) | 1 | 19 | `on_toggle` | | [Alerta](https://albakore.github.io/pyzed/es/library/alert/#eventos) | 1 | 19 | `on_close` | | [Atajos](https://albakore.github.io/pyzed/es/library/shortcuts/#eventos) | 1 | 19 | `on_query` | | [Aviso](https://albakore.github.io/pyzed/es/library/callout/#eventos) | 1 | 19 | `on_dismiss` | | [Barra de estado](https://albakore.github.io/pyzed/es/library/status-bar/#eventos) | 1 | 19 | `on_press` | | [Barra de título](https://albakore.github.io/pyzed/es/library/title-bar/#eventos) | 1 | 19 | `on_press` | | [Botón](https://albakore.github.io/pyzed/es/library/button/#eventos) | 1 | 19 | `on_press` | | [Botón de icono](https://albakore.github.io/pyzed/es/library/icon-button/#eventos) | 1 | 19 | `on_press` | | [Campo de búsqueda](https://albakore.github.io/pyzed/es/library/search-input/#eventos) | 1 | 19 | `on_input` | | [Capa](https://albakore.github.io/pyzed/es/library/layer/#eventos) | 1 | 19 | `on_dismiss` | | [Capa de diálogo](https://albakore.github.io/pyzed/es/library/dialog-layer/#eventos) | 1 | 19 | `on_dismiss` | | [Casilla](https://albakore.github.io/pyzed/es/library/checkbox/#eventos) | 1 | 19 | `on_toggle` | | [Catálogo](https://albakore.github.io/pyzed/es/library/catalog/#eventos) | 1 | 21 | `on_install` | | [Cifra](https://albakore.github.io/pyzed/es/library/stat/#eventos) | 1 | 19 | `on_press` | | [Desplazable](https://albakore.github.io/pyzed/es/library/scrollable/#eventos) | 1 | 19 | `on_scroll` | | [Desplegable](https://albakore.github.io/pyzed/es/library/disclosure/#eventos) | 1 | 19 | `on_toggle` | | [División](https://albakore.github.io/pyzed/es/library/split/#eventos) | 1 | 19 | `on_resize` | | [Elección](https://albakore.github.io/pyzed/es/library/choice/#eventos) | 1 | 19 | `on_cancel` | | [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/workspace/#eventos) | 1 | 19 | `on_resize` | | [Grupo de alternancia](https://albakore.github.io/pyzed/es/library/toggle-group/#eventos) | 1 | 19 | `on_select` | | [Grupo de radios](https://albakore.github.io/pyzed/es/library/radio-group/#eventos) | 1 | 19 | `on_select` | | [Insignia](https://albakore.github.io/pyzed/es/library/badge/#eventos) | 1 | 19 | `on_press` | | [Interruptor](https://albakore.github.io/pyzed/es/library/switch/#eventos) | 1 | 19 | `on_toggle` | | [Lista virtual](https://albakore.github.io/pyzed/es/library/virtual-list/#eventos) | 1 | 19 | `on_scroll` | | [Markdown](https://albakore.github.io/pyzed/es/library/markdown/#eventos) | 1 | 19 | `on_link` | | [Menú](https://albakore.github.io/pyzed/es/library/menu/#eventos) | 1 | 19 | `on_press` | | [Modal](https://albakore.github.io/pyzed/es/library/modal/#eventos) | 1 | 19 | `on_close` | | [Panel](https://albakore.github.io/pyzed/es/library/panel/#eventos) | 1 | 19 | `on_resize` | | [Popover](https://albakore.github.io/pyzed/es/library/popover/#eventos) | 1 | 19 | `on_dismiss` | | [Redimensionable](https://albakore.github.io/pyzed/es/library/resizable/#eventos) | 1 | 19 | `on_resize` | | [Segmentado](https://albakore.github.io/pyzed/es/library/segmented/#eventos) | 1 | 19 | `on_select` | | [Selector de archivos](https://albakore.github.io/pyzed/es/library/file-input/#eventos) | 1 | 19 | `on_change` | | [Tarjeta](https://albakore.github.io/pyzed/es/library/card/#eventos) | 1 | 19 | `on_press` | | [Visor de referencia](https://albakore.github.io/pyzed/es/library/reference-browser/#eventos) | 1 | 19 | `on_open_url` | | [Acerca de](https://albakore.github.io/pyzed/es/library/about/#eventos) | 0 | 18 | — | | [Animación](https://albakore.github.io/pyzed/es/library/animation/#eventos) | 0 | 18 | — | | [Avatar](https://albakore.github.io/pyzed/es/library/avatar/#eventos) | 0 | 18 | — | | [Barra de herramientas](https://albakore.github.io/pyzed/es/library/toolbar/#eventos) | 0 | 18 | — | | [Cabecera de lista](https://albakore.github.io/pyzed/es/library/list-header/#eventos) | 0 | 18 | — | | [Cabecera de panel](https://albakore.github.io/pyzed/es/library/panel-header/#eventos) | 0 | 18 | — | | [Campo de formulario](https://albakore.github.io/pyzed/es/library/field/#eventos) | 0 | 18 | — | | [Código QR](https://albakore.github.io/pyzed/es/library/qr-code/#eventos) | 0 | 18 | — | | [Columna](https://albakore.github.io/pyzed/es/library/column/#eventos) | 0 | 18 | — | | [Contador](https://albakore.github.io/pyzed/es/library/count-badge/#eventos) | 0 | 18 | — | | [Contenedor](https://albakore.github.io/pyzed/es/library/container/#eventos) | 0 | 18 | — | | [Cuadrícula](https://albakore.github.io/pyzed/es/library/grid/#eventos) | 0 | 18 | — | | [Disposición anclada](https://albakore.github.io/pyzed/es/library/anchor-layout/#eventos) | 0 | 18 | — | | [Divisor](https://albakore.github.io/pyzed/es/library/divider/#eventos) | 0 | 18 | — | | [Dona](https://albakore.github.io/pyzed/es/library/donut/#eventos) | 0 | 18 | — | | [Efectos](https://albakore.github.io/pyzed/es/library/effects/#eventos) | 0 | 18 | — | | [Encabezado](https://albakore.github.io/pyzed/es/library/heading/#eventos) | 0 | 18 | — | | [Espacio](https://albakore.github.io/pyzed/es/library/spacing/#eventos) | 0 | 18 | — | | [Estado vacío](https://albakore.github.io/pyzed/es/library/empty-state/#eventos) | 0 | 18 | — | | [Etiqueta](https://albakore.github.io/pyzed/es/library/label/#eventos) | 0 | 18 | — | | [Facepile](https://albakore.github.io/pyzed/es/library/facepile/#eventos) | 0 | 18 | — | | [Fila](https://albakore.github.io/pyzed/es/library/row/#eventos) | 0 | 18 | — | | [Flex](https://albakore.github.io/pyzed/es/library/flex/#eventos) | 0 | 18 | — | | [Gráfico de barras](https://albakore.github.io/pyzed/es/library/bar-chart/#eventos) | 0 | 18 | — | | [Grupo de ajustes](https://albakore.github.io/pyzed/es/library/settings-group/#eventos) | 0 | 18 | — | | [Icono](https://albakore.github.io/pyzed/es/library/icon/#eventos) | 0 | 18 | — | | [Imagen](https://albakore.github.io/pyzed/es/library/image/#eventos) | 0 | 18 | — | | [Indicador](https://albakore.github.io/pyzed/es/library/indicator/#eventos) | 0 | 18 | — | | [Kbd](https://albakore.github.io/pyzed/es/library/kbd/#eventos) | 0 | 18 | — | | [Medidor de contexto](https://albakore.github.io/pyzed/es/library/context-meter/#eventos) | 0 | 18 | — | | [Minigráfico](https://albakore.github.io/pyzed/es/library/sparkline/#eventos) | 0 | 18 | — | | [Pila](https://albakore.github.io/pyzed/es/library/stack/#eventos) | 0 | 18 | — | | [Pista de atajo](https://albakore.github.io/pyzed/es/library/keybinding-hint/#eventos) | 0 | 18 | — | | [Plan](https://albakore.github.io/pyzed/es/library/plan/#eventos) | 0 | 20 | — | | [Progreso](https://albakore.github.io/pyzed/es/library/progress/#eventos) | 0 | 18 | — | | [Responsivo](https://albakore.github.io/pyzed/es/library/responsive/#eventos) | 0 | 18 | — | | [Resumen de cambios](https://albakore.github.io/pyzed/es/library/diff-stat/#eventos) | 0 | 18 | — | | [Shader](https://albakore.github.io/pyzed/es/library/shader/#eventos) | 0 | 18 | — | | [Spinner](https://albakore.github.io/pyzed/es/library/spinner/#eventos) | 0 | 18 | — | | [Terminal](https://albakore.github.io/pyzed/es/library/terminal/#eventos) | 0 | 18 | — | | [Texto](https://albakore.github.io/pyzed/es/library/text/#eventos) | 0 | 18 | — | | [Texto enriquecido](https://albakore.github.io/pyzed/es/library/rich-text/#eventos) | 0 | 18 | — | | [Tooltip](https://albakore.github.io/pyzed/es/library/tooltip/#eventos) | 0 | 18 | — | | [Vista de diferencias](https://albakore.github.io/pyzed/es/library/diff-view/#eventos) | 0 | 18 | — | Sigue con [Apps, ventanas, formularios y componentes](https://albakore.github.io/pyzed/es/guides/classes/), que explica cómo crear tus propios eventos, e [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/), con las tres formas de manejarlos. ### Acerca de > Una caja Acerca de con icono, versión, detalles y enlaces. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/about/ :::tip[En pocas palabras] La caja “Acerca de esta app”: nombre, versión y enlaces. ::: Una caja Acerca de: un icono, el nombre y la versión de la app, una descripción, algunos detalles `(etiqueta, valor)` y botones de enlace. `ui.about` · Categoría: [Código y herramientas](https://albakore.github.io/pyzed/es/library/#código-y-herramientas) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#about) **Código:** ```python ui.about("Notes", "1.2.0", "A small notes app built with Pyzed.", icon="notebook-pen", details=[("Python", "3.12"), ("Pyzed", "0.4")], links=[("Website", on_website), ("GitHub", on_github)]) ``` #### Cuándo usarlo - En un `ui.modal` que se abre desde el menú Ayuda. #### Uso ```python from pyzed import ui ui.about("Notes", "1.2.0", "A small notes app built with Pyzed.", icon="notebook-pen", details=[("Python", "3.12"), ("Pyzed", "0.4")], links=[("Website", on_website), ("GitHub", on_github)]) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido ```python ui.about("Notes", "1.2.0") ``` ```python ui.about("Notes", "1.2.0", "A small notes app.", icon="notebook-pen", details=[("Python", "3.12"), ("Pyzed", "0.4")]) ``` ```python ui.about("Notes", "1.2.0", icon="notebook-pen", links=[("Website", on_web), ("GitHub", on_gh)]) ``` #### Eventos Acerca de no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.about` ```python about( name, version='', description='', *, icon='app-window', details=(), links=(), theme=None, ) ``` Un cuadro Acerca de: icono, nombre, versión, una descripción, algunos detalles y botones de enlace. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `name` | *obligatorio* | Nombre de la app. | | `version` | `""` | Versión. | | `description` | `""` | Una o dos frases. | | `icon` | `"app-window"` | Icono de Lucide. | | `details` | `()` | Filas `(etiqueta, valor)`. | | `links` | `()` | Botones `(etiqueta, on_press)`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#about) #### Relacionados - [Modal](https://albakore.github.io/pyzed/es/library/modal/): Una superficie de diálogo con título, contenido y botones al pie. - [Atajos](https://albakore.github.io/pyzed/es/library/shortcuts/): Una lista de atajos agrupada y con búsqueda. ### Acordeón > Secciones plegables apiladas, como los paneles de una pantalla de ajustes. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/accordion/ :::tip[En pocas palabras] Varias secciones apiladas; cada una se abre o se cierra al pulsar su título. ::: Secciones plegables apiladas separadas por divisores. Las secciones son tuplas `(key, title, content)` u objetos `ui.AccordionSection` (con `icon` y un widget `end` en la cabecera). El estado abierto es tuyo: `open` es un conjunto de claves, un dict `{clave: bool}` o `True`, y `on_toggle` recibe la clave. `ui.accordion` · Categoría: [Listas y menús](https://albakore.github.io/pyzed/es/library/#listas-y-menús) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#accordion) **Código:** ```python rows = lambda *items: pz.column([ui.field(k, ui.switch(v, on_toggle)) for k, v in items], spacing=8) pz.container(ui.accordion([ ("editor", "Editor", rows(("Soft wrap", True), ("Minimap", False))), ("files", "Files", rows(("Autosave", True))), ("git", "Git", ui.muted("Collapsed sections only show their header.")), ], open={"editor", "files"}, on_toggle=on_toggle, icons={"editor": "file-code", "files": "folder", "git": "git-branch"}), width=400) ``` #### Cuándo usarlo - Para paneles largos de ajustes o propiedades en los que solo hacen falta algunas secciones a la vez. - Para un solo bloque plegable, usa un [Desplegable](https://albakore.github.io/pyzed/es/library/disclosure/). #### Uso ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.open = {"editor"} def view(self): return ui.accordion( [("editor", "Editor", ui.muted("…")), ("files", "Files", ui.muted("…"))], open=self.open, on_toggle=lambda key: self.open.symmetric_difference_update({key}), ) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Secciones abiertas ```python pz.container(ui.accordion([("a", "Editor", ui.muted("…")), ("b", "Files", ui.muted("…")), ("c", "Git", ui.muted("…"))], open=set(), on_toggle=on_toggle), width=320) ``` ```python pz.container(ui.accordion([("a", "Editor", ui.muted("Font, wrapping, minimap.")), ("b", "Files", ui.muted("…"))], open={"a"}, on_toggle=on_toggle), width=320) ``` ```python pz.container(ui.accordion([("a", "Editor", ui.muted("Font, wrapping.")), ("b", "Files", ui.muted("Autosave."))], open=True, on_toggle=on_toggle), width=320) ``` ##### Contenido ```python pz.container(ui.accordion([("e", "Editor", ui.muted("…")), ("g", "Git", ui.muted("…"))], open=set(), icons={"e": "file-code", "g": "git-branch"}, on_toggle=on_toggle), width=320) ``` ```python pz.container(ui.accordion([ui.AccordionSection("p", "Problems", ui.muted("…"), icon="circle-x", end=ui.count_badge(3, tint="error"))], open=set(), on_toggle=on_toggle), width=320) ``` #### Eventos Acordeón tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_toggle` | `(key)` | Se llama con la clave de la cabecera pulsada. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Settings(pz.App): def __init__(self): self.open = {"editor"} def toggle(self, key): # on_toggle: the key of the clicked header self.open ^= {key} def view(self): return ui.accordion([("editor", "Editor", ui.muted("Font, tabs, wrapping")), ("files", "Files", ui.muted("Autosave, encoding"))], open=self.open, on_toggle=self.toggle) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Toggle: key: str class Settings(pz.App): open = frozenset({"editor"}) def update(self, message): match message: case Toggle(key): self.open = self.open ^ {key} def view(self): return ui.accordion([("editor", "Editor", ui.muted("…")), ("files", "Files", ui.muted("…"))], open=self.open, on_toggle=Toggle) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: open: frozenset = frozenset({"editor"}) def view(state, set_state): return ui.accordion([("editor", "Editor", ui.muted("…")), ("files", "Files", ui.muted("…"))], open=state.open, on_toggle=lambda key: set_state(open=state.open ^ {key})) ``` #### Propiedades y API ##### `ui.accordion` ```python accordion( sections, *, open=(), on_toggle=None, icons=None, spacing=8, padding=(6, 12, 10, 12), theme=None, ) ``` Secciones plegables apiladas y separadas por divisores, como los paneles de una pantalla de ajustes. Cada cabecera ocupa todo el ancho, así que el contenido queda alineado con el resto del panel. El estado abierto es tuyo: cámbialo en `on_toggle`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `sections` | *obligatorio* | Elementos `ui.AccordionSection(key, title, content, icon=None, end=None)` (o tuplas `(key, title, content)`). | | `open` | `()` | El conjunto de ids abiertos, un dict `{id: bool}` o `True` para todos. | | `icons` | `None` | `{id: icono}`: un icono de Lucide para cada cabecera. | | `spacing` | `8` | Espacio alrededor de los divisores entre secciones. | | `padding` | `(6, 12, 10, 12)` | Relleno alrededor del contenido de una sección abierta. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_toggle` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#accordion) ##### `ui.AccordionSection` ```python class AccordionSection ``` Una sección de `ui.accordion`: `key`, `title`, `content` y, opcionales, `icon` y un widget `end` en la cabecera. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `key` | *obligatorio* | Clave que identifica la suscripción (por defecto, el mensaje o la función). | | `title` | *obligatorio* | Título. | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `end` | `None` | Widgets al final (derecha). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#accordionsection) #### Relacionados - [Desplegable](https://albakore.github.io/pyzed/es/library/disclosure/): Una sección plegable con flecha. - [Grupo de ajustes](https://albakore.github.io/pyzed/es/library/settings-group/): Un bloque con título de filas de ajustes, para pantallas de ajustes. ### Alerta > Un mensaje con un botón. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/alert/ :::tip[En pocas palabras] Una caja de mensaje con un solo botón de Aceptar. ::: Un mensaje con un solo botón de Aceptar. `kind` (`"info"`, `"success"`, `"warning"`, `"error"`) añade un icono. `ui.alert` · Categoría: [Diálogos](https://albakore.github.io/pyzed/es/library/#diálogos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#alert) **Código:** ```python ui.dialog_layer(main_view(), ui.alert("The export finished: 128 files were written.", on_close, title="Export", kind="success", width=360)) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para avisar de algo que hay que confirmar (una exportación terminó, un archivo no se pudo abrir). #### Uso ```python from pyzed import ui ui.dialog_layer(main_view(), ui.alert("The export finished: 128 files were written.", on_close, title="Export", kind="success", width=360)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Tipo ```python ui.alert("The file was moved to the trash.", on_close, width=320) ``` ```python ui.alert("A new version is available.", on_close, kind="info", width=320) ``` ```python ui.alert("128 files were exported.", on_close, kind="success", title="Export", width=320) ``` ```python ui.alert("The disk is almost full.", on_close, kind="warning", width=320) ``` ```python ui.alert("notes.py could not be opened.", on_close, kind="error", title="Error", width=320) ``` ##### Opciones ```python ui.alert("Shortcuts changed in this version.", on_close, ok_label="Got it", width=320) ``` #### Eventos Alerta tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_close` | `()` | El botón y el icono de cerrar. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Export(pz.App): def __init__(self): self.done = True def view(self): alert = ui.alert("128 files were written.", lambda: setattr(self, "done", False), # on_close title="Export", kind="success") return ui.dialog_layer(main_view(), alert if self.done else None) ``` **Declarativo:** ```python class Export(pz.App): done = True def update(self, message): if message == "ok": self.done = False def view(self): return ui.dialog_layer(main_view(), ui.alert("128 files were written.", "ok", title="Export") if self.done else None) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: done: bool = True def view(state, set_state): alert = ui.alert("128 files were written.", lambda: set_state(done=False), title="Export") return ui.dialog_layer(main_view(), alert if state.done else None) ``` #### Propiedades y API ##### `ui.alert` ```python alert(message, on_close, *, title=None, kind=None, ok_label=None, width=400, theme=None) ``` Un mensaje con un botón. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `message` | *obligatorio* | Texto o widget. | | `title` | `None` | Título. | | `kind` | `None` | `"info"`, `"success"`, `"warning"` o `"error"` añade un icono. | | `ok_label` | `None` | Texto del botón (por defecto: Aceptar, traducido). | | `width` | `400` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_close` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#alert) #### Relacionados - [Confirmación](https://albakore.github.io/pyzed/es/library/confirm/): Una pregunta de sí o no, en rojo para acciones destructivas. - [Capa de diálogo](https://albakore.github.io/pyzed/es/library/dialog-layer/): Centra un diálogo encima de tu vista con el fondo oscurecido. - [Notificación](https://albakore.github.io/pyzed/es/library/toast/): Notificaciones apiladas en la esquina inferior derecha. ### Animación > Presets, keyframes y transiciones para cualquier widget. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/animation/ :::tip[En pocas palabras] Hace que las cosas se muevan: aparecer, deslizarse, rebotar o brillar, sin trabajo extra. ::: Todo widget tiene `animate()` y `transition()`. `animate` reproduce uno de los 47 presets o una `pz.Animation` cuando el widget aparece, en hover, al pulsar, con foco, al hacer clic o mientras `active`; `transition` anima los cambios de propiedades como CSS. `pz.stagger` hace entrar una lista en cascada y `pz.presence` anima la entrada y salida del contenido cuando cambia. Todo se reproduce de forma nativa, sin llamar a Python en cada fotograma. Ver la [guía de animación](https://albakore.github.io/pyzed/es/animation/). `pz.Animation` · Categoría: [Contenido](https://albakore.github.io/pyzed/es/library/#contenido) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#animation) **Código:** ```python pz.column(pz.stagger([ ui.card(ui.label(name), width=320).animate("lift", on="hover") for name in ("fade_in_up", "lift on hover", "stagger") ], "fade_in_up", step=60), spacing=10) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para mostrar que algo apareció, cambió o necesita atención: un aviso, una fila nueva, la sacudida de un error. - Para la respuesta en hover y al pulsar de tarjetas y botones propios. - Para transiciones entre páginas y pestañas con `pz.presence`. #### Uso ```python import pyzed as pz from pyzed import ui class Inbox(pz.App): errors = 0 page = 0 def view(self): return pz.column([ ui.input(self.query, self.search).animate("shake", key=self.errors), pz.presence(self.page_view(), key=self.page, enter="fade_in_right", exit="fade_out"), ui.button("Record", self.record).animate("glow_pulse", active=self.recording), ], spacing=12) ``` #### Eventos Animación no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Consejos :::tip Las capturas y `pz.set_reduced_motion(True)` muestran cada animación en reposo, así que los tests son estables. Respeta a quien apaga el movimiento. ::: #### Propiedades y API ##### `pz.Animation` ```python @dataclass class Animation(keyframes=(), duration=300, delay=0, easing='ease', repeat=1, direction='normal', fill='both', on='mount', category='custom', description='', _frames=()) ``` Una animación por keyframes, como una regla CSS `@keyframes` con su timing. [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#animation) ##### `pz.stagger` ```python stagger(items, animation='fade_in_up', *, step=40, delay=0, **options) ``` `items` con la misma animación, cada uno empezando `step` milisegundos después del anterior (una lista que entra en cascada): [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#stagger) ##### `pz.presence` ```python presence( content, key, *, enter='fade_in', exit='fade_out', id=None, duration=None, easing=None, ) ``` Muestra `content` y, cuando cambia `key`, anima la salida del contenido anterior (`exit`) mientras entra el nuevo (`enter`), como transiciones entre páginas: [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#presence) ##### `pz.define_animation` ```python define_animation(name, animation=None, **options) ``` Da nombre a una animación para que `animate(nombre)` la reproduzca; la devuelve. [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#define_animation) ##### `pz.set_reduced_motion` ```python set_reduced_motion(on=True) ``` Apaga el movimiento (`True`) o lo vuelve a encender: las animaciones muestran al instante su estado de reposo. [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#set_reduced_motion) #### Relacionados - [Efectos](https://albakore.github.io/pyzed/es/library/effects/): Glow, bloom, glass y opacidad como propiedades de estilo. - [Tarjeta](https://albakore.github.io/pyzed/es/library/card/): Una superficie con borde, título y acciones opcionales. - [Notificación](https://albakore.github.io/pyzed/es/library/toast/): Notificaciones apiladas en la esquina inferior derecha. ### Árbol > Un árbol de archivos plegable con iconos y colores de git. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/tree/ :::tip[En pocas palabras] Una lista de carpetas que puedes abrir y cerrar, como los archivos de un proyecto. ::: Un panel de proyecto: carpetas plegables, un icono por tipo de archivo y colores de estado de git. `TreeNode.from_paths(paths, status=...)` crea los nodos, pone las carpetas primero y propaga el estado de git a las carpetas con cambios. El id de cada nodo es su ruta; el conjunto `expanded` y el id `selected` son tuyos. `ui.tree` · Categoría: [Listas y menús](https://albakore.github.io/pyzed/es/library/#listas-y-menús) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#tree) **Código:** ```python nodes = ui.TreeNode.from_paths( ["src/main.rs", "src/app.rs", "src/ui/button.rs", "src/ui/mod.rs", "tests/app.rs", "Cargo.toml"], status={"src/app.rs": "modified", "src/ui/button.rs": "created"}, ) pz.container(ui.tree(nodes, expanded={"src", "src/ui"}, selected="src/app.rs", on_toggle=on_toggle, on_select=on_select), width=280) ``` #### Cuándo usarlo - Para archivos y cualquier jerarquía: esquemas, categorías, secciones de ajustes. - `status` es `"modified"`, `"created"`, `"deleted"`, `"conflict"` o `"ignored"`. #### Uso ```python import pyzed as pz from pyzed import ui class Explorer(pz.App): def __init__(self): self.nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "Cargo.toml"]) self.expanded = {"src"} self.selected = None def toggle(self, folder): self.expanded ^= {folder} def view(self): return ui.tree(self.nodes, expanded=self.expanded, selected=self.selected, on_toggle=self.toggle, on_select=lambda path: setattr(self, "selected", path)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Desplegado ```python nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"]) pz.container(ui.tree(nodes, expanded=set()), width=240) ``` ```python nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"]) pz.container(ui.tree(nodes, expanded={"src"}), width=240) ``` ```python nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"]) pz.container(ui.tree(nodes, expanded={"src", "src/ui"}, selected="src/ui/button.rs"), width=240) ``` ##### Estado de git ```python nodes = ui.TreeNode.from_paths(["src/app.rs", "src/new.rs", "src/old.rs", "build.log"], status={"src/app.rs": "modified", "src/new.rs": "created", "src/old.rs": "deleted", "build.log": "ignored"}) pz.container(ui.tree(nodes, expanded={"src"}), width=240) ``` ```python nodes = ui.TreeNode.from_paths(["merge.py", "ok.py"], status={"merge.py": "conflict"}) pz.container(ui.tree(nodes, expanded=set()), width=240) ``` ##### Densidad ```python nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"]) pz.container(ui.tree(nodes, expanded={"src", "src/ui"}, item_height=20, indent=10), width=240) ``` ```python nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "src/ui/mod.rs", "src/ui/button.rs", "Cargo.toml"]) pz.container(ui.tree(nodes, expanded={"src"}, item_height=30, indent=20), width=240) ``` ##### Estados y eventos `on_open(id)` se ejecuta con un doble clic en un archivo y `on_context(id)` con un clic derecho. ```python pz.container(ui.tree(ui.TreeNode.from_paths(["src/app.py", "src/ui.py", "README.md"]), expanded={"src"}, selected={"src/app.py", "README.md"}, focused="src/ui.py", on_open=on_open, on_context=on_context), width=300) ``` #### Ejemplos ##### Una carpeta real Crea las rutas con `pathlib` y pásalas a `from_paths`. ```python from pathlib import Path root = Path(".") paths = [p.relative_to(root).as_posix() for p in root.rglob("*") if p.is_file() and ".git" not in p.parts] nodes = ui.TreeNode.from_paths(paths) ``` ##### Nodos a mano Las carpetas son nodos con `children`. **Código:** ```python nodes = [ ui.TreeNode("Inbox", children=[ui.TreeNode("Welcome", id="inbox/1", icon="mail")], id="inbox"), ui.TreeNode("Archive", children=[], id="archive"), ui.TreeNode("Notes.md", id="notes", icon="notebook-pen"), ] pz.container(ui.tree(nodes, expanded={"inbox"}, selected="inbox/1"), width=260) ``` #### Eventos Árbol tiene **4 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_toggle` | `(id)` | Se llama con el id de la carpeta pulsada. | | `on_select` | `(id)` | Se llama con el id del archivo pulsado. | | `on_open` | `(id)` | Recibe el id del archivo al que se hace doble clic. | | `on_context` | `(id)` | Recibe el id de la fila en la que se hace clic derecho (muestra un `ui.menu` ahí). | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Explorer(pz.App): def __init__(self): self.nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "Cargo.toml"]) self.expanded = {"src"} self.selected = None def toggle(self, folder): # on_toggle: the folder's id (its path) self.expanded ^= {folder} def view(self): return ui.tree(self.nodes, expanded=self.expanded, selected=self.selected, on_toggle=self.toggle, on_select=lambda path: setattr(self, "selected", path), # click on a file on_open=lambda path: print("open", path), # double click on_context=lambda path: print("menu for", path)) # right click ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Toggle: folder: str @dataclass class Select: path: str class Explorer(pz.App): nodes = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "Cargo.toml"]) expanded = frozenset({"src"}) selected = None def update(self, message): match message: case Toggle(folder): self.expanded = self.expanded ^ {folder} case Select(path): self.selected = path def view(self): return ui.tree(self.nodes, expanded=self.expanded, selected=self.selected, on_toggle=Toggle, on_select=Select) ``` **Funcional:** ```python from dataclasses import dataclass NODES = ui.TreeNode.from_paths(["src/main.rs", "src/app.rs", "Cargo.toml"]) @dataclass(frozen=True) class State: expanded: frozenset = frozenset({"src"}) selected: str = "" def view(state, set_state): return ui.tree(NODES, expanded=state.expanded, selected=state.selected, on_toggle=lambda folder: set_state(expanded=state.expanded ^ {folder}), on_select=lambda path: set_state(selected=path)) ``` #### Propiedades y API ##### `ui.tree` ```python tree( nodes, *, expanded, selected=None, on_toggle=None, on_select=None, on_open=None, on_context=None, focused=None, indent=14, item_height=24, theme=None, ) ``` Un árbol de archivos plegable, como el panel de proyecto de un editor de código. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `nodes` | *obligatorio* | Lista de `TreeNode`. | | `expanded` | *obligatorio* | Conjunto de ids de carpetas abiertas. | | `selected` | `None` | Un id o una colección de ids (selección múltiple). | | `focused` | `None` | Resalta el borde de la fila en la que está el teclado. | | `indent` | `14` | Sangría a la izquierda, en píxeles. | | `item_height` | `24` | Alto de cada fila. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_toggle`, `on_select`, `on_open`, `on_context` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#tree) ##### `ui.TreeNode` ```python @dataclass class TreeNode(name, children=None, id=None, status=None, icon=None, icon_color=None) ``` Un archivo o carpeta para `ui.tree`. Las carpetas tienen `children` (lista). `status` es `"modified"`, `"created"`, `"deleted"`, `"conflict"` o `"ignored"`. `TreeNode.from_paths(paths, status=None)` construye el árbol a partir de rutas como `"src/main.rs"`, con las carpetas primero y el estado de git propagado a las carpetas. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `name` | *obligatorio* | Nombre visible. | | `children` | `None` | Hijos (`None` si es un archivo). | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `status` | `None` | Estado de git. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `icon_color` | `None` | Color del icono: un token del tema o un color hex. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#treenode) #### Relacionados - [Fila de lista](https://albakore.github.io/pyzed/es/library/list-item/): Una fila de lista con icono, texto y un hueco al final. - [Panel](https://albakore.github.io/pyzed/es/library/panel/): Un panel lateral acoplable y redimensionable. - [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/workspace/): La disposición de una ventana estilo editor: barra de título, docks, centro y barra de estado. ### Área de ratón > Hace que cualquier widget reaccione a clics, hover, arrastres y la rueda. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/mouse-area/ :::tip[En pocas palabras] Una capa invisible que permite que cualquier cosa (una imagen, una tarjeta, un dibujo) note los clics y el ratón. ::: `pz.mouse_area` envuelve cualquier widget y avisa de lo que hace el ratón encima: pulsaciones (izquierda, derecha, central, doble clic), entrar y salir, moverse (`on_move(x, y)`) y la rueda (`on_scroll(dx, dy)`); `cursor` cambia el puntero. `pz.hover(base, top)` muestra `top` encima de `base` solo mientras el puntero está encima, y `pz.opaque` impide que los clics lleguen a lo que hay debajo de un widget en un `stack`. `pz.mouse_area` · Categoría: [Acciones](https://albakore.github.io/pyzed/es/library/#acciones) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#mouse_area) **Código:** ```python pz.mouse_area( ui.card(pz.row([pz.icon("folder", size=20), ui.label("Open project…")], spacing=10, align="center"), width=280), on_press=on_open, on_right_press=on_menu, cursor="pointer", ) ``` #### Cuándo usarlo - Para que una tarjeta, una imagen o un dibujo propio se puedan pulsar sin convertirlos en botón. - Para menús de clic derecho (`on_right_press`), gestos de arrastre (`on_move`) y zoom con la rueda. - `pz.hover` para acciones que aparecen al pasar el ratón, como el botón de cerrar de una pestaña. #### Uso ```python import pyzed as pz class Board(pz.App): def __init__(self): self.pointer = (0, 0) self.zoom = 1.0 def view(self): return pz.mouse_area( self.drawing(), on_move=lambda x, y: setattr(self, "pointer", (x, y)), on_scroll=lambda dx, dy: setattr(self, "zoom", max(0.1, self.zoom + dy * 0.1)), on_double_click=self.reset, cursor="crosshair", ) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Cursor La forma del puntero solo se ve con un ratón real; la captura muestra el widget envuelto. ```python pz.mouse_area(ui.card(ui.label("Click me"), width=180), on_press=on_open, cursor="pointer") ``` ```python pz.mouse_area(ui.card(ui.label("Drag me"), width=180), on_move=on_drag, cursor="grab") ``` ```python pz.mouse_area(pz.canvas([draw.circle(60, 40, 24, fill="#74ade8")], width=120, height=80), on_press=on_pick, cursor="crosshair") ``` ##### Contenido en hover `pz.hover` solo dibuja su `top` mientras el puntero está encima: aquí, el botón de cerrar de una fila. ```python pz.hover(ui.list_item("notes.py", on_open, icon="file-text"), pz.container(ui.icon_button("x", on_close, size="xs"), align_x="end", width="fill")) ``` ```python pz.hover(ui.list_item("notes.py", on_open, icon="file-text"), pz.container(ui.icon_button("x", on_close, size="xs"), align_x="end", width="fill")) ``` #### Eventos Área de ratón tiene **10 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_press` | `()` | Clic izquierdo (al pulsar). | | `on_release` | `()` | Al soltar el botón izquierdo. | | `on_right_press` | `()` | Clic derecho (menús contextuales). | | `on_double_click` | `()` | Doble clic. | | `on_enter` | `()` | El ratón entra. | | `on_exit` | `()` | El ratón sale. | | `on_move` | `(x, y)` | Se llama con `(x, y)` al mover el ratón. | | `on_scroll` | `(dx, dy)` | Callable que recibe `(dx, dy)` en líneas al usar la rueda. | | `on_middle_press` | `()` | Clic con el botón central. | | `on_middle_release` | `()` | Se suelta el botón central (para desplazar la vista, por ejemplo). | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Board(pz.App): def __init__(self): self.pointer, self.hovering, self.zoom = (0, 0), False, 1.0 def view(self): return pz.mouse_area( ui.card(ui.label(f"{self.pointer} · zoom {self.zoom:.1f}"), width=320), on_enter=lambda: setattr(self, "hovering", True), on_exit=lambda: setattr(self, "hovering", False), on_move=lambda x, y: setattr(self, "pointer", (round(x), round(y))), on_scroll=lambda dx, dy: setattr(self, "zoom", max(0.1, self.zoom + dy * 0.1)), on_double_click=lambda: setattr(self, "zoom", 1.0), on_right_press=lambda: print("context menu"), cursor="crosshair", ) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Moved: x: float y: float class Board(pz.App): pointer = (0, 0) def update(self, message): match message: case Moved(x, y): self.pointer = (round(x), round(y)) case "reset": self.pointer = (0, 0) def view(self): return pz.mouse_area(ui.card(ui.label(str(self.pointer)), width=320), on_move=Moved, on_double_click="reset") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: hovering: bool = False def view(state, set_state): return pz.mouse_area(ui.card(ui.label("Hovering" if state.hovering else "Point at me"), width=320), on_enter=lambda: set_state(hovering=True), on_exit=lambda: set_state(hovering=False)) ``` #### Consejos :::tip Los estados de hover y del puntero solo existen con un ratón real, así que las vistas previas los dibujan tal como se ven; mira los clips animados del [Showcase](https://albakore.github.io/pyzed/es/showcase/) para verlos en marcha. ::: #### Propiedades y API ##### `pz.mouse_area` ```python mouse_area( content, *, on_press=None, on_release=None, on_right_press=None, on_double_click=None, on_enter=None, on_exit=None, on_move=None, on_scroll=None, on_middle_press=None, on_middle_release=None, cursor=None, ) ``` Detecta eventos del ratón sobre su contenido. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `cursor` | `None` | `"pointer"`, `"text"`, `"grab"`, `"grabbing"`, `"crosshair"`, `"not-allowed"`, `"resize-horizontal"`, `"resize-vertical"`, `"move"` o `"default"`. | Eventos: `on_press`, `on_release`, `on_right_press`, `on_double_click`, `on_enter`, `on_exit`, `on_move`, `on_scroll`, `on_middle_press`, `on_middle_release` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#mouse_area) ##### `pz.hover` ```python hover(base, top) ``` Muestra `top` encima de `base` solo mientras el ratón está encima (por ejemplo, el botón de cerrar de una pestaña). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `base` | *obligatorio* | Widget de abajo. | | `top` | *obligatorio* | Widget que aparece al pasar el ratón. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#hover) ##### `pz.opaque` ```python opaque(content) ``` Hace que un widget bloquee el ratón para lo que está debajo en un `stack`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#opaque) #### Relacionados - [Botón](https://albakore.github.io/pyzed/es/library/button/): El botón con tema: variantes, tamaños, iconos y atajos. - [Popover](https://albakore.github.io/pyzed/es/library/popover/): Contenido flotante en un punto: menús contextuales, selectores de color. - [Pila](https://albakore.github.io/pyzed/es/library/stack/): Superpone los hijos; `pin` coloca uno en x, y. ### Atajos > Una lista de atajos agrupada y con búsqueda. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/shortcuts/ :::tip[En pocas palabras] Una lista con buscador de todos los atajos de teclado de una app. ::: Una lista de atajos agrupada y con búsqueda para un diálogo de Atajos de teclado. `groups` son pares `(título, [(acción, teclas), ...])`. `ui.shortcuts` · Categoría: [Código y herramientas](https://albakore.github.io/pyzed/es/library/#código-y-herramientas) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#shortcuts) **Código:** ```python pz.container(ui.shortcuts([ ("File", [("Save", "ctrl-s"), ("Open file", "ctrl-p")]), ("View", [("Command palette", "ctrl-shift-p"), ("Toggle left dock", "ctrl-b")]), ], on_query=on_query, height=240), width=420) ``` #### Cuándo usarlo - Dentro de un `ui.modal` que se abre desde el menú Ayuda o con Ctrl+K Ctrl+S. #### Uso ```python import pyzed as pz from pyzed import ui pz.container(ui.shortcuts([ ("File", [("Save", "ctrl-s"), ("Open file", "ctrl-p")]), ("View", [("Command palette", "ctrl-shift-p"), ("Toggle left dock", "ctrl-b")]), ], on_query=on_query, height=240), width=420) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python pz.container(ui.shortcuts([("File", [("Save", "ctrl-s"), ("Open", "ctrl-o")])], on_query=on_query, height=130), width=360) ``` ```python pz.container(ui.shortcuts([("View", [("Command palette", "ctrl-shift-p"), ("Zoom in", "ctrl-=")])], query="pal", on_query=on_query, height=110), width=360) ``` #### Eventos Atajos tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_query` | `(text)` | Añade un buscador. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Help(pz.App): def __init__(self): self.query = "" def view(self): return ui.shortcuts([("File", [("Save", "ctrl-s"), ("Open file", "ctrl-p")])], query=self.query, on_query=lambda q: setattr(self, "query", q)) # adds search ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Search: query: str class Help(pz.App): query = "" def update(self, message): match message: case Search(query): self.query = query def view(self): return ui.shortcuts([("File", [("Save", "ctrl-s")])], query=self.query, on_query=Search) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: query: str = "" def view(state, set_state): return ui.shortcuts([("File", [("Save", "ctrl-s"), ("Open file", "ctrl-p")])], query=state.query, on_query=lambda q: set_state(query=q)) ``` #### Propiedades y API ##### `ui.shortcuts` ```python shortcuts(groups, *, query='', on_query=None, height=380, theme=None) ``` Una lista de atajos de teclado agrupada y con búsqueda (para un diálogo de atajos). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `groups` | *obligatorio* | `[(título, [(etiqueta, teclas), ...]), ...]`, teclas como `"ctrl-s"`. | | `query` | `""` | Texto del filtro. | | `height` | `380` | Alto de la lista. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_query` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#shortcuts) #### Relacionados - [Kbd](https://albakore.github.io/pyzed/es/library/kbd/): Atajos de teclado dibujados como teclas, con símbolos en macOS. - [Pista de atajo](https://albakore.github.io/pyzed/es/library/keybinding-hint/): Una pista discreta tipo “Abrir archivo Ctrl P”. - [Acerca de](https://albakore.github.io/pyzed/es/library/about/): Una caja Acerca de con icono, versión, detalles y enlaces. ### Avatar > Un avatar redondo con iniciales en un color de colaborador. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/avatar/ :::tip[En pocas palabras] Un círculo con las iniciales de alguien, en su propio color. ::: Un círculo con iniciales. `index` elige uno de los colores de colaborador (player) del tema, así la misma persona conserva su color; `color` lo fija directamente. `ui.avatar` · Categoría: [Visualización](https://albakore.github.io/pyzed/es/library/#visualización) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#avatar) **Código:** ```python pz.row([ ui.avatar("AL", index=0), ui.avatar("KV", index=1), ui.avatar("MZ", index=2, size=28), ui.avatar("JD", index=3, size=36), ], spacing=8, align="center") ``` #### Cuándo usarlo - Para la cuenta en la barra de título, autores de comentarios o colaboradores. - Para varias personas a la vez usa [Facepile](https://albakore.github.io/pyzed/es/library/facepile/). #### Uso ```python import pyzed as pz from pyzed import ui pz.row([ ui.avatar("AL", index=0), ui.avatar("KV", index=1), ui.avatar("MZ", index=2, size=28), ui.avatar("JD", index=3, size=36), ], spacing=8, align="center") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Color de colaborador ```python ui.avatar("AL", index=0) ``` ```python ui.avatar("KV", index=1) ``` ```python ui.avatar("MZ", index=2) ``` ```python ui.avatar("JD", index=3) ``` ```python ui.avatar("SR", index=4) ``` ```python ui.avatar("PB", index=5) ``` ##### Tamaño ```python ui.avatar("KV", size=18) ``` ```python ui.avatar("KV", size=22) ``` ```python ui.avatar("KV", size=32) ``` ```python ui.avatar("KV", size=48) ``` ##### Color ```python ui.avatar("PZ", color="#c678dd", size=32) ``` ```python ui.avatar("UI", color="#e5c07b", size=32) ``` ##### Estados y eventos `image=` muestra una foto en lugar de las iniciales y `tooltip=` nombra a la persona al pasar el ratón. ```python ui.avatar("KK", status="online", size=32) ``` ```python ui.avatar("AL", status="away", index=2, size=32) ``` ```python ui.avatar("MZ", status="busy", index=4, size=32) ``` #### Eventos Avatar no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.avatar` ```python avatar( initials, *, color=None, size=22, index=0, image=None, status=None, tooltip=None, theme=None, ) ``` Un avatar redondo con iniciales. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `initials` | *obligatorio* | Iniciales (se usan las dos primeras). | | `color` | `None` | Color de fondo. | | `size` | `22` | Diámetro. | | `index` | `0` | Elige un color de colaborador del tema (`theme.players`). | | `image` | `None` | Una imagen (ruta o bytes) en lugar de las iniciales. | | `status` | `None` | Punto de presencia: `"online"`, `"away"`, `"busy"`, `"offline"` o un color. | | `tooltip` | `None` | Texto al pasar el ratón (el nombre de la persona). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#avatar) #### Relacionados - [Facepile](https://albakore.github.io/pyzed/es/library/facepile/): Avatares de colaboradores superpuestos con +N. - [Barra de título](https://albakore.github.io/pyzed/es/library/title-bar/): La barra superior con los selectores de proyecto y rama. ### Aviso > Un aviso en línea con título, texto y acciones. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/callout/ :::tip[En pocas palabras] Una caja de color dentro de la página con un mensaje: un consejo, un aviso o un error. ::: Un aviso en línea (un callout o banner): un icono, un título, una descripción opcional, botones de acción y un botón de cerrar con `on_dismiss`. `tint` es `"info"` (por defecto), `"success"`, `"warning"`, `"error"` o cualquier token del tema. `ui.callout` · Categoría: [Estado y avisos](https://albakore.github.io/pyzed/es/library/#estado-y-avisos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#callout) **Código:** ```python pz.column([ ui.callout("A new version is out", "Pyzed 0.4 adds the component designer.", tint="info", actions=[ui.button("What's new", on_open, size="sm")], on_dismiss=on_dismiss), ui.callout("Could not save", "The file is read-only.", tint="error"), ], spacing=10, width=520) ``` #### Cuándo usarlo - Para mensajes que pertenecen a un lugar de la interfaz: un archivo de solo lectura, una versión nueva, una sincronización fallida. - Para mensajes pasajeros, usa una [Notificación](https://albakore.github.io/pyzed/es/library/toast/). #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ ui.callout("A new version is out", "Pyzed 0.4 adds the component designer.", tint="info", actions=[ui.button("What's new", on_open, size="sm")], on_dismiss=on_dismiss), ui.callout("Could not save", "The file is read-only.", tint="error"), ], spacing=10, width=520) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Tinte Cada tinte elige un icono acorde; `icon=` elige otro. ```python pz.container(ui.callout("Heads up", "Pyzed 0.5 is out."), width=380) ``` ```python pz.container(ui.callout("Synced", "All changes are saved.", tint="success"), width=380) ``` ```python pz.container(ui.callout("Unsaved changes", "Save before closing.", tint="warning"), width=380) ``` ```python pz.container(ui.callout("Could not save", "The file is read-only.", tint="error"), width=380) ``` ```python pz.container(ui.callout("Tip", "Ctrl+Shift+P opens the palette.", tint="accent", icon="lightbulb"), width=380) ``` ##### Contenido ```python pz.container(ui.callout("Read-only file"), width=380) ``` ```python pz.container(ui.callout("A new version is out", "Restart to update.", actions=[ui.button("Restart", on_restart, size="sm")]), width=380) ``` ```python pz.container(ui.callout("Welcome!", "Open a folder to start.", on_dismiss=on_dismiss), width=380) ``` #### Eventos Aviso tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_dismiss` | `()` | Añade un botón de cerrar. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Updates(pz.App): def __init__(self): self.visible = True def view(self): if not self.visible: return ui.muted("No news.") return ui.callout("A new version is out", "Pyzed adds the component designer.", tint="info", on_dismiss=lambda: setattr(self, "visible", False)) # adds the ✕ ``` **Declarativo:** ```python class Updates(pz.App): visible = True def update(self, message): if message == "dismiss": self.visible = False def view(self): return ui.callout("A new version is out", tint="info", on_dismiss="dismiss") if self.visible \ else ui.muted("No news.") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: visible: bool = True def view(state, set_state): if not state.visible: return ui.muted("No news.") return ui.callout("A new version is out", tint="info", on_dismiss=lambda: set_state(visible=False)) ``` #### Propiedades y API ##### `ui.callout` ```python callout( title, description=None, *, tint='info', icon=None, actions=(), on_dismiss=None, theme=None, ) ``` Un aviso en línea (un callout o banner). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | *obligatorio* | Título (texto o widget). | | `description` | `None` | Texto secundario. | | `tint` | `"info"` | `"info"`, `"success"`, `"warning"`, `"error"` o `"hint"`: elige el color y el icono. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `actions` | `()` | Widgets extra (normalmente botones) que se muestran como acciones. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_dismiss` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#callout) #### Relacionados - [Notificación](https://albakore.github.io/pyzed/es/library/toast/): Notificaciones apiladas en la esquina inferior derecha. - [Alerta](https://albakore.github.io/pyzed/es/library/alert/): Un mensaje con un botón. - [Estado vacío](https://albakore.github.io/pyzed/es/library/empty-state/): Un marcador centrado para listas y búsquedas vacías. ### Aviso de inicio de sesión > Pide iniciar sesión y luego muestra el paso del navegador, un código y Cancelar. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/auth-prompt/ :::tip[En pocas palabras] Una caja que te pide iniciar sesión, con un botón para cada forma de hacerlo. ::: `ui.AuthPrompt` es un componente que pide iniciar sesión, para un agente o un servicio que lo necesita. Muestra un título (`title`), un `detail` opcional y un botón por cada forma de entrar: `methods` son diccionarios con `id`, `name` y, opcionales, `description` (se ve como tooltip) e `icon`, y el primero es el botón principal. Elegir uno llega a `on_pick` con su id. Mientras `waiting` tiene ese id muestra un spinner y lo que está pasando (`waiting_text`, o "Esperando al navegador…"), el código que hay que escribir (`code`) con un botón para copiarlo si lo hay, Abrir la página otra vez si hay `url`, y Cancelar. `error` muestra un mensaje en rojo. `ui.AuthPrompt` · Categoría: [Agentes de IA](https://albakore.github.io/pyzed/es/library/#agentes-de-ia) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#authprompt) **Código:** ```python pz.container(ui.AuthPrompt( title="Sign in to use the agent", detail="Your account is used to run the model.", methods=[{"id": "browser", "name": "Log in with the browser", "icon": "globe"}, {"id": "key", "name": "Use an API key", "icon": "key-round"}], on_pick=on_pick, ), width=440) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Donde un agente o un servicio conectado necesita que el usuario inicie sesión para funcionar. - Para un inicio de sesión con código: muestra el `code` que el usuario escribe en la página que se abrió. #### Uso ```python import pyzed as pz from pyzed import ui class Agent(pz.App): def __init__(self): self.waiting, self.code = "", "" def sign_in(self, method): self.waiting = method self.code = start_device_login() # opens the browser and returns the code to type def view(self): return ui.AuthPrompt(title="Sign in to use the agent", waiting=self.waiting, code=self.code, methods=[{"id": "browser", "name": "Log in with the browser"}], on_pick=lambda e: self.sign_in(e.value), on_cancel=lambda e: setattr(self, "waiting", ""), on_copy=lambda e: pz.Command.copy(e.value)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Formas de entrar El primer método es el botón principal (relleno). ```python pz.container(ui.AuthPrompt(title="Sign in to continue", methods=[{"id": "browser", "name": "Log in with the browser"}]), width=380) ``` ```python pz.container(ui.AuthPrompt(title="Sign in to continue", methods=[ {"id": "browser", "name": "Log in with the browser", "icon": "globe"}, {"id": "key", "name": "Use an API key", "icon": "key-round"}, ]), width=380) ``` ```python pz.container(ui.AuthPrompt(title="Sign in to continue", detail="The agent needs your account.", methods=[{"id": "browser", "name": "Log in with the browser"}]), width=380) ``` ```python pz.container(ui.AuthPrompt(title="Sign in to continue", error="The sign-in was cancelled.", methods=[{"id": "browser", "name": "Try again"}]), width=380) ``` ##### Esperando ```python pz.container(ui.AuthPrompt(title="Sign in to continue", waiting="browser"), width=380) ``` ```python pz.container(ui.AuthPrompt(title="Sign in to continue", waiting="cli", waiting_text="Finish signing in in the terminal."), width=380) ``` ```python pz.container(ui.AuthPrompt(title="Sign in to continue", waiting="device", code="WDJB-MJHT", url="https://example.com/device"), width=380) ``` #### Eventos Aviso de inicio de sesión tiene **4 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_pick` | `(event)` | Se eligió un método (`event.value` es su id). | | `on_cancel` | `(event)` | Cancelar. | | `on_open_url` | `(event)` | Abrir la página otra vez. | | `on_copy` | `(event)` | Se copió el código. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Agent(pz.App): def __init__(self): self.waiting, self.code = "", "" def sign_in(self, method): # on_pick: event.value is the method's id self.waiting, self.code = method, "ABCD-1234" def view(self): return ui.AuthPrompt(title="Sign in to use the agent", waiting=self.waiting, code=self.code, methods=[{"id": "browser", "name": "Log in with the browser"}], on_pick=lambda e: self.sign_in(e.value), on_cancel=lambda e: setattr(self, "waiting", ""), on_copy=lambda e: pz.Command.copy(e.value)) # the code ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Picked: method: str class Agent(pz.App): waiting = "" def update(self, message): match message: case Picked(method): self.waiting = method case "cancel": self.waiting = "" def view(self): return ui.AuthPrompt(title="Sign in", waiting=self.waiting, methods=[{"id": "key", "name": "Use an API key"}], on_pick=lambda e: Picked(e.value), on_cancel="cancel") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: waiting: str = "" def view(state, set_state): return ui.AuthPrompt(title="Sign in", waiting=state.waiting, methods=[{"id": "browser", "name": "Log in with the browser"}], on_pick=lambda e: set_state(waiting=e.value), on_cancel=lambda e: set_state(waiting="")) ``` #### Consejos :::tip El botón de copiar solo avisa a `on_copy` con el código en `event.value`; devuelve `pz.Command.copy(e.value)` desde ahí para ponerlo en el portapapeles. ::: #### Propiedades y API ##### `ui.AuthPrompt` ```python class AuthPrompt ``` Un componente que pide iniciar sesión, como Zed cuando un agente lo necesita: un botón por cada forma de entrar y, mientras una está en curso, qué pasa (el navegador o una terminal está abierto), un código para escribir si lo hay, Abrir la página otra vez y Cancelar. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | `""` | El título. | | `detail` | `""` | Una línea debajo. | | `methods` | `None` | Dicts con `id`, `name` y, opcionalmente, `description` e `icon`; el primero es el principal. | | `waiting` | `""` | El id del método en curso (`""` si no hay). | | `waiting_text` | `""` | Lo que se muestra mientras espera. | | `code` | `""` | Un código para escribir (inicio de sesión por dispositivo). | | `url` | `""` | La página que se abrió. | | `error` | `""` | Un error a mostrar. | | `icon_name` | `"key-round"` | El icono. | | `theme` | `None` | Tema. | Eventos: `on_pick`, `on_cancel`, `on_open_url`, `on_copy` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#authprompt) #### Relacionados - [Aviso](https://albakore.github.io/pyzed/es/library/callout/): Un aviso en línea con título, texto y acciones. - [Botón](https://albakore.github.io/pyzed/es/library/button/): El botón con tema: variantes, tamaños, iconos y atajos. - [Fila de servicio](https://albakore.github.io/pyzed/es/library/service-row/): Una fila de ajustes para un servidor MCP, un agente o una skill: estado, interruptor y menú. ### Barra de cambios > Los archivos que cambió un agente, con Conservar todo y Rechazar todo. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/edits-bar/ :::tip[En pocas palabras] Una barra con los archivos que cambió un asistente de IA, para conservar o deshacer los cambios. ::: `ui.EditsBar` es un componente que lista los archivos que cambió un agente: `3 archivos cambiados` con las líneas añadidas y quitadas, Rechazar todo y Conservar todo. Un clic en el título despliega una fila por archivo con su resumen de cambios y botones para rechazar o conservar solo ese archivo; un clic en el nombre de un archivo llega a `on_open`. `files` son diccionarios u objetos con `path` y, opcionales, las líneas añadidas (`added`) y quitadas (`removed`). Los eventos de un archivo (`on_keep_file`, `on_reject_file`, `on_open`) llevan la ruta en `event.value`. `ui.EditsBar` · Categoría: [Agentes de IA](https://albakore.github.io/pyzed/es/library/#agentes-de-ia) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#editsbar) **Código:** ```python pz.container(ui.EditsBar(files=[ {"path": "app/storage.py", "added": 12, "removed": 3}, {"path": "app/forms/login.py", "added": 40, "removed": 0}, {"path": "tests/test_login.py", "added": 18, "removed": 2}, ], on_keep=on_keep, on_reject=on_reject), width=520) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Bajo el chat de un agente, cuando ya editó archivos, para que el usuario los revise antes de conservarlos. #### Uso ```python import pyzed as pz from pyzed import ui class Review(pz.App): def __init__(self): self.files = [{"path": "app/storage.py", "added": 12, "removed": 3}] def keep(self, path=None): # None: every file self.files = [f for f in self.files if path is not None and f["path"] != path] def view(self): return ui.EditsBar(files=self.files, on_keep=lambda e: self.keep(), on_keep_file=lambda e: self.keep(e.value), on_open=lambda e: self.open(e.value)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Estado Las desplegadas se dibujan como quedan tras un clic en la cabecera. ```python pz.container(ui.EditsBar(files=[{"path": "app.py", "added": 12, "removed": 3}, {"path": "ui/form.py", "added": 40, "removed": 0}]), width=440) ``` ```python pz.container(ui.EditsBar(files=[{"path": "app.py", "added": 12, "removed": 3}, {"path": "ui/form.py", "added": 40, "removed": 0}]), width=440) ``` ```python pz.container(ui.EditsBar(files=[{"path": "app.py", "added": 2, "removed": 2}]), width=440) ``` ```python pz.container(ui.EditsBar(files=[{"path": "app.py"}, {"path": "README.md"}]), width=440) ``` #### Eventos Barra de cambios tiene **5 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_keep` | `(event)` | Conservar todo. | | `on_reject` | `(event)` | Rechazar todo. | | `on_keep_file` | `(event)` | Conservar en un archivo (`event.value` es su ruta). | | `on_reject_file` | `(event)` | Rechazar en un archivo. | | `on_open` | `(event)` | Se hizo clic en un archivo. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Review(pz.App): def __init__(self): self.files = [{"path": "app/storage.py", "added": 12, "removed": 3}, {"path": "tests/test_storage.py", "added": 8, "removed": 0}] def drop(self, path=None): # None: every file self.files = [f for f in self.files if path is not None and f["path"] != path] def view(self): # event.value is the file's path return ui.EditsBar(files=self.files, on_keep=lambda e: self.drop(), on_reject=lambda e: self.drop(), on_keep_file=lambda e: self.drop(e.value), on_reject_file=lambda e: self.drop(e.value), on_open=lambda e: print("open", e.value)) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Done: path: str = "" class Review(pz.App): files = [{"path": "app/storage.py", "added": 12, "removed": 3}] def update(self, message): match message: case Done(""): self.files = [] case Done(path): self.files = [f for f in self.files if f["path"] != path] def view(self): return ui.EditsBar(files=self.files, on_keep=Done(), on_keep_file=lambda e: Done(e.value)) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: files: tuple = ({"path": "app/storage.py", "added": 12, "removed": 3},) def view(state, set_state): without = lambda path: tuple(f for f in state.files if f["path"] != path) return ui.EditsBar(files=list(state.files), on_keep=lambda e: set_state(files=()), on_keep_file=lambda e: set_state(files=without(e.value))) ``` #### Propiedades y API ##### `ui.EditsBar` ```python class EditsBar ``` Un componente con los archivos que cambió un agente, como la barra de cambios de Zed: `3 archivos cambiados` con Rechazar todo y Conservar todo, que se despliega en una fila por archivo con sus líneas, Rechazar y Conservar. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `files` | `None` | Dicts u objetos con `path` y, opcionalmente, `added` y `removed` (líneas). | | `theme` | `None` | Tema. | Eventos: `on_keep`, `on_reject`, `on_keep_file`, `on_reject_file`, `on_open` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#editsbar) #### Relacionados - [Vista de diferencias](https://albakore.github.io/pyzed/es/library/diff-view/): Un cambio como líneas de colores: lo añadido en verde, lo quitado en rojo. - [Resumen de cambios](https://albakore.github.io/pyzed/es/library/diff-stat/): `+12 −3` con bloques al estilo de GitHub. - [Llamada a herramienta](https://albakore.github.io/pyzed/es/library/tool-call/): Un paso de un agente de IA: qué hizo, su resultado y Permitir o Rechazar. ### Barra de estado > La barra inferior: diagnósticos, posición del cursor, lenguaje. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/status-bar/ :::tip[En pocas palabras] La barra fina de abajo de la ventana con datos pequeños: errores, número de línea, lenguaje. ::: La barra inferior con elementos al principio (`start`) y al final (`end`). `ui.status_item` muestra un icono y/o texto, se puede pulsar, marcar como `selected` (un dock abierto) y llevar un `tooltip` con `keys`. `ui.status_bar` · Categoría: [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/#espacio-de-trabajo) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#status_bar) **Código:** ```python ui.status_bar( [ui.status_item(icon="panel-left", on_press=on_toggle, selected=True, tooltip="Project panel", keys="ctrl-b"), ui.status_item("0", icon="circle-x"), ui.status_item("2", icon="triangle-alert", icon_color="warning")], [ui.status_item("Ln 12, Col 8"), ui.status_item("UTF-8"), ui.status_item("Python", icon="file-code")], ) ``` #### Cuándo usarlo - Para el estado siempre visible: errores y avisos, posición del cursor, lenguaje, rama de git. #### Uso ```python from pyzed import ui ui.status_bar( [ui.status_item(icon="panel-left", on_press=on_toggle, selected=True, tooltip="Project panel", keys="ctrl-b"), ui.status_item("0", icon="circle-x"), ui.status_item("2", icon="triangle-alert", icon_color="warning")], [ui.status_item("Ln 12, Col 8"), ui.status_item("UTF-8"), ui.status_item("Python", icon="file-code")], ) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Elementos ```python ui.status_item("Ln 12, Col 8") ``` ```python ui.status_item("Python", icon="file-code") ``` ```python ui.status_item("2", icon="triangle-alert", icon_color="warning") ``` ```python ui.status_item(icon="panel-left", on_press=on_toggle, selected=True) ``` ```python ui.status_item("main", icon="git-branch", color="accent") ``` ##### Barra ```python pz.container(ui.status_bar([ui.status_item("0", icon="circle-x")], [ui.status_item("UTF-8"), ui.status_item("Python")]), width=420) ``` #### Eventos Barra de estado tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_press` | `()` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Ide(pz.App): def __init__(self): self.show_left = True def view(self): return ui.status_bar( [ui.status_item(icon="panel-left", selected=self.show_left, tooltip="Project panel", on_press=lambda: setattr(self, "show_left", not self.show_left))], [ui.status_item("Python", icon="file-code")], ) ``` **Declarativo:** ```python class Ide(pz.App): show_left = True def update(self, message): if message == "toggle left": self.show_left = not self.show_left def view(self): return ui.status_bar([ui.status_item(icon="panel-left", on_press="toggle left", selected=self.show_left)]) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: show_left: bool = True def view(state, set_state): return ui.status_bar([ui.status_item(icon="panel-left", selected=state.show_left, on_press=lambda: set_state(show_left=not state.show_left))]) ``` #### Propiedades y API ##### `ui.status_bar` ```python status_bar(start=(), end=(), *, height=28, theme=None) ``` La barra de estado inferior. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `start` | `()` | Widgets al inicio (izquierda). | | `end` | `()` | Widgets al final (derecha). | | `height` | `28` | Alto. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#status_bar) ##### `ui.status_item` ```python status_item( content=None, on_press=None, *, icon=None, color=None, icon_color=None, selected=False, tooltip=None, keys=None, theme=None, ) ``` Un icono y/o texto en la barra de estado (diagnósticos, posición del cursor, lenguaje…). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | `None` | Texto. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `color` | `None` | Color del texto o del icono. | | `icon_color` | `None` | Color del icono: un token del tema o un color hex. | | `selected` | `False` | Lo marca como seleccionado. | | `tooltip` | `None` | Texto del tooltip. | | `keys` | `None` | Atajo que se muestra al final, escrito como texto: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_press` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#status_item) #### Relacionados - [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/workspace/): La disposición de una ventana estilo editor: barra de título, docks, centro y barra de estado. - [Barra de título](https://albakore.github.io/pyzed/es/library/title-bar/): La barra superior con los selectores de proyecto y rama. - [Contador](https://albakore.github.io/pyzed/es/library/count-badge/): Un contador redondo que muestra 99+ por encima del máximo. ### Barra de herramientas > La barra bajo las pestañas: migas de pan y acciones. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/toolbar/ :::tip[En pocas palabras] Una barra fina con las acciones de lo que estás viendo. ::: La barra bajo las pestañas de un editor de código: widgets al principio (`start`, normalmente migas de pan) y al final (`end`, acciones del buffer). `ui.toolbar` · Categoría: [Navegación](https://albakore.github.io/pyzed/es/library/#navegación) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#toolbar) **Código:** ```python ui.toolbar( [ui.breadcrumbs([("file-code", "src"), "main.rs", "fn main"])], [ui.icon_button("search", on_search, size="sm"), ui.icon_button("sparkles", on_assist, size="sm")], ) ``` #### Cuándo usarlo - Entre la barra de pestañas y el editor, o arriba de cualquier vista de contenido. #### Uso ```python from pyzed import ui ui.toolbar( [ui.breadcrumbs([("file-code", "src"), "main.rs", "fn main"])], [ui.icon_button("search", on_search, size="sm"), ui.icon_button("sparkles", on_assist, size="sm")], ) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido ```python pz.container(ui.toolbar([ui.breadcrumbs(["src", "main.rs"])]), width=400) ``` ```python pz.container(ui.toolbar([], [ui.icon_button("search", on_search, size="sm"), ui.icon_button("sparkles", on_assist, size="sm")]), width=400) ``` ```python pz.container(ui.toolbar([ui.label("Preview")], [ui.button("Publish", on_publish, size="sm", variant="solid")], height=40), width=400) ``` ##### Estados y eventos ```python pz.container(ui.toolbar([ui.breadcrumbs(["src", "app.py"])], [ui.icon_button("play", on_press)], center=ui.muted("Running…", size="sm")), width=440) ``` #### Eventos Barra de herramientas no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.toolbar` ```python toolbar(start=(), end=(), *, center=None, height=32, theme=None) ``` La barra bajo las pestañas: breadcrumbs a la izquierda y acciones del buffer a la derecha. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `start` | `()` | Widgets al inicio (izquierda). | | `end` | `()` | Widgets al final (derecha). | | `center` | `None` | Se mantiene en el centro de la barra sean cuales sean los anchos de los lados. | | `height` | `32` | Alto. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#toolbar) #### Relacionados - [Barra de pestañas](https://albakore.github.io/pyzed/es/library/tab-bar/): Pestañas de editor con iconos, puntos de cambios y botón de cerrar. - [Migas de pan](https://albakore.github.io/pyzed/es/library/breadcrumbs/): `src › main.rs › fn main`, pulsables si quieres. - [Botón de icono](https://albakore.github.io/pyzed/es/library/icon-button/): Un botón cuadrado con solo un icono y un tooltip opcional. ### Barra de pestañas > Pestañas de editor con iconos, puntos de cambios y botón de cerrar. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/tab-bar/ :::tip[En pocas palabras] Las pestañas de arriba de un editor, una por documento abierto. ::: Una fila de pestañas de editor con controles opcionales al principio y al final. `ui.Tab.for_file(path)` crea una pestaña con el nombre del archivo, su icono y la ruta como id. `dirty=True` muestra un punto que se convierte en el botón de cerrar al pasar el ratón, `preview=True` pone el nombre en cursiva y `status` lo tiñe con un color de git. `on_select(id)` y `on_close(id)` reciben el id de la pestaña. `ui.tab_bar` · Categoría: [Navegación](https://albakore.github.io/pyzed/es/library/#navegación) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#tab_bar) **Código:** ```python ui.tab_bar( [ui.Tab.for_file("src/main.rs"), ui.Tab.for_file("src/app.rs", dirty=True, status="modified"), ui.Tab.for_file("README.md", preview=True)], "src/main.rs", on_select=on_select, on_close=on_close, start=[ui.icon_button("arrow-left", on_back, size="sm"), ui.icon_button("arrow-right", on_forward, size="sm")], end=[ui.icon_button("plus", on_new, size="sm"), ui.icon_button("columns-2", on_split, size="sm")], ) ``` #### Cuándo usarlo - Para los documentos abiertos de un editor, o cualquier conjunto de vistas que se abren y cierran. #### Uso ```python import pyzed as pz from pyzed import ui class Editor(pz.App): def __init__(self): self.open, self.active = ["main.py", "app.py"], "main.py" def close(self, path): i = self.open.index(path) self.open.remove(path) if self.active == path: self.active = self.open[min(i, len(self.open) - 1)] if self.open else None def view(self): return ui.tab_bar([ui.Tab.for_file(p) for p in self.open], self.active, on_select=lambda p: setattr(self, "active", p), on_close=self.close) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Pestañas ```python pz.container(ui.tab_bar([ui.Tab.for_file("main.rs"), ui.Tab.for_file("app.rs")], "main.rs", on_close=on_close), width=400) ``` ```python pz.container(ui.tab_bar([ui.Tab.for_file("main.rs"), ui.Tab.for_file("notes.md", dirty=True)], "main.rs", on_close=on_close), width=400) ``` ```python pz.container(ui.tab_bar([ui.Tab.for_file("README.md", preview=True)], "README.md", on_close=on_close), width=400) ``` ```python pz.container(ui.tab_bar([ui.Tab.for_file("app.py", status="modified"), ui.Tab.for_file("new.py", status="created")], "app.py", on_close=on_close), width=400) ``` ```python pz.container(ui.tab_bar([ui.Tab("home", "Home", icon="house"), ui.Tab("logs", "Logs", icon="scroll-text")], "home", on_select=on_select), width=400) ``` ##### Controles ```python pz.container(ui.tab_bar([ui.Tab.for_file("main.rs")], "main.rs", on_close=on_close, start=[ui.icon_button("arrow-left", on_back, size="sm")], end=[ui.icon_button("plus", on_new, size="sm")]), width=400) ``` ```python pz.row([ui.tab(ui.Tab.for_file("main.rs"), active=True), ui.tab(ui.Tab.for_file("app.rs"), active=False, dimmed=True)]) ``` ##### Estados y eventos Un clic central cierra una pestaña, `on_context(id)` se ejecuta con un clic derecho y `on_new` con un doble clic en el espacio vacío. ```python ui.tab_bar([ui.Tab("app", "app.py", icon="file", pinned=True), ui.Tab("ui", "ui.py", icon="file")], "ui", on_close=on_close, on_context=on_context, on_new=on_new) ``` #### Eventos Barra de pestañas tiene **7 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_select` | `ui.tab_bar`, `ui.tab` | `(tab_id)` | Callable que recibe el elemento elegido. | | `on_close` | `ui.tab_bar`, `ui.tab` | `(tab_id)` | Mensaje o callable que se envía al pulsar la X. | | `on_drag` | `ui.tab_bar` | `(tab_id)` | Se llama con el id de la pestaña cuando empieza a arrastrarse. | | `on_drop` | `ui.tab_bar` | `(tab_id, event)` | Se llama con el id de la pestaña y el `Event` al soltar la pestaña arrastrada: `event.window_x`/`window_y` (fuera de la ventana si se soltó fuera) y `event.screen`. | | `on_drag_over` | `ui.tab_bar` | `(index)` | Mientras `dragging` tiene valor: se llama con el índice de inserción bajo el puntero, o `None` cuando sale de la barra. | | `on_context` | `ui.tab_bar` | `(tab_id)` | Recibe el id de la pestaña en la que se hace clic derecho. | | `on_new` | `ui.tab_bar` | `()` | Se envía con un doble clic en el espacio vacío tras las pestañas. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Editor(pz.App): def __init__(self): self.open, self.active = ["main.py", "app.py"], "main.py" self.untitled = 0 def close(self, path): # on_close: the id of the tab i = self.open.index(path) self.open.remove(path) if self.active == path: self.active = self.open[min(i, len(self.open) - 1)] if self.open else None def new(self): # on_new: double click after the tabs self.untitled += 1 self.open.append(f"untitled-{self.untitled}") self.active = self.open[-1] def view(self): return ui.tab_bar([ui.Tab.for_file(p) for p in self.open], self.active, on_select=lambda p: setattr(self, "active", p), on_close=self.close, on_new=self.new, on_context=lambda p: print("menu for", p)) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Activate: path: str @dataclass class Close: path: str class Editor(pz.App): open, active = ["main.py", "app.py"], "main.py" def update(self, message): match message: case Activate(path): self.active = path case Close(path): self.open = [p for p in self.open if p != path] self.active = self.open[0] if self.open else None def view(self): return ui.tab_bar([ui.Tab.for_file(p) for p in self.open], self.active, on_select=Activate, on_close=Close) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: open: tuple = ("main.py", "app.py") active: str = "main.py" def view(state, set_state): def close(path): rest = tuple(p for p in state.open if p != path) set_state(open=rest, active=state.active if state.active != path else (rest[0] if rest else "")) return ui.tab_bar([ui.Tab.for_file(p) for p in state.open], state.active, on_select=lambda p: set_state(active=p), on_close=close) ``` #### Propiedades y API ##### `ui.tab_bar` ```python tab_bar( tabs, active, *, on_select=None, on_close=None, start=(), end=(), height=32, on_drag=None, on_drop=None, on_drag_over=None, dragging=None, drop_index=None, on_context=None, on_new=None, theme=None, ) ``` Una fila de pestañas con controles opcionales al principio y al final (navegación, dividir, +). El botón de cerrar aparece en la activa y al pasar el ratón. Las pestañas se pueden arrastrar para reordenarlas o sacarlas de la ventana: ver [Pestañas arrastrables](https://albakore.github.io/pyzed/es/guides/windows/#pestañas-arrastrables-y-ventanas-desprendibles). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `tabs` | *obligatorio* | Lista de `Tab`. | | `active` | *obligatorio* | Id de la pestaña activa. | | `start` | `()` | Widgets al inicio (izquierda). | | `end` | `()` | Widgets al final (derecha). | | `height` | `32` | Alto. | | `dragging` | `None` | Id de la pestaña que se arrastra (la apaga y activa `on_drag_over`). | | `drop_index` | `None` | Dibuja la marca de inserción antes de la pestaña de este índice (`len(tabs)`: después de la última). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_select`, `on_close`, `on_drag`, `on_drop`, `on_drag_over`, `on_context`, `on_new` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#tab_bar) ##### `ui.Tab` ```python @dataclass class Tab(id, label, icon=None, icon_color=None, dirty=False, preview=False, status=None, pinned=False, tooltip=None) ``` Una pestaña. `Tab.for_file(path, **kwargs)` la crea con el nombre y el icono del archivo. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `id` | *obligatorio* | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `label` | *obligatorio* | Texto de la etiqueta. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `icon_color` | `None` | Color del icono: un token del tema o un color hex. | | `dirty` | `False` | Cambios sin guardar (muestra un punto). | | `preview` | `False` | Pestaña de vista previa (en cursiva). | | `status` | `None` | Estado de git (tiñe el nombre). | | `pinned` | `False` | Muestra solo el icono, no tiene botón de cerrar y va primera en `tab_bar`. | | `tooltip` | `None` | Texto al pasar el ratón (la ruta completa…). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#tab) ##### `ui.tab` ```python tab(item, *, active, on_select=None, on_close=None, height=32, dimmed=False, theme=None) ``` Una sola pestaña. Normalmente se usa `tab_bar`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `item` | *obligatorio* | El `Tab`. | | `active` | *obligatorio* | Si es la activa. | | `height` | `32` | Alto. | | `dimmed` | `False` | Apaga la etiqueta (la pestaña que se arrastra). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_select`, `on_close` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#tab-1) #### Relacionados - [Barra de herramientas](https://albakore.github.io/pyzed/es/library/toolbar/): La barra bajo las pestañas: migas de pan y acciones. - [Migas de pan](https://albakore.github.io/pyzed/es/library/breadcrumbs/): `src › main.rs › fn main`, pulsables si quieres. - [Segmentado](https://albakore.github.io/pyzed/es/library/segmented/): Un grupo de botones de texto excluyentes. ### Barra de título > La barra superior con los selectores de proyecto y rama. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/title-bar/ :::tip[En pocas palabras] La barra de lo más alto de la ventana, con el nombre del proyecto y botones rápidos. ::: La barra superior de la ventana: widgets al principio (`start`, selectores de proyecto y rama), un `center` opcional y widgets al final (`end`, cuenta, notificaciones). `ui.title_button` es el botón compacto que usa (`pyzed ▾`, `main`), con estilos `chevron` y `muted`. `ui.title_bar` · Categoría: [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/#espacio-de-trabajo) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#title_bar) **Código:** ```python ui.title_bar( [ui.title_button("pyzed", on_projects, chevron=True), ui.title_button("main", on_branches, icon="git-branch", muted=True)], [ui.facepile(["Ana", "Luis"]), ui.icon_button("bell", on_bell, size="sm"), ui.avatar("KV")], center=ui.muted("notes.py"), ) ``` #### Cuándo usarlo - Arriba de un `ui.workspace`, o de cualquier ventana con `decorations=False`. #### Uso ```python from pyzed import ui ui.title_bar( [ui.title_button("pyzed", on_projects, chevron=True), ui.title_button("main", on_branches, icon="git-branch", muted=True)], [ui.facepile(["Ana", "Luis"]), ui.icon_button("bell", on_bell, size="sm"), ui.avatar("KV")], center=ui.muted("notes.py"), ) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Botones de título ```python ui.title_button("pyzed", on_projects) ``` ```python ui.title_button("pyzed", on_projects, chevron=True) ``` ```python ui.title_button("main", on_branches, icon="git-branch", muted=True) ``` ##### Barra ```python pz.container(ui.title_bar([ui.title_button("pyzed", chevron=True)]), width=420) ``` ```python pz.container(ui.title_bar([], [], center=ui.muted("notes.py — Notes")), width=420) ``` ```python pz.container(ui.title_bar([], [ui.icon_button("bell", on_bell, size="sm"), ui.avatar("KV")]), width=420) ``` #### Eventos Barra de título tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_press` | `()` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Ide(pz.App): def __init__(self): self.picking = None def view(self): return ui.title_bar([ ui.title_button("pyzed", lambda: setattr(self, "picking", "project"), chevron=True), ui.title_button("main", lambda: setattr(self, "picking", "branch"), icon="git-branch", muted=True), ], center=ui.muted(f"Pick a {self.picking}" if self.picking else "notes.py")) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Pick: what: str class Ide(pz.App): picking = None def update(self, message): match message: case Pick(what): self.picking = what def view(self): return ui.title_bar([ui.title_button("pyzed", Pick("project"), chevron=True), ui.title_button("main", Pick("branch"), icon="git-branch", muted=True)]) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: project: str = "pyzed" projects: tuple = ("pyzed", "notes") def view(state, set_state): nxt = state.projects[(state.projects.index(state.project) + 1) % len(state.projects)] return ui.title_bar([ui.title_button(state.project, lambda: set_state(project=nxt), chevron=True)]) ``` #### Propiedades y API ##### `ui.title_bar` ```python title_bar(start=(), end=(), *, center=None, height=34, theme=None) ``` La barra superior: selectores de proyecto y rama a la izquierda, cuenta y acciones a la derecha. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `start` | `()` | Widgets al inicio (izquierda). | | `end` | `()` | Widgets al final (derecha). | | `center` | `None` | Widget centrado. | | `height` | `34` | Alto. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#title_bar) ##### `ui.title_button` ```python title_button( content, on_press=None, *, icon=None, muted=False, chevron=False, theme=None, ) ``` Un botón compacto para la barra de título (`pyzed ▾`, `main`…). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Texto. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `muted` | `False` | Texto atenuado. | | `chevron` | `False` | Añade un ▾. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_press` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#title_button) #### Relacionados - [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/workspace/): La disposición de una ventana estilo editor: barra de título, docks, centro y barra de estado. - [Barra de estado](https://albakore.github.io/pyzed/es/library/status-bar/): La barra inferior: diagnósticos, posición del cursor, lenguaje. - [Facepile](https://albakore.github.io/pyzed/es/library/facepile/): Avatares de colaboradores superpuestos con +N. - [Marco de ventana](https://albakore.github.io/pyzed/es/library/window-frame/): Tu propia barra de título, botones de ventana y menús. ### Botón > El botón con tema: variantes, tamaños, iconos y atajos. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/button/ :::tip[En pocas palabras] Un botón es algo que pulsas para que la app haga algo: guardar, enviar, abrir, borrar. ::: Muestra un botón con el estilo del tema. Toma los colores del tema actual, puede llevar un icono y un atajo de teclado, y tiene varias variantes y tamaños. Cualquier parámetro de estilo de `pz.button` (`bg`, `hover_bg`, `color`, `shadow`, `radius`…) reemplaza al del tema. `ui.button` · Categoría: [Acciones](https://albakore.github.io/pyzed/es/library/#acciones) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#button) **Código:** ```python pz.row([ ui.button("Cancel", on_cancel, variant="ghost"), ui.button("Save", on_save, icon="save", variant="solid", keys="ctrl-s"), ], spacing=8) ``` #### Cuándo usarlo - Para las acciones principales de un formulario, diálogo o barra. - Usa `variant="solid"` para la acción principal y `"ghost"` para las secundarias. - Para un botón con solo un icono, usa [Botón de icono](https://albakore.github.io/pyzed/es/library/icon-button/). #### Uso ```python import pyzed as pz from pyzed import ui class Editor(pz.App): def save(self): ... def view(self): return ui.button("Save", self.save, icon="save", keys="ctrl-s") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Variante `filled` es la de por defecto; usa `solid` para la única acción principal de una pantalla. ```python ui.button("Filled", on_press) ``` ```python ui.button("Ghost", on_press, variant="ghost") ``` ```python ui.button("Subtle", on_press, variant="subtle") ``` ```python ui.button("Outline", on_press, variant="outline") ``` ```python ui.button("Solid", on_press, variant="solid") ``` ```python ui.button("Tinted", on_press, variant="tinted") ``` ##### Colores semánticos Un nombre de color como `variant` da un botón teñido; `tint=` colorea uno `solid`. ```python ui.button("Upgrade", on_press, variant="accent", icon="sparkles") ``` ```python ui.button("Done", on_press, variant="success", icon="check") ``` ```python ui.button("Careful", on_press, variant="warning", icon="triangle-alert") ``` ```python ui.button("Delete", on_press, variant="error", icon="trash-2") ``` ```python ui.button("Details", on_press, variant="info", icon="info") ``` ```python ui.button("Delete forever", on_press, variant="solid", tint="error") ``` ##### Tamaño ```python ui.button("Extra small", on_press, size="xs") ``` ```python ui.button("Small", on_press, size="sm") ``` ```python ui.button("Medium", on_press, size="md") ``` ```python ui.button("Large", on_press, size="lg") ``` ##### Contenido ```python ui.button("Save", on_press, icon="save") ``` ```python ui.button("Next", on_press, icon="arrow-right", icon_position="end") ``` ```python ui.button("Save", on_press, icon="save", keys="ctrl-s") ``` ```python ui.button("Online", on_press, icon="circle", icon_color="success") ``` ```python ui.button(icon="plus", on_press=on_press) ``` ##### Estado Un botón sin `on_press` se dibuja deshabilitado, así las acciones sin terminar lo parecen. ```python ui.button("Selected", on_press, selected=True) ``` ```python ui.button("Disabled", on_press, disabled=True) ``` ```python ui.button("Not wired yet") ``` ```python pz.container(ui.button("Sign in", on_press, variant="solid", full_width=True), width=280) ``` ##### Estilo propio Los argumentos de estilo de `pz.button` (`bg`, `hover_bg`, `pressed_bg`, `color`, `border_color`, `shadow`, `radius`, `padding`) reemplazan los valores del tema solo en ese botón. ```python ui.button("Lifted", on_press, variant="solid", shadow=("#00000066", (0, 4), 12)) ``` ```python ui.button("Custom", on_press, bg="#c678dd", hover_bg="#d08ae6", color="#ffffff") ``` ```python ui.button("Gold border", on_press, variant="outline", border_color="#e5c07b") ``` ```python ui.button("Pill", on_press, variant="solid", radius=999) ``` ##### Estados y eventos ```python ui.button("Saving", on_press, icon="save", loading=True) ``` ```python ui.button("Save", on_press, tooltip="Writes the file") ``` ##### Widget base El widget `pz.*` de debajo: sin tema, todos los colores y tamaños son tuyos. ```python pz.button("pz.button", on_press, padding=(6, 14), bg="#3a7bc8", color="#ffffff", radius=6) ``` ```python pz.button(pz.row([pz.icon("heart", size=14), pz.text("Like")], spacing=6, align="center"), on_press, padding=(6, 12), bg="#c678dd22", color="#c678dd", border_color="#c678dd", border_width=1, radius=999) ``` #### Ejemplos ##### Ancho completo `full_width=True` estira el botón hasta el ancho de su contenedor. **Código:** ```python pz.column([ ui.button("Sign in", on_press, variant="solid", full_width=True), ui.button("Create an account", on_press, variant="outline", full_width=True), ], spacing=8, width=320) ``` #### Eventos Botón tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_press` | `ui.button`, `pz.button` | `()` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Editor(pz.App): def __init__(self): self.saved = 0 def save(self): # on_press: called with nothing self.saved += 1 def view(self): return pz.row([ ui.button("Save", self.save, icon="save", variant="solid", keys="ctrl-s"), ui.button("Reset", lambda: setattr(self, "saved", 0), variant="ghost"), ui.label(f"Saved {self.saved} times"), ], spacing=8, align="center") ``` **Declarativo:** ```python class Editor(pz.App): saved = 0 def update(self, message): # every button sends a message here match message: case "save": self.saved += 1 case "reset": self.saved = 0 def view(self): return pz.row([ ui.button("Save", "save", icon="save", variant="solid", keys="ctrl-s"), ui.button("Reset", "reset", variant="ghost"), ui.label(f"Saved {self.saved} times"), ], spacing=8, align="center") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: saved: int = 0 def view(state, set_state): # a pure function: state in, widgets out return pz.row([ ui.button("Save", lambda: set_state(saved=state.saved + 1), icon="save", variant="solid"), ui.button("Reset", lambda: set_state(saved=0), variant="ghost"), ui.label(f"Saved {state.saved} times"), ], spacing=8, align="center") ``` #### Consejos :::tip Para pasarle un valor al callback, átalo con un argumento por defecto: `ui.button(nombre, lambda n=nombre: self.abrir(n))`. ::: #### Propiedades y API ##### `ui.button` ```python button( content=None, on_press=None, *, icon=None, icon_position='start', icon_color=None, variant='filled', tint=None, size='md', selected=False, disabled=False, full_width=False, keys=None, loading=False, tooltip=None, theme=None, **style, ) ``` Un botón con tema, con variantes, tamaños, un icono y un atajo. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | `None` | Texto o widget. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `icon_position` | `"start"` | `"start"` o `"end"`. | | `icon_color` | `None` | Color del icono: un token del tema o un color hex. | | `variant` | `"filled"` | `"filled"`, `"ghost"`, `"subtle"`, `"outline"`, `"solid"`, `"tinted"`, o un color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"`. | | `tint` | `None` | Color de las variantes `solid` y `tinted`. | | `size` | `"md"` | `"xs"`, `"sm"`, `"md"` o `"lg"`. | | `selected` | `False` | Lo marca como seleccionado. | | `disabled` | `False` | Lo deshabilita (ignora los clics). | | `full_width` | `False` | Ocupa todo el ancho. | | `keys` | `None` | Atajo que se muestra al final, escrito como texto: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `loading` | `False` | Muestra un spinner en lugar del icono e ignora las pulsaciones (mantén una suscripción `pz.every(1/30, ...)` mientras gira). | | `tooltip` | `None` | Texto que aparece al pasar el ratón. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | | `**style` | — | Cualquier parámetro de estilo de `pyzed.button` (`bg`, `hover_bg`, `color`…). | Eventos: `on_press` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#button) ##### `pz.button` ```python button( content, on_press=None, *, padding=None, width=None, height=None, bg=None, color=None, border_color=None, border_width=0.0, radius=None, shadow=None, hover_bg=None, hover_color=None, hover_border_color=None, pressed_bg=None, disabled_color=None, clip=False, ) ``` Un botón sin estilo predefinido. Para el botón con tema usa `ui.button`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido del botón. | | `padding` | `None` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `color` | `None` | Color del texto o del icono. | | `border_color` | `None` | Color del borde. | | `border_width` | `0.0` | Grosor del borde en píxeles. | | `radius` | `None` | Radio de las esquinas: un número o `(sup_izq, sup_der, inf_der, inf_izq)`. | | `shadow` | `None` | Sombra: `(color, (dx, dy), desenfoque)`. | | `hover_bg` | `None` | Fondo al pasar el ratón. | | `hover_color` | `None` | Color del texto al pasar el ratón. | | `hover_border_color` | `None` | Color del borde al pasar el ratón. | | `pressed_bg` | `None` | Fondo mientras se pulsa. | | `disabled_color` | `None` | Color del texto cuando está deshabilitado. | | `clip` | `False` | Recorta el contenido que se sale de los límites. | Eventos: `on_press` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#button) #### Relacionados - [Botón de icono](https://albakore.github.io/pyzed/es/library/icon-button/): Un botón cuadrado con solo un icono y un tooltip opcional. - [Botón dividido](https://albakore.github.io/pyzed/es/library/split-button/): Una acción principal más una flecha que abre más opciones. - [Grupo de alternancia](https://albakore.github.io/pyzed/es/library/toggle-group/): Un control segmentado solo con iconos. - [Tooltip](https://albakore.github.io/pyzed/es/library/tooltip/): Una pista que aparece al pasar el ratón, con su atajo opcional. ### Botón de icono > Un botón cuadrado con solo un icono y un tooltip opcional. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/icon-button/ :::tip[En pocas palabras] Un botón pequeño con solo un dibujo (una lupa, un engranaje) en lugar de palabras. ::: Un botón cuadrado y compacto que muestra un icono de [Lucide](https://lucide.dev). Es transparente por defecto, así que encaja en barras de herramientas, cabeceras de panel y barras de pestañas. `tooltip=` añade un tooltip al pasar el ratón. `ui.icon_button` · Categoría: [Acciones](https://albakore.github.io/pyzed/es/library/#acciones) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#icon_button) **Código:** ```python pz.row([ ui.icon_button("search", on_search, tooltip="Search"), ui.icon_button("settings", on_settings, tooltip="Settings"), ui.icon_button("bell", on_notifications, selected=True), ui.icon_button("plus", on_new, variant="filled"), ], spacing=6) ``` #### Cuándo usarlo - Para acciones frecuentes con un icono evidente (buscar, ajustes, cerrar, añadir). - Dale siempre un `tooltip` cuando el icono solo pueda ser ambiguo. - `selected=True` muestra un estado activo (un panel abierto, un filtro activado). #### Uso ```python import pyzed as pz from pyzed import ui pz.row([ ui.icon_button("search", on_search, tooltip="Search"), ui.icon_button("settings", on_settings, tooltip="Settings"), ui.icon_button("bell", on_notifications, selected=True), ui.icon_button("plus", on_new, variant="filled"), ], spacing=6) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Tamaño ```python ui.icon_button("star", on_press, size="xs") ``` ```python ui.icon_button("star", on_press, size="sm") ``` ```python ui.icon_button("star", on_press, size="md") ``` ```python ui.icon_button("star", on_press, size="lg") ``` ##### Variante ```python ui.icon_button("settings", on_press) ``` ```python ui.icon_button("settings", on_press, variant="filled") ``` ```python ui.icon_button("settings", on_press, variant="outline") ``` ```python ui.icon_button("settings", on_press, variant="solid") ``` ```python ui.icon_button("settings", on_press, variant="tinted") ``` ##### Color ```python ui.icon_button("sparkles", on_press, color="accent") ``` ```python ui.icon_button("circle-check", on_press, color="success") ``` ```python ui.icon_button("triangle-alert", on_press, color="warning") ``` ```python ui.icon_button("trash-2", on_press, color="error", variant="outline") ``` ##### Estado Dibujado tal como lo muestra el puntero: los tooltips solo se abren al pasar el ratón. ```python ui.icon_button("panel-left", on_press, selected=True) ``` ```python ui.icon_button("lock", on_press, disabled=True) ``` ```python ui.icon_button("search", on_press, tooltip="Search") ``` ##### Estados y eventos ```python ui.icon_button("bell", on_press, indicator="accent") ``` ```python ui.icon_button("trash-2", on_press, tint="error") ``` ```python ui.icon_button("refresh-cw", on_press, loading=True) ``` ```python ui.icon_button("search", on_press, tooltip="Search", keys="ctrl-f") ``` #### Eventos Botón de icono tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_press` | `()` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Toolbar(pz.App): def __init__(self): self.bold = False def toggle_bold(self): self.bold = not self.bold def view(self): return pz.row([ ui.icon_button("bold", self.toggle_bold, selected=self.bold, tooltip="Bold", keys="ctrl-b"), ui.icon_button("trash-2", self.clear, tooltip="Clear", disabled=not self.bold), ], spacing=4) def clear(self): self.bold = False ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class ToggleBold: pass class Toolbar(pz.App): bold = False def update(self, message): if isinstance(message, ToggleBold): self.bold = not self.bold def view(self): return ui.icon_button("bold", ToggleBold(), selected=self.bold, tooltip="Bold") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: bold: bool = False def view(state, set_state): return ui.icon_button("bold", lambda: set_state(bold=not state.bold), selected=state.bold, tooltip="Bold") ``` #### Consejos :::tip Ejecuta `pyzed icons` para buscar los nombres de iconos. ::: #### Propiedades y API ##### `ui.icon_button` ```python icon_button( name, on_press=None, *, size='md', variant='ghost', color=None, selected=False, disabled=False, tooltip=None, keys=None, tint=None, loading=False, indicator=None, theme=None, ) ``` Un botón cuadrado con solo un icono. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `name` | *obligatorio* | Nombre del icono. | | `size` | `"md"` | `"xs"`, `"sm"`, `"md"` o `"lg"`. | | `variant` | `"ghost"` | Como en `ui.button`. Por defecto `"ghost"`. | | `color` | `None` | Color del icono. | | `selected` | `False` | Lo marca como seleccionado. | | `disabled` | `False` | Lo deshabilita (ignora los clics). | | `tooltip` | `None` | Texto del tooltip. | | `keys` | `None` | Atajo que se muestra en el tooltip. | | `tint` | `None` | Lo colorea (`"error"`, `"accent"`…). | | `loading` | `False` | Cambia el icono por un spinner e ignora las pulsaciones. | | `indicator` | `None` | Añade un punto de este color arriba a la derecha (sin leer, cambios…). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_press` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#icon_button) #### Relacionados - [Botón](https://albakore.github.io/pyzed/es/library/button/): El botón con tema: variantes, tamaños, iconos y atajos. - [Tooltip](https://albakore.github.io/pyzed/es/library/tooltip/): Una pista que aparece al pasar el ratón, con su atajo opcional. - [Barra de herramientas](https://albakore.github.io/pyzed/es/library/toolbar/): La barra bajo las pestañas: migas de pan y acciones. ### Botón dividido > Una acción principal más una flecha que abre más opciones. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/split-button/ :::tip[En pocas palabras] Un botón con dos partes: la grande hace lo habitual y la flechita muestra otras formas de hacerlo. ::: Un botón con un chevron separado a la derecha. `on_press` ejecuta la acción principal y `on_menu` suele abrir un `ui.menu` dentro de un `ui.popover` con las alternativas. `ui.split_button` · Categoría: [Acciones](https://albakore.github.io/pyzed/es/library/#acciones) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#split_button) **Código:** ```python pz.row([ ui.split_button("Run", on_run, on_menu, icon="play"), ui.split_button("Deploy", on_deploy, on_menu, variant="solid"), ], spacing=12) ``` #### Cuándo usarlo - Cuando una acción es la habitual pero hay algunas variantes (Ejecutar / Ejecutar con argumentos). - Evítalo si las opciones son igual de importantes: usa un [Select](https://albakore.github.io/pyzed/es/library/select/). #### Uso ```python import pyzed as pz from pyzed import ui class Runner(pz.App): def __init__(self): self.menu_open = False def view(self): button = ui.split_button("Run", self.run, lambda: setattr(self, "menu_open", True), icon="play") menu = ui.menu([ui.MenuItem("Run with arguments…", self.run_args), ui.MenuItem("Debug", self.debug, icon="bug")]) if self.menu_open else None return ui.popover(button, menu, y=32, on_dismiss=lambda: setattr(self, "menu_open", False)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Variante ```python ui.split_button("Run", on_run, on_menu, icon="play") ``` ```python ui.split_button("Run", on_run, on_menu, icon="play", variant="ghost") ``` ```python ui.split_button("Run", on_run, on_menu, icon="play", variant="outline") ``` ```python ui.split_button("Deploy", on_deploy, on_menu, icon="rocket", variant="solid") ``` ##### Tamaño ```python ui.split_button("Build", on_build, on_menu, size="sm") ``` ```python ui.split_button("Build", on_build, on_menu, size="md") ``` ```python ui.split_button("Build", on_build, on_menu, size="lg") ``` ##### Contenido ```python ui.split_button("Merge", on_merge, on_menu) ``` ```python ui.split_button("Merge", on_merge, on_menu, icon="git-merge") ``` ##### Estados y eventos ```python ui.split_button("Deploy", on_press, on_menu, tint="success") ``` ```python ui.split_button("Run", on_press, on_menu, disabled=True) ``` #### Eventos Botón dividido tiene **2 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_press` | `()` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | | `on_menu` | `()` | Mensaje o callable del botón del menú. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Runner(pz.App): def __init__(self): self.menu_open = False self.last = "nothing yet" def run(self, how="run"): self.last, self.menu_open = how, False def view(self): button = ui.split_button("Run", self.run, # on_press: the main action lambda: setattr(self, "menu_open", True), # on_menu: the arrow icon="play") menu = ui.menu([ui.MenuItem("Debug", lambda: self.run("debug"), icon="bug")]) return pz.column([ ui.popover(button, menu if self.menu_open else None, y=32, on_dismiss=lambda: setattr(self, "menu_open", False)), ui.label(f"Last: {self.last}"), ], spacing=8) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Run: how: str = "run" class Runner(pz.App): menu_open = False last = "nothing yet" def update(self, message): match message: case Run(how): self.last, self.menu_open = how, False case "menu": self.menu_open = True case "dismiss": self.menu_open = False def view(self): menu = ui.menu([ui.MenuItem("Debug", Run("debug"), icon="bug")]) return ui.popover(ui.split_button("Run", Run(), "menu", icon="play"), menu if self.menu_open else None, y=32, on_dismiss="dismiss") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: menu_open: bool = False last: str = "nothing yet" def view(state, set_state): run = lambda how: set_state(last=how, menu_open=False) menu = ui.menu([ui.MenuItem("Debug", lambda: run("debug"), icon="bug")]) return ui.popover( ui.split_button("Run", lambda: run("run"), lambda: set_state(menu_open=True), icon="play"), menu if state.menu_open else None, y=32, on_dismiss=lambda: set_state(menu_open=False)) ``` #### Propiedades y API ##### `ui.split_button` ```python split_button( content, on_press=None, on_menu=None, *, icon=None, variant='filled', tint=None, size='md', disabled=False, tooltip=None, theme=None, ) ``` Un botón con una flecha aparte para más opciones. `on_press` ejecuta la acción principal y `on_menu` normalmente abre un `ui.menu` en un `ui.popover`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Texto del botón. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `variant` | `"filled"` | Como en `ui.button`. | | `tint` | `None` | Como en `ui.button`. | | `size` | `"md"` | `"xs"`, `"sm"`, `"md"` o `"lg"`. | | `disabled` | `False` | Desactiva las dos mitades. | | `tooltip` | `None` | Tooltip de la mitad principal. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_press`, `on_menu` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#split_button) #### Relacionados - [Botón](https://albakore.github.io/pyzed/es/library/button/): El botón con tema: variantes, tamaños, iconos y atajos. - [Menú](https://albakore.github.io/pyzed/es/library/menu/): Un menú contextual con iconos, atajos y marcas. - [Popover](https://albakore.github.io/pyzed/es/library/popover/): Contenido flotante en un punto: menús contextuales, selectores de color. ### Buscador > Una lista con búsqueda en una superficie flotante, con coincidencia difusa. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/picker/ :::tip[En pocas palabras] Un buscador flotante con una lista de resultados que se reduce mientras escribes. ::: Un buscador sobre una lista de resultados en una superficie flotante: la base de la paleta de comandos y del buscador de archivos. `ui.fuzzy_filter(query, items, key=)` ordena los elementos (el mejor primero) y devuelve `[(item, positions), ...]`; pasa esos pares a `picker` para resaltar las coincidencias. `ui.fuzzy_match` puntúa una cadena y `ui.highlighted` dibuja una con sus coincidencias. `ui.picker` · Categoría: [Capas flotantes](https://albakore.github.io/pyzed/es/library/#capas-flotantes) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#picker) **Código:** ```python files = ["src/app.rs", "src/render.rs", "python/pyzed/app.py", "python/pyzed/theme.py", "README.md"] items = ui.fuzzy_filter("app", [ui.PaletteItem(f, on_open, icon="file-code") for f in files], key=lambda item: item.label) ui.picker("app", on_query, items, selected=0, placeholder="Search files…", width=480) ``` #### Cuándo usarlo - Para tus propios buscadores: archivos, símbolos, ramas, proyectos, emoji. #### Uso ```python import pyzed as pz from pyzed import ui class Finder(pz.App): def __init__(self): self.query, self.selected = "", 0 self.files = ["src/app.rs", "src/render.rs", "README.md"] def view(self): matches = ui.fuzzy_filter(self.query, self.files) items = [(ui.PaletteItem(f, lambda f=f: self.open(f), icon="file"), positions) for f, positions in matches[:50]] return ui.picker(self.query, lambda q: setattr(self, "query", q), items, selected=self.selected, placeholder="Search files…") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Resultados ```python files = ["src/app.rs", "src/render.rs", "app.py"] items = ui.fuzzy_filter("app", [ui.PaletteItem(f, on_open, icon="file-code") for f in files], key=lambda item: item.label) ui.picker("app", on_query, items, width=380) ``` ```python ui.picker("zzz", on_query, [], empty_text="No files match", width=380) ``` ```python ui.picker("", on_query, [(ui.PaletteItem("main", on_open, icon="git-branch", detail="2 hours ago"), []), (ui.PaletteItem("docs", on_open, icon="git-branch", detail="yesterday"), [])], width=380) ``` ##### Resaltado ```python t = pz.theme.current() score, positions = ui.fuzzy_match("tgsw", "editor: toggle soft wrap") ui.highlighted("editor: toggle soft wrap", positions, color=t.text, match_color=t.accent, size=15) ``` #### Ejemplos ##### Resaltar coincidencias `fuzzy_match` devuelve `(score, positions)` o `None`; `highlighted` pinta esas posiciones. **Código:** ```python t = pz.theme.current() score, positions = ui.fuzzy_match("tgsw", "editor: toggle soft wrap") ui.highlighted("editor: toggle soft wrap", positions, color=t.text, match_color=t.accent, size=16) ``` #### Eventos Buscador tiene **2 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_query` | `(text)` | Callable que recibe el texto nuevo. | | `on_submit` | `()` | Mensaje o callable que se envía al pulsar Enter. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Finder(pz.App): def __init__(self): self.query, self.opened = "", None self.files = ["src/app.rs", "src/render.rs", "README.md"] def matches(self): return ui.fuzzy_filter(self.query, self.files) def open_first(self): # on_submit: Enter found = self.matches() self.opened = found[0][0] if found else None def view(self): items = [(ui.PaletteItem(f, lambda f=f: setattr(self, "opened", f), icon="file"), positions) for f, positions in self.matches()] return ui.picker(self.query, lambda q: setattr(self, "query", q), items, # on_query on_submit=self.open_first, placeholder="Search files…") ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Query: text: str @dataclass class Open: path: str class Finder(pz.App): query, opened = "", None files = ["src/app.rs", "src/render.rs", "README.md"] def update(self, message): match message: case Query(text): self.query = text case Open(path): self.opened = path def view(self): items = [(ui.PaletteItem(f, Open(f), icon="file"), p) for f, p in ui.fuzzy_filter(self.query, self.files)] return ui.picker(self.query, Query, items, placeholder="Search files…") ``` **Funcional:** ```python from dataclasses import dataclass FILES = ["src/app.rs", "src/render.rs", "README.md"] @dataclass(frozen=True) class State: query: str = "" opened: str = "" def view(state, set_state): items = [(ui.PaletteItem(f, lambda f=f: set_state(opened=f), icon="file"), p) for f, p in ui.fuzzy_filter(state.query, FILES)] return ui.picker(state.query, lambda q: set_state(query=q), items, placeholder="Search files…") ``` #### Propiedades y API ##### `ui.picker` ```python picker( query, on_query, items, *, selected=0, placeholder='Search…', input_id='picker', on_submit=None, width=560, max_height=340, empty_text='No matches', theme=None, ) ``` Una lista con buscador en una superficie flotante. Es la base de la paleta y del buscador de archivos. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `query` | *obligatorio* | Texto buscado. | | `items` | *obligatorio* | Pares `(PaletteItem, posiciones)`, por ejemplo de `fuzzy_filter`. | | `selected` | `0` | Índice resaltado. | | `placeholder` | `"Search…"` | Texto de ayuda cuando está vacío. | | `input_id` | `"picker"` | Id del campo. | | `width` | `560` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `max_height` | `340` | Alto máximo en píxeles. | | `empty_text` | `"No matches"` | Texto cuando no hay resultados. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_query`, `on_submit` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#picker) ##### `ui.fuzzy_filter` ```python fuzzy_filter(query, items, key=) ``` Filtra y ordena `items` por puntuación difusa. Devuelve `[(item, posiciones), ...]`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `query` | *obligatorio* | Texto buscado. | | `items` | *obligatorio* | Elementos. | | `key` | `` | Función que da el texto de cada elemento. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#fuzzy_filter) ##### `ui.fuzzy_match` ```python fuzzy_match(query, candidate) ``` Devuelve `(puntuación, posiciones)` si todas las letras de `query` aparecen en orden en `candidate`; si no, `None`. Premia los inicios de palabra y las letras consecutivas. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `query` | *obligatorio* | Texto buscado. | | `candidate` | *obligatorio* | Texto candidato. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#fuzzy_match) ##### `ui.highlighted` ```python highlighted(text, positions, *, color, match_color, size=13.5, weight=None) ``` Texto con los caracteres de `positions` resaltados (coincidencias de la búsqueda difusa). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `text` | *obligatorio* | Texto. | | `positions` | *obligatorio* | Índices a resaltar. | | `color` | *obligatorio* | Color del texto o del icono. | | `match_color` | *obligatorio* | Color de las coincidencias. | | `size` | `13.5` | Tamaño. | | `weight` | `None` | Peso del texto. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#highlighted) #### Relacionados - [Paleta de comandos](https://albakore.github.io/pyzed/es/library/command-palette/): Una paleta de comandos con búsqueda difusa y atajos. - [Campo de búsqueda](https://albakore.github.io/pyzed/es/library/search-input/): Un campo con lupa, para filtrar. - [Capa](https://albakore.github.io/pyzed/es/library/layer/): Dibuja una capa encima de tu vista; al pulsar fuera se cierra. ### Cabecera de lista > Un título pequeño de sección para listas, con hueco al final. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/list-header/ :::tip[En pocas palabras] Un título pequeño encima de un grupo de filas. ::: Una cabecera de sección discreta para listas, con un widget opcional al final (normalmente un botón de icono). `ui.list_header` · Categoría: [Listas y menús](https://albakore.github.io/pyzed/es/library/#listas-y-menús) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#list_header) **Código:** ```python pz.column([ ui.list_header("RECENT", end=ui.icon_button("plus", on_new, size="xs")), ui.list_item("main.py", on_open, icon="file-code", inset=True), ui.list_item("app.py", on_open, icon="file-code", inset=True), ui.list_header("PINNED"), ui.list_item("README.md", on_open, icon="file-text", inset=True), ], width=300) ``` #### Cuándo usarlo - Para dividir una lista en grupos (RECIENTES, FIJADOS) o añadirle una acción. #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ ui.list_header("RECENT", end=ui.icon_button("plus", on_new, size="xs")), ui.list_item("main.py", on_open, icon="file-code", inset=True), ui.list_item("app.py", on_open, icon="file-code", inset=True), ui.list_header("PINNED"), ui.list_item("README.md", on_open, icon="file-text", inset=True), ], width=300) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python pz.container(ui.list_header("RECENT"), width=240) ``` ```python pz.container(ui.list_header("PINNED", end=ui.icon_button("plus", on_new, size="xs")), width=240) ``` ```python pz.container(ui.list_header("CHANGES", end=ui.count_badge(4)), width=240) ``` #### Eventos Cabecera de lista no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.list_header` ```python list_header(title, *, end=None, theme=None) ``` Un encabezado de sección para listas. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | *obligatorio* | Título. | | `end` | `None` | Widgets al final (derecha). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#list_header) #### Relacionados - [Fila de lista](https://albakore.github.io/pyzed/es/library/list-item/): Una fila de lista con icono, texto y un hueco al final. - [Cabecera de panel](https://albakore.github.io/pyzed/es/library/panel-header/): Icono, título y acciones encima de un dock. ### Cabecera de panel > Icono, título y acciones encima de un dock. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/panel-header/ :::tip[En pocas palabras] La fila de título de un panel lateral, con sus botones. ::: La cabecera de un dock o panel: un icono opcional, el título y botones de acción a la derecha. `ui.panel_header` · Categoría: [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/#espacio-de-trabajo) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#panel_header) **Código:** ```python pz.column([ ui.panel_header("Project", [ui.icon_button("plus", on_new, size="xs"), ui.icon_button("ellipsis", on_menu, size="xs")]), ui.panel_header("Terminal", [ui.icon_button("x", on_close, size="xs")], icon="terminal"), ], spacing=12, width=320) ``` #### Cuándo usarlo - Arriba de cada dock (Proyecto, Esquema, Terminal) para nombrarlo y poner sus acciones. #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ ui.panel_header("Project", [ui.icon_button("plus", on_new, size="xs"), ui.icon_button("ellipsis", on_menu, size="xs")]), ui.panel_header("Terminal", [ui.icon_button("x", on_close, size="xs")], icon="terminal"), ], spacing=12, width=320) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido ```python pz.container(ui.panel_header("Project"), width=280) ``` ```python pz.container(ui.panel_header("Terminal", icon="terminal"), width=280) ``` ```python pz.container(ui.panel_header("Project", [ui.icon_button("plus", on_new, size="xs"), ui.icon_button("ellipsis", on_menu, size="xs")]), width=280) ``` ```python pz.container(ui.panel_header("Outline", height=40), width=280) ``` #### Eventos Cabecera de panel no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.panel_header` ```python panel_header(title, actions=(), *, icon=None, height=32, theme=None) ``` La cabecera de un panel: icono, título y acciones. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | *obligatorio* | Título. | | `actions` | `()` | Widgets extra (normalmente botones) que se muestran como acciones. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `height` | `32` | Alto. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#panel_header) #### Relacionados - [Panel](https://albakore.github.io/pyzed/es/library/panel/): Un panel lateral acoplable y redimensionable. - [Cabecera de lista](https://albakore.github.io/pyzed/es/library/list-header/): Un título pequeño de sección para listas, con hueco al final. ### Calendario > Una cuadrícula del mes para elegir un día, un rango o varios días, siempre a la vista. Componente de Pyzed para GUIs en Python. Source: https://albakore.github.io/pyzed/es/library/calendar/ :::tip[En pocas palabras] Un mes en pantalla, como un calendario de pared, donde haces clic en los días para elegirlos. ::: `ui.Calendar` muestra uno o varios meses. `mode` elige un solo día (`"single"`), un rango (`"range"`, dos clics) o cualquier número de días (`"multiple"`, cada clic marca o desmarca uno). La cabecera cambia a una cuadrícula de meses y luego de años para saltar lejos, y las flechas del teclado mueven el día enfocado (Re Pág/Av Pág por meses, Enter lo elige). Los selectores de esta sección abren uno de estos en una ventana flotante. `ui.Calendar` · Categoría: [Fechas y horas](https://albakore.github.io/pyzed/es/library/#fechas-y-horas) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#calendar) **Código:** ```python pz.row([ ui.Calendar(value="2026-10-22", today_button=True, on_change=on_change), ui.Calendar(value=("2026-10-05", "2026-10-14"), mode="range", disabled=["weekend"], on_change=on_change), ], spacing=24) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - En un panel lateral o un tablero, donde el calendario se queda a la vista. - `first_weekday` (`"monday"`, `"sunday"`...) y `week_numbers=True` siguen las costumbres locales. - Dentro de un campo de formulario, usa [Selector de fecha](https://albakore.github.io/pyzed/es/library/date-picker/). #### Uso ```python import pyzed as pz from pyzed import ui class Planner(pz.App): def __init__(self): self.days = [] def view(self): return pz.row([ ui.Calendar(value=self.days, mode="multiple", week_numbers=True, on_change=lambda e: setattr(self, "days", e.value)), pz.column([ui.label(ui.format_date(d, "ddd D MMM")) for d in self.days], spacing=4), ], spacing=16) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Modo ```python ui.Calendar(value="2026-10-22", on_change=on_change) ``` ```python ui.Calendar(value=("2026-10-05", "2026-10-14"), mode="range", on_change=on_change) ``` ```python ui.Calendar(value=["2026-10-06", "2026-10-13", "2026-10-20", "2026-10-27"], mode="multiple", on_change=on_change) ``` ##### Opciones ```python ui.Calendar(value="2026-10-22", week_numbers=True, first_weekday="monday", on_change=on_change) ``` ```python ui.Calendar(value="2026-10-22", first_weekday="sunday", on_change=on_change) ``` ```python ui.Calendar(value="2026-10-22", min="2026-10-08", max="2026-10-28", disabled=["weekend"], on_change=on_change) ``` ```python ui.Calendar(value="2026-10-22", show_outside=False, today_button=True, on_change=on_change) ``` ##### Vistas Un clic en el título pasa a los meses, y otro a los años. ```python ui.Calendar(value=("2026-10-26", "2026-11-04"), mode="range", months=2, on_change=on_change) ``` ```python ui.Calendar(value="2026-10-22", on_change=on_change) ``` ```python ui.Calendar(value="2026-10-22", on_change=on_change) ``` #### Eventos Calendario tiene **2 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_change` | `(event)` | Recibe `event.value` (una fecha, una tupla o una lista). | | `on_month` | `(event)` | Recibe `event.value`, el `(año, mes)` que se muestra. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Planner(pz.App): def __init__(self): self.days = [] def changed(self, event): # on_change: the list of days ("multiple" mode) self.days = event.value def view(self): return pz.row([ ui.Calendar(value=self.days, mode="multiple", on_change=self.changed), ui.label(f"{len(self.days)} days"), ], spacing=16) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Day: value: object class Planner(pz.App): day = None def update(self, message): match message: case Day(value): self.day = value def view(self): return ui.Calendar(value=self.day, today_button=True, on_change=lambda e: Day(e.value)) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: stay: tuple = ("2026-10-05", "2026-10-09") def view(state, set_state): return ui.Calendar(value=state.stay, mode="range", months=2, on_change=lambda e: set_state(stay=e.value)) ``` #### Propiedades y API ##### `ui.Calendar` ```python class Calendar ``` Un componente con un mes donde elegir un día (`mode="single"`), un rango (`"range"`: el primer clic lo empieza y el segundo lo termina) o varios días (`"multiple"`). El título abre una cuadrícula de meses y después de años. Tras un clic recibe el teclado: las flechas mueven, Enter elige y Re Pág/Av Pág cambian de mes. Ver la página [Calendario](https://albakore.github.io/pyzed/es/library/calendar/). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | `None` | Una fecha (`datetime.date` o `"2026-10-22"`), un par `(inicio, fin)` o una lista de fechas, según `mode`. | | `mode` | `"single"` | `"single"`, `"range"` o `"multiple"`. | | `min` | `None` | El primer día que se puede elegir. | | `max` | `None` | El último día que se puede elegir. | | `disabled` | `()` | Días que no se pueden elegir: fechas, nombres de día en inglés (`"sunday"`) o `"weekends"`. | | `first_weekday` | `"monday"` | `"monday"`, `"sunday"` o `"saturday"`. | | `months` | `1` | Meses que se ven uno al lado del otro. | | `week_numbers` | `False` | Muestra el número de semana ISO de cada fila. | | `show_outside` | `True` | Muestra los días de los meses vecinos. | | `today_button` | `False` | Añade un enlace Hoy. | Eventos: `on_change`, `on_month` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#calendar) #### Relacionados - [Selector de fecha](https://albakore.github.io/pyzed/es/library/date-picker/): Un campo de fecha con un calendario desplegable, en el formato que elijas. - [Selector de rango de fechas](https://albakore.github.io/pyzed/es/library/date-range-picker/): Dos fechas en un campo, con dos meses lado a lado y rangos rápidos. ### Cambios sin guardar > “¿Guardar los cambios antes de cerrar?”. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/unsaved-changes/ :::tip[En pocas palabras] La pregunta “¿Guardar los cambios antes de cerrar?”. ::: “¿Guardar los cambios de *nombre* antes de cerrar?” con No guardar, Cancelar y Guardar. `ui.unsaved_changes` · Categoría: [Diálogos](https://albakore.github.io/pyzed/es/library/#diálogos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#unsaved_changes) **Código:** ```python ui.dialog_layer(main_view(), ui.unsaved_changes("notes.py", on_save, on_discard, on_cancel)) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Al cerrar una pestaña o una ventana con cambios sin guardar. #### Uso ```python from pyzed import ui ui.dialog_layer(main_view(), ui.unsaved_changes("notes.py", on_save, on_discard, on_cancel)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python ui.unsaved_changes("notes.py", on_save, on_discard, on_cancel) ``` ```python ui.unsaved_changes("report.pzform", on_save, on_discard, on_cancel, width=360) ``` #### Eventos Cambios sin guardar tiene **3 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_save` | `()` | Guardar. | | `on_discard` | `()` | No guardar. | | `on_cancel` | `()` | Cancelar. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Notes(pz.App): def __init__(self): self.closing = True def save(self): self.closing = False # write the file, then close def view(self): dialog = ui.unsaved_changes("notes.py", self.save, lambda: setattr(self, "closing", False), # on_discard lambda: setattr(self, "closing", False)) # on_cancel return ui.dialog_layer(main_view(), dialog if self.closing else None) ``` **Declarativo:** ```python class Notes(pz.App): closing = True def update(self, message): match message: case "save" | "discard": self.closing = False case "cancel": self.closing = False def view(self): dialog = ui.unsaved_changes("notes.py", "save", "discard", "cancel") return ui.dialog_layer(main_view(), dialog if self.closing else None) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: closing: bool = True saved: bool = False def view(state, set_state): dialog = ui.unsaved_changes("notes.py", lambda: set_state(saved=True, closing=False), lambda: set_state(closing=False), lambda: set_state(closing=False)) return ui.dialog_layer(main_view(), dialog if state.closing else None) ``` #### Propiedades y API ##### `ui.unsaved_changes` ```python unsaved_changes(name, on_save, on_discard, on_cancel, *, width=440, theme=None) ``` «¿Guardar los cambios de *nombre* antes de cerrar?» con No guardar, Cancelar y Guardar. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `name` | *obligatorio* | Nombre del archivo o documento. | | `width` | `440` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_save`, `on_discard`, `on_cancel` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#unsaved_changes) #### Relacionados - [Confirmación](https://albakore.github.io/pyzed/es/library/confirm/): Una pregunta de sí o no, en rojo para acciones destructivas. - [Elección](https://albakore.github.io/pyzed/es/library/choice/): Un mensaje con tus propios botones. - [Barra de pestañas](https://albakore.github.io/pyzed/es/library/tab-bar/): Pestañas de editor con iconos, puntos de cambios y botón de cerrar. ### Campo de búsqueda > Un campo con lupa, para filtrar. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/search-input/ :::tip[En pocas palabras] Una caja de texto con una lupa, para escribir lo que buscas. ::: Un `input` con el icono de una lupa y `"Search…"` como placeholder. Acepta el resto de parámetros de [`ui.input`](https://albakore.github.io/pyzed/es/library/input/). `ui.search_input` · Categoría: [Formularios](https://albakore.github.io/pyzed/es/library/#formularios) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#search_input) **Código:** ```python pz.column([ ui.search_input("", on_query), ui.search_input("button", on_query, placeholder="Search components…"), ], spacing=10, width=360) ``` #### Cuándo usarlo - Encima de una lista, árbol o tabla para filtrarla mientras escribes. - Combínalo con `ui.fuzzy_filter` para ordenar primero los mejores resultados. #### Uso ```python import pyzed as pz from pyzed import ui class Finder(pz.App): def __init__(self): self.query = "" self.files = ["main.py", "app.py", "theme.py"] def view(self): matches = ui.fuzzy_filter(self.query, self.files) return pz.column([ ui.search_input(self.query, lambda q: setattr(self, "query", q)), *[ui.list_item(name, icon="file") for name, _ in matches], ], spacing=6) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido ```python ui.search_input("", on_query, width=260) ``` ```python ui.search_input("button", on_query, width=260) ``` ```python ui.search_input("", on_query, placeholder="Search components…", width=260) ``` ##### Tamaño ```python ui.search_input("", on_query, size="sm", width=260) ``` ```python ui.search_input("", on_query, size="lg", width=260) ``` ```python ui.search_input("", on_query, borderless=True, width=260) ``` #### Eventos Campo de búsqueda tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_input` | `(text)` | Callable que recibe el texto nuevo en cada pulsación. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Finder(pz.App): def __init__(self): self.query = "" self.files = ["main.py", "app.py", "theme.py"] def search(self, text): # on_input: every keystroke self.query = text def view(self): matches = ui.fuzzy_filter(self.query, self.files) return pz.column([ui.search_input(self.query, self.search), *[ui.list_item(name, icon="file") for name, _ in matches]], spacing=6) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Search: query: str class Finder(pz.App): query = "" files = ["main.py", "app.py", "theme.py"] def update(self, message): match message: case Search(query): self.query = query def view(self): matches = ui.fuzzy_filter(self.query, self.files) return pz.column([ui.search_input(self.query, Search), *[ui.list_item(name, icon="file") for name, _ in matches]], spacing=6) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: query: str = "" files: tuple = ("main.py", "app.py", "theme.py") def view(state, set_state): matches = ui.fuzzy_filter(state.query, list(state.files)) return pz.column([ui.search_input(state.query, lambda q: set_state(query=q)), *[ui.list_item(name, icon="file") for name, _ in matches]], spacing=6) ``` #### Propiedades y API ##### `ui.search_input` ```python search_input(value, on_input=None, *, placeholder='Search…', **kwargs) ``` Un `input` con icono de lupa. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | *obligatorio* | Texto actual. | | `placeholder` | `"Search…"` | Texto de ayuda cuando está vacío. | | `**kwargs` | — | Los de `input`. | Eventos: `on_input` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#search_input) #### Relacionados - [Campo de texto](https://albakore.github.io/pyzed/es/library/input/): Un campo de texto controlado con icono, placeholder y modo contraseña. - [Buscador](https://albakore.github.io/pyzed/es/library/picker/): Una lista con búsqueda en una superficie flotante, con coincidencia difusa. - [Paleta de comandos](https://albakore.github.io/pyzed/es/library/command-palette/): Una paleta de comandos con búsqueda difusa y atajos. ### Campo de formulario > Una fila de ajustes: título y descripción a la izquierda, control a la derecha. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/field/ :::tip[En pocas palabras] Una fila de un formulario: el nombre de un ajuste a la izquierda y su control a la derecha. ::: Una fila de formulario como las de una pantalla de ajustes: el título (y una descripción opcional) a la izquierda y cualquier control a la derecha. Apila varias en una columna o en un [Grupo de ajustes](https://albakore.github.io/pyzed/es/library/settings-group/). `ui.field` · Categoría: [Formularios](https://albakore.github.io/pyzed/es/library/#formularios) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#field) **Código:** ```python pz.column([ ui.field("Autosave", ui.switch(True, on_toggle), description="Saves when you switch tabs"), ui.field("Format on save", ui.checkbox(False, on_toggle)), ui.field("Font size", ui.number_field(14, on_change, suffix="px")), ], spacing=14, width=440) ``` #### Cuándo usarlo - Para preferencias y formularios en los que cada control necesita una etiqueta y quizá una pista. - Alinea interruptores, selects y campos numéricos de anchos distintos. #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ ui.field("Autosave", ui.switch(True, on_toggle), description="Saves when you switch tabs"), ui.field("Format on save", ui.checkbox(False, on_toggle)), ui.field("Font size", ui.number_field(14, on_change, suffix="px")), ], spacing=14, width=440) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Control ```python pz.container(ui.field("Autosave", ui.switch(True, on_toggle)), width=380) ``` ```python pz.container(ui.field("Font size", ui.number_field(14, on_change, suffix="px")), width=380) ``` ```python pz.container(ui.field("Theme", ui.select(["One Dark", "One Light"], "One Dark", on_select, width=150)), width=380) ``` ```python pz.container(ui.field("Name", ui.input("Pyzed", on_input, width=180)), width=380) ``` ##### Ayuda y errores ```python pz.container(ui.field("Autosave", ui.switch(True, on_toggle), description="Saves when you switch tabs"), width=380) ``` ```python pz.container(ui.field("Port", ui.number_field(80, on_change), error="Ports below 1024 need admin rights"), width=380) ``` ##### Estados y eventos ```python pz.container(ui.field("Name", ui.input("", on_input, width=180), required=True), width=380) ``` ```python pz.container(ui.field("Email", ui.input("ada@", on_input, invalid=True), vertical=True, required=True, error="Enter a valid email"), width=320) ``` #### Eventos Campo de formulario no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.field` ```python field( title, control, *, description=None, error=None, required=False, vertical=False, theme=None, ) ``` Una fila de formulario: título y descripción a la izquierda, control a la derecha (como una pantalla de ajustes). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | *obligatorio* | Título. | | `control` | *obligatorio* | El control (switch, input…). | | `description` | `None` | Texto de ayuda. | | `error` | `None` | Un mensaje de validación que se muestra bajo el título con el color de error (ver `pz.validation_errors`). | | `required` | `False` | Añade un `*` rojo tras el título. | | `vertical` | `False` | Pone el control bajo el título. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#field) #### Relacionados - [Grupo de ajustes](https://albakore.github.io/pyzed/es/library/settings-group/): Un bloque con título de filas de ajustes, para pantallas de ajustes. - [Interruptor](https://albakore.github.io/pyzed/es/library/switch/): Un interruptor para ajustes que se aplican al momento. - [Campo numérico](https://albakore.github.io/pyzed/es/library/number-field/): Un número con botones −/+, límites, paso y sufijo. ### Campo de texto > Un campo de texto controlado con icono, placeholder y modo contraseña. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/input/ :::tip[En pocas palabras] Una caja donde la gente escribe una línea de texto: un nombre, un correo, una contraseña. ::: Un campo de texto de una línea con el estilo del tema. Es **controlado**: muestra `value` y avisa de cada tecla a `on_input`, así que guardas el texto en tu estado y lo devuelves en `view()`. `on_submit` se ejecuta al pulsar Enter. `ui.input` · Categoría: [Formularios](https://albakore.github.io/pyzed/es/library/#formularios) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#input) **Código:** ```python pz.column([ ui.input("", on_input, placeholder="Project name"), ui.input("ada@example.com", on_input, icon="mail"), ui.input("hunter2", on_input, secure=True, icon="lock"), ], spacing=10, width=360) ``` #### Cuándo usarlo - Para nombres, rutas, búsquedas, correos y contraseñas (`secure=True`). - Para texto de varias líneas o código usa el [Editor](https://albakore.github.io/pyzed/es/library/editor/). - Dale un `id` para enfocarlo desde código con `pz.Command.focus(id)`. #### Uso ```python import pyzed as pz from pyzed import ui class Form(pz.App): def __init__(self): self.name = "" def view(self): return ui.input(self.name, lambda text: setattr(self, "name", text), placeholder="Project name", on_submit=self.create, id="name") def on_start(self): return pz.Command.focus("name") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido ```python ui.input("", on_input, placeholder="Project name", width=260) ``` ```python ui.input("Pyzed", on_input, width=260) ``` ```python ui.input("ada@example.com", on_input, icon="mail", width=260) ``` ```python ui.input("hunter2", on_input, secure=True, icon="lock", width=260) ``` ##### Tamaño ```python ui.input("Small", on_input, size="sm", width=260) ``` ```python ui.input("Medium", on_input, size="md", width=260) ``` ```python ui.input("Large", on_input, size="lg", width=260) ``` ##### Estilo ```python ui.input("Borderless", on_input, borderless=True, width=260) ``` ```python ui.input("SELECT * FROM users", on_input, mono=True, width=260) ``` ```python ui.input("Can't edit me", width=260) ``` ##### Validación Muestra los errores con `error=` de [Campo de formulario](https://albakore.github.io/pyzed/es/library/field/), junto al campo. ```python ui.field("Email", ui.input("ada@", on_input, width=220), error="Enter a valid email") ``` ##### Estados y eventos El foco, la pérdida de foco y las teclas vienen de los eventos comunes: `.on(focus=..., blur=..., key_down=...)`. ```python ui.input("Locked", on_input, disabled=True, width=220) ``` ```python ui.input("Copy me, not edit me", on_input, readonly=True, width=220) ``` ```python ui.input("ada@", on_input, invalid=True, width=220) ``` ```python ui.input("pyzed", on_input, on_clear=on_clear, width=220) ``` ```python ui.input("12345678", on_input, max_length=8, width=220) ``` ```python ui.input("centered", on_input, align="center", width=220) ``` ##### Widget base El widget `pz.*` de debajo: sin tema, todos los colores y tamaños son tuyos. ```python pz.text_input("", placeholder="pz.text_input", on_input=on_input, width=260, padding=8, radius=8, border_color="#c678dd", focus_border_color="#e5c07b") ``` #### Eventos Campo de texto tiene **3 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_input` | `ui.input`, `pz.text_input` | `(text)` | Callable que recibe el texto nuevo en cada pulsación. | | `on_submit` | `ui.input`, `pz.text_input` | `()` | Mensaje o callable que se envía al pulsar Enter. | | `on_clear` | `ui.input` | `()` | Muestra una ✕ mientras hay texto; se envía al pulsarla. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class SignUp(pz.App): def __init__(self): self.email = "" self.sent = False def set_email(self, text): # on_input: the new text on every keystroke self.email, self.sent = text, False def send(self): # on_submit: Enter self.sent = "@" in self.email def view(self): return pz.column([ ui.input(self.email, self.set_email, placeholder="Email", icon="mail", id="email", on_submit=self.send, on_clear=lambda: self.set_email(""), invalid=bool(self.email) and "@" not in self.email), ui.label("Thanks!" if self.sent else "Press Enter to send"), ], spacing=8, width=320) def on_start(self): return pz.Command.focus("email") ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Typed: text: str class SignUp(pz.App): email = "" sent = False def update(self, message): match message: case Typed(text): self.email, self.sent = text, False case "send": self.sent = "@" in self.email case "clear": self.email = "" def view(self): return ui.input(self.email, Typed, placeholder="Email", icon="mail", on_submit="send", on_clear="clear") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: email: str = "" sent: bool = False def view(state, set_state): valid = "@" in state.email return pz.column([ ui.input(state.email, lambda text: set_state(email=text, sent=False), placeholder="Email", on_submit=lambda: set_state(sent=valid), invalid=bool(state.email) and not valid), ui.label("Thanks!" if state.sent else "Press Enter to send"), ], spacing=8, width=320) ``` #### Propiedades y API ##### `ui.input` ```python input( value, on_input=None, *, placeholder='', icon=None, on_submit=None, on_clear=None, id=None, size='md', secure=False, width='fill', borderless=False, mono=False, disabled=False, readonly=False, invalid=False, max_length=None, align='start', theme=None, ) ``` Un campo de texto. El foco, la pérdida de foco y las teclas vienen de los eventos comunes: `.on(focus=..., blur=..., key_down=...)`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | *obligatorio* | Texto actual. | | `placeholder` | `""` | Texto de ayuda cuando está vacío. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `size` | `"md"` | `"sm"`, `"md"` o `"lg"`. | | `secure` | `False` | Oculta el texto. | | `width` | `"fill"` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `borderless` | `False` | Sin borde ni fondo (como en la paleta de comandos). | | `mono` | `False` | Usa la fuente monoespaciada (IBM Plex Mono). | | `disabled` | `False` | Lo atenúa e ignora lo que se escribe. | | `readonly` | `False` | Se puede seleccionar y copiar, pero no cambiar. | | `invalid` | `False` | Pinta el borde con el color de error. | | `max_length` | `None` | Ignora los cambios más largos que esto. | | `align` | `"start"` | Alineación del texto: `"start"`, `"center"` o `"end"`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_input`, `on_submit`, `on_clear` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#input) ##### `pz.text_input` ```python text_input( value, *, placeholder='', on_input=None, on_submit=None, id=None, size=None, font=None, padding=None, width=None, secure=False, icon=None, icon_size=None, bg=None, border_color=None, hover_border_color=None, focus_border_color=None, border_width=None, radius=None, color=None, placeholder_color=None, selection_color=None, icon_color=None, align_x=None, ) ``` Un campo de texto controlado: muestra `value` y te avisa de los cambios con `on_input`. Para el campo con tema usa `ui.input`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | *obligatorio* | Texto actual. | | `placeholder` | `""` | Texto de ayuda cuando está vacío. | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `size` | `None` | Tamaño de la fuente. | | `font` | `None` | Familia de la fuente. | | `padding` | `None` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `secure` | `False` | Oculta el texto (contraseñas). | | `icon` | `None` | Carácter del icono (usa `pyzed.icons.glyph(nombre)`). | | `icon_size` | `None` | Tamaño del icono. | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `border_color` | `None` | Color del borde. | | `hover_border_color` | `None` | Color del borde al pasar el ratón. | | `focus_border_color` | `None` | Color del borde con el foco. | | `border_width` | `None` | Grosor del borde en píxeles. | | `radius` | `None` | Radio de las esquinas: un número o `(sup_izq, sup_der, inf_der, inf_izq)`. | | `color` | `None` | Color del texto o del icono. | | `placeholder_color` | `None` | Color del placeholder. | | `selection_color` | `None` | Color de la selección. | | `icon_color` | `None` | Color del icono. | | `align_x` | `None` | Alineación horizontal: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | Eventos: `on_input`, `on_submit` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#text_input) #### Relacionados - [Campo de búsqueda](https://albakore.github.io/pyzed/es/library/search-input/): Un campo con lupa, para filtrar. - [Campo de formulario](https://albakore.github.io/pyzed/es/library/field/): Una fila de ajustes: título y descripción a la izquierda, control a la derecha. - [Campo numérico](https://albakore.github.io/pyzed/es/library/number-field/): Un número con botones −/+, límites, paso y sufijo. ### Campo numérico > Un número con botones −/+, límites, paso y sufijo. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/number-field/ :::tip[En pocas palabras] Una caja con un número y botones − / + para hacerlo más pequeño o más grande. ::: Un campo numérico con botones − y +. También se puede escribir el valor: `on_change` recibe el número ya ajustado a `min`/`max` y el texto que no es un número se ignora. `ui.number_field` · Categoría: [Formularios](https://albakore.github.io/pyzed/es/library/#formularios) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#number_field) **Código:** ```python pz.row([ ui.number_field(14, on_change, min=8, max=32, suffix="px"), ui.number_field(4, on_change, min=1, max=8), ui.number_field(0.5, on_change, step=0.1, suffix="s", width=140), ], spacing=12) ``` #### Cuándo usarlo - Para valores exactos con límites razonables: tamaño de fuente, ancho de tabulación, puerto, reintentos. - Usa `suffix` para las unidades (`"px"`, `"ms"`, `"%"`). #### Uso ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.font_size = 14 def view(self): return ui.number_field(self.font_size, lambda v: setattr(self, "font_size", v), min=8, max=32, suffix="px") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python ui.number_field(4, on_change, min=1, max=8) ``` ```python ui.number_field(14, on_change, min=8, max=32, suffix="px") ``` ```python ui.number_field(0.5, on_change, step=0.1, suffix="s", width=140) ``` ```python ui.number_field(8080, on_change, min=1, max=65535, align="start", width=140) ``` ##### Límites En un límite su botón se atenúa y el valor no puede ir más allá. ```python ui.number_field(1, on_change, min=1, max=8) ``` ```python ui.number_field(8, on_change, min=1, max=8) ``` ##### Estados y eventos Tras pulsarlo, ↑/↓ lo mueven un paso (con Shift, diez) e Inicio/Fin saltan a `min`/`max`. ```python ui.number_field(9.5, on_change, step=0.5, prefix="$", decimals=2, width=150) ``` ```python ui.number_field(70000, on_change, invalid=True, width=150) ``` ```python ui.number_field(4, on_change, min=1, max=8, disabled=True) ``` #### Eventos Campo numérico tiene **2 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_change` | `(value)` | Callable que recibe el nuevo valor. | | `on_submit` | `()` | Se envía con Enter. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Settings(pz.App): def __init__(self): self.font_size = 14 self.applied = 14 def view(self): return pz.row([ ui.number_field(self.font_size, lambda v: setattr(self, "font_size", v), # on_change min=8, max=32, suffix="px", on_submit=lambda: setattr(self, "applied", self.font_size)), # Enter pz.text("Preview", size=self.applied), ], spacing=12, align="center") ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class FontSize: value: float class Settings(pz.App): font_size = 14 def update(self, message): match message: case FontSize(value): self.font_size = value def view(self): return ui.number_field(self.font_size, FontSize, min=8, max=32, suffix="px") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: font_size: float = 14 def view(state, set_state): return pz.row([ui.number_field(state.font_size, lambda v: set_state(font_size=v), min=8, max=32), pz.text("Preview", size=state.font_size)], spacing=12, align="center") ``` #### Propiedades y API ##### `ui.number_field` ```python number_field( value, on_change=None, *, min=None, max=None, step=1, width=120, suffix=None, prefix=None, align='center', decimals=None, disabled=False, invalid=False, id=None, on_submit=None, steppers=True, drag=True, drag_step=None, theme=None, ) ``` Un campo numérico con botones −/+ que también se puede arrastrar, como en Figma. `on_change(valor)` recibe el número ya limitado a `min`/`max`; lo que no se puede leer como número se ignora. Mantén pulsado y mueve el ratón de lado a lado sobre el número para cambiarlo (sigue aunque salgas de la ventana); haz doble clic para escribir un valor, y Enter, Escape o un clic fuera para terminar. Tras pulsarlo, ↑/↓ lo mueven un paso (con Shift, diez) e Inicio/Fin saltan a `min`/`max`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | *obligatorio* | Valor actual. | | `min` | `None` | Mínimo. | | `max` | `None` | Máximo. | | `step` | `1` | Paso de los botones. | | `width` | `120` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `suffix` | `None` | Unidad que se muestra al final (`"px"`, `"%"`…). | | `prefix` | `None` | Texto antes del número (`"$"`…). | | `align` | `"center"` | Dónde va el número entre los botones: `"center"` (por defecto), `"start"` o `"end"`. | | `decimals` | `None` | Decimales que muestra (por defecto, los de `step`). | | `disabled` | `False` | Lo atenúa. | | `invalid` | `False` | Pinta el borde con el color de error. | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `steppers` | `True` | Muestra los botones −/+ (`False`: un campo compacto, como en una rejilla de propiedades). | | `drag` | `True` | Deja cambiarlo arrastrando el ratón (`False`: un campo de texto normal). | | `drag_step` | `None` | Cuánto suma cada píxel arrastrado (por defecto `step`, hasta 1; los pasos menores de 1 piden tres píxeles cada uno). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_change`, `on_submit` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#number_field) #### Relacionados - [Deslizador](https://albakore.github.io/pyzed/es/library/slider/): Elige un número de un rango arrastrando. - [Campo de texto](https://albakore.github.io/pyzed/es/library/input/): Un campo de texto controlado con icono, placeholder y modo contraseña. - [Campo de formulario](https://albakore.github.io/pyzed/es/library/field/): Una fila de ajustes: título y descripción a la izquierda, control a la derecha. ### Capa > Dibuja una capa encima de tu vista; al pulsar fuera se cierra. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/layer/ :::tip[En pocas palabras] Pone algo encima de la pantalla, como un buscador, y lo cierra si pulsas fuera. ::: Apila `overlay` encima de `base`. Al pulsar fuera de la capa se envía `on_dismiss`, y `backdrop` oscurece la vista de detrás. Con `overlay=None` devuelve `base` sin cambios, así que puede envolver siempre tu vista. `align_x`, `align_y` y `offset` colocan la capa (por defecto centrada arriba, como una paleta de comandos). `ui.layer` · Categoría: [Capas flotantes](https://albakore.github.io/pyzed/es/library/#capas-flotantes) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#layer) **Código:** ```python ui.layer( main_view(), ui.picker("", on_query, [(ui.PaletteItem("notes.py", on_open, icon="file-code"), []), (ui.PaletteItem("README.md", on_open, icon="file-text"), [])], placeholder="Search files…", width=420), on_dismiss=on_close, backdrop="#00000055", offset=40, ) ``` #### Cuándo usarlo - Para mostrar paletas, buscadores, modales y menús encima de todo. - Para diálogos, [Capa de diálogo](https://albakore.github.io/pyzed/es/library/dialog-layer/) ya los centra sobre un fondo oscurecido. #### Uso ```python import pyzed as pz from pyzed import ui class App(pz.App): def __init__(self): self.finder_open = False def view(self): overlay = self.finder() if self.finder_open else None return ui.layer(self.main_view(), overlay, on_dismiss=lambda: setattr(self, "finder_open", False)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Posición ```python ui.layer(main_view(), ui.card(ui.label("Overlay"), width=200), on_dismiss=on_close) ``` ```python ui.layer(main_view(), ui.card(ui.label("Overlay"), width=200), align_y="center", on_dismiss=on_close) ``` ```python ui.layer(main_view(), ui.card(ui.label("Overlay"), width=200), align_x="end", align_y="end", offset=16, on_dismiss=on_close) ``` ##### Fondo Con `overlay=None` la capa devuelve tu vista sin cambios. ```python ui.layer(main_view(), ui.card(ui.label("Overlay"), width=200), backdrop="#00000088", align_y="center", on_dismiss=on_close) ``` ```python ui.layer(main_view(), None) ``` #### Eventos Capa tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_dismiss` | `()` | Mensaje o callable que se envía al cerrar. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class App(pz.App): def __init__(self): self.finder_open = True def view(self): finder = ui.card(ui.muted("Search files…"), width=320) return ui.layer(main_view(), finder if self.finder_open else None, on_dismiss=lambda: setattr(self, "finder_open", False)) # click outside ``` **Declarativo:** ```python class App(pz.App): finder_open = True def update(self, message): match message: case "open finder": self.finder_open = True case "close finder": self.finder_open = False def view(self): overlay = ui.card(ui.muted("Search files…"), width=320) if self.finder_open else None return ui.layer(main_view(), overlay, on_dismiss="close finder") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: finder_open: bool = True def view(state, set_state): overlay = ui.card(ui.muted("Search files…"), width=320) if state.finder_open else None return ui.layer(main_view(), overlay, on_dismiss=lambda: set_state(finder_open=False)) ``` #### Propiedades y API ##### `ui.layer` ```python layer( base, overlay=None, *, on_dismiss=None, backdrop=None, align_x='center', align_y='start', offset=72, ) ``` Apila `overlay` sobre `base`. Hacer clic fuera envía `on_dismiss`. Con `overlay=None` devuelve `base` tal cual. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `base` | *obligatorio* | La interfaz de debajo. | | `overlay` | `None` | Lo que flota encima (modal, paleta, menú). | | `backdrop` | `None` | Color del fondo que oscurece la interfaz. | | `align_x` | `"center"` | Alineación horizontal: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `align_y` | `"start"` | Alineación vertical: `"start"`/`"top"`, `"center"`, `"end"`/`"bottom"`. | | `offset` | `72` | Distancia desde arriba cuando `align_y` es `"start"`. | Eventos: `on_dismiss` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#layer) #### Relacionados - [Modal](https://albakore.github.io/pyzed/es/library/modal/): Una superficie de diálogo con título, contenido y botones al pie. - [Popover](https://albakore.github.io/pyzed/es/library/popover/): Contenido flotante en un punto: menús contextuales, selectores de color. - [Capa de diálogo](https://albakore.github.io/pyzed/es/library/dialog-layer/): Centra un diálogo encima de tu vista con el fondo oscurecido. ### Capa de diálogo > Centra un diálogo encima de tu vista con el fondo oscurecido. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/dialog-layer/ :::tip[En pocas palabras] Oscurece la app y muestra un diálogo en el centro, para que la pregunta no pase desapercibida. ::: Muestra un diálogo centrado encima de `base` con el fondo oscurecido; al pulsar el fondo se llama a `on_dismiss`. Con `dialog=None` devuelve `base` sin cambios, así que guarda en tu estado qué diálogo está abierto y envuelve siempre la vista con ella. `ui.dialog_layer` · Categoría: [Diálogos](https://albakore.github.io/pyzed/es/library/#diálogos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#dialog_layer) **Código:** ```python ui.dialog_layer( main_view(), ui.confirm("Delete notes.py? This can't be undone.", on_delete, on_cancel, title="Delete file", danger=True, width=340), on_dismiss=on_cancel, ) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Alrededor de toda tu vista, para mostrar cualquiera de estos diálogos o un `ui.modal`. #### Uso ```python import pyzed as pz from pyzed import ui class Files(pz.App): def __init__(self): self.asking = False def close(self): self.asking = False def view(self): dialog = None if self.asking: dialog = ui.confirm("Delete main.py?", self.delete, self.close, title="Delete file", danger=True) return ui.dialog_layer(self.main_view(), dialog, on_dismiss=self.close) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Diálogo ```python ui.dialog_layer(main_view(), ui.alert("Export finished.", on_close, kind="success", width=280)) ``` ```python ui.dialog_layer(main_view(), ui.modal(ui.muted("Any content."), title="Modal", on_close=on_close, width=280), on_dismiss=on_close) ``` ```python ui.dialog_layer(main_view(), None) ``` #### Eventos Capa de diálogo tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_dismiss` | `()` | Se envía al hacer clic en el fondo. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Files(pz.App): def __init__(self): self.asking = True def close(self): self.asking = False def view(self): dialog = ui.confirm("Delete main.py?", self.close, self.close, danger=True) if self.asking else None return ui.dialog_layer(main_view(), dialog, on_dismiss=self.close) # click on the backdrop ``` **Declarativo:** ```python class Files(pz.App): asking = True def update(self, message): if message in ("delete", "cancel"): self.asking = False def view(self): dialog = ui.confirm("Delete main.py?", "delete", "cancel", danger=True) if self.asking else None return ui.dialog_layer(main_view(), dialog, on_dismiss="cancel") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: asking: bool = True def view(state, set_state): close = lambda: set_state(asking=False) dialog = ui.confirm("Delete main.py?", close, close, danger=True) if state.asking else None return ui.dialog_layer(main_view(), dialog, on_dismiss=close) ``` #### Consejos :::tip Las etiquetas de los botones (Aceptar, Cancelar, Guardar…) se traducen con [`pyzed.i18n`](https://albakore.github.io/pyzed/es/guides/i18n/). ::: #### Propiedades y API ##### `ui.dialog_layer` ```python dialog_layer(base, dialog=None, *, on_dismiss=None, theme=None) ``` Muestra un diálogo centrado sobre `base` con un fondo oscurecido. Con `dialog=None` devuelve `base` sin cambios, así que puede envolver siempre la vista. Ver [Diálogos](https://albakore.github.io/pyzed/es/guides/i18n/#diálogos). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `base` | *obligatorio* | La interfaz de debajo. | | `dialog` | `None` | Un diálogo de las funciones de abajo, o `ui.modal`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_dismiss` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#dialog_layer) #### Relacionados - [Alerta](https://albakore.github.io/pyzed/es/library/alert/): Un mensaje con un botón. - [Confirmación](https://albakore.github.io/pyzed/es/library/confirm/): Una pregunta de sí o no, en rojo para acciones destructivas. - [Pregunta](https://albakore.github.io/pyzed/es/library/prompt/): Pide una línea de texto. - [Modal](https://albakore.github.io/pyzed/es/library/modal/): Una superficie de diálogo con título, contenido y botones al pie. ### Casilla > Una casilla de verificación con etiqueta opcional. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/checkbox/ :::tip[En pocas palabras] Un cuadrado que marcas para decir que sí a una opción. ::: Una casilla con los colores del tema. `on_toggle` recibe el nuevo `bool`; `label` añade un texto pulsable al lado. `ui.checkbox` · Categoría: [Formularios](https://albakore.github.io/pyzed/es/library/#formularios) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#checkbox) **Código:** ```python pz.column([ ui.checkbox(True, on_toggle, label="Format on save"), ui.checkbox(False, on_toggle, label="Show hidden files"), ui.checkbox(True, on_toggle, label="Use tabs"), ], spacing=10) ``` #### Cuándo usarlo - Para opciones que se aplican después (un formulario que envías) o una lista de opciones independientes. - Para un ajuste que se aplica al momento, un [Interruptor](https://albakore.github.io/pyzed/es/library/switch/) se entiende mejor. #### Uso ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.accepted = False def view(self): return ui.checkbox(self.accepted, lambda value: setattr(self, "accepted", value), label="I accept the terms") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Estado ```python ui.checkbox(True, on_toggle, label="Format on save") ``` ```python ui.checkbox(False, on_toggle, label="Format on save") ``` ```python ui.checkbox(True, label="Managed by your admin") ``` ##### Contenido ```python ui.checkbox(True, on_toggle) ``` ```python pz.column([ui.checkbox(True, on_toggle, label="Python"), ui.checkbox(False, on_toggle, label="Rust"), ui.checkbox(True, on_toggle, label="TypeScript")], spacing=8) ``` ##### Estados y eventos ```python ui.checkbox(None, on_toggle, label="Some files") ``` ```python ui.checkbox(True, on_toggle, label="Locked", disabled=True) ``` ```python ui.checkbox(True, on_toggle, label="Small", size="sm") ``` ```python ui.checkbox(True, on_toggle, label="Large", size="lg") ``` ##### Widget base El widget `pz.*` de debajo: sin tema, todos los colores y tamaños son tuyos. ```python pz.checkbox(True, "pz.checkbox", on_toggle=on_toggle, accent="#c678dd", size=18) ``` #### Eventos Casilla tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_toggle` | `ui.checkbox`, `pz.checkbox` | `(checked)` | Callable que recibe el nuevo `bool` (o mensaje que se envía). | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Settings(pz.App): def __init__(self): self.options = {"Format on save": True, "Show hidden files": False} def toggle(self, name, checked): # on_toggle gets True or False self.options[name] = checked def view(self): return pz.column([ ui.checkbox(on, lambda checked, name=name: self.toggle(name, checked), label=name) for name, on in self.options.items() ], spacing=10) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Toggle: name: str checked: bool class Settings(pz.App): options = {"Format on save": True, "Show hidden files": False} def update(self, message): match message: case Toggle(name, checked): self.options = {**self.options, name: checked} def view(self): return pz.column([ui.checkbox(on, lambda c, n=name: Toggle(n, c), label=name) for name, on in self.options.items()], spacing=10) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: format_on_save: bool = True hidden_files: bool = False def view(state, set_state): return pz.column([ ui.checkbox(state.format_on_save, lambda c: set_state(format_on_save=c), label="Format on save"), ui.checkbox(state.hidden_files, lambda c: set_state(hidden_files=c), label="Show hidden files"), ], spacing=10) ``` #### Consejos :::tip Al pulsarla o llegar con Tab tiene el foco del teclado (un anillo aparece al usar las teclas): Espacio la marca o desmarca. ::: #### Propiedades y API ##### `ui.checkbox` ```python checkbox( checked, on_toggle=None, *, label=None, size='md', disabled=False, id=None, theme=None, ) ``` Una casilla de verificación. `checked=None` muestra el estado mixto (parte de un grupo marcada); al pulsarla se marca. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `checked` | *obligatorio* | Estado actual. | | `label` | `None` | Texto de la etiqueta. | | `size` | `"md"` | `"sm"`, `"md"` o `"lg"`. | | `disabled` | `False` | La atenúa e ignora los clics. | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_toggle` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#checkbox) ##### `pz.checkbox` ```python checkbox( checked, label=None, *, on_toggle=None, size=None, text_size=None, accent=None, bg=None, border_color=None, check_color=None, color=None, ) ``` Una casilla de verificación. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `checked` | *obligatorio* | Estado actual. | | `label` | `None` | Texto de la etiqueta. | | `size` | `None` | Tamaño de la casilla. | | `text_size` | `None` | Tamaño del texto. | | `accent` | `None` | Color de acento (parte activa). | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `border_color` | `None` | Color del borde. | | `check_color` | `None` | Color de la marca. | | `color` | `None` | Color del texto o del icono. | Eventos: `on_toggle` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#checkbox) #### Relacionados - [Interruptor](https://albakore.github.io/pyzed/es/library/switch/): Un interruptor para ajustes que se aplican al momento. - [Grupo de radios](https://albakore.github.io/pyzed/es/library/radio-group/): Opciones excluyentes como botones de radio. - [Campo de formulario](https://albakore.github.io/pyzed/es/library/field/): Una fila de ajustes: título y descripción a la izquierda, control a la derecha. ### Catálogo > Cosas para instalar, con buscador e Instalar o Actualizar. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/catalog/ :::tip[En pocas palabras] Una lista con buscador de complementos, como una tienda, cada uno con un botón Instalar. ::: `ui.Catalog` es un componente para cosas que instalar (agentes de un registro, servidores MCP, skills): un buscador sobre una lista con desplazamiento en la que cada fila muestra el nombre, la versión (`version`) y la descripción (`description`), y a la derecha Instalar, Instalado o Actualizar a una versión nueva. `items` son diccionarios u objetos con `id`, `name` y, opcionales, `version`, `description`, `installed` (un bool) y `update` (la versión nueva). Instalar y Actualizar llegan a `on_install` con el id en `event.value`; mientras `busy` tiene un id, esa fila muestra un spinner y los demás botones de Instalar se desactivan. `height` es la altura de la lista. `ui.Catalog` · Categoría: [Agentes de IA](https://albakore.github.io/pyzed/es/library/#agentes-de-ia) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#catalog) **Código:** ```python pz.container(ui.Catalog(items=[ {"id": "gemini", "name": "Gemini CLI", "version": "0.64.0", "description": "A coding agent for the terminal.", "installed": True, "update": "0.65.0"}, {"id": "codex", "name": "Codex CLI", "version": "0.40.0", "description": "Writes and runs code for you."}, {"id": "goose", "name": "Goose", "version": "1.9.0", "description": "An open source agent.", "installed": True}, {"id": "opencode", "name": "OpenCode", "version": "0.12.1", "description": "An agent that edits whole projects."}, ], busy="codex", height=220, on_install=on_install), width=460) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para añadir agentes, servidores MCP, skills o plugins desde una lista, dentro de un diálogo o una página de ajustes. #### Uso ```python import pyzed as pz from pyzed import ui class Agents(pz.App): def __init__(self): self.registry = load_registry() # [{"id", "name", "version", "description", "installed"}, ...] self.installing = "" def view(self): return ui.Catalog(items=self.registry, busy=self.installing, on_install=lambda e: self.install(e.value)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Estado Mientras se instala uno, los demás botones de Instalar se desactivan. ```python pz.container(ui.Catalog(items=[{"id": "goose", "name": "Goose", "version": "1.9.0", "description": "An open source agent."}], height=50), width=380) ``` ```python pz.container(ui.Catalog(items=[{"id": "goose", "name": "Goose", "version": "1.9.0", "description": "An open source agent.", "installed": True}], height=50), width=380) ``` ```python pz.container(ui.Catalog(items=[{"id": "goose", "name": "Goose", "version": "1.9.0", "description": "An open source agent.", "installed": True, "update": "2.0.0"}], height=50), width=380) ``` ```python pz.container(ui.Catalog(items=[ {"id": "goose", "name": "Goose", "version": "1.9.0", "description": "An open source agent."}, {"id": "codex", "name": "Codex CLI", "version": "0.40.0", "description": "Writes and runs code."}, ], busy="goose", height=100), width=380) ``` ```python pz.container(ui.Catalog(items=[{"id": "goose", "name": "Goose"}], height=50), width=380) ``` #### Eventos Catálogo tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_install` | `(event)` | Se pulsó Instalar o Actualizar (`event.value` es el id). | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Agents(pz.App): def __init__(self): self.items = [{"id": "goose", "name": "Goose", "version": "1.9.0"}, {"id": "codex", "name": "Codex CLI", "version": "0.40.0"}] self.installing = "" def install(self, ident): # on_install: event.value is the item's id self.installing = ident def view(self): return ui.Catalog(items=self.items, busy=self.installing, height=200, on_install=lambda e: self.install(e.value)) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Install: ident: str class Agents(pz.App): items = [{"id": "goose", "name": "Goose", "version": "1.9.0"}] installing = "" def update(self, message): match message: case Install(ident): self.installing = ident def view(self): return ui.Catalog(items=self.items, busy=self.installing, height=200, on_install=lambda e: Install(e.value)) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: installed: frozenset = frozenset() ITEMS = [{"id": "goose", "name": "Goose", "version": "1.9.0"}] def view(state, set_state): items = [{**i, "installed": i["id"] in state.installed} for i in ITEMS] return ui.Catalog(items=items, height=200, on_install=lambda e: set_state(installed=state.installed | {e.value})) ``` #### Propiedades y API ##### `ui.Catalog` ```python class Catalog ``` Un componente para cosas que instalar con un buscador (agentes de un registro, servidores, skills…): cada fila muestra Instalar, Instalado o Actualizar a una versión nueva. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `items` | `None` | Dicts u objetos con `id`, `name` y, opcionalmente, `version`, `description`, `installed` y `update`. | | `busy` | `""` | El id que se está instalando (un indicador de carga). | | `height` | `320` | Alto de la lista. | | `theme` | `None` | Tema. | Eventos: `on_install` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#catalog) #### Relacionados - [Fila de servicio](https://albakore.github.io/pyzed/es/library/service-row/): Una fila de ajustes para un servidor MCP, un agente o una skill: estado, interruptor y menú. - [Campo de búsqueda](https://albakore.github.io/pyzed/es/library/search-input/): Un campo con lupa, para filtrar. - [Fila de lista](https://albakore.github.io/pyzed/es/library/list-item/): Una fila de lista con icono, texto y un hueco al final. ### Chip > Una etiqueta compacta: pulsable como filtro y con ✕ para quitarla. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/chip/ :::tip[En pocas palabras] Una etiqueta pequeña y redondeada. Púlsala para activar o quitar un filtro, o pulsa su ✕ para quitarla. ::: Una etiqueta pequeña y redondeada. Con `on_press` funciona como un filtro que se activa (`selected=True` la resalta) y con `on_remove` muestra un botón para quitarla, como en una lista de etiquetas. `ui.chip` · Categoría: [Acciones](https://albakore.github.io/pyzed/es/library/#acciones) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#chip) **Código:** ```python pz.row([ ui.chip("python", icon="tag", selected=True, on_press=on_filter), ui.chip("rust", on_press=on_filter), ui.chip("iced", on_remove=on_remove), ui.chip("zed", icon="sparkles", on_remove=on_remove), ], spacing=6) ``` #### Cuándo usarlo - Para etiquetas, filtros y valores elegidos (una selección múltiple, destinatarios). - Para estados de solo lectura usa una [Insignia](https://albakore.github.io/pyzed/es/library/badge/). #### Uso ```python import pyzed as pz from pyzed import ui class Tags(pz.App): def __init__(self): self.tags = ["python", "rust", "iced"] def view(self): return pz.row([ui.chip(t, on_remove=lambda t=t: self.tags.remove(t)) for t in self.tags], spacing=6, wrap=True) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Tipo ```python ui.chip("python") ``` ```python ui.chip("rust", on_press=on_filter) ``` ```python ui.chip("python", on_press=on_filter, selected=True) ``` ```python ui.chip("iced", on_remove=on_remove) ``` ##### Contenido ```python ui.chip("design", icon="tag") ``` ```python ui.chip("ada@example.com", icon="user", on_remove=on_remove) ``` ```python ui.chip("favorites", icon="star", selected=True, on_press=on_filter) ``` ##### Estados y eventos ```python ui.chip("Failing", on_press=on_press, selected=True, tint="error") ``` ```python ui.chip("Archived", on_press=on_press, on_remove=on_remove, disabled=True) ``` #### Eventos Chip tiene **2 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_press` | `()` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | | `on_remove` | `()` | Muestra una ✕ que envía esto al pulsarla. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Filters(pz.App): def __init__(self): self.tags = ["python", "rust", "iced"] self.active = "python" def view(self): return pz.row([ ui.chip(t, selected=t == self.active, on_press=lambda t=t: setattr(self, "active", t), # pick it on_remove=lambda t=t: self.tags.remove(t)) # its ✕ for t in self.tags ], spacing=6, wrap=True) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Pick: tag: str @dataclass class Remove: tag: str class Filters(pz.App): tags = ["python", "rust", "iced"] active = "python" def update(self, message): match message: case Pick(tag): self.active = tag case Remove(tag): self.tags = [t for t in self.tags if t != tag] def view(self): return pz.row([ui.chip(t, selected=t == self.active, on_press=Pick(t), on_remove=Remove(t)) for t in self.tags], spacing=6, wrap=True) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: tags: tuple = ("python", "rust", "iced") active: str = "python" def view(state, set_state): return pz.row([ ui.chip(t, selected=t == state.active, on_press=lambda t=t: set_state(active=t), on_remove=lambda t=t: set_state(tags=tuple(x for x in state.tags if x != t))) for t in state.tags ], spacing=6, wrap=True) ``` #### Propiedades y API ##### `ui.chip` ```python chip( content, *, on_press=None, on_remove=None, selected=False, icon=None, tint='accent', disabled=False, theme=None, ) ``` Una etiqueta compacta, opcionalmente pulsable (filtros) y con botón para quitarla. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Texto. | | `selected` | `False` | Lo marca como seleccionado. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `tint` | `"accent"` | Color cuando está seleccionada (un token del tema). | | `disabled` | `False` | La atenúa e ignora las pulsaciones. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_press`, `on_remove` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#chip) #### Relacionados - [Insignia](https://albakore.github.io/pyzed/es/library/badge/): Una pastilla pequeña para estados, versiones y contadores. - [Grupo de alternancia](https://albakore.github.io/pyzed/es/library/toggle-group/): Un control segmentado solo con iconos. ### Cifra > Una cifra de dashboard con variación y minigráfico. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/stat/ :::tip[En pocas palabras] Un número grande con su nombre y si subió o bajó, como en un panel de control. ::: Un panel de dashboard: un título, un valor grande, una variación opcional (`delta`, mostrada como `+4.2%` en verde o rojo) y una `trend` opcional dibujada como minigráfico. `ui.stat` · Categoría: [Datos](https://albakore.github.io/pyzed/es/library/#datos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#stat) **Código:** ```python pz.row([ ui.stat("Users", "1,284", delta=12.5, trend=[3, 5, 4, 6, 8, 7, 9], width=200), ui.stat("Errors", "37", delta=-8.0, width=200), ui.stat("Build time", "1m 12s", width=200), ], spacing=12) ``` #### Cuándo usarlo - Para las cifras clave en la parte de arriba de un dashboard o informe. #### Uso ```python import pyzed as pz from pyzed import ui pz.row([ ui.stat("Users", "1,284", delta=12.5, trend=[3, 5, 4, 6, 8, 7, 9], width=200), ui.stat("Errors", "37", delta=-8.0, width=200), ui.stat("Build time", "1m 12s", width=200), ], spacing=12) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido ```python ui.stat("Build time", "1m 12s", width=190) ``` ```python ui.stat("Users", "1,284", delta=12.5, width=190) ``` ```python ui.stat("Errors", "37", delta=-8.0, width=190) ``` ```python ui.stat("Revenue", "$9.4k", delta=4.2, trend=[3, 5, 4, 6, 8, 7, 9], width=190) ``` ##### Estados y eventos Con `inverse=True` una bajada es una buena noticia (verde), para errores o latencia. ```python ui.stat("Errors", "12", delta=-8.0, inverse=True, icon="bug", width=180) ``` ```python ui.stat("Users", "1,204", delta=4.2, icon="users", on_press=on_press, width=180) ``` #### Eventos Cifra tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_press` | `()` | Hace pulsable la tarjeta. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Dashboard(pz.App): def __init__(self): self.detail = None def view(self): return pz.column([ pz.row([ui.stat("Users", "1,284", delta=12.5, width=200, on_press=lambda: setattr(self, "detail", "Users")), # the whole card ui.stat("Errors", "37", delta=-8.0, width=200, on_press=lambda: setattr(self, "detail", "Errors"))], spacing=12), ui.muted(f"Details of {self.detail}" if self.detail else "Click a number"), ], spacing=8) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Open: metric: str class Dashboard(pz.App): detail = None def update(self, message): match message: case Open(metric): self.detail = metric def view(self): return pz.row([ui.stat(m, v, width=200, on_press=Open(m)) for m, v in (("Users", "1,284"), ("Errors", "37"))], spacing=12) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: detail: str = "" def view(state, set_state): return pz.row([ui.stat(m, v, width=200, on_press=lambda m=m: set_state(detail=m)) for m, v in (("Users", "1,284"), ("Errors", "37"))], spacing=12) ``` #### Propiedades y API ##### `ui.stat` ```python stat( title, value, *, delta=None, trend=None, width=None, icon=None, inverse=False, on_press=None, theme=None, ) ``` Un número de panel: título, valor grande, variación opcional (`+4.2%`) y sparkline. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | *obligatorio* | Título. | | `value` | *obligatorio* | Valor actual. | | `delta` | `None` | Variación en %; verde si es positiva, roja si no. | | `trend` | `None` | Valores para la sparkline. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `icon` | `None` | Icono antes del título. | | `inverse` | `False` | Pinta de verde una bajada y de rojo una subida (errores, latencia…). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_press` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#stat) #### Relacionados - [Minigráfico](https://albakore.github.io/pyzed/es/library/sparkline/): Un gráfico de líneas diminuto para tendencias. - [Gráfico de barras](https://albakore.github.io/pyzed/es/library/bar-chart/): Un gráfico de barras verticales con etiquetas. - [Dona](https://albakore.github.io/pyzed/es/library/donut/): Un gráfico de dona con una etiqueta en el centro. ### Código QR > Un código QR para cualquier texto o URL. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/qr-code/ :::tip[En pocas palabras] Un código de barras cuadrado que lee la cámara de un móvil, para compartir un enlace. ::: Dibuja un código QR que codifica `data`. `cell_size` fija el tamaño de cada módulo; `color` y `bg` cambian sus colores. `pz.qr_code` · Categoría: [Contenido](https://albakore.github.io/pyzed/es/library/#contenido) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#qr_code) **Código:** ```python pz.row([ pz.qr_code("https://albakore.github.io/pyzed/", cell_size=5), pz.qr_code("pyzed", cell_size=5, color="#74ade8", bg="#282c33"), ], spacing=24, align="center") ``` #### Cuándo usarlo - Para pasar un enlace, una contraseña de Wi-Fi o un código de emparejamiento a un móvil. #### Uso ```python import pyzed as pz pz.row([ pz.qr_code("https://albakore.github.io/pyzed/", cell_size=5), pz.qr_code("pyzed", cell_size=5, color="#74ade8", bg="#282c33"), ], spacing=24, align="center") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python pz.qr_code("https://albakore.github.io/pyzed/", cell_size=3) ``` ```python pz.qr_code("pyzed", cell_size=5) ``` ```python pz.qr_code("pyzed", cell_size=5, color="#74ade8", bg="#1e2228") ``` #### Eventos Código QR no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.qr_code` ```python qr_code(data, *, cell_size=4.0, color=None, bg=None) ``` Un código QR que codifica `data`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `data` | *obligatorio* | El texto o URL a codificar. | | `cell_size` | `4.0` | Tamaño de cada módulo en píxeles. | | `color` | `None` | Color del texto o del icono. | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#qr_code) #### Relacionados - [Imagen](https://albakore.github.io/pyzed/es/library/image/): Imágenes con ajuste, opacidad y radio; un visor con zoom. - [Lienzo](https://albakore.github.io/pyzed/es/library/canvas/): Dibujo 2D con formas, trazos, texto y eventos de ratón. ### Columna > Apila los hijos en vertical. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/column/ :::tip[En pocas palabras] Pone las cosas una debajo de la otra, de arriba abajo. ::: Apila sus hijos en vertical, con `spacing`, `padding`, `align` en el eje cruzado y un `max_width` opcional. Como `row`, las listas anidadas se aplanan y se ignoran `None`/`False`. `pz.column` · Categoría: [Disposición](https://albakore.github.io/pyzed/es/library/#disposición) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#column) **Código:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([ pz.column([box("1"), box("2"), box("3")], spacing=8), pz.column([box("wide", w=160), box("center"), box("end")], spacing=8, align="center"), pz.column([box("top"), pz.vspace(), box("bottom")], spacing=8, height=170), ], spacing=32) ``` #### Cuándo usarlo - Para formularios, listas, contenido de páginas y la raíz de casi cualquier `view()`. #### Uso ```python import pyzed as pz from pyzed import ui box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([ pz.column([box("1"), box("2"), box("3")], spacing=8), pz.column([box("wide", w=160), box("center"), box("end")], spacing=8, align="center"), pz.column([box("top"), pz.vspace(), box("bottom")], spacing=8, height=170), ], spacing=32) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Alineación cruzada ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([box("wide", w=160), box("B"), box("C")], spacing=8, align="start") ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([box("wide", w=160), box("B"), box("C")], spacing=8, align="center") ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([box("wide", w=160), box("B"), box("C")], spacing=8, align="end") ``` ##### Opciones ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([box("1"), box("2")], spacing=16) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([box("top"), pz.vspace(), box("bottom")], height=170) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.column([box("1"), box("2")], spacing=8, padding=16), border_color=pz.theme.current().border, border_width=1, radius=8) ``` #### Eventos Columna no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.column` ```python column( children=None, *, spacing=0.0, padding=None, width=None, height=None, align=None, max_width=None, clip=False, ) ``` Apila los hijos en vertical. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `children` | `None` | Lista de hijos. Se aplanan listas anidadas y se ignoran `None` y `False`, así que puedes escribir `cond and widget`. | | `spacing` | `0.0` | Separación entre hijos, en píxeles. | | `padding` | `None` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `align` | `None` | Alineación de los hijos en el eje cruzado: `"start"`, `"center"` o `"end"`. | | `max_width` | `None` | Ancho máximo en píxeles. | | `clip` | `False` | Recorta el contenido que se sale de los límites. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#column) #### Relacionados - [Fila](https://albakore.github.io/pyzed/es/library/row/): Coloca los hijos en horizontal. - [Desplazable](https://albakore.github.io/pyzed/es/library/scrollable/): Desplaza el contenido que no cabe. - [Espacio](https://albakore.github.io/pyzed/es/library/spacing/): Huecos fijos y separadores flexibles. ### Confirmación > Una pregunta de sí o no, en rojo para acciones destructivas. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/confirm/ :::tip[En pocas palabras] Pregunta “¿seguro?” antes de hacer algo que no se puede deshacer. ::: Hace una pregunta de sí o no. `danger=True` pinta de rojo el botón de confirmar; `confirm_label` y `cancel_label` cambian el texto de los botones. `ui.confirm` · Categoría: [Diálogos](https://albakore.github.io/pyzed/es/library/#diálogos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#confirm) **Código:** ```python ui.dialog_layer(main_view(), ui.confirm( "Delete notes.py? This can't be undone.", on_delete, on_cancel, title="Delete file", danger=True, confirm_label="Delete", width=360, )) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Antes de acciones destructivas o irreversibles: borrar, sobrescribir, descartar. #### Uso ```python from pyzed import ui ui.dialog_layer(main_view(), ui.confirm( "Delete notes.py? This can't be undone.", on_delete, on_cancel, title="Delete file", danger=True, confirm_label="Delete", width=360, )) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Tipo ```python ui.confirm("Close all tabs?", on_confirm, on_cancel, title="Close tabs", width=320) ``` ```python ui.confirm("Delete notes.py?", on_delete, on_cancel, title="Delete file", danger=True, confirm_label="Delete", width=320) ``` ```python ui.confirm("Overwrite the existing file?", on_confirm, on_cancel, kind="warning", width=320) ``` ```python ui.confirm("Push 3 commits to main?", on_push, on_cancel, confirm_label="Push", cancel_label="Not now", width=320) ``` #### Eventos Confirmación tiene **2 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_confirm` | `()` | El botón de confirmar. | | `on_cancel` | `()` | Cancelar, el icono de cerrar. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Files(pz.App): def __init__(self): self.files = ["notes.py", "todo.txt"] self.deleting = "notes.py" # the file waiting for an answer def delete(self): # on_confirm self.files.remove(self.deleting) self.deleting = None def view(self): dialog = None if self.deleting: dialog = ui.confirm(f"Delete {self.deleting}?", self.delete, lambda: setattr(self, "deleting", None), # on_cancel title="Delete file", danger=True, confirm_label="Delete") return ui.dialog_layer(main_view(), dialog) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Delete: name: str class Files(pz.App): files = ["notes.py", "todo.txt"] deleting = "notes.py" def update(self, message): match message: case Delete(name): self.files = [f for f in self.files if f != name] self.deleting = None case "cancel": self.deleting = None def view(self): dialog = ui.confirm(f"Delete {self.deleting}?", Delete(self.deleting), "cancel", danger=True) \ if self.deleting else None return ui.dialog_layer(main_view(), dialog) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: files: tuple = ("notes.py", "todo.txt") deleting: str = "notes.py" def view(state, set_state): if not state.deleting: return main_view() keep = tuple(f for f in state.files if f != state.deleting) return ui.dialog_layer(main_view(), ui.confirm( f"Delete {state.deleting}?", lambda: set_state(files=keep, deleting=""), lambda: set_state(deleting=""), danger=True)) ``` #### Propiedades y API ##### `ui.confirm` ```python confirm( message, on_confirm, on_cancel, *, title=None, confirm_label=None, cancel_label=None, danger=False, kind=None, width=420, theme=None, ) ``` Hace una pregunta de sí o no. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `message` | *obligatorio* | Texto o widget. | | `title` | `None` | Título. | | `confirm_label` | `None` | Texto del botón de confirmar. | | `cancel_label` | `None` | Texto del botón de cancelar. | | `danger` | `False` | Pinta el botón de rojo y lo llama Eliminar. | | `kind` | `None` | Añade un icono, como en `alert`. | | `width` | `420` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_confirm`, `on_cancel` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#confirm) #### Relacionados - [Alerta](https://albakore.github.io/pyzed/es/library/alert/): Un mensaje con un botón. - [Elección](https://albakore.github.io/pyzed/es/library/choice/): Un mensaje con tus propios botones. - [Cambios sin guardar](https://albakore.github.io/pyzed/es/library/unsaved-changes/): “¿Guardar los cambios antes de cerrar?” ### Consola > Un registro estilo dev tools con niveles, filtro, copiar y limpiar. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/console/ :::tip[En pocas palabras] Una lista de mensajes de la app (información, avisos, errores) que puedes filtrar y copiar. ::: `ui.Log` reúne entradas (seguro entre hilos, con un tope de `limit`, líneas repetidas agrupadas) y puede capturar `print` y las trazas de error. `ui.console` las muestra como la consola de las dev tools de un navegador: un filtro, un interruptor por nivel con su cuenta, copiar y limpiar. Los niveles son `debug`, `info`, `event`, `stdout`, `stderr`, `warning` y `error` (`ui.LOG_LEVELS`). Con un `ui.LogSelection` las líneas se pueden seleccionar (clic, Shift+clic, Ctrl+clic) y una línea seleccionada muestra su `detail` (un traceback); `on_context` abre tu menú con el clic derecho y `sources` añade un desplegable para ver las líneas de un origen. `ui.console` · Categoría: [Código y herramientas](https://albakore.github.io/pyzed/es/library/#código-y-herramientas) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#console) **Código:** ```python log = ui.Log() log.write("info", "Loaded ui/main.pzform") log.write("event", "button1.click") log.write("stdout", "Saving 3 files…") log.write("warning", "settings.json not found, using defaults") log.write("error", "ValueError: invalid literal for int()") pz.container(ui.console(log.entries, on_clear=on_clear, on_copy=on_copy, on_query=on_query), width=600, height=220) ``` #### Cuándo usarlo - Para un panel de depuración en tu app, o para mostrar lo que hace una tarea en segundo plano. #### Uso ```python import pyzed as pz from pyzed import ui class Debug(pz.App): def __init__(self): self.log = ui.Log(on_write=lambda: pz.post("log")) # redraw when a line arrives self.log.capture() # also collect print() and stderr self.query, self.hidden = "", set() def view(self): return ui.console(self.log.entries, query=self.query, on_query=lambda q: setattr(self, "query", q), hidden=self.hidden, on_toggle_level=lambda lvl: self.hidden.symmetric_difference_update({lvl}), on_clear=self.log.clear, on_copy=lambda: pz.Command.copy(self.log.text())) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python log = ui.Log() for level in ui.LOG_LEVELS: log.write(level, f"{level} message") pz.container(ui.console(log.entries, on_clear=on_clear), width=520, height=220) ``` ```python log = ui.Log() for level in ui.LOG_LEVELS: log.write(level, f"{level} message") pz.container(ui.console(log.entries, hidden={"info", "event"}, on_toggle_level=on_toggle), width=520, height=200) ``` ```python log = ui.Log() log.write("info", "started") log.write("error", "ValueError: bad input") pz.container(ui.console(log.entries, query="error", on_query=on_query), width=520, height=140) ``` #### Eventos Consola tiene **8 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_query` | `ui.console` | `(text)` | Recibe cada edición del filtro. | | `on_toggle_level` | `ui.console` | `(level)` | Se llama con el nivel del botón pulsado. | | `on_clear` | `ui.console` | `()` | Añade el botón Limpiar. | | `on_copy` | `ui.console` | `()` | Añade el botón Copiar. | | `on_context` | `ui.console` | `(line)` | Se llama con la línea del clic derecho (`None` bajo las líneas): abre un menú ahí (copiar, ir al origen, limpiar...). | | `on_open` | `ui.console` | `(line)` | Se llama con la línea del doble clic. | | `on_source` | `ui.console` | `(source)` | Recibe el origen elegido en el desplegable (`None` para todos). | | `on_write` | `ui.Log` | `()` | Se llama tras cada escritura (desde cualquier hilo). | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Debug(pz.App): def __init__(self): self.log = ui.Log() self.log.write("info", "Started") self.log.write("error", "ValueError: bad input") self.log.on_write = lambda: pz.post("log") # a line arrived (from any thread) self.query, self.hidden = "", set() def update(self, message): pass # "log" just redraws def view(self): return ui.console(self.log.entries, query=self.query, hidden=self.hidden, on_query=lambda q: setattr(self, "query", q), # the filter on_toggle_level=lambda lvl: self.hidden.symmetric_difference_update({lvl}), on_clear=self.log.clear, # adds Clear on_copy=lambda: pz.Command.copy(self.log.text()), # adds Copy on_open=lambda entry: print("open", entry.text)) # double click ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Filter: query: str @dataclass class ToggleLevel: level: str class Debug(pz.App): log = ui.Log() query, hidden = "", frozenset() def update(self, message): match message: case Filter(query): self.query = query case ToggleLevel(level): self.hidden = self.hidden ^ {level} case "clear": self.log.clear() def view(self): return ui.console(self.log.entries, query=self.query, hidden=self.hidden, on_query=Filter, on_toggle_level=ToggleLevel, on_clear="clear") ``` **Funcional:** ```python from dataclasses import dataclass LOG = ui.Log() LOG.write("info", "Started") @dataclass(frozen=True) class State: query: str = "" hidden: frozenset = frozenset() def view(state, set_state): return ui.console(LOG.entries, query=state.query, hidden=state.hidden, on_query=lambda q: set_state(query=q), on_toggle_level=lambda lvl: set_state(hidden=state.hidden ^ {lvl})) ``` #### Propiedades y API ##### `ui.console` ```python console( entries, *, query='', on_query=None, hidden=(), on_toggle_level=None, on_clear=None, on_copy=None, actions=(), selection=None, on_context=None, on_open=None, sources=(), source=None, on_source=None, show_source=False, placeholder=None, empty=None, font_size=12.5, id=None, theme=None, ) ``` Una vista de registro como la consola de las herramientas de desarrollo del navegador: filtro, un botón por nivel con su cuenta, copiar y limpiar, y las entradas en monoespaciada, siempre desplazada a la más nueva. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `entries` | *obligatorio* | Elementos `ui.LogEntry` (`log.entries`). | | `query` | `""` | Texto del filtro. | | `hidden` | `()` | Niveles ocultos. | | `actions` | `()` | Widgets extra al final de la barra. | | `selection` | `None` | Un `ui.LogSelection`: las líneas se pueden seleccionar con el ratón y una línea seleccionada con `detail` lo muestra debajo. | | `sources` | `()` | Pares `(valor, etiqueta)`: añade un desplegable que muestra las líneas de un `source`. | | `source` | `None` | El origen que se muestra (`None` para todos). | | `show_source` | `False` | Etiqueta cada línea con su origen. | | `placeholder` | `None` | Texto del filtro vacío. | | `empty` | `None` | Lo que se ve mientras no hay líneas. | | `font_size` | `12.5` | Tamaño de las entradas. | | `id` | `None` | Id del scrollable. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_query`, `on_toggle_level`, `on_clear`, `on_copy`, `on_context`, `on_open`, `on_source` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#console) ##### `ui.Log` ```python class Log ``` Las entradas de un `ui.console`: seguro entre hilos, limitado a `limit` y con las líneas repetidas agrupadas. `write(level, text)` añade una línea; `capture()` (o `with log:`) copia la salida de `print` y los tracebacks; `pipe(stream)` lee la salida de un subproceso en segundo plano; `text()` devuelve todo para copiarlo (`text(ids)` solo esas líneas). `write` también acepta el `source` de la línea, su `detail` (un traceback) y su `origin` (`(ruta, línea)`) y devuelve la entrada; `find(id)` busca una. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `limit` | `5000` | Máximo de entradas guardadas. | Eventos: `on_write` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#log) ##### `ui.LogEntry` ```python @dataclass class LogEntry(level, text, time=time.time(), count=1, source='', detail='', origin=None, id=0) ``` Una línea de un `ui.Log`: `level`, `text`, `time`, `count` (líneas idénticas seguidas), `source`, `detail`, `origin`, su número `id`, `clock` (`HH:MM:SS`) y `line()` (la entrada como texto). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `level` | *obligatorio* | Nivel. | | `text` | *obligatorio* | La línea. | | `time` | `time()` | Cuándo se escribió (`time.time()`). | | `count` | `1` | Líneas idénticas seguidas. | | `source` | `""` | De dónde viene (`"project"`, `"agent"`...); `ui.console` puede mostrarlo y filtrarlo. | | `detail` | `""` | Más texto que se ve bajo la línea mientras está seleccionada (un traceback). | | `origin` | `None` | `(ruta, línea)` del código al que se refiere. | | `id` | `0` | Su número en el log (lo que guarda un `ui.LogSelection`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#logentry) ##### `ui.LogSelection` ```python class LogSelection ``` Las líneas seleccionadas de un `ui.console`, como en una lista de archivos: un clic selecciona una línea, Shift+clic un rango desde la última pulsada y Ctrl+clic (Cmd en macOS) añade o quita una. Guarda una en tu app y pásala a `ui.console(selection=...)`; `select(ids)` selecciona justo esas (Ctrl+A), `clear()` ninguna, `keep(entries)` olvida las líneas que ya no están e `ids` son los `id` de las seleccionadas para `log.text(ids)`. [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#logselection) ##### `ui.LOG_LEVELS` Los niveles que colorea `ui.console`, en el orden de sus filtros: debug, info, event, stdout, stderr, warning, error. [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#log_levels) #### Relacionados - [Terminal](https://albakore.github.io/pyzed/es/library/terminal/): Una vista de terminal de solo lectura con colores ANSI. - [Editor](https://albakore.github.io/pyzed/es/library/editor/): Un editor de código completo: resaltado, plegado, minimapa, marcas de git. ### Contador > Un contador redondo que muestra 99+ por encima del máximo. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/count-badge/ :::tip[En pocas palabras] Un numerito redondo, como el número de mensajes sin leer en el icono de una app. ::: Un contador redondo para notificaciones o problemas. Por encima de `max` muestra `99+`. `ui.count_badge` · Categoría: [Estado y avisos](https://albakore.github.io/pyzed/es/library/#estado-y-avisos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#count_badge) **Código:** ```python pz.row([ ui.count_badge(3), ui.count_badge(42, tint="success"), ui.count_badge(120, tint="error"), pz.row([ui.icon_button("bell", on_press), ui.count_badge(7)], spacing=2, align="center"), ], spacing=16, align="center") ``` #### Cuándo usarlo - Junto a un icono o pestaña para indicar cuántos elementos sin leer o errores hay. - Para estados de texto usa una [Insignia](https://albakore.github.io/pyzed/es/library/badge/). #### Uso ```python import pyzed as pz from pyzed import ui pz.row([ ui.count_badge(3), ui.count_badge(42, tint="success"), ui.count_badge(120, tint="error"), pz.row([ui.icon_button("bell", on_press), ui.count_badge(7)], spacing=2, align="center"), ], spacing=16, align="center") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Cuenta Por encima de `max` muestra `max+`. ```python ui.count_badge(3) ``` ```python ui.count_badge(42) ``` ```python ui.count_badge(120) ``` ```python ui.count_badge(12, max=9) ``` ##### Tinte ```python ui.count_badge(5, tint="success") ``` ```python ui.count_badge(5, tint="warning") ``` ```python ui.count_badge(5, tint="error") ``` ##### Junto a un icono ```python pz.row([ui.icon_button("bell", on_press), ui.count_badge(7)], spacing=2, align="center") ``` ##### Estados y eventos Con `show_zero=False` no dibuja nada en 0 pero mantiene su lugar en el diseño. ```python pz.row([ui.icon_button("bell", on_press), ui.count_badge(0, show_zero=False)], align="center") ``` #### Eventos Contador no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.count_badge` ```python count_badge(count, *, tint='accent', max=99, show_zero=True, theme=None) ``` Un contador redondo (notificaciones, problemas…). Muestra `99+` por encima de `max`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `count` | *obligatorio* | El número. | | `tint` | `"accent"` | Color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` o un token. | | `max` | `99` | Máximo antes de mostrar `N+`. | | `show_zero` | `True` | Con `False`, un 0 no dibuja nada. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#count_badge) #### Relacionados - [Insignia](https://albakore.github.io/pyzed/es/library/badge/): Una pastilla pequeña para estados, versiones y contadores. - [Indicador](https://albakore.github.io/pyzed/es/library/indicator/): Un punto de color para estado, presencia o cambios sin guardar. - [Barra de estado](https://albakore.github.io/pyzed/es/library/status-bar/): La barra inferior: diagnósticos, posición del cursor, lenguaje. ### Contenedor > Tamaño, relleno, alineación, fondo, borde y sombra para un hijo. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/container/ :::tip[En pocas palabras] Una caja alrededor de una cosa, para darle un tamaño, un margen, un fondo o un borde. ::: Envuelve un hijo para darle tamaño, relleno y alineación, y para dibujar un fondo (color o degradado), un borde, esquinas redondeadas y una sombra. `pz.center(child)` es un contenedor que centra su hijo en todo el espacio disponible. `pz.container` · Categoría: [Disposición](https://albakore.github.io/pyzed/es/library/#disposición) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#container) **Código:** ```python t = pz.theme.current() pz.row([ pz.container(ui.label("padding + bg"), padding=16, bg=t.element, radius=8), pz.container(ui.label("border"), padding=16, border_color=t.accent, border_width=2, radius=8), pz.container(ui.label("shadow", color="#ffffff"), padding=16, radius=12, gradient=(135, ["#7c3aed", "#3b82f6"]), shadow=("#00000066", (0, 6), 18)), ], spacing=16, align="center") ``` #### Cuándo usarlo - Siempre que un widget necesite una caja: tarjetas, zonas de color, tamaños fijos, centrar. #### Uso ```python import pyzed as pz from pyzed import ui t = pz.theme.current() pz.row([ pz.container(ui.label("padding + bg"), padding=16, bg=t.element, radius=8), pz.container(ui.label("border"), padding=16, border_color=t.accent, border_width=2, radius=8), pz.container(ui.label("shadow", color="#ffffff"), padding=16, radius=12, gradient=(135, ["#7c3aed", "#3b82f6"]), shadow=("#00000066", (0, 6), 18)), ], spacing=16, align="center") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Caja ```python pz.container(ui.label("padding + bg"), padding=16, bg=pz.theme.current().element, radius=8) ``` ```python pz.container(ui.label("border"), padding=16, border_color=pz.theme.current().accent, border_width=2, radius=8) ``` ```python pz.container(ui.label("pill"), padding=(6, 16), bg=pz.theme.current().element, radius=999) ``` ##### Pintura ```python pz.container(ui.label("gradient", color="#ffffff"), padding=16, radius=10, gradient=(135, ["#7c3aed", "#3b82f6"])) ``` ```python pz.container(ui.label("shadow"), padding=16, radius=10, bg=pz.theme.current().elevated, shadow=("#00000066", (0, 6), 18)) ``` ##### Alineación ```python pz.container(ui.label("centered"), center=True, width=220, height=90, border_color=pz.theme.current().border, border_width=1, radius=8) ``` ```python pz.container(ui.label("bottom right"), align_x="end", align_y="end", padding=8, width=220, height=90, border_color=pz.theme.current().border, border_width=1, radius=8) ``` #### Ejemplos ##### Centrar `pz.center` (o `center=True`) centra el hijo en el espacio que recibe. **Código:** ```python pz.container(pz.center(ui.label("centered")), width=360, height=120, border_color=pz.theme.current().border, border_width=1, radius=8) ``` #### Eventos Contenedor no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.container` ```python container( content=None, *, padding=None, width=None, height=None, max_width=None, max_height=None, align_x=None, align_y=None, center=False, bg=None, color=None, border_color=None, border_width=0.0, radius=None, shadow=None, clip=False, gradient=None, ) ``` Envuelve un hijo para darle tamaño, alineación, fondo, borde, radio y sombra. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | `None` | El hijo (o `None` para una caja vacía, útil como rectángulo de color). | | `padding` | `None` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `max_width` | `None` | Ancho máximo en píxeles. | | `max_height` | `None` | Alto máximo en píxeles. | | `align_x` | `None` | Alineación horizontal: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `align_y` | `None` | Alineación vertical: `"start"`/`"top"`, `"center"`, `"end"`/`"bottom"`. | | `center` | `False` | Centra el contenido en ambos ejes y ocupa todo el espacio. | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `color` | `None` | Color del texto o del icono. | | `border_color` | `None` | Color del borde. | | `border_width` | `0.0` | Grosor del borde en píxeles. | | `radius` | `None` | Radio de las esquinas: un número o `(sup_izq, sup_der, inf_der, inf_izq)`. | | `shadow` | `None` | Sombra: `(color, (dx, dy), desenfoque)`. | | `clip` | `False` | Recorta el contenido que se sale de los límites. | | `gradient` | `None` | Degradado lineal de fondo: `(ángulo, [colores])` o `(ángulo, [(posición, color), ...])`. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#container) ##### `pz.center` ```python center(content, **kwargs) ``` Centra `content` en todo el espacio disponible. Equivale a `container(content, center=True)`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `**kwargs` | — | Cualquier parámetro de `container`. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#center) #### Relacionados - [Tarjeta](https://albakore.github.io/pyzed/es/library/card/): Una superficie con borde, título y acciones opcionales. - [Pila](https://albakore.github.io/pyzed/es/library/stack/): Superpone los hijos; `pin` coloca uno en x, y. - [Fila](https://albakore.github.io/pyzed/es/library/row/): Coloca los hijos en horizontal. ### Cuadrícula > Columnas iguales, o pistas como en CSS con expansión. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/grid/ :::tip[En pocas palabras] Ordena las cosas en filas y columnas, como azulejos en una pared. ::: Coloca los hijos en una cuadrícula. Con `columns=3` las columnas son iguales, y `max_width` mete tantas como quepan. Con listas de pistas (`columns=[200, "1fr", "auto"]`) funciona como un grid de CSS, y `pz.cell(child, column=, row=, column_span=, row_span=)` coloca un hijo a mano. `pz.grid` · Categoría: [Disposición](https://albakore.github.io/pyzed/es/library/#disposición) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#grid) **Código:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([box(str(i), w="fill") for i in range(1, 9)], columns=4, spacing=8), width=440) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para galerías, dashboards y maquetas de página con cabecera, barra lateral y contenido. #### Uso ```python import pyzed as pz from pyzed import ui box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([box(str(i), w="fill") for i in range(1, 9)], columns=4, spacing=8), width=440) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Columnas ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([box(str(i), w="fill") for i in range(1, 5)], columns=2, spacing=8, height=44), width=220) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([box(str(i), w="fill") for i in range(1, 7)], columns=3, spacing=8, height=44), width=300) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([box(str(i), w="fill") for i in range(1, 9)], columns=4, spacing=8, height=44), width=380) ``` ##### Pistas ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([box("100 px", w="fill"), box("1fr", w="fill")], columns=[100, "1fr"], spacing=8), width=340, height=44) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([pz.cell(box("header", w="fill"), column=0, row=0, column_span=2), pz.cell(box("a", w="fill"), column=0, row=1), pz.cell(box("b", w="fill"), column=1, row=1)], columns=["1fr", "1fr"], spacing=8), width=300, height=96) ``` #### Ejemplos ##### Pistas y expansión Los números son píxeles, `"1fr"` reparte el espacio libre y `"auto"` se ajusta al contenido. **Código:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.grid([ pz.cell(box("header", w="fill"), column=0, row=0, column_span=2), pz.cell(box("sidebar", w="fill", h="fill"), column=0, row=1), pz.cell(box("content", w="fill", h="fill"), column=1, row=1), ], columns=[120, "1fr"], rows=[44, 110], spacing=8), width=440) ``` #### Eventos Cuadrícula no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.grid` ```python grid( children=None, *, columns=None, rows=None, max_width=None, spacing=0.0, row_spacing=None, height=None, aspect_ratio=None, width=None, padding=None, align_x=None, align_y=None, ) ``` Coloca los hijos en una rejilla. Con `columns=3` las columnas son iguales (o `max_width` mete tantas como quepan). Con listas de pistas (`columns=[200, "1fr", "auto"]`, `rows=...`) funciona como un grid de CSS: los números son píxeles, `"auto"` se ajusta al contenido y `"fr"` reparte el espacio que sobra. Envuelve los hijos en `pz.cell` para colocarlos y hacer que ocupen varias celdas; el resto llena las celdas libres fila a fila. Ver [Layout](https://albakore.github.io/pyzed/es/guides/layout/#rejillas). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `children` | `None` | Lista de hijos. Se aplanan listas anidadas y se ignoran `None` y `False`, así que puedes escribir `cond and widget`. | | `columns` | `None` | Número de columnas iguales, o pistas: una lista (`[200, "1fr", "auto"]`) o un texto (`"200 1fr auto"`). | | `rows` | `None` | Pistas de las filas, como `columns`. Las filas que no están en la lista son `"auto"`. | | `max_width` | `None` | Mete tantas columnas de como mucho este ancho como quepan (solo con `columns=`). | | `spacing` | `0.0` | Espacio entre celdas. | | `row_spacing` | `None` | Espacio entre filas, si es distinto. | | `height` | `None` | Con `columns=`: alto de cada fila. Con pistas: alto de la rejilla. | | `aspect_ratio` | `None` | Mantiene las celdas en esta proporción ancho/alto (solo con `columns=`). | | `width` | `None` | Con `columns=`: píxeles. Con pistas: ancho de la rejilla (por defecto `"fill"`). | | `padding` | `None` | Espacio alrededor de las celdas. | | `align_x` | `None` | Alineación horizontal de cada elemento en su celda: `"start"`, `"center"`, `"end"`. | | `align_y` | `None` | Alineación vertical de cada elemento en su celda. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#grid) ##### `pz.cell` ```python cell( content, *, column=None, row=None, column_span=1, row_span=1, align_x=None, align_y=None, ) ``` Coloca un hijo en una rejilla con pistas: su columna y fila (desde 0) y cuántas ocupa. Los hijos sin celda llenan las celdas libres fila a fila. Fuera de una rejilla es solo el hijo. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | El widget. | | `column` | `None` | Índice de columna (desde 0). | | `row` | `None` | Índice de fila (desde 0). | | `column_span` | `1` | Columnas que ocupa. | | `row_span` | `1` | Filas que ocupa. | | `align_x` | `None` | Alineación horizontal en la celda. | | `align_y` | `None` | Alineación vertical en la celda. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#cell) #### Relacionados - [Flex](https://albakore.github.io/pyzed/es/library/flex/): Flexbox de CSS: justify, align, gap, wrap y grow. - [Fila](https://albakore.github.io/pyzed/es/library/row/): Coloca los hijos en horizontal. - [Columna](https://albakore.github.io/pyzed/es/library/column/): Apila los hijos en vertical. ### Deslizador > Elige un número de un rango arrastrando. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/slider/ :::tip[En pocas palabras] Una barra con un botón que arrastras para elegir una cantidad, como el control del volumen. ::: Un deslizador con los colores del tema entre `min` y `max` (de 0 a 1 por defecto) que avanza de `step` en `step`. `on_change` recibe el nuevo valor mientras arrastras. `ui.slider` · Categoría: [Formularios](https://albakore.github.io/pyzed/es/library/#formularios) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#slider) **Código:** ```python pz.column([ ui.slider(0.35, on_change), ui.slider(70, on_change, min=0, max=100, step=1), ], spacing=18, width=360) ``` #### Cuándo usarlo - Para valores en los que importa más la sensación que el número exacto: volumen, opacidad, zoom. - Cuando haga falta escribir un valor exacto, usa un [Campo numérico](https://albakore.github.io/pyzed/es/library/number-field/). #### Uso ```python import pyzed as pz from pyzed import ui class Player(pz.App): def __init__(self): self.volume = 70 def view(self): return pz.row([ ui.slider(self.volume, lambda v: setattr(self, "volume", v), min=0, max=100, step=1), ui.label(f"{self.volume:.0f}%"), ], spacing=12, align="center") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Valor ```python ui.slider(0.0, on_change, width=240) ``` ```python ui.slider(0.35, on_change, width=240) ``` ```python ui.slider(1.0, on_change, width=240) ``` ##### Rango y paso ```python ui.slider(70, on_change, min=0, max=100, step=1, width=240) ``` ```python pz.column([ui.slider(40, on_change, min=0, max=100, step=10, width=240), ui.muted("40 %")], spacing=6, align="center") ``` ##### Estados y eventos `on_release` se ejecuta al terminar de arrastrar: guarda o aplica ahí en lugar de en cada movimiento. ```python ui.slider(70, on_change, min=0, max=100, step=1, show_value=True, width=240) ``` ```python ui.slider(0.35, on_change, show_value="{:.0%}", width=240) ``` ```python ui.slider(6, on_change, min=0, max=10, step=1, vertical=True, height=110, show_value=True) ``` ```python ui.slider(0.6, on_change, disabled=True, width=240) ``` ##### Widget base El widget `pz.*` de debajo: sin tema, todos los colores y tamaños son tuyos. ```python pz.slider(0, 100, 60, on_change=on_change, width=240, accent="#e5c07b", knob_color="#ffffff") ``` ```python pz.vertical_slider(0, 100, 60, on_change=on_change, height=120, accent="#56b6c2") ``` #### Eventos Deslizador tiene **2 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_change` | `ui.slider`, `pz.slider` | `(value)` | Callable que recibe el nuevo valor. | | `on_release` | `ui.slider`, `pz.slider` | `()` | Se envía al terminar de arrastrar (guarda o aplica ahí). | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Player(pz.App): def __init__(self): self.volume = 70 self.saved = 70 def view(self): return pz.row([ ui.slider(self.volume, lambda v: setattr(self, "volume", v), # on_change: while dragging min=0, max=100, step=1, on_release=lambda: setattr(self, "saved", self.volume)), # on_release: once ui.label(f"{self.volume:.0f}% (saved {self.saved:.0f}%)"), ], spacing=12, align="center", width=420) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Volume: value: float class Player(pz.App): volume = 70 saved = 70 def update(self, message): match message: case Volume(value): self.volume = value case "save": self.saved = self.volume def view(self): return ui.slider(self.volume, Volume, min=0, max=100, step=1, on_release="save") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: volume: float = 70 def view(state, set_state): return pz.row([ ui.slider(state.volume, lambda v: set_state(volume=v), min=0, max=100, step=1), ui.label(f"{state.volume:.0f}%"), ], spacing=12, align="center", width=420) ``` #### Consejos :::tip Al pulsarlo o llegar con Tab tiene el foco del teclado: las flechas lo mueven un `step`, con Shift (o Re Pág/Av Pág) diez, e Inicio/Fin saltan a `min`/`max`. ::: #### Propiedades y API ##### `ui.slider` ```python slider( value, on_change=None, *, min=0.0, max=1.0, step=0.01, on_release=None, vertical=False, width=None, height=None, disabled=False, show_value=False, id=None, theme=None, ) ``` Un deslizador. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | *obligatorio* | Valor actual. | | `min` | `0.0` | Mínimo. | | `max` | `1.0` | Máximo. | | `step` | `0.01` | Paso. | | `vertical` | `False` | Lo pone de pie. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `disabled` | `False` | Lo atenúa. | | `show_value` | `False` | Muestra el número al lado: `True`, o un formato como `"{:.0%}"`. | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_change`, `on_release` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#slider) ##### `pz.slider` ```python slider( min, max, value, *, step=1.0, on_change=None, on_release=None, width=None, height=None, vertical=False, accent=None, bg=None, knob_color=None, border_color=None, ) ``` Un deslizador entre `min` y `max`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `min` | *obligatorio* | Mínimo. | | `max` | *obligatorio* | Máximo. | | `value` | *obligatorio* | Valor actual. | | `step` | `1.0` | Paso. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `vertical` | `False` | Orientación vertical. | | `accent` | `None` | Color de acento (parte activa). | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `knob_color` | `None` | Color del tirador. | | `border_color` | `None` | Color del borde. | Eventos: `on_change`, `on_release` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#slider) ##### `pz.vertical_slider` ```python vertical_slider(min, max, value, **kwargs) ``` Un deslizador vertical (`slider(..., vertical=True)`). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `min` | *obligatorio* | Mínimo. | | `max` | *obligatorio* | Máximo. | | `value` | *obligatorio* | Valor actual. | | `**kwargs` | — | Los de `slider`. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#vertical_slider) #### Relacionados - [Campo numérico](https://albakore.github.io/pyzed/es/library/number-field/): Un número con botones −/+, límites, paso y sufijo. - [Progreso](https://albakore.github.io/pyzed/es/library/progress/): Una barra de progreso fina en un color semántico. ### Desplazable > Desplaza el contenido que no cabe. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/scrollable/ :::tip[En pocas palabras] Te deja desplazar el contenido que no cabe, con una barra de desplazamiento. ::: Hace que su contenido se pueda desplazar en vertical, en horizontal o en `"both"`. Dale un `id` para desplazarlo desde código (`pz.Command.scroll_to`), `anchor_bottom=True` para quedarse al final (chats, registros), y personaliza el carril y la barra con el tema o tus colores. `pz.scrollable` · Categoría: [Disposición](https://albakore.github.io/pyzed/es/library/#disposición) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#scrollable) **Código:** ```python pz.container(pz.scrollable( pz.column([ui.list_item(f"Item {i}", on_open, icon="file", inset=True) for i in range(1, 30)]), height=220, ), width=300) ``` #### Cuándo usarlo - Para listas largas, documentos y cualquier contenido más alto que su espacio. #### Uso ```python import pyzed as pz from pyzed import ui pz.container(pz.scrollable( pz.column([ui.list_item(f"Item {i}", on_open, icon="file", inset=True) for i in range(1, 30)]), height=220, ), width=300) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Dirección ```python pz.container(pz.scrollable(pz.column([ui.list_item(f"Item {i}", icon="file", inset=True) for i in range(1, 30)]), height=130), width=240) ``` ```python pz.container(pz.scrollable(pz.container(pz.row([ui.chip(f"tag {i}") for i in range(1, 25)], spacing=6), padding=(0, 0, 14, 0)), direction="horizontal"), width=300) ``` #### Eventos Desplazable tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_scroll` | `(info)` | Callable que recibe un dict con `x`, `y`, `relative_x`, `relative_y`, `width`, `height`, `content_width` y `content_height`. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Feed(pz.App): def __init__(self): self.items = [f"Post {i}" for i in range(1, 30)] self.at_end = False def scrolled(self, info): # on_scroll: x, y, relative_x, relative_y, sizes self.at_end = info["relative_y"] > 0.95 def view(self): return pz.column([ pz.scrollable(pz.column([ui.list_item(p, icon="file") for p in self.items]), height=200, on_scroll=self.scrolled), ui.muted("You reached the end" if self.at_end else "Scroll down"), ], spacing=6, width=300) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Scrolled: info: dict class Feed(pz.App): items = [f"Post {i}" for i in range(1, 30)] def update(self, message): match message: case Scrolled(info) if info["relative_y"] > 0.95: self.items = self.items + [f"Post {len(self.items) + 1}"] # load more def view(self): return pz.scrollable(pz.column([ui.list_item(p) for p in self.items]), height=200, on_scroll=Scrolled) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: progress: float = 0.0 def view(state, set_state): return pz.column([ ui.progress(state.progress), pz.scrollable(pz.column([ui.list_item(f"Post {i}") for i in range(30)]), height=200, on_scroll=lambda info: set_state(progress=info["relative_y"])), ], spacing=6, width=300) ``` #### Propiedades y API ##### `pz.scrollable` ```python scrollable( content, *, direction='vertical', width=None, height=None, padding=None, id=None, anchor_bottom=False, rail=None, scroller=None, scroller_hover=None, scrollbar_width=8.0, scroller_width=None, margin=0.0, on_scroll=None, ) ``` Hace desplazable su contenido. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `direction` | `"vertical"` | `"vertical"`, `"horizontal"` o `"both"`. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `padding` | `None` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `anchor_bottom` | `False` | Mantiene el scroll pegado al final (logs, terminales, chats). | | `rail` | `None` | Color del carril de la barra. | | `scroller` | `None` | Color del tirador. | | `scroller_hover` | `None` | Color del tirador al pasar el ratón. | | `scrollbar_width` | `8.0` | Ancho de la barra. | | `scroller_width` | `None` | Ancho del tirador (por defecto, el de la barra). | | `margin` | `0.0` | Margen entre la barra y el contenido. | Eventos: `on_scroll` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#scrollable) #### Relacionados - [Columna](https://albakore.github.io/pyzed/es/library/column/): Apila los hijos en vertical. - [Fila de lista](https://albakore.github.io/pyzed/es/library/list-item/): Una fila de lista con icono, texto y un hueco al final. ### Desplegable > Una sección plegable con flecha. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/disclosure/ :::tip[En pocas palabras] Una sección con una flechita que muestra u oculta detalles extra. ::: Una cabecera con una flecha que muestra u oculta `content`. El estado abierto es tuyo: cámbialo en `on_toggle`. `ui.disclosure` · Categoría: [Listas y menús](https://albakore.github.io/pyzed/es/library/#listas-y-menús) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#disclosure) **Código:** ```python pz.column([ ui.disclosure("Advanced options", ui.muted("Shown while the section is open."), open=True, on_toggle=on_toggle), ui.disclosure("Environment variables", ui.muted("Hidden"), open=False, on_toggle=on_toggle, end=ui.badge("3")), ], spacing=10, width=380) ``` #### Cuándo usarlo - Para esconder opciones avanzadas o detalles hasta que hagan falta. #### Uso ```python import pyzed as pz from pyzed import ui class Form(pz.App): def __init__(self): self.advanced = False def view(self): return ui.disclosure("Advanced options", self.advanced_options(), open=self.advanced, on_toggle=lambda: setattr(self, "advanced", not self.advanced)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Estado ```python pz.container(ui.disclosure("Advanced options", ui.muted("…"), open=False, on_toggle=on_toggle), width=300) ``` ```python pz.container(ui.disclosure("Advanced options", ui.muted("Shown while open."), open=True, on_toggle=on_toggle), width=300) ``` ```python pz.container(ui.disclosure("Environment", ui.muted("…"), open=False, on_toggle=on_toggle, end=ui.badge("3")), width=300) ``` #### Eventos Desplegable tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_toggle` | `()` | Mensaje o callable al pulsar la cabecera. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Form(pz.App): def __init__(self): self.advanced = False def view(self): return ui.disclosure("Advanced options", ui.muted("Proxy, timeouts…"), open=self.advanced, on_toggle=lambda: setattr(self, "advanced", not self.advanced)) # no argument ``` **Declarativo:** ```python class Form(pz.App): advanced = False def update(self, message): if message == "toggle advanced": self.advanced = not self.advanced def view(self): return ui.disclosure("Advanced options", ui.muted("…"), open=self.advanced, on_toggle="toggle advanced") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: advanced: bool = False def view(state, set_state): return ui.disclosure("Advanced options", ui.muted("…"), open=state.advanced, on_toggle=lambda: set_state(advanced=not state.advanced)) ``` #### Propiedades y API ##### `ui.disclosure` ```python disclosure(title, content, *, open, on_toggle=None, end=None, theme=None) ``` Una sección plegable: cabecera con flecha que muestra u oculta `content`. El estado abierto es tuyo: cámbialo en `on_toggle`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | *obligatorio* | Título (texto o widget). | | `content` | *obligatorio* | Contenido. | | `open` | *obligatorio* | Si está abierta. | | `end` | `None` | Widgets al final (derecha). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_toggle` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#disclosure) #### Relacionados - [Acordeón](https://albakore.github.io/pyzed/es/library/accordion/): Secciones plegables apiladas, como los paneles de una pantalla de ajustes. - [Tarjeta](https://albakore.github.io/pyzed/es/library/card/): Una superficie con borde, título y acciones opcionales. ### Diálogo de error > Qué salió mal, dónde y el traceback, con Copiar e Ir al origen. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/error-dialog/ :::tip[En pocas palabras] Una ventana que aparece cuando algo falla, diciendo qué salió mal y dónde, con un botón para copiarlo en un informe de error. ::: `ui.error_dialog(info, on_close)` muestra un error en un diálogo: su mensaje, dónde pasó, el archivo y la línea de donde viene y el traceback en un editor de solo lectura, donde se puede seleccionar y copiar cualquier parte. Sus botones copian el informe entero, van al origen (`on_go_to`, con el texto `go_to_label`), lo muestran entre los logs (`on_logs`) y cierran. `more` añade una línea que dice cuántos errores más llegaron después. `info` es un `ui.ErrorInfo`; muestra el diálogo con una [Capa de diálogo](https://albakore.github.io/pyzed/es/library/dialog-layer/). `ui.error_dialog` · Categoría: [Diálogos](https://albakore.github.io/pyzed/es/library/#diálogos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#error_dialog) **Código:** ```python def average(values): return sum(values) / len(values) try: average([]) except ZeroDivisionError as err: info = ui.ErrorInfo.from_exception(err, where="Totals") ui.dialog_layer(main_view(), ui.error_dialog(info, on_close, on_go_to=on_go_to, on_logs=on_logs, width=620)) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Cuando algo que pidió el usuario falla (guardar, exportar, ejecutar un script) y los detalles importan. - Desde el botón Detalles de un [Límite de errores](https://albakore.github.io/pyzed/es/library/error-boundary/). - Para un mensaje corto sin traceback, usa una [Alerta](https://albakore.github.io/pyzed/es/library/alert/) con `kind="error"`. #### Uso ```python import pyzed as pz from pyzed import ui class Notes(pz.App): def __init__(self): self.error = None def save(self): try: write_notes(self.notes) except OSError as err: self.error = ui.ErrorInfo.from_exception(err, where="Save") def close_error(self): self.error = None def view(self): dialog = ui.error_dialog(self.error, self.close_error) if self.error else None return ui.dialog_layer(self.page(), dialog, on_dismiss=self.close_error) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones Sin traceback el diálogo no muestra la caja de código, como con un `ErrorInfo` que construyes tú. ```python def average(values): return sum(values) / len(values) try: average([]) except ZeroDivisionError as err: info = ui.ErrorInfo.from_exception(err, where="Totals") ui.error_dialog(info, on_close, width=480) ``` ```python def average(values): return sum(values) / len(values) try: average([]) except ZeroDivisionError as err: info = ui.ErrorInfo.from_exception(err, where="Totals") ui.error_dialog(info, on_close, on_go_to=on_go_to, go_to_label="Open app.py", width=480) ``` ```python def average(values): return sum(values) / len(values) try: average([]) except ZeroDivisionError as err: info = ui.ErrorInfo.from_exception(err, where="Totals") ui.error_dialog(info, on_close, on_logs=on_logs, width=480) ``` ```python def average(values): return sum(values) / len(values) try: average([]) except ZeroDivisionError as err: info = ui.ErrorInfo.from_exception(err, where="Totals") ui.error_dialog(info, on_close, more=2, width=480) ``` **} alt={** ```python def average(values): return sum(values) / len(values) try: average([]) except ZeroDivisionError as err: info = ui.ErrorInfo.from_exception(err, where="Totals") ui.error_dialog(info, on_close, title="The totals could not be computed", width=480) ``` ```python info = ui.ErrorInfo("PermissionError", "[Errno 13] Permission denied: 'notes.md'", where="Save") ui.error_dialog(info, on_close, width=480) ``` #### Eventos Diálogo de error tiene **3 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_close` | `()` | Botón Cerrar y Escape. | | `on_go_to` | `()` | Añade el botón de ir al origen (abre el archivo en la línea). | | `on_logs` | `()` | Añade el botón Ver en Logs. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Notes(pz.App): def __init__(self): try: open("/no/such/file") except OSError as err: self.error = ui.ErrorInfo.from_exception(err, where="Open") def close(self): # on_close: the button and Escape self.error = None def view(self): dialog = ui.error_dialog(self.error, self.close, on_go_to=lambda: print("go to", self.error.origin), # adds Go to on_logs=lambda: print("show logs")) if self.error else None return ui.dialog_layer(main_view(), dialog, on_dismiss=self.close) ``` **Declarativo:** ```python class Notes(pz.App): error = ui.ErrorInfo("FileNotFoundError", "No such file: notes.txt") def update(self, message): match message: case "close": self.error = None case "logs": print("show logs") def view(self): dialog = ui.error_dialog(self.error, "close", on_logs="logs") if self.error else None return ui.dialog_layer(main_view(), dialog, on_dismiss="close") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: error: object = ui.ErrorInfo("FileNotFoundError", "No such file: notes.txt") def view(state, set_state): close = lambda: set_state(error=None) dialog = ui.error_dialog(state.error, close) if state.error else None return ui.dialog_layer(main_view(), dialog, on_dismiss=close) ``` #### Consejos :::tip `on_go_to` no recibe argumentos: toma el archivo y la línea de `info.origin` (un par `(ruta, línea)`, o `None`) y ábrelo ahí. La caja del traceback es un editor de solo lectura, así que se puede seleccionar una sola línea y copiarla, mientras que Copiar el error copia `info.report()`. ::: #### Propiedades y API ##### `ui.error_dialog` ```python error_dialog( info, on_close, *, title=None, on_go_to=None, go_to_label=None, on_logs=None, more=0, width=680, theme=None, ) ``` Un diálogo con un error: su mensaje, dónde pasó, el archivo y la línea de los que viene y el traceback en un editor de solo lectura (selecciona y copia cualquier parte), con botones para copiarlo todo, ir al origen, verlo en los logs y cerrar. Muéstralo con `ui.dialog_layer` o `ui.layer`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `info` | *obligatorio* | Un `ui.ErrorInfo`. | | `title` | `None` | Título ("Algo salió mal"). | | `go_to_label` | `None` | Su texto ("Ir al origen"). | | `more` | `0` | Cuántos errores más llegaron después (una línea lo dice). | | `width` | `680` | Ancho en píxeles o un tamaño de modal. | | `theme` | `None` | Tema a usar. | Eventos: `on_close`, `on_go_to`, `on_logs` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#error_dialog) #### Relacionados - [Límite de errores](https://albakore.github.io/pyzed/es/library/error-boundary/): Dibuja una parte de la vista, o una tarjeta de error en su lugar cuando falla. - [Alerta](https://albakore.github.io/pyzed/es/library/alert/): Un mensaje con un botón. - [Capa de diálogo](https://albakore.github.io/pyzed/es/library/dialog-layer/): Centra un diálogo encima de tu vista con el fondo oscurecido. - [Modal](https://albakore.github.io/pyzed/es/library/modal/): Una superficie de diálogo con título, contenido y botones al pie. ### Disposición anclada > Posiciones libres como en un diseñador de formularios, con anclas que siguen al redimensionar. Componente de Pyzed para GUIs en Python. Source: https://albakore.github.io/pyzed/es/library/anchor-layout/ :::tip[En pocas palabras] Coloca las cosas justo donde las pones y dice a qué esquina se pega cada una cuando la ventana crece. ::: Coloca los hijos en posiciones exactas, como un diseñador visual de formularios, y dice cómo sigue cada uno un cambio de tamaño: `pz.anchor(child, x=, y=, width=, height=, anchor=)` mantiene la distancia a los bordes a los que está anclado, se estira entre bordes opuestos y si no se queda centrado. Presets como `"top_right"`, `"bottom_wide"`, `"center"` o `"full"` cubren los casos habituales. `pyzed.anchors.place` es la cuenta que hay detrás, para tus propias disposiciones. `pz.anchor_layout` · Categoría: [Disposición](https://albakore.github.io/pyzed/es/library/#disposición) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#anchor_layout) **Código:** ```python t = pz.theme.current() pz.container(pz.anchor_layout([ pz.anchor(ui.heading("Report", level=3), x=16, y=12, width=200, height=28, anchor="top_left"), pz.anchor(ui.icon_button("x", on_close), x=356, y=12, width=28, height=28, anchor="top_right"), pz.anchor(ui.input("", on_input, placeholder="Search…"), x=16, y=56, width=368, height=30, anchor="top_wide"), pz.anchor(ui.button("Close", on_close, variant="solid"), x=284, y=172, width=100, height=30, anchor="bottom_right"), ], design=(400, 216)), width=400, height=216, bg=t.panel, border_color=t.border, border_width=1, radius=8) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para formularios dibujados en Pyzed Design (es lo que genera la vista de Formulario) o traídos de Visual Basic, Qt Designer o WinForms. - Para HUDs de juegos y capas en las que las cosas se pegan a las esquinas. - Para formularios que crecen y encogen con la ventana, mejor `row`, `column` y `grid`. #### Uso ```python import pyzed as pz from pyzed import ui class Report(pz.App): def view(self): return pz.anchor_layout([ pz.anchor(ui.heading("Report"), x=16, y=12, width=368, height=28, anchor="top_wide"), pz.anchor(self.table(), x=16, y=48, width=368, height=196, anchor="full"), pz.anchor(ui.button("Close", self.close), x=284, y=252, width=100, height=32, anchor="bottom_right"), ], design=(400, 300)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Presets Cada disposición se diseñó a 110 × 44 y se dibuja a 260 × 110: la insignia mantiene su distancia a los bordes a los que está anclada. ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(ui.badge("top_left"), x=10, y=10, width=90, height=24, anchor="top_left")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(ui.badge("top_right"), x=10, y=10, width=90, height=24, anchor="top_right")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(ui.badge("bottom_left"), x=10, y=10, width=90, height=24, anchor="bottom_left")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(ui.badge("bottom_right"), x=10, y=10, width=90, height=24, anchor="bottom_right")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(ui.badge("center"), x=10, y=10, width=90, height=24, anchor="center")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ##### Estirar ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(pz.container(width="fill", height="fill", bg=t.accent, radius=4), x=10, y=10, width=90, height=24, anchor="top_wide")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(pz.container(width="fill", height="fill", bg=t.accent, radius=4), x=10, y=10, width=90, height=24, anchor="bottom_wide")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(pz.container(width="fill", height="fill", bg=t.accent, radius=4), x=10, y=10, width=90, height=24, anchor="left_tall")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(pz.container(width="fill", height="fill", bg=t.accent, radius=4), x=10, y=10, width=90, height=24, anchor="center_wide")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` ```python t = pz.theme.current() pz.container(pz.anchor_layout([pz.anchor(pz.container(width="fill", height="fill", bg=t.accent, radius=4), x=10, y=10, width=90, height=24, anchor="full")], design=(110, 44)), width=260, height=110, border_color=t.border, border_width=1, radius=8) ``` #### Eventos Disposición anclada no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Consejos :::tip `design` es el tamaño en el que se midieron las posiciones; cuando la disposición tiene más o menos espacio, cada hijo se mueve o se estira desde ahí. Pruébalo en vivo: arrastra la esquina de un formulario en Pyzed Design. ::: #### Propiedades y API ##### `pz.anchor_layout` ```python anchor_layout(children, *, design=None, width='fill', height='fill') ``` Coloca libremente hijos hechos con `pz.anchor`, como un formulario diseñado: al cambiar de tamaño, cada hijo mantiene su distancia a los bordes a los que está anclado, se estira entre bordes opuestos y se queda centrado en los demás casos. Los últimos hijos se dibujan encima. Ver [Disposiciones ancladas](https://albakore.github.io/pyzed/es/guides/layout/#disposiciones-ancladas). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `children` | *obligatorio* | Hijos hechos con `pz.anchor(...)` (`None` se ignora). | | `design` | `None` | `(ancho, alto)` en el que se colocaron los hijos; por defecto, el más pequeño que los contiene. | | `width` | `"fill"` | Ancho. | | `height` | `"fill"` | Alto. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#anchor_layout) ##### `pz.anchor` ```python anchor( content, *, x=0, y=0, width=100, height=28, anchor='top_left', min_width=0, min_height=0, max_width=0, max_height=0, pivot=(0.5, 0.5), rotate=0, ) ``` Coloca un hijo de `pz.anchor_layout` en `x`, `y` con `width` y `height` (en el tamaño de diseño de la disposición) y dice cómo sigue a un cambio de tamaño. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | El widget. | | `x` | `0` | Borde izquierdo en el tamaño de diseño. | | `y` | `0` | Borde superior en el tamaño de diseño. | | `width` | `100` | Ancho en el tamaño de diseño. | | `height` | `28` | Alto en el tamaño de diseño. | | `anchor` | `"top_left"` | Un preset de `pyzed.anchors.PRESETS` (`"top_right"`, `"bottom_wide"`, `"center"`, `"full"`...), palabras (`"left right bottom"`) o una tupla de bordes. | | `min_width` | `0` | Ancho mínimo al estirarse (`0`: sin límite). | | `min_height` | `0` | Alto mínimo al estirarse. | | `max_width` | `0` | Ancho máximo al estirarse (`0`: sin límite). | | `max_height` | `0` | Alto máximo al estirarse. | | `pivot` | `(0.5, 0.5)` | `(x, y)` de `0` a `1`: el punto que queda fijo cuando un límite lo frena, y alrededor del que gira `rotate`. | | `rotate` | `0` | Grados en sentido horario. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#anchor) ##### `pz.anchors.place` ```python place( rect, anchor, design, actual, *, min_size=(0, 0), max_size=(0, 0), pivot=(0.5, 0.5), ) ``` Dónde va una caja diseñada en un padre de tamaño `design` dentro de un padre de tamaño `actual`, siguiendo su anclaje, con límites de tamaño y un pivote. La cuenta detrás de `pz.anchor_layout` y de los formularios diseñados. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `rect` | *obligatorio* | `(x, y, ancho, alto)` en el tamaño de diseño. | | `anchor` | *obligatorio* | Un preset, palabras o bordes. | | `design` | *obligatorio* | El tamaño de diseño del padre. | | `actual` | *obligatorio* | El tamaño actual del padre. | | `min_size` | `(0, 0)` | Límites inferiores `(ancho, alto)` (`0`: ninguno). | | `max_size` | `(0, 0)` | Límites superiores `(ancho, alto)` (`0`: ninguno). | | `pivot` | `(0.5, 0.5)` | El punto que queda fijo cuando un límite frena el estiramiento. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#place) #### Relacionados - [Pila](https://albakore.github.io/pyzed/es/library/stack/): Superpone los hijos; `pin` coloca uno en x, y. - [Cuadrícula](https://albakore.github.io/pyzed/es/library/grid/): Columnas iguales, o pistas como en CSS con expansión. - [Contenedor](https://albakore.github.io/pyzed/es/library/container/): Tamaño, relleno, alineación, fondo, borde y sombra para un hijo. ### División > Dos paneles con un divisor que se arrastra. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/split/ :::tip[En pocas palabras] Dos zonas lado a lado (o una encima de otra) con una línea que arrastras para cambiar su tamaño. ::: Separa dos widgets con un divisor que se arrastra, en el color de borde del tema (acento mientras se arrastra). El `ratio` es tuyo: guárdalo en `on_resize(ratio)`. `vertical=True` apila los paneles. `pz.split` es la versión base con `color`, `hover_color`, `thickness` y `handle` configurables. `ui.split` · Categoría: [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/#espacio-de-trabajo) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#split) **Código:** ```python t = pz.theme.current() pane = lambda title, bg: pz.container(ui.label(title), padding=12, width="fill", height="fill", bg=bg) ui.split(pane("Left", t.panel), ui.split(pane("Top", t.editor), pane("Bottom", t.background), vertical=True), ratio=0.4, on_resize=on_resize) ``` #### Cuándo usarlo - Para editores lado a lado, una vista previa junto a su código o un editor encima de una terminal. - Para un solo panel lateral redimensionable, [Redimensionable](https://albakore.github.io/pyzed/es/library/resizable/) es más simple. #### Uso ```python import pyzed as pz from pyzed import ui class Panes(pz.App): def __init__(self): self.ratio = 0.3 def view(self): return ui.split(self.sidebar(), self.editor(), ratio=self.ratio, on_resize=lambda r: setattr(self, "ratio", r)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Dirección ```python t = pz.theme.current() ui.split(pz.container(ui.label("A"), padding=10, width="fill", height="fill", bg=t.panel), pz.container(ui.label("B"), padding=10, width="fill", height="fill"), on_resize=on_resize) ``` ```python t = pz.theme.current() ui.split(pz.container(ui.label("Top"), padding=10, width="fill", height="fill", bg=t.panel), pz.container(ui.label("Bottom"), padding=10, width="fill", height="fill"), vertical=True) ``` ```python t = pz.theme.current() ui.split(pz.container(ui.label("25%"), padding=10, width="fill", height="fill", bg=t.panel), pz.container(ui.label("75%"), padding=10, width="fill", height="fill"), ratio=0.25) ``` #### Eventos División tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_resize` | `ui.split`, `pz.split` | `(ratio)` | Callable que recibe la nueva proporción. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Panes(pz.App): def __init__(self): self.ratio = 0.3 def view(self): return ui.split(ui.muted(f"Sidebar ({self.ratio:.0%})"), main_view(), ratio=self.ratio, on_resize=lambda r: setattr(self, "ratio", r)) # the share of the first pane ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Ratio: value: float class Panes(pz.App): ratio = 0.3 def update(self, message): match message: case Ratio(value): self.ratio = value def view(self): return ui.split(ui.muted("Sidebar"), main_view(), ratio=self.ratio, on_resize=Ratio) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: ratio: float = 0.3 def view(state, set_state): return ui.split(ui.muted("Sidebar"), main_view(), ratio=state.ratio, on_resize=lambda r: set_state(ratio=r)) ``` #### Propiedades y API ##### `ui.split` ```python split( first, second, *, ratio=0.5, on_resize=None, vertical=False, min_size=80, theme=None, **kwargs, ) ``` `pz.split` con el divisor del color de borde del tema (acento al arrastrarlo). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `first` | *obligatorio* | Primer panel. | | `second` | *obligatorio* | Segundo panel. | | `ratio` | `0.5` | Proporción del primero (0–1). | | `vertical` | `False` | Orientación vertical. | | `min_size` | `80` | Tamaño mínimo de cada panel. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | | `**kwargs` | — | Los de `pz.split`. | Eventos: `on_resize` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#split) ##### `pz.split` ```python split( first, second, *, ratio=0.5, on_resize=None, vertical=False, min_size=80.0, min_size_second=None, color=None, hover_color=None, thickness=1.0, handle=8.0, width=None, height=None, ) ``` Dos paneles con un divisor que se arrastra. Para el estilo del tema usa `ui.split`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `first` | *obligatorio* | Primer panel. | | `second` | *obligatorio* | Segundo panel. | | `ratio` | `0.5` | Proporción del primer panel (0–1). El divisor conserva la proporción a la que lo arrastra el usuario hasta que esto cambia. | | `vertical` | `False` | Apila los paneles arriba/abajo. | | `min_size` | `80.0` | Tamaño mínimo del primer panel. | | `min_size_second` | `None` | Tamaño mínimo del segundo panel. | | `color` | `None` | Color del divisor. | | `hover_color` | `None` | Color del divisor al pasar el ratón o arrastrar. | | `thickness` | `1.0` | Grosor de la línea. | | `handle` | `8.0` | Ancho de la zona que se puede arrastrar. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | Eventos: `on_resize` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#split) #### Relacionados - [Redimensionable](https://albakore.github.io/pyzed/es/library/resizable/): Un panel cuyo borde se arrastra para cambiar su tamaño. - [Panel](https://albakore.github.io/pyzed/es/library/panel/): Un panel lateral acoplable y redimensionable. - [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/workspace/): La disposición de una ventana estilo editor: barra de título, docks, centro y barra de estado. ### Divisor > Una línea fina en el color de borde del tema. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/divider/ :::tip[En pocas palabras] Una línea fina que separa grupos de cosas. ::: Una línea de un píxel en el color de borde del tema, horizontal por defecto (`vertical=True` para una vertical). `pz.rule` es la versión base con tu propio `color` y `thickness`. `ui.divider` · Categoría: [Visualización](https://albakore.github.io/pyzed/es/library/#visualización) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#divider) **Código:** ```python pz.column([ ui.label("Account"), ui.divider(), pz.row([ui.label("Edit"), ui.divider(vertical=True), ui.label("View"), ui.divider(vertical=True), ui.label("Help")], spacing=12, height=20), ui.divider(), ui.muted("Sign out"), ], spacing=10, width=300) ``` #### Cuándo usarlo - Para separar grupos en una columna o elementos en una barra. - Prueba primero con espacio; añade un divisor cuando los grupos necesiten un corte visual. #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ ui.label("Account"), ui.divider(), pz.row([ui.label("Edit"), ui.divider(vertical=True), ui.label("View"), ui.divider(vertical=True), ui.label("Help")], spacing=12, height=20), ui.divider(), ui.muted("Sign out"), ], spacing=10, width=300) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Dirección ```python pz.column([ui.label("Account"), ui.divider(), ui.label("Sign out")], spacing=8, width=220) ``` ```python pz.row([ui.label("Edit"), ui.divider(vertical=True), ui.label("View")], spacing=10, height=18) ``` ##### Color ```python pz.container(ui.divider(color="accent"), width=220) ``` ```python pz.container(pz.rule(color="#e5c07b", thickness=3), width=220) ``` ##### Estados y eventos ```python pz.container(ui.divider(label="or"), width=300) ``` ```python pz.column([ui.label("Above"), ui.divider(spacing=12), ui.label("Below")], width=300) ``` #### Eventos Divisor no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.divider` ```python divider(*, vertical=False, color=None, label=None, spacing=0, theme=None) ``` Una línea divisoria con el color del tema. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `vertical` | `False` | Vertical en lugar de horizontal. | | `color` | `None` | Color del texto o del icono. | | `label` | `None` | Texto atenuado en el centro de una línea horizontal (`"o"`, una fecha…). | | `spacing` | `0` | Espacio a ambos lados de la línea. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#divider) ##### `pz.rule` ```python rule(*, vertical=False, thickness=1.0, color=None) ``` Una línea divisoria horizontal (o vertical). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `vertical` | `False` | Línea vertical en lugar de horizontal. | | `thickness` | `1.0` | Grosor en píxeles. | | `color` | `None` | Color del texto o del icono. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#rule) #### Relacionados - [Tarjeta](https://albakore.github.io/pyzed/es/library/card/): Una superficie con borde, título y acciones opcionales. - [Barra de herramientas](https://albakore.github.io/pyzed/es/library/toolbar/): La barra bajo las pestañas: migas de pan y acciones. ### Dona > Un gráfico de dona con una etiqueta en el centro. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/donut/ :::tip[En pocas palabras] Un anillo dividido en porciones de colores que muestra las partes de un todo. ::: Un gráfico de dona con `values` y una `label` en el centro. `colors` reemplaza la paleta del tema. `ui.donut` · Categoría: [Datos](https://albakore.github.io/pyzed/es/library/#datos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#donut) **Código:** ```python pz.row([ ui.donut([60, 25, 15], label="60%"), ui.donut([45, 35, 20], colors=["#e5c07b", "#c678dd", "#56b6c2"], size=120, thickness=20, label="3"), ], spacing=32, align="center") ``` #### Cuándo usarlo - Para mostrar las partes de un todo (uso de disco, lenguajes de un proyecto) con tres o cuatro porciones. #### Uso ```python import pyzed as pz from pyzed import ui pz.row([ ui.donut([60, 25, 15], label="60%"), ui.donut([45, 35, 20], colors=["#e5c07b", "#c678dd", "#56b6c2"], size=120, thickness=20, label="3"), ], spacing=32, align="center") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python ui.donut([60, 25, 15], label="60%") ``` ```python ui.donut([45, 35, 20], colors=["#e5c07b", "#c678dd", "#56b6c2"], label="3") ``` ```python ui.donut([70, 30], thickness=6, label="70%") ``` ```python ui.donut([40, 30, 20, 10], size=140, thickness=24) ``` ```python ui.donut([82, 18], size=110, label="82%") ``` #### Eventos Dona no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.donut` ```python donut(values, *, colors=None, size=96, thickness=14, label=None, theme=None) ``` Un gráfico de anillo; `label` se dibuja en el centro. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `values` | *obligatorio* | Valores. | | `colors` | `None` | Colores (por defecto, los del tema). | | `size` | `96` | Tamaño. | | `thickness` | `14` | Grosor del anillo. | | `label` | `None` | Texto central. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#donut) #### Relacionados - [Gráfico de barras](https://albakore.github.io/pyzed/es/library/bar-chart/): Un gráfico de barras verticales con etiquetas. - [Cifra](https://albakore.github.io/pyzed/es/library/stat/): Una cifra de dashboard con variación y minigráfico. ### Editor > Un editor de código completo: resaltado, plegado, minimapa, marcas de git. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/editor/ :::tip[En pocas palabras] Una caja para escribir código, con colores, números de línea y todo lo que espera un programador. ::: Un editor de código con resaltado de sintaxis en los colores del tema (más de 100 lenguajes), números de línea, marcas de git, cierre automático de pares, plegado, minimapa y zoom de fuente. Es **no controlado**: el texto, el cursor y el historial viven en Rust, ligados al `id`, y `text` solo es el valor inicial. `on_change` recibe el texto completo y `on_cursor` la `(línea, columna)`. `pz.code_editor` es el widget base sin tema. `ui.editor` · Categoría: [Código y herramientas](https://albakore.github.io/pyzed/es/library/#código-y-herramientas) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#editor) **Código:** ```python code = '''import pyzed as pz from pyzed import ui class Counter(pz.App): def __init__(self): self.count = 0 def view(self): return ui.button(f"Clicks: {self.count}", self.increment) ''' pz.container(ui.editor("counter.py", code, path="counter.py", markers={7: "modified", 10: "created"}), width=620, height=290) ``` #### Cuándo usarlo - Para código, archivos de configuración, registros editables o cualquier texto largo. - Usa la ruta del archivo como `id`: cada archivo conserva su cursor e historial al cambiar de pestaña. - Para reemplazar el texto desde código, devuelve `pz.Command.set_editor_text(id, text)`. #### Uso ```python import pyzed as pz from pyzed import ui from pathlib import Path class Editor(pz.App): def __init__(self): self.path = "main.py" self.text = Path(self.path).read_text() def save(self): Path(self.path).write_text(self.text) def view(self): return ui.editor(self.path, self.text, path=self.path, on_change=lambda text: setattr(self, "text", text)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Lenguaje ```python pz.container(ui.editor("v-py", "class Counter(pz.App):\n count = 0\n\n def view(self):\n return ui.button('Add', self.add)", path="counter.py"), width=470, height=110) ``` ```python pz.container(ui.editor("v-rs", "fn main() {\n println!(\"hi\");\n}\n", language="rust"), width=420, height=90) ``` ```python pz.container(ui.editor("v-json", '{\n "name": "notes",\n "version": 2\n}\n', language="json"), width=420, height=110) ``` ##### Opciones ```python pz.container(ui.editor("v-g", "class Counter(pz.App):\n count = 0\n\n def view(self):\n return ui.button('Add', self.add)", path="a.py", gutter=False), width=470, height=110) ``` ```python pz.container(ui.editor("v-m", "class Counter(pz.App):\n count = 0\n\n def view(self):\n return ui.button('Add', self.add)", path="a.py", markers={2: "created", 5: "modified"}), width=470, height=110) ``` ```python pz.container(ui.editor("v-mm", "class Counter(pz.App):\n count = 0\n\n def view(self):\n return ui.button('Add', self.add)", path="a.py", minimap=True), width=470, height=124) ``` ```python pz.container(ui.editor("v-ws", "class Counter(pz.App):\n count = 0\n\n def view(self):\n return ui.button('Add', self.add)", path="a.py", show_whitespace=True), width=470, height=110) ``` ```python pz.container(ui.editor("v-ph", "", placeholder="Write something…"), width=420, height=60) ``` ```python pz.container(ui.editor("v-fs", "print(\"big\")\n", path="a.py", font_size=16), width=420, height=60) ``` #### Ejemplos ##### Plegado, code lens y minimapa Los bloques se pliegan por indentación (Ctrl+Shift+[ y ]), `annotations` añade un code lens encima de una línea y `minimap=True` dibuja la vista general a la derecha. **Código:** ```python code = '''import pyzed as pz from pyzed import ui class Counter(pz.App): def __init__(self): self.count = 0 def increment(self): self.count += 1 def view(self): return ui.button(f"Clicks: {self.count}", self.increment) ''' pz.container(ui.editor("lens.py", code, path="counter.py", minimap=True, annotations={5: "1 reference"}), width=680, height=290) ``` ##### Cursor y espacios `cursor_shape` es `"bar"`, `"block"`, `"underline"` o `"hollow"`; `show_whitespace=True` dibuja espacios y tabuladores; `wrap=True` parte las líneas largas. **Código:** ```python pz.container(ui.editor("ws.py", "def greet(name):\n return f'Hello, {name}'\n", path="ws.py", cursor_shape="block", show_whitespace=True, font_size=15), width=620, height=120) ``` #### Eventos Editor tiene **6 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_change` | `ui.editor`, `pz.code_editor` | `(text)` | Callable que recibe el nuevo valor. | | `on_cursor` | `ui.editor`, `pz.code_editor` | `(line, column)` | Callable que recibe `(línea, columna)`. | | `on_scroll` | `ui.editor`, `pz.code_editor` | `(info)` | Se llama al desplazar. | | `on_zoom` | `ui.editor`, `pz.code_editor` | `(step)` | Callable que recibe `1`, `-1` o `0` con Ctrl+= / Ctrl+- / Ctrl+0 y Ctrl+rueda. | | `on_action` | `ui.editor`, `pz.code_editor` | `(name)` | Callable que recibe el nombre de cada acción del editor que se ejecuta (`"undo"`, `"select_next"`…), por ejemplo para contarlas. | | `on_hover` | `ui.editor`, `pz.code_editor` | `()` | Función que recibe `{line, column, start, end, x, y, height}` (la palabra bajo el ratón, desde 1, y su lugar en el editor) cuando el ratón se detiene sobre ella, y `None` cuando se va (tarjetas de información). | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Editor(pz.App): def __init__(self): self.text = "print('hello')\n" self.cursor = (1, 1) self.font_size = 14 def zoom(self, step): # on_zoom: 1, -1 or 0 (Ctrl+=, Ctrl+-, Ctrl+0) self.font_size = 14 if step == 0 else self.font_size + step def view(self): line, column = self.cursor return pz.column([ ui.editor("main.py", self.text, path="main.py", font_size=self.font_size, height=200, on_change=lambda text: setattr(self, "text", text), # every edit on_cursor=lambda ln, col: setattr(self, "cursor", (ln, col)), # from 1 on_zoom=self.zoom, on_action=lambda name: print("ran", name)), # "undo"... ui.status_bar([], [ui.status_item(f"Ln {line}, Col {column}")]), ]) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Edited: text: str @dataclass class Moved: line: int column: int class Editor(pz.App): text, cursor = "print('hello')\n", (1, 1) def update(self, message): match message: case Edited(text): self.text = text case Moved(line, column): self.cursor = (line, column) def view(self): return ui.editor("main.py", self.text, path="main.py", height=200, on_change=Edited, on_cursor=Moved) # the classes build the messages ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: text: str = "print('hello')\n" line: int = 1 column: int = 1 def view(state, set_state): return pz.column([ ui.editor("main.py", state.text, path="main.py", height=200, on_change=lambda text: set_state(text=text), on_cursor=lambda line, column: set_state(line=line, column=column)), ui.label(f"Ln {state.line}, Col {state.column}"), ]) ``` #### Propiedades y API ##### `ui.editor` ```python editor( id, text='', *, language=None, path=None, on_change=None, on_cursor=None, markers=None, font_size=None, line_height=None, gutter=True, wrap=False, active_line=True, height='fill', placeholder=None, annotations=None, show_whitespace=False, tab_size=None, hard_tabs=False, backspace_unindent=True, repeat_delay=None, repeat_rate=None, capture_keys=(), on_scroll=None, auto_close=True, folding=True, minimap=False, cursor_shape='bar', cursor_blink=True, cursor_width=2.0, cursor_color=None, on_zoom=None, on_action=None, highlights=None, match_brackets=True, selection_highlight=True, font=None, keymap=None, scrollbar=True, readonly=False, on_hover=None, hover_delay=500, sticky_scroll=0, theme=None, ) ``` Un editor de código: resaltado con los colores del tema, números de línea y marcadores de git. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `id` | *obligatorio* | Identifica el buffer (usa la ruta del archivo). | | `text` | `""` | Texto inicial. | | `language` | `None` | Lenguaje. Si falta, se deduce de `path`. | | `path` | `None` | Ruta del archivo. | | `markers` | `None` | `{línea: "created" \| "modified" \| "deleted"}`. | | `font_size` | `None` | Tamaño de la fuente (por defecto, el del tema). | | `line_height` | `None` | Alto de línea. | | `gutter` | `True` | Muestra los números de línea. | | `wrap` | `False` | Ajuste de línea. | | `active_line` | `True` | Color de fondo de la línea del cursor. | | `height` | `"fill"` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `placeholder` | `None` | Texto de ayuda cuando está vacío. | | `annotations` | `None` | `{línea: "texto"}` o `{línea: ("texto", color)}`: texto virtual al final de la línea (code lens, diagnósticos). | | `show_whitespace` | `False` | Dibuja los espacios como `·` y los tabuladores como `→`. | | `tab_size` | `None` | Tab inserta espacios hasta el siguiente múltiplo; Enter mantiene la sangría (un nivel más tras `:`); Backspace quita un nivel. | | `hard_tabs` | `False` | Con `tab_size`: Tab, Enter y sangrar (Ctrl+]) usan tabulaciones en vez de espacios. | | `backspace_unindent` | `True` | Con `tab_size`: Backspace en los espacios iniciales quita un nivel entero (`False`: un espacio). | | `repeat_delay` | `None` | Milisegundos que esperan Backspace y Suprimir, mantenidos, antes de repetirse (`None`: los del sistema). | | `repeat_rate` | `None` | Borrados por segundo como máximo mientras se mantiene Backspace o Suprimir (`None`: el ritmo del sistema). | | `capture_keys` | `()` | Teclas que el editor deja al `on_key` de la app (`"up"`, `"enter"`…), por ejemplo con una lista de autocompletado abierta. | | `auto_close` | `True` | Cierra paréntesis, corchetes, llaves y comillas al escribir (y envuelve la selección). | | `folding` | `True` | Flechas de plegado en el margen; Ctrl+Shift+[ pliega y Ctrl+Shift+] despliega. | | `minimap` | `False` | `True` (o un ancho en píxeles) muestra un minimapa a la derecha. | | `cursor_shape` | `"bar"` | `"bar"`, `"block"`, `"underline"` o `"hollow"`. | | `cursor_blink` | `True` | Si el cursor parpadea. | | `cursor_width` | `2.0` | Ancho del cursor de barra. | | `cursor_color` | `None` | Color del cursor (por defecto, el color de jugador del tema). | | `highlights` | `None` | Rangos pintados detrás del texto con el color de búsqueda del tema: `[(línea, columna, línea_final, columna_final)]`, desde 1, con el final excluido. | | `match_brackets` | `True` | Remarca el corchete pareja del que está junto al cursor (Ctrl+M salta a él). | | `selection_highlight` | `True` | Resalta las demás apariciones del texto seleccionado (Ctrl+D añade la siguiente como cursor). | | `font` | `None` | Tipografía: `"mono"` por defecto; `"sans"` hace un cuadro de texto de varias líneas, como el cuadro de mensaje de un chat. | | `keymap` | `None` | Diccionario `{atajo: acción}` al estilo de Zed (`{"ctrl-shift-d": "duplicate_line_down"}`); `None` como acción quita una tecla por defecto. Créalo a partir de cambios por acción con `pyzed.ui.editor.keymap_for`. | | `scrollbar` | `True` | `False` oculta las barras de desplazamiento. | | `readonly` | `False` | `True` deja seleccionar y copiar el texto, pero no cambiarlo (un log, un traceback). | | `hover_delay` | `500` | Milisegundos que el ratón debe quedarse sobre una palabra antes de llamar a `on_hover`. | | `sticky_scroll` | `0` | Cuántas líneas de los bloques que rodean al código visible (`class`, `def`, `if`…) quedan fijas arriba al desplazarse (`0`: ninguna). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_change`, `on_cursor`, `on_scroll`, `on_zoom`, `on_action`, `on_hover` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#editor) ##### `ui.language_token` ```python language_token(path) ``` Devuelve el token de lenguaje del resaltador para una ruta (`"main.rs"` → `"rs"`). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `path` | *obligatorio* | Ruta del archivo. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#language_token) ##### `pz.code_editor` ```python code_editor( id, text='', *, language='txt', size=14.0, line_height=None, font=None, width=None, height=None, padding=None, syntax=None, bg=None, color=None, selection_color=None, placeholder=None, placeholder_color=None, gutter=True, gutter_color=None, gutter_active_color=None, gutter_width=None, markers=None, wrap=False, on_change=None, on_cursor=None, active_line=None, scrollbar_color=None, annotations=None, whitespace=None, tab_size=None, capture_keys=None, on_scroll=None, auto_close=False, folding=False, minimap=None, minimap_thumb=None, cursor_shape='bar', cursor_width=2.0, cursor_blink=True, cursor_color=None, on_zoom=None, on_action=None, highlights=None, highlight_color=None, match_brackets=True, selection_highlight=True, keymap=None, scrollbar=True, readonly=False, hard_tabs=False, backspace_unindent=True, repeat_delay=None, repeat_rate=None, on_hover=None, hover_delay=500.0, sticky_scroll=0, ) ``` Un editor de código con resaltado de sintaxis, números de línea y marcadores de git, basado en `text_editor` de Iced. Es **no controlado**: `text` solo es el valor inicial para ese `id`; el texto, el cursor y el historial viven en Rust. Usa `Command.set_editor_text` para reemplazarlo. Para el editor con tema usa [`ui.editor`](https://albakore.github.io/pyzed/es/components/code/). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `id` | *obligatorio* | Identifica el buffer. Reutilízalo para conservar el estado entre renders. | | `text` | `""` | Texto inicial. | | `language` | `"txt"` | Extensión o nombre del lenguaje (`"py"`, `"rs"`, `"json"`…). | | `size` | `14.0` | Tamaño de la fuente. | | `line_height` | `None` | Alto de línea en píxeles. | | `font` | `None` | Familia (por defecto, monoespaciada). | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `padding` | `None` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `syntax` | `None` | Mapa de scopes TextMate a colores (lo da `Theme.syntax_scopes()`). | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `color` | `None` | Color del texto o del icono. | | `selection_color` | `None` | Color de la selección. | | `placeholder` | `None` | Texto de ayuda cuando está vacío. | | `placeholder_color` | `None` | Color del placeholder. | | `gutter` | `True` | Muestra los números de línea. | | `gutter_color` | `None` | Color de los números de línea. | | `gutter_active_color` | `None` | Color del número de la línea del cursor. | | `gutter_width` | `None` | Ancho del margen (automático por defecto). | | `markers` | `None` | `{línea: color}` para las marcas de git del margen (líneas desde 1). | | `wrap` | `False` | Ajuste de línea. | | `active_line` | `None` | Color de fondo de la línea del cursor. | | `scrollbar_color` | `None` | Color de la barra de desplazamiento. | | `annotations` | `None` | `{línea: "texto"}` o `{línea: ("texto", color)}`: texto virtual al final de la línea (code lens, diagnósticos). | | `whitespace` | `None` | Dibuja los espacios como `·` y los tabuladores como `→` en este color. | | `tab_size` | `None` | Tab inserta espacios hasta el siguiente múltiplo; Enter mantiene la sangría; Backspace quita un nivel. | | `capture_keys` | `None` | Teclas que el editor deja al `on_key` de la app (`"up"`, `"enter"`…), por ejemplo con una lista de autocompletado abierta. | | `auto_close` | `False` | Al escribir un paréntesis, corchete, llave o comilla también inserta el de cierre. | | `folding` | `False` | Flechas de plegado en el margen y Ctrl+Shift+[ / ]. | | `minimap` | `None` | Ancho del minimapa en píxeles (`None`: sin minimapa). | | `minimap_thumb` | `None` | Color del resaltado del área visible en el minimapa. | | `cursor_shape` | `"bar"` | `"bar"`, `"block"`, `"underline"` o `"hollow"`. | | `cursor_width` | `2.0` | Ancho del cursor de barra. | | `cursor_blink` | `True` | Si el cursor parpadea. | | `cursor_color` | `None` | Color del cursor. | | `highlights` | `None` | Rangos pintados detrás del texto (resultados de búsqueda): `[(línea, columna, línea_final, columna_final)]`, desde 1, con el final excluido. | | `highlight_color` | `None` | Color de `highlights`. | | `match_brackets` | `True` | Remarca el corchete pareja del que está junto al cursor. | | `selection_highlight` | `True` | Resalta las demás apariciones del texto seleccionado. | | `keymap` | `None` | Diccionario `{atajo: acción}` al estilo de Zed; `None` como acción quita una tecla por defecto (`pyzed.ui.editor.keymap_for` crea uno a partir de cambios por acción). | | `scrollbar` | `True` | `False` oculta las barras de desplazamiento. | | `readonly` | `False` | `True` deja seleccionar y copiar el texto, pero no cambiarlo (un log, un traceback). | | `hard_tabs` | `False` | Con `tab_size`: Tab, Enter y sangrar (Ctrl+]) usan tabulaciones en vez de espacios. | | `backspace_unindent` | `True` | Con `tab_size`: Backspace en los espacios iniciales quita un nivel entero (`False`: un espacio). | | `repeat_delay` | `None` | Milisegundos que esperan Backspace y Suprimir, mantenidos, antes de repetirse (`None`: los del sistema). | | `repeat_rate` | `None` | Borrados por segundo como máximo mientras se mantiene Backspace o Suprimir (`None`: el ritmo del sistema). | | `hover_delay` | `500.0` | Milisegundos que el ratón debe quedarse sobre una palabra antes de llamar a `on_hover`. | | `sticky_scroll` | `0` | Cuántas líneas de los bloques que rodean al código visible (`class`, `def`, `if`…) quedan fijas arriba al desplazarse (`0`: ninguna). | Eventos: `on_change`, `on_cursor`, `on_scroll`, `on_zoom`, `on_action`, `on_hover` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#code_editor) #### Relacionados - [Terminal](https://albakore.github.io/pyzed/es/library/terminal/): Una vista de terminal de solo lectura con colores ANSI. - [Consola](https://albakore.github.io/pyzed/es/library/console/): Un registro estilo dev tools con niveles, filtro, copiar y limpiar. - [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/workspace/): La disposición de una ventana estilo editor: barra de título, docks, centro y barra de estado. ### Efectos > Glow, bloom, glass y opacidad como propiedades de estilo. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/effects/ :::tip[En pocas palabras] Brillos, cristal esmerilado y transparencia que puedes añadir a cualquier caja. ::: Los efectos visuales son propiedades de [estilo](https://albakore.github.io/pyzed/es/theming/styles/): `glow` (un halo suave), `bloom` (un resplandor amplio y brillante), `glass` (un panel translúcido y esmerilado), `opacity`, y `translate` y `scale`, que desplazan o escalan el dibujo sin cambiar el layout. Las animaciones también pueden cambiar `glow`. `pz.Style` · Categoría: [Contenido](https://albakore.github.io/pyzed/es/library/#contenido) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#style) **Código:** ```python pz.container(pz.row([ pz.container(ui.label("Glass"), width=150, height=70, padding=14).style(glass=True, radius=12), pz.container(ui.label("Glow"), width=150, height=70, padding=14).style( background="#0b1220", radius=12, glow=("#22d3ee", 18)), pz.container(ui.label("Bloom"), width=150, height=70, padding=14).style( background="#1a0b16", radius=12, bloom=("#f472b6", 0.8)), pz.container(ui.label("Opacity"), width=150, height=70, padding=14).style( background="#0b1220", radius=12, opacity=0.5), ], spacing=24), padding=32).style( background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=14) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para destacar la acción principal o un estado en vivo (grabando, conectado). - Para paneles flotantes sobre fondos coloridos (glass). #### Uso ```python import pyzed as pz from pyzed import ui pz.container(pz.row([ pz.container(ui.label("Glass"), width=150, height=70, padding=14).style(glass=True, radius=12), pz.container(ui.label("Glow"), width=150, height=70, padding=14).style( background="#0b1220", radius=12, glow=("#22d3ee", 18)), pz.container(ui.label("Bloom"), width=150, height=70, padding=14).style( background="#1a0b16", radius=12, bloom=("#f472b6", 0.8)), pz.container(ui.label("Opacity"), width=150, height=70, padding=14).style( background="#0b1220", radius=12, opacity=0.5), ], spacing=24), padding=32).style( background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=14) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Efectos ```python pz.container(pz.container(ui.label("Glass"), width=130, height=60, padding=12).style(glass=True, radius=12), padding=24).style(background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=12) ``` ```python pz.container(pz.container(ui.label("Glow"), width=130, height=60, padding=12).style(background="#0b1220", radius=12, glow=("#22d3ee", 18)), padding=24).style(background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=12) ``` ```python pz.container(pz.container(ui.label("Bloom"), width=130, height=60, padding=12).style(background="#1a0b16", radius=12, bloom=("#f472b6", 0.8)), padding=24).style(background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=12) ``` ```python pz.container(pz.container(ui.label("Opacity"), width=130, height=60, padding=12).style(background="#0b1220", radius=12, opacity=0.5), padding=24).style(background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=12) ``` ```python pz.container(pz.container(ui.label("Scale"), width=130, height=60, padding=12).style(background="#0b1220", radius=12, scale=1.15), padding=24).style(background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=12) ``` ```python pz.container(pz.container(ui.label("Translate"), width=130, height=60, padding=12).style(background="#0b1220", radius=12, translate=(16, 8)), padding=24).style(background=pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=90), radius=12) ``` #### Eventos Efectos no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.Style` ```python class Style ``` Un conjunto reutilizable de propiedades de estilo para `widget.style(...)`, el `style=` de un componente o `App.style`. Combina estilos con `|` (gana el de la derecha). Ver [Estilos](https://albakore.github.io/pyzed/es/theming/styles/). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `*styles` | — | Estilos, dicts o nombres de estilo de partida. | | `**props` | — | Propiedades: `background`, `color`, `font`, `weight`, `italic`, `size`, `line_height`, `border_color`, `border_width`, `border` (`(ancho, color)`), `radius`, `shadow`, `padding`, `margin`, `width`, `height`, `max_width`, `max_height`, `min_width`, `min_height`, `align_x`, `align_y`, `clip`. Efectos: `opacity`, `translate` (`(x, y)`, píxeles o `"50%"`), `scale`, `glow` (un color, un tamaño o `(color, tamaño)`), `bloom` (un color o `(color, intensidad)`) y `glass` (`True`, un tinte o `{"tint", "strength"}`). Estados: `hover`, `pressed` (o `active`) y `focus`, cada uno con las propiedades que se muestran entonces. `transition`: milisegundos, `True` o `{"duration", "easing", "delay"}`. `media`: `{consulta: estilo}` que se aplica cuando la ventana la cumple. Los colores pueden ser tokens del tema. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#style) #### Relacionados - [Animación](https://albakore.github.io/pyzed/es/library/animation/): Presets, keyframes y transiciones para cualquier widget. - [Tarjeta](https://albakore.github.io/pyzed/es/library/card/): Una superficie con borde, título y acciones opcionales. ### Elección > Un mensaje con tus propios botones. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/choice/ :::tip[En pocas palabras] Una pregunta con varias respuestas posibles, cada una un botón. ::: Un mensaje con tus propios botones: `options` son pares `(etiqueta, on_press)` y el último es el principal. `ui.choice` · Categoría: [Diálogos](https://albakore.github.io/pyzed/es/library/#diálogos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#choice) **Código:** ```python ui.dialog_layer(main_view(), ui.choice( "notes.py already exists in this folder.", [("Keep both", on_keep), ("Skip", on_skip), ("Replace", on_replace)], on_cancel, title="File exists", width=420, )) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Cuando una pregunta tiene más de dos respuestas (Conservar ambos / Reemplazar / Omitir). #### Uso ```python from pyzed import ui ui.dialog_layer(main_view(), ui.choice( "notes.py already exists in this folder.", [("Keep both", on_keep), ("Skip", on_skip), ("Replace", on_replace)], on_cancel, title="File exists", width=420, )) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python ui.choice("Open the project in…", [("This window", on_here), ("New window", on_new)], on_cancel, width=360) ``` ```python ui.choice("notes.py already exists.", [("Keep both", on_keep), ("Skip", on_skip), ("Replace", on_replace)], on_cancel, title="File exists", width=400) ``` #### Eventos Elección tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_cancel` | `()` | El icono de cerrar. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Copy(pz.App): def __init__(self): self.asking, self.answer = True, None def answer_with(self, what): self.asking, self.answer = False, what def view(self): dialog = ui.choice("notes.py already exists in this folder.", [("Keep both", lambda: self.answer_with("both")), # one handler per option ("Replace", lambda: self.answer_with("replace"))], lambda: setattr(self, "asking", False), # on_cancel: the ✕ title="File exists") return ui.dialog_layer(main_view(), dialog if self.asking else None) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Answer: what: str class Copy(pz.App): asking = True def update(self, message): match message: case Answer(what): self.asking = False print("chose", what) case "cancel": self.asking = False def view(self): dialog = ui.choice("notes.py already exists.", [("Keep both", Answer("both")), ("Replace", Answer("replace"))], "cancel") return ui.dialog_layer(main_view(), dialog if self.asking else None) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: answer: str = "" def view(state, set_state): if state.answer: return ui.label(f"You chose: {state.answer}") return ui.dialog_layer(main_view(), ui.choice( "notes.py already exists.", [(label, lambda a=answer: set_state(answer=a)) for label, answer in (("Keep both", "both"), ("Replace", "replace"))], lambda: set_state(answer="cancel"))) ``` #### Propiedades y API ##### `ui.choice` ```python choice(message, options, on_cancel, *, title=None, width=440, theme=None) ``` Un mensaje con tus propios botones. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `message` | *obligatorio* | Cualquier valor de Python, o un callable. | | `options` | *obligatorio* | Pares `(texto, on_press)`; el último es el botón principal. | | `title` | `None` | Título. | | `width` | `440` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_cancel` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#choice) #### Relacionados - [Confirmación](https://albakore.github.io/pyzed/es/library/confirm/): Una pregunta de sí o no, en rojo para acciones destructivas. - [Cambios sin guardar](https://albakore.github.io/pyzed/es/library/unsaved-changes/): “¿Guardar los cambios antes de cerrar?” ### Encabezado > Títulos en seminegrita en tres niveles. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/heading/ :::tip[En pocas palabras] Un título: texto más grande y en negrita que nombra una página o una sección. ::: Un título en seminegrita. `level=1` (por defecto), `2` o `3` eligen el tamaño, así los títulos son coherentes en todas las pantallas. `ui.heading` · Categoría: [Visualización](https://albakore.github.io/pyzed/es/library/#visualización) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#heading) **Código:** ```python pz.column([ ui.heading("Heading 1"), ui.heading("Heading 2", level=2), ui.heading("Heading 3", level=3), ui.muted("Body text below the headings."), ], spacing=6) ``` #### Cuándo usarlo - Para el título de una página, una sección de un diálogo o un grupo de ajustes. - Para un título pequeño de lista en mayúsculas, usa [Cabecera de lista](https://albakore.github.io/pyzed/es/library/list-header/). #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ ui.heading("Heading 1"), ui.heading("Heading 2", level=2), ui.heading("Heading 3", level=3), ui.muted("Body text below the headings."), ], spacing=6) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Nivel ```python ui.heading("Project settings") ``` ```python ui.heading("Project settings", level=2) ``` ```python ui.heading("Project settings", level=3) ``` ```python ui.heading("Project settings", level=4) ``` ##### Color ```python ui.heading("Welcome back", level=2, color="accent") ``` ```python pz.column([ui.heading("Billing", level=2), ui.muted("Invoices and payment methods.")], spacing=4) ``` #### Eventos Encabezado no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.heading` ```python heading(content, *, level=1, theme=None, **kwargs) ``` Un título en semibold. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `level` | `1` | 1 (26 px), 2 (20 px), 3 (16 px) o 4 (13.5 px). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | | `**kwargs` | — | Los de `label`. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#heading) #### Relacionados - [Etiqueta](https://albakore.github.io/pyzed/es/library/label/): Texto con color, tamaño y peso del tema; `muted` para texto secundario. - [Tarjeta](https://albakore.github.io/pyzed/es/library/card/): Una superficie con borde, título y acciones opcionales. ### Espacio > Huecos fijos y separadores flexibles. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/spacing/ :::tip[En pocas palabras] Espacio vacío: un hueco fijo, o un muelle que separa las cosas. ::: `pz.space(width, height)` es una caja vacía de un tamaño dado. `pz.hspace()` y `pz.vspace()` son huecos flexibles que ocupan el espacio libre y empujan a sus hermanos a los extremos de una fila o columna. `pz.space` · Categoría: [Disposición](https://albakore.github.io/pyzed/es/library/#disposición) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#space) **Código:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([ pz.row([box("left"), pz.hspace(), box("right")], width=440), pz.row([box("a"), pz.space(width=60), box("60 px later", w=140)]), ], spacing=16) ``` #### Cuándo usarlo - Para llevar acciones a la derecha de una barra, o un pie al fondo de un panel. #### Uso ```python import pyzed as pz from pyzed import ui box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([ pz.row([box("left"), pz.hspace(), box("right")], width=440), pz.row([box("a"), pz.space(width=60), box("60 px later", w=140)]), ], spacing=16) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("left"), pz.hspace(), box("right")], width=320) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("a"), pz.space(width=60), box("b")]) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([box("top"), pz.vspace(), box("bottom")], height=150) ``` #### Eventos Espacio no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.space` ```python space(width=None, height=None) ``` Espacio vacío de un tamaño dado. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#space) ##### `pz.hspace` ```python hspace(width='fill') ``` Hueco horizontal flexible: empuja a los hermanos de una fila hacia los extremos. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `width` | `"fill"` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#hspace) ##### `pz.vspace` ```python vspace(height='fill') ``` Hueco vertical flexible: empuja a los hermanos de una columna hacia los extremos. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `height` | `"fill"` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#vspace) #### Relacionados - [Fila](https://albakore.github.io/pyzed/es/library/row/): Coloca los hijos en horizontal. - [Columna](https://albakore.github.io/pyzed/es/library/column/): Apila los hijos en vertical. - [Divisor](https://albakore.github.io/pyzed/es/library/divider/): Una línea fina en el color de borde del tema. ### Espacio de trabajo > La disposición de una ventana estilo editor: barra de título, docks, centro y barra de estado. Componente de Pyzed para GUIs en Python. Source: https://albakore.github.io/pyzed/es/library/workspace/ :::tip[En pocas palabras] Toda la disposición de una ventana tipo editor: barra arriba, paneles laterales, la zona principal y una barra abajo. ::: Construye el marco de una ventana estilo editor: la barra de título arriba, docks a la izquierda, a la derecha y abajo, el centro y la barra de estado. Pasa `None` para ocultar un dock (así funciona Ctrl+B); `left_width`, `right_width` y `bottom_height` fijan su tamaño y `on_resize` avisa al arrastrar. `ui.workspace` · Categoría: [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/#espacio-de-trabajo) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#workspace-1) **Código:** ```python files = ui.TreeNode.from_paths(["src/main.py", "src/ui.py", "README.md"], status={"src/ui.py": "modified"}) ui.workspace( pz.column([ ui.tab_bar([ui.Tab.for_file("src/main.py")], "src/main.py", on_close=on_close), ui.editor("ws-main", "import pyzed as pz\n\n\ndef main():\n pz.run(App)\n", path="main.py"), ]), title_bar=ui.title_bar([ui.title_button("pyzed", chevron=True), ui.title_button("main", icon="git-branch", muted=True)], [ui.icon_button("bell", on_bell, size="sm"), ui.avatar("KV")]), left=pz.column([ui.panel_header("Project", [ui.icon_button("plus", on_new, size="xs")]), ui.tree(files, expanded={"src"}, selected="src/main.py")]), bottom=pz.column([ui.panel_header("Terminal", icon="terminal"), ui.terminal("$ python main.py\n\x1b[32m✓\x1b[0m done")]), status_bar=ui.status_bar([ui.status_item("0", icon="circle-x"), ui.status_item("1", icon="triangle-alert")], [ui.status_item("Ln 4, Col 8"), ui.status_item("Python")]), left_width=200, bottom_height=110, ) ``` #### Cuándo usarlo - Como raíz de apps tipo editor: IDEs, apps de notas, clientes de bases de datos, gestores de archivos. #### Uso ```python import pyzed as pz from pyzed import ui class Ide(pz.App): def __init__(self): self.show_left = True def on_key(self, combo): if combo == "ctrl-b": self.show_left = not self.show_left def view(self): return ui.workspace( self.editor_area(), title_bar=ui.title_bar([ui.title_button("my-project", chevron=True)]), left=self.project_panel() if self.show_left else None, status_bar=ui.status_bar([], [ui.status_item("Python")]), ) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Docks ```python ui.workspace(main_view(), status_bar=ui.status_bar([], [ui.status_item("Python")])) ``` ```python ui.workspace(main_view(), left=pz.column([ui.panel_header("Project"), ui.list_item("main.py", icon="file-code", selected=True, inset=True)]), left_width=150) ``` ```python ui.workspace(main_view(), bottom=pz.column([ui.panel_header("Terminal", icon="terminal"), ui.terminal("$ python main.py")]), bottom_height=90) ``` ```python ui.workspace(main_view(), right=pz.column([ui.panel_header("Outline"), ui.list_item("class Notes", icon="box")]), right_width=150) ``` ```python ui.workspace(main_view(), title_bar=ui.title_bar([ui.title_button("pyzed", chevron=True)], [ui.avatar("KV")])) ``` #### Eventos Espacio de trabajo tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_resize` | `(side, size)` | Se llama con `(side, size)` cuando el usuario termina de cambiar el tamaño de un panel arrastrando su borde interior. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Ide(pz.App): def __init__(self): self.left_width, self.bottom_height = 220, 160 def resized(self, side, size): # on_resize: which dock and its new size if side == "left": self.left_width = size elif side == "bottom": self.bottom_height = size def view(self): return ui.workspace(main_view(), left=ui.panel_header("Project"), bottom=ui.panel_header("Terminal"), left_width=self.left_width, bottom_height=self.bottom_height, on_resize=self.resized) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Resized: side: str size: float class Ide(pz.App): sizes = {"left": 220, "bottom": 160} def update(self, message): match message: case Resized(side, size): self.sizes = {**self.sizes, side: size} def view(self): return ui.workspace(main_view(), left=ui.panel_header("Project"), bottom=ui.panel_header("Terminal"), left_width=self.sizes["left"], bottom_height=self.sizes["bottom"], on_resize=Resized) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: left: float = 220 bottom: float = 160 def view(state, set_state): return ui.workspace(main_view(), left=ui.panel_header("Project"), bottom=ui.panel_header("Terminal"), left_width=state.left, bottom_height=state.bottom, on_resize=lambda side, size: set_state(**{side: size})) ``` #### Propiedades y API ##### `ui.workspace` ```python workspace( center, *, title_bar=None, left=None, right=None, bottom=None, status_bar=None, left_width=240, right_width=320, bottom_height=220, on_resize=None, theme=None, ) ``` El layout de una ventana estilo editor: barra de título, docks izquierdo, derecho e inferior, centro y barra de estado. Pasa `None` para ocultar un dock. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `center` | *obligatorio* | Contenido central (normalmente pestañas + editor). | | `title_bar` | `None` | Barra de título (`ui.title_bar`). | | `left` | `None` | Dock izquierdo. | | `right` | `None` | Dock derecho. | | `bottom` | `None` | Dock inferior. | | `status_bar` | `None` | Barra de estado (`ui.status_bar`). | | `left_width` | `240` | Ancho del dock izquierdo. | | `right_width` | `320` | Ancho del dock derecho. | | `bottom_height` | `220` | Alto del dock inferior. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_resize` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#workspace-1) #### Relacionados - [Barra de título](https://albakore.github.io/pyzed/es/library/title-bar/): La barra superior con los selectores de proyecto y rama. - [Barra de estado](https://albakore.github.io/pyzed/es/library/status-bar/): La barra inferior: diagnósticos, posición del cursor, lenguaje. - [Panel](https://albakore.github.io/pyzed/es/library/panel/): Un panel lateral acoplable y redimensionable. - [División](https://albakore.github.io/pyzed/es/library/split/): Dos paneles con un divisor que se arrastra. ### Estado vacío > Un marcador centrado para listas y búsquedas vacías. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/empty-state/ :::tip[En pocas palabras] Lo que muestra una pantalla cuando aún no tiene nada, con una pista de qué hacer después. ::: Un icono, un título, una descripción y acciones centrados para un lugar que aún no tiene nada que mostrar. `ui.empty_state` · Categoría: [Estado y avisos](https://albakore.github.io/pyzed/es/library/#estado-y-avisos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#empty_state) **Código:** ```python ui.empty_state("No results", "Try a different search.", icon="search", actions=[ui.button("Clear filters", on_clear, size="sm")]) ``` #### Cuándo usarlo - Para una lista vacía, una búsqueda sin resultados o un panel antes de abrir el primer archivo. - Ofrece el siguiente paso como acción (Crear, Quitar filtros, Abrir carpeta). #### Uso ```python from pyzed import ui ui.empty_state("No results", "Try a different search.", icon="search", actions=[ui.button("Clear filters", on_clear, size="sm")]) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido ```python ui.empty_state("No notifications") ``` ```python ui.empty_state("No results", "Try a different search.", icon="search") ``` ```python ui.empty_state("No projects yet", "Create one to get started.", icon="folder-plus", actions=[ui.button("New project", on_new, variant="solid", size="sm")]) ``` ##### Estados y eventos ```python ui.empty_state("No results", "Try another search", icon="search", size="sm", height="shrink") ``` ```python ui.empty_state("No projects", "Create one to start", icon="folder", size="lg", height=220) ``` #### Eventos Estado vacío no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.empty_state` ```python empty_state( title, description=None, *, icon='inbox', actions=(), size='md', height='fill', theme=None, ) ``` Un marcador centrado para listas vacías, búsquedas sin resultados, etc. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | *obligatorio* | Título. | | `description` | `None` | Texto secundario. | | `icon` | `"inbox"` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `actions` | `()` | Widgets extra (normalmente botones) que se muestran como acciones. | | `size` | `"md"` | `"sm"`, `"md"` o `"lg"`. | | `height` | `"fill"` | `"fill"` (por defecto), píxeles o `"shrink"`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#empty_state) #### Relacionados - [Aviso](https://albakore.github.io/pyzed/es/library/callout/): Un aviso en línea con título, texto y acciones. - [Pista de atajo](https://albakore.github.io/pyzed/es/library/keybinding-hint/): Una pista discreta tipo “Abrir archivo Ctrl P”. - [Tarjeta](https://albakore.github.io/pyzed/es/library/card/): Una superficie con borde, título y acciones opcionales. ### Etiqueta > Texto con color, tamaño y peso del tema; `muted` para texto secundario. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/label/ :::tip[En pocas palabras] Texto normal en la pantalla, con los colores del tema actual. ::: Texto que toma el color del tema. `color` es un token del tema (`"text"`, `"accent"`, `"error"`…) o un color hex, y `size` es un paso con nombre (`"xs"` a `"2xl"`) o un número. `ui.muted(...)` es una etiqueta con el color `"text_muted"`, para texto secundario. `ui.label` · Categoría: [Visualización](https://albakore.github.io/pyzed/es/library/#visualización) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#label) **Código:** ```python pz.column([ ui.label("Pyzed draws native UIs with Iced."), ui.muted("Secondary text uses the muted color."), ui.label("Accent and semibold", color="accent", weight="semibold"), ui.label("def mono(): ...", mono=True, size="sm"), ], spacing=8) ``` #### Cuándo usarlo - Para cualquier texto en una interfaz con tema: sigue los cambios de tema, a diferencia de un `pz.text` con colores fijos. - Usa `ui.muted` para descripciones, pistas y metadatos. - Para títulos usa [Encabezado](https://albakore.github.io/pyzed/es/library/heading/). #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ ui.label("Pyzed draws native UIs with Iced."), ui.muted("Secondary text uses the muted color."), ui.label("Accent and semibold", color="accent", weight="semibold"), ui.label("def mono(): ...", mono=True, size="sm"), ], spacing=8) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Tamaño Los tamaños con nombre siguen el tamaño de fuente del tema; un número es un tamaño en píxeles. ```python ui.label("Extra small", size="xs") ``` ```python ui.label("Small", size="sm") ``` ```python ui.label("Medium", size="md") ``` ```python ui.label("Large", size="lg") ``` ```python ui.label("Extra large", size="xl") ``` ```python ui.label("Huge", size="2xl") ``` ##### Color ```python ui.label("Text") ``` ```python ui.muted("Muted text") ``` ```python ui.label("Accent", color="accent") ``` ```python ui.label("Success", color="success") ``` ```python ui.label("Warning", color="warning") ``` ```python ui.label("Error", color="error") ``` ```python ui.label("Any hex color", color="#c678dd") ``` ##### Peso y estilo ```python ui.label("Normal", weight="normal", size="lg") ``` ```python ui.label("Medium", weight="medium", size="lg") ``` ```python ui.label("Semibold", weight="semibold", size="lg") ``` ```python ui.label("Bold", weight="bold", size="lg") ``` ```python ui.label("Italic", italic=True, size="lg") ``` ```python ui.label("def mono(): ...", mono=True) ``` #### Eventos Etiqueta no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.label` ```python label( content, *, color='text', size='md', weight=None, mono=False, italic=False, wrap=True, theme=None, **kwargs, ) ``` Texto con un color del tema. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `color` | `"text"` | Token del tema o color hex. Por defecto `"text"`. | | `size` | `"md"` | `"xs"` (11), `"sm"` (12), `"md"` (13.5), `"lg"` (16), `"xl"` (20), `"2xl"` (26) o un número. | | `weight` | `None` | Peso: `"medium"`, `"semibold"`, `"bold"`… | | `mono` | `False` | Usa la fuente monoespaciada (IBM Plex Mono). | | `italic` | `False` | Cursiva. | | `wrap` | `True` | Ajuste de línea. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | | `**kwargs` | — | Se pasan a `pyzed.text`. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#label) ##### `ui.muted` ```python muted(content, **kwargs) ``` Un `label` con color `"text_muted"`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `**kwargs` | — | Los de `label`. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#muted) #### Relacionados - [Encabezado](https://albakore.github.io/pyzed/es/library/heading/): Títulos en seminegrita en tres niveles. - [Kbd](https://albakore.github.io/pyzed/es/library/kbd/): Atajos de teclado dibujados como teclas, con símbolos en macOS. - [Insignia](https://albakore.github.io/pyzed/es/library/badge/): Una pastilla pequeña para estados, versiones y contadores. ### Facepile > Avatares de colaboradores superpuestos con +N. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/facepile/ :::tip[En pocas palabras] Una fila de avatares superpuestos que muestra quién está. ::: Avatares superpuestos de colaboradores, como en la barra de título de un editor colaborativo. Los que pasan de `max_shown` se agrupan en `+N`. `ui.facepile` · Categoría: [Datos](https://albakore.github.io/pyzed/es/library/#datos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#facepile) **Código:** ```python pz.column([ ui.facepile(["Ana", "Luis", "Kev", "Marta"]), ui.facepile(["Ana", "Luis", "Kev", "Marta", "Sam", "Jo", "Lee", "Max"], size=28, max_shown=4), ], spacing=16, align="center") ``` #### Cuándo usarlo - Para mostrar quién está en un proyecto, una llamada o un documento sin listar los nombres. #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ ui.facepile(["Ana", "Luis", "Kev", "Marta"]), ui.facepile(["Ana", "Luis", "Kev", "Marta", "Sam", "Jo", "Lee", "Max"], size=28, max_shown=4), ], spacing=16, align="center") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python ui.facepile(["Ana", "Luis"]) ``` ```python ui.facepile(["Ana", "Luis", "Kev", "Marta", "Sam"]) ``` ```python ui.facepile(["Ana", "Luis", "Kev", "Marta", "Sam", "Jo", "Lee"], max_shown=3) ``` ```python ui.facepile(["Ana", "Luis", "Kev", "Marta"], size=32, overlap=10) ``` #### Eventos Facepile no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.facepile` ```python facepile(people, *, size=22, overlap=6, max_shown=5, theme=None) ``` Avatares superpuestos de colaboradores, como en la barra de título de un editor colaborativo. Los que pasan de `max_shown` se agrupan en `+N`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `people` | *obligatorio* | Iniciales o nombres. | | `size` | `22` | Tamaño. | | `overlap` | `6` | Solapamiento en píxeles. | | `max_shown` | `5` | Cuántos mostrar. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#facepile) #### Relacionados - [Avatar](https://albakore.github.io/pyzed/es/library/avatar/): Un avatar redondo con iniciales en un color de colaborador. - [Barra de título](https://albakore.github.io/pyzed/es/library/title-bar/): La barra superior con los selectores de proyecto y rama. ### Fila > Coloca los hijos en horizontal. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/row/ :::tip[En pocas palabras] Pone las cosas una al lado de la otra, de izquierda a derecha. ::: Coloca sus hijos en horizontal, con `spacing` entre ellos y `align` en el eje cruzado. `wrap=True` pasa a la línea siguiente los hijos que no caben. Las listas se aplanan y se ignoran `None`/`False`, así que `cond and widget` funciona. `pz.row` · Categoría: [Disposición](https://albakore.github.io/pyzed/es/library/#disposición) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#row) **Código:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([ pz.row([box("A"), box("B"), box("C")], spacing=8), pz.row([box("Start"), pz.hspace(), box("End")], spacing=8, width=420), pz.row([box("tall", h=70), box("center"), box("end")], spacing=8, align="center"), ], spacing=14) ``` #### Cuándo usarlo - Para barras, grupos de botones, filas de formulario: todo lo que va lado a lado. - Separa elementos con `pz.hspace()` entre ellos. #### Uso ```python import pyzed as pz from pyzed import ui box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([ pz.row([box("A"), box("B"), box("C")], spacing=8), pz.row([box("Start"), pz.hspace(), box("End")], spacing=8, width=420), pz.row([box("tall", h=70), box("center"), box("end")], spacing=8, align="center"), ], spacing=14) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Espaciado ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("A"), box("B"), box("C")]) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("A"), box("B"), box("C")], spacing=8) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("A"), box("B"), box("C")], spacing=24) ``` ##### Alineación cruzada ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("tall", h=70), box("B"), box("C")], spacing=8, align="start") ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("tall", h=70), box("B"), box("C")], spacing=8, align="center") ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("tall", h=70), box("B"), box("C")], spacing=8, align="end") ``` ##### Opciones ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.row([box("Start"), pz.hspace(), box("End")], width=320) ``` ```python pz.container(pz.row([ui.chip(t) for t in ["python", "rust", "iced", "desktop", "gui", "async", "themes"]], spacing=6, wrap=True, line_spacing=6), width=260) ``` #### Ejemplos ##### Salto de línea Con `wrap=True` los hijos pasan a nuevas líneas, como etiquetas. **Código:** ```python pz.container(pz.row([ui.chip(t) for t in ["python", "rust", "iced", "zed", "desktop", "gui", "async", "themes", "canvas", "markdown"]], spacing=6, wrap=True, line_spacing=6), width=360) ``` #### Eventos Fila no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.row` ```python row( children=None, *, spacing=0.0, padding=None, width=None, height=None, align=None, clip=False, wrap=False, line_spacing=None, ) ``` Coloca los hijos en horizontal. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `children` | `None` | Lista de hijos. Se aplanan listas anidadas y se ignoran `None` y `False`, así que puedes escribir `cond and widget`. | | `spacing` | `0.0` | Separación entre hijos, en píxeles. | | `padding` | `None` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `align` | `None` | Alineación de los hijos en el eje cruzado: `"start"`, `"center"` o `"end"`. | | `clip` | `False` | Recorta el contenido que se sale de los límites. | | `wrap` | `False` | Ajuste de línea / pasa los hijos a una nueva fila cuando no caben. | | `line_spacing` | `None` | Separación entre filas cuando `wrap=True`. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#row) #### Relacionados - [Columna](https://albakore.github.io/pyzed/es/library/column/): Apila los hijos en vertical. - [Flex](https://albakore.github.io/pyzed/es/library/flex/): Flexbox de CSS: justify, align, gap, wrap y grow. - [Espacio](https://albakore.github.io/pyzed/es/library/spacing/): Huecos fijos y separadores flexibles. ### Fila de lista > Una fila de lista con icono, texto y un hueco al final. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/list-item/ :::tip[En pocas palabras] Una fila de una lista: un icono, un nombre y quizá algo a la derecha. ::: Una fila de lista: un icono, el texto y huecos `start`/`end` para cualquier widget. Admite `selected`, `focused` (borde de foco), `disabled`, anidar con `indent` y `on_right_press` para un menú contextual. `ui.list_item` · Categoría: [Listas y menús](https://albakore.github.io/pyzed/es/library/#listas-y-menús) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#list_item) **Código:** ```python pz.column([ ui.list_item("main.py", on_open, icon="file-code", icon_color="#3572a5", selected=True, inset=True), ui.list_item("README.md", on_open, icon="file-text", inset=True, end=ui.badge("M", tint="warning")), ui.list_item("Cargo.toml", on_open, icon="file-cog", inset=True, end=ui.kbd("ctrl-1", size="xs")), ui.list_item("old.txt", on_open, icon="file", inset=True, disabled=True), ], width=320) ``` #### Cuándo usarlo - Para cualquier lista vertical: archivos recientes, resultados, categorías de ajustes, personas. - Usa `inset=True` dentro de paneles para que el resaltado tenga margen. #### Uso ```python import pyzed as pz from pyzed import ui class Recent(pz.App): def __init__(self): self.files = ["main.py", "app.py", "README.md"] self.current = "main.py" def view(self): return pz.column([ ui.list_item(f, lambda f=f: setattr(self, "current", f), icon="file", selected=f == self.current, inset=True) for f in self.files ]) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido ```python pz.container(ui.list_item("README.md", on_open), width=240) ``` ```python pz.container(ui.list_item("README.md", on_open, icon="file-text"), width=240) ``` ```python pz.container(ui.list_item("main.py", on_open, icon="file-code", icon_color="#3572a5"), width=240) ``` ```python pz.container(ui.list_item("app.py", on_open, icon="file-code", end=ui.badge("M", tint="warning")), width=240) ``` ```python pz.container(ui.list_item("Buy milk", on_open, start=ui.checkbox(True, on_toggle)), width=240) ``` ```python pz.container(ui.list_item("Delete project", on_open, icon="trash-2", color="error"), width=240) ``` ##### Estado ```python pz.container(ui.list_item("main.py", on_open, icon="file-code", selected=True), width=240) ``` ```python pz.container(ui.list_item("main.py", on_open, icon="file-code", focused=True), width=240) ``` ```python pz.container(ui.list_item("old.txt", on_open, icon="file", disabled=True), width=240) ``` ```python pz.container(ui.list_item("main.py", on_open, icon="file-code", selected=True, inset=True), width=240) ``` ```python pz.container(ui.list_item("button.rs", on_open, icon="file-code", indent=24), width=240) ``` ##### Estados y eventos `on_right_press` abre un menú contextual y `tooltip=` aparece al pasar el ratón. ```python pz.container(ui.list_item("main.py", on_press, icon="file", description="src/app"), width=300) ``` ```python pz.container(ui.list_item("Rename me", on_press, icon="pencil", on_double_press=on_rename), width=300) ``` #### Eventos Fila de lista tiene **3 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_press` | `()` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | | `on_right_press` | `()` | Clic derecho. | | `on_double_press` | `()` | Se envía con un doble clic (abrir, renombrar…). | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Recent(pz.App): def __init__(self): self.files = ["main.py", "app.py", "README.md"] self.current = "main.py" self.opened = None def view(self): return pz.column([ ui.list_item(f, lambda f=f: setattr(self, "current", f), icon="file", # on_press selected=f == self.current, inset=True, on_double_press=lambda f=f: setattr(self, "opened", f), on_right_press=lambda f=f: print("menu for", f)) for f in self.files ], width=280) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Select: path: str @dataclass class Open: path: str class Recent(pz.App): files = ["main.py", "app.py", "README.md"] current, opened = "main.py", None def update(self, message): match message: case Select(path): self.current = path case Open(path): self.opened = path def view(self): return pz.column([ui.list_item(f, Select(f), icon="file", selected=f == self.current, on_double_press=Open(f)) for f in self.files], width=280) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: files: tuple = ("main.py", "app.py", "README.md") current: str = "main.py" def file_row(name, selected, select): return ui.list_item(name, select, icon="file", selected=selected, inset=True) def view(state, set_state): return pz.column([file_row(f, f == state.current, lambda f=f: set_state(current=f)) for f in state.files], width=280) ``` #### Propiedades y API ##### `ui.list_item` ```python list_item( content, on_press=None, *, icon=None, icon_color=None, start=None, end=None, selected=False, focused=False, indent=0, height=26, color=None, inset=False, disabled=False, on_right_press=None, on_double_press=None, description=None, tooltip=None, theme=None, ) ``` Una fila de lista: icono, texto y un hueco al final. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Texto o widget. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `icon_color` | `None` | Color del icono: un token del tema o un color hex. | | `start` | `None` | Widgets al inicio (izquierda). | | `end` | `None` | Widgets al final (derecha). | | `selected` | `False` | Lo marca como seleccionado. | | `focused` | `False` | Dibuja un borde de foco. | | `indent` | `0` | Sangría a la izquierda, en píxeles. | | `height` | `26` | Alto de la fila. | | `color` | `None` | Color del texto. | | `inset` | `False` | Con margen y esquinas redondeadas (menús, paletas). | | `disabled` | `False` | Lo deshabilita (ignora los clics). | | `description` | `None` | Una segunda línea atenuada (la fila crece a 40 px salvo que fijes `height`). | | `tooltip` | `None` | Texto al pasar el ratón. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_press`, `on_right_press`, `on_double_press` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#list_item) #### Relacionados - [Cabecera de lista](https://albakore.github.io/pyzed/es/library/list-header/): Un título pequeño de sección para listas, con hueco al final. - [Árbol](https://albakore.github.io/pyzed/es/library/tree/): Un árbol de archivos plegable con iconos y colores de git. - [Menú](https://albakore.github.io/pyzed/es/library/menu/): Un menú contextual con iconos, atajos y marcas. ### Fila de servicio > Una fila de ajustes para un servidor MCP, un agente o una skill: estado, interruptor y menú. Componente de Pyzed para GUIs en Python. Source: https://albakore.github.io/pyzed/es/library/service-row/ :::tip[En pocas palabras] Una línea de una página de ajustes para una herramienta que usa el asistente: si funciona, un interruptor para encenderla o apagarla y un menú con más opciones. ::: `ui.ServiceRow` es un componente para algo que usa un agente de IA (un servidor MCP, un agente externo, una skill) en una página de ajustes: un icono, el nombre con su versión (`version`) y un punto de estado, el `detail` (su comando o URL, en letra monoespaciada) y un `summary`; luego un spinner mientras está `busy`, un botón principal (`primary`, como Actualizar o Instalar), un interruptor (`enabled`; `None` lo oculta) y un menú `⋯` con las acciones (`actions`, de `ui.MenuItem`). `status` colorea el punto: `running` e `installed` en verde, `starting` en amarillo, `update` en el color de acento, `error` en rojo, `off` y `missing` en gris. `status_text` es el tooltip del punto, y se ve bajo el nombre con `error`, `missing` y `update`. `ui.ServiceRow` · Categoría: [Agentes de IA](https://albakore.github.io/pyzed/es/library/#agentes-de-ia) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#servicerow) **Código:** ```python pz.column([ ui.ServiceRow(name="github", detail="npx -y @modelcontextprotocol/server-github", icon_name="github", status="running", enabled=True, on_toggle=on_toggle, actions=[ui.MenuItem("Edit", on_edit, icon="pencil")]), ui.ServiceRow(name="filesystem", detail="uvx mcp-server-filesystem ~/notes", status="error", status_text="Exited with code 1", enabled=True, on_toggle=on_toggle), ui.ServiceRow(name="Gemini CLI", version="0.64.0", icon_name="bot", status="update", status_text="0.65.0 is available", primary="Update", on_primary=on_update), ], spacing=4, width=560) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - En los ajustes de una app con agentes de IA: una fila por servidor MCP, agente o skill. - Para cualquier lista de servicios que se pueden encender y apagar, actualizar o quitar. #### Uso ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.on = True def view(self): return ui.ServiceRow(name="github", detail="npx -y @modelcontextprotocol/server-github", status="running" if self.on else "off", enabled=self.on, on_toggle=lambda e: setattr(self, "on", e.value), actions=[ui.MenuItem("Remove", self.remove, icon="trash-2", danger=True)]) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Estado `enabled=False` además atenúa el nombre y el icono. ```python pz.container(ui.ServiceRow(name="github", detail="npx -y server-github", status="running", enabled=True), width=420) ``` ```python pz.container(ui.ServiceRow(name="github", detail="npx -y server-github", status="starting", enabled=True), width=420) ``` ```python pz.container(ui.ServiceRow(name="github", detail="npx -y server-github", status="error", status_text="Exited with code 1", enabled=True), width=420) ``` ```python pz.container(ui.ServiceRow(name="github", detail="npx -y server-github", status="off", enabled=False), width=420) ``` ```python pz.container(ui.ServiceRow(name="Gemini CLI", version="0.64.0", icon_name="bot", status="installed"), width=420) ``` ```python pz.container(ui.ServiceRow(name="Codex CLI", icon_name="bot", status="missing", status_text="Not installed yet", primary="Install"), width=420) ``` ```python pz.container(ui.ServiceRow(name="Gemini CLI", version="0.64.0", icon_name="bot", status="update", status_text="0.65.0 is available", primary="Update"), width=420) ``` ##### Opciones El menú `⋯` se dibuja abierto, como queda tras pulsarlo. ```python pz.container(ui.ServiceRow(name="notes", icon_name="book-open", status="running", summary="Reads and writes the notes folder."), width=420) ``` ```python pz.container(ui.ServiceRow(name="Gemini CLI", version="0.64.0", icon_name="bot", busy=True, primary="Update"), width=420) ``` ```python pz.container(ui.ServiceRow(name="github", detail="npx -y server-github", status="running", enabled=True, actions=[ui.MenuItem("Edit", on_edit, icon="pencil"), ui.MenuItem("Remove", on_remove, icon="trash-2", danger=True)]), width=420) ``` #### Eventos Fila de servicio tiene **2 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_toggle` | `(event)` | Se cambió el interruptor. | | `on_primary` | `(event)` | Se pulsó el botón principal. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Settings(pz.App): def __init__(self): self.on = True self.updates = 0 def view(self): return ui.ServiceRow(name="github", status="running" if self.on else "off", enabled=self.on, primary="Update", on_toggle=lambda e: setattr(self, "on", e.value), # the switch on_primary=lambda e: setattr(self, "updates", self.updates + 1)) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Enable: on: bool class Settings(pz.App): on = True def update(self, message): match message: case Enable(on): self.on = on def view(self): return ui.ServiceRow(name="github", enabled=self.on, status="running" if self.on else "off", on_toggle=lambda e: Enable(e.value)) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: on: bool = True def view(state, set_state): return ui.ServiceRow(name="github", enabled=state.on, status="running" if state.on else "off", on_toggle=lambda e: set_state(on=e.value)) ``` #### Propiedades y API ##### `ui.ServiceRow` ```python class ServiceRow ``` Un componente para algo que usa un agente (un servidor MCP, un agente externo, una skill), como las filas de los ajustes de agentes de Zed: un punto de estado, el nombre y el comando, un interruptor, un botón principal y un menú `⋯`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `name` | `""` | Nombre. | | `detail` | `""` | El comando o la URL, en letra monoespaciada. | | `summary` | `""` | Una línea sobre él. | | `icon_name` | `"plug"` | El icono. | | `status` | `""` | `running`, `starting`, `error`, `off`, `installed`, `missing` o `update` (el color del punto). | | `status_text` | `""` | El tooltip del punto; se muestra bajo el nombre en errores y actualizaciones. | | `version` | `""` | Una versión o un origen. | | `enabled` | `None` | El interruptor (`None` lo oculta). | | `busy` | `False` | Muestra un indicador de carga. | | `primary` | `""` | Texto del botón principal (Actualizar, Instalar…). | | `actions` | `None` | Entradas `ui.MenuItem` del menú `⋯`. | | `theme` | `None` | Tema. | Eventos: `on_toggle`, `on_primary` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#servicerow) #### Relacionados - [Catálogo](https://albakore.github.io/pyzed/es/library/catalog/): Cosas para instalar, con buscador e Instalar o Actualizar. - [Interruptor](https://albakore.github.io/pyzed/es/library/switch/): Un interruptor para ajustes que se aplican al momento. - [Menú](https://albakore.github.io/pyzed/es/library/menu/): Un menú contextual con iconos, atajos y marcas. - [Grupo de ajustes](https://albakore.github.io/pyzed/es/library/settings-group/): Un bloque con título de filas de ajustes, para pantallas de ajustes. ### Flex > Flexbox de CSS: justify, align, gap, wrap y grow. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/flex/ :::tip[En pocas palabras] Reparte las cosas a lo largo de una línea: juntas, con el mismo espacio o con una que crece. ::: Coloca los hijos en un eje como el flexbox de CSS: `justify` en el eje principal (`start`, `center`, `end`, `between`, `around`, `evenly`), `align` en el eje cruzado, `gap` y `wrap`. `pz.flex_item(child, grow=, basis=)` hace crecer a un hijo o le da un tamaño base. `pz.flex` · Categoría: [Disposición](https://albakore.github.io/pyzed/es/library/#disposición) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#flex) **Código:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([ pz.flex([box("A"), box("B"), box("C")], justify="between", width=440), pz.flex([box("fixed"), pz.flex_item(box("grows", w="fill"), grow=1), box("fixed")], gap=8, width=440), pz.flex([box("A"), box("B")], justify="center", gap=8, width=440), ], spacing=14) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Cuando `row`/`column` no bastan: reparto uniforme, elementos que crecen, tarjetas que saltan de línea. #### Uso ```python import pyzed as pz from pyzed import ui box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.column([ pz.flex([box("A"), box("B"), box("C")], justify="between", width=440), pz.flex([box("fixed"), pz.flex_item(box("grows", w="fill"), grow=1), box("fixed")], gap=8, width=440), pz.flex([box("A"), box("B")], justify="center", gap=8, width=440), ], spacing=14) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Justificar ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("A"), box("B")], justify="start", gap=8, width=360) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("A"), box("B")], justify="center", gap=8, width=360) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("A"), box("B")], justify="end", gap=8, width=360) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("A"), box("B"), box("C")], justify="between", width=360) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("A"), box("B"), box("C")], justify="around", width=360) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("A"), box("B"), box("C")], justify="evenly", width=360) ``` ##### Crecer ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box("fixed"), pz.flex_item(box("grows", w="fill"), grow=1), box("fixed")], gap=8, width=360) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.flex([box(str(i)) for i in range(1, 7)], gap=8, wrap=True, width=300) ``` #### Eventos Flex no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.flex` ```python flex( children=(), *, direction='row', justify='start', align='start', gap=0, wrap=False, padding=None, width=None, height=None, ) ``` Coloca los hijos a lo largo de un eje, como flexbox de CSS, con `justify` en el eje principal y `align` en el transversal. Usa `pz.flex_item` para que un hijo crezca o tenga un tamaño fijo. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `children` | `()` | Los widgets, o `pz.flex_item(...)` para hijos con su propio tamaño. | | `direction` | `"row"` | `"row"` o `"column"`. | | `justify` | `"start"` | `"start"`, `"center"`, `"end"`, `"between"`, `"around"` o `"evenly"`. No tiene efecto si algún hijo crece. | | `align` | `"start"` | Alineación transversal: `"start"`, `"center"` o `"end"`. | | `gap` | `0` | Espacio entre hijos. | | `wrap` | `False` | Pasa a nuevas líneas (solo filas con `justify="start"`). | | `padding` | `None` | Espacio alrededor de los hijos. | | `width` | `None` | Ancho. | | `height` | `None` | Alto. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#flex) ##### `pz.flex_item` ```python flex_item(content, *, grow=1, basis=None, align=None) ``` Da tamaño a un hijo de `pz.flex`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | El widget. | | `grow` | `1` | Parte del espacio libre en el eje principal (`0` mantiene su tamaño natural). | | `basis` | `None` | Un tamaño fijo en píxeles en el eje principal, en lugar de crecer. | | `align` | `None` | Alineación transversal de este hijo. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#flex_item) #### Relacionados - [Cuadrícula](https://albakore.github.io/pyzed/es/library/grid/): Columnas iguales, o pistas como en CSS con expansión. - [Fila](https://albakore.github.io/pyzed/es/library/row/): Coloca los hijos en horizontal. ### Gráfico de barras > Un gráfico de barras verticales con etiquetas. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/bar-chart/ :::tip[En pocas palabras] Un gráfico de barras para comparar unas pocas cantidades. ::: Un gráfico de barras verticales dibujado con `pz.canvas` en los colores del tema, con etiquetas opcionales bajo las barras. `ui.bar_chart` · Categoría: [Datos](https://albakore.github.io/pyzed/es/library/#datos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#bar_chart) **Código:** ```python ui.bar_chart([12, 19, 8, 15, 22, 17, 9], labels=["M", "T", "W", "T", "F", "S", "S"], width=360, height=180) ``` #### Cuándo usarlo - Para comparar unos pocos valores lado a lado (por día, por categoría). #### Uso ```python from pyzed import ui ui.bar_chart([12, 19, 8, 15, 22, 17, 9], labels=["M", "T", "W", "T", "F", "S", "S"], width=360, height=180) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python ui.bar_chart([12, 19, 8, 15, 22], labels=["M", "T", "W", "T", "F"], width=240, height=120) ``` ```python ui.bar_chart([4, 8, 6, 9, 3, 7, 5, 10], width=240, height=100) ``` ```python ui.bar_chart([5, 9, 7, 12], labels=["Q1", "Q2", "Q3", "Q4"], width=240, height=120, tint="success") ``` ```python ui.bar_chart([30, 12, 18], labels=["cpu", "io", "net"], width=240, height=120, tint="warning") ``` #### Eventos Gráfico de barras no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.bar_chart` ```python bar_chart(values, *, labels=None, width=320, height=160, tint='accent', theme=None) ``` Un gráfico de barras vertical con etiquetas opcionales. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `values` | *obligatorio* | Valores. | | `labels` | `None` | Etiquetas bajo cada barra. | | `width` | `320` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `160` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `tint` | `"accent"` | Color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` o un token. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#bar_chart) #### Relacionados - [Dona](https://albakore.github.io/pyzed/es/library/donut/): Un gráfico de dona con una etiqueta en el centro. - [Minigráfico](https://albakore.github.io/pyzed/es/library/sparkline/): Un gráfico de líneas diminuto para tendencias. - [Cifra](https://albakore.github.io/pyzed/es/library/stat/): Una cifra de dashboard con variación y minigráfico. ### Grupo de ajustes > Un bloque con título de filas de ajustes, para pantallas de ajustes. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/settings-group/ :::tip[En pocas palabras] Un bloque de ajustes con título, como una sección de una ventana de preferencias. ::: Un bloque con título de filas `ui.field` separadas por líneas, con una descripción opcional, como una pantalla de ajustes. `ui.settings_group` · Categoría: [Datos](https://albakore.github.io/pyzed/es/library/#datos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#settings_group) **Código:** ```python pz.container(ui.settings_group("Editor", [ ui.field("Font size", ui.number_field(14, on_change, suffix="px")), ui.field("Soft wrap", ui.switch(True, on_toggle)), ui.field("Theme", ui.select(["One Dark", "One Light"], "One Dark", on_select, width=150)), ], description="Editor preferences."), width=460) ``` #### Cuándo usarlo - Para organizar una pantalla de preferencias en secciones (Editor, Archivos, Git…). #### Uso ```python import pyzed as pz from pyzed import ui pz.container(ui.settings_group("Editor", [ ui.field("Font size", ui.number_field(14, on_change, suffix="px")), ui.field("Soft wrap", ui.switch(True, on_toggle)), ui.field("Theme", ui.select(["One Dark", "One Light"], "One Dark", on_select, width=150)), ], description="Editor preferences."), width=460) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python pz.container(ui.settings_group("Files", [ ui.field("Autosave", ui.switch(True, on_toggle)), ui.field("Trim whitespace", ui.checkbox(False, on_toggle)), ]), width=380) ``` ```python pz.container(ui.settings_group("Git", [ ui.field("Inline blame", ui.switch(False, on_toggle)), ], description="Shown in the editor gutter."), width=380) ``` ##### Estados y eventos ```python pz.container(ui.settings_group("Editor", [ui.field("Wrap lines", ui.switch(True, on_toggle))], end=ui.button("Reset", on_press, size="sm", variant="ghost")), width=420) ``` #### Eventos Grupo de ajustes no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.settings_group` ```python settings_group(title, rows, *, description=None, end=None, theme=None) ``` Un bloque con título de filas `ui.field` separadas por líneas, como una pantalla de ajustes. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | *obligatorio* | Título del grupo. | | `rows` | *obligatorio* | Las filas (normalmente `ui.field`). | | `description` | `None` | Texto secundario. | | `end` | `None` | Widget a la derecha del título (un botón Restablecer…). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#settings_group) #### Relacionados - [Campo de formulario](https://albakore.github.io/pyzed/es/library/field/): Una fila de ajustes: título y descripción a la izquierda, control a la derecha. - [Acordeón](https://albakore.github.io/pyzed/es/library/accordion/): Secciones plegables apiladas, como los paneles de una pantalla de ajustes. - [Tarjeta](https://albakore.github.io/pyzed/es/library/card/): Una superficie con borde, título y acciones opcionales. ### Grupo de alternancia > Un control segmentado solo con iconos. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/toggle-group/ :::tip[En pocas palabras] Una fila de botones con iconos en la que solo uno puede estar activo, como elegir alinear a la izquierda, al centro o a la derecha. ::: Una fila de botones de icono en la que hay exactamente uno seleccionado, como la alineación del texto en un editor. `options` son pares `(valor, icono)` y `on_select` recibe el valor elegido. `ui.toggle_group` · Categoría: [Acciones](https://albakore.github.io/pyzed/es/library/#acciones) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#toggle_group) **Código:** ```python pz.row([ ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], "center", on_align), ui.toggle_group([("list", "list"), ("grid", "layout-grid")], "grid", on_layout), ], spacing=16) ``` #### Cuándo usarlo - Para un conjunto pequeño de modos excluyentes con iconos claros. - Con etiquetas de texto, usa [Segmentado](https://albakore.github.io/pyzed/es/library/segmented/). #### Uso ```python import pyzed as pz from pyzed import ui class Toolbar(pz.App): def __init__(self): self.align = "left" def view(self): return ui.toggle_group( [("left", "align-left"), ("center", "align-center"), ("right", "align-right")], self.align, lambda value: setattr(self, "align", value), ) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python ui.toggle_group([("list", "list"), ("grid", "layout-grid")], "list", on_select) ``` ```python ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], "center", on_select) ``` ```python ui.toggle_group([("b", "bold"), ("i", "italic"), ("u", "underline"), ("s", "strikethrough")], "b", on_select) ``` ##### Selección ```python ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], "left", on_select) ``` ```python ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], "right", on_select) ``` ```python ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], None, on_select) ``` ##### Estados y eventos Un tercer elemento en una opción añade un tooltip: `(valor, icono, tooltip)`. ```python ui.toggle_group([("b", "bold"), ("i", "italic"), ("u", "underline")], {"b", "u"}, on_select, multiple=True) ``` ```python ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], "left", on_select, disabled={"right"}) ``` #### Eventos Grupo de alternancia tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_select` | `(value)` | Callable que recibe el elemento elegido. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Toolbar(pz.App): def __init__(self): self.align = "left" def set_align(self, value): # on_select gets the value of the pressed option self.align = value def view(self): return ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], self.align, self.set_align) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Align: value: str class Toolbar(pz.App): align = "left" def update(self, message): match message: case Align(value): self.align = value def view(self): return ui.toggle_group([("left", "align-left"), ("center", "align-center")], self.align, Align) # the class builds the message from the value ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: align: str = "left" def view(state, set_state): return ui.toggle_group([("left", "align-left"), ("center", "align-center"), ("right", "align-right")], state.align, lambda value: set_state(align=value)) ``` #### Propiedades y API ##### `ui.toggle_group` ```python toggle_group( options, selected, on_select=None, *, disabled=False, multiple=False, theme=None, ) ``` Un control segmentado solo con iconos. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `options` | *obligatorio* | `(valor, nombre_de_icono)` o `(valor, nombre_de_icono, tooltip)`. | | `selected` | *obligatorio* | Valor actual (una colección con `multiple`). | | `disabled` | `False` | `True`, o los valores a desactivar. | | `multiple` | `False` | Cada botón activa o desactiva su valor. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_select` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#toggle_group) #### Relacionados - [Segmentado](https://albakore.github.io/pyzed/es/library/segmented/): Un grupo de botones de texto excluyentes. - [Grupo de radios](https://albakore.github.io/pyzed/es/library/radio-group/): Opciones excluyentes como botones de radio. - [Botón de icono](https://albakore.github.io/pyzed/es/library/icon-button/): Un botón cuadrado con solo un icono y un tooltip opcional. ### Grupo de radios > Opciones excluyentes como botones de radio. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/radio-group/ :::tip[En pocas palabras] Unas cuantas opciones redondas en las que elegir una desmarca las demás. ::: Un conjunto de botones de radio con una opción elegida. `options` son cadenas o pares `(valor, etiqueta)`; `on_select` recibe el valor. `horizontal=True` los coloca en fila. `ui.radio_group` · Categoría: [Formularios](https://albakore.github.io/pyzed/es/library/#formularios) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#radio_group) **Código:** ```python pz.row([ ui.radio_group(["Spaces", "Tabs"], "Spaces", on_select), ui.radio_group([("lf", "LF"), ("crlf", "CRLF"), ("cr", "CR")], "lf", on_select, horizontal=True), ], spacing=40, align="center") ``` #### Cuándo usarlo - Cuando cada opción necesita su etiqueta y conviene verlas todas (indentación: espacios o tabuladores). - Para muchas opciones usa un [Select](https://albakore.github.io/pyzed/es/library/select/). #### Uso ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.indent = "spaces" def view(self): return ui.radio_group([("spaces", "Spaces"), ("tabs", "Tabs")], self.indent, lambda value: setattr(self, "indent", value)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Dirección ```python ui.radio_group(["Spaces", "Tabs"], "Spaces", on_select) ``` ```python ui.radio_group(["LF", "CRLF", "CR"], "LF", on_select, horizontal=True) ``` ```python ui.radio_group(["Small", "Medium", "Large"], "Medium", on_select, horizontal=True, spacing=16) ``` ##### Opciones ```python ui.radio_group([("lf", "Unix (LF)"), ("crlf", "Windows (CRLF)")], "crlf", on_select) ``` ```python ui.radio_group(["Yes", "No"], None, on_select, horizontal=True) ``` ##### Estados y eventos ```python ui.radio_group(["LF", "CRLF", "CR"], "LF", on_select, horizontal=True, disabled={"CR"}) ``` ```python ui.radio_group(["Spaces", "Tabs"], "Spaces", on_select, descriptions={"Spaces": "Indent with 4 spaces", "Tabs": "One tab per level"}) ``` ##### Widget base El widget `pz.*` de debajo: sin tema, todos los colores y tamaños son tuyos. ```python pz.row([pz.radio("Cats", True, on_select, accent="#e5c07b"), pz.radio("Dogs", False, on_select, accent="#e5c07b")], spacing=16) ``` #### Eventos Grupo de radios tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_select` | `ui.radio_group`, `pz.radio` | `(value)` | Callable que recibe el valor elegido. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Settings(pz.App): def __init__(self): self.indent = "spaces" def set_indent(self, value): # on_select gets the value, not the label self.indent = value def view(self): return ui.radio_group([("spaces", "Spaces"), ("tabs", "Tabs")], self.indent, self.set_indent) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Indent: value: str class Settings(pz.App): indent = "spaces" def update(self, message): match message: case Indent(value): self.indent = value def view(self): return ui.radio_group([("spaces", "Spaces"), ("tabs", "Tabs")], self.indent, Indent) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: indent: str = "spaces" def view(state, set_state): return ui.radio_group([("spaces", "Spaces"), ("tabs", "Tabs")], state.indent, lambda value: set_state(indent=value), horizontal=True) ``` #### Consejos :::tip Al pulsarlo o llegar con Tab el grupo tiene el foco del teclado: las flechas eligen la opción anterior o la siguiente (saltando las desactivadas) e Inicio/Fin la primera y la última. ::: #### Propiedades y API ##### `ui.radio_group` ```python radio_group( options, selected, on_select=None, *, horizontal=False, spacing=8, disabled=False, descriptions=None, id=None, theme=None, ) ``` Opciones mutuamente excluyentes como botones de radio. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `options` | *obligatorio* | Textos o pares `(valor, etiqueta)`. | | `selected` | *obligatorio* | Valor actual. | | `horizontal` | `False` | En fila en lugar de columna. | | `spacing` | `8` | Separación entre hijos, en píxeles. | | `disabled` | `False` | `True`, o los valores a desactivar. | | `descriptions` | `None` | Asocia valores a una línea atenuada bajo su etiqueta. | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_select` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#radio_group) ##### `pz.radio` ```python radio( label, selected, on_select=None, *, size=None, text_size=None, spacing=None, accent=None, bg=None, border_color=None, color=None, ) ``` Un botón de radio. Agrupa varios tú mismo (o usa `ui.radio_group`); `on_select` se envía al pulsarlo. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `label` | *obligatorio* | Texto de la etiqueta. | | `selected` | *obligatorio* | Si está marcado. | | `size` | `None` | Tamaño. | | `text_size` | `None` | Tamaño del texto. | | `spacing` | `None` | Separación entre el círculo y el texto. | | `accent` | `None` | Color de acento (parte activa). | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `border_color` | `None` | Color del borde. | | `color` | `None` | Color del texto o del icono. | Eventos: `on_select` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#radio) #### Relacionados - [Segmentado](https://albakore.github.io/pyzed/es/library/segmented/): Un grupo de botones de texto excluyentes. - [Select](https://albakore.github.io/pyzed/es/library/select/): Un desplegable para elegir una opción. - [Casilla](https://albakore.github.io/pyzed/es/library/checkbox/): Una casilla de verificación con etiqueta opcional. ### Historial de hilos > Conversaciones pasadas, con búsqueda y agrupadas por día. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/thread-history/ :::tip[En pocas palabras] La lista de tus chats anteriores, ordenados por día, con una caja para buscarlos. ::: `ui.ThreadHistory` es un componente para conversaciones pasadas: un buscador y los hilos agrupados según cuándo se actualizaron (Hoy, Ayer, Esta semana, Este mes, Antes). Cada fila muestra el título, un `detail` opcional (el modelo o el agente) y un botón para borrarlo. `threads` son diccionarios u objetos con `id`, `title`, `updated` (una marca de `time.time()`) y `detail`; `selected` marca el que está abierto. Elegir un hilo llega a `on_open` y el botón de la papelera a `on_delete`, los dos con el id del hilo en `event.value`. El componente guarda el texto de búsqueda por su cuenta. `ui.ThreadHistory` · Categoría: [Agentes de IA](https://albakore.github.io/pyzed/es/library/#agentes-de-ia) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#threadhistory) **Código:** ```python import time now = time.time() pz.container(ui.ThreadHistory(threads=[ {"id": "1", "title": "Add a login form", "updated": now - 60, "detail": "Claude"}, {"id": "2", "title": "Fix the failing tests", "updated": now - 900, "detail": "Claude"}, {"id": "3", "title": "Dark theme for the settings", "updated": now - 86400, "detail": "Gemini CLI"}, {"id": "4", "title": "Export the table to CSV", "updated": now - 4 * 86400}, {"id": "5", "title": "First steps", "updated": now - 60 * 86400}, ], selected="1", on_open=on_open, on_delete=on_delete), width=380, height=320) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - En un panel lateral o un popover de una app de chat, para volver a abrir una conversación anterior. #### Uso ```python import pyzed as pz from pyzed import ui class Chats(pz.App): def __init__(self): self.threads = load_threads() # [{"id", "title", "updated", "detail"}, ...] self.open_id = "" def delete(self, ident): self.threads = [t for t in self.threads if t["id"] != ident] def view(self): return ui.ThreadHistory(threads=self.threads, selected=self.open_id, on_open=lambda e: setattr(self, "open_id", e.value), on_delete=lambda e: self.delete(e.value)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Estado Las de búsqueda se dibujan con texto escrito en la caja (el componente lo guarda). ```python import time now = time.time() threads = [ {"id": "1", "title": "Add a login form", "updated": now - 60, "detail": "Claude"}, {"id": "2", "title": "Fix the failing tests", "updated": now - 86400}, {"id": "3", "title": "Export the table to CSV", "updated": now - 4 * 86400}, ] pz.container(ui.ThreadHistory(threads=threads), width=320, height=260) ``` ```python import time now = time.time() threads = [ {"id": "1", "title": "Add a login form", "updated": now - 60, "detail": "Claude"}, {"id": "2", "title": "Fix the failing tests", "updated": now - 86400}, {"id": "3", "title": "Export the table to CSV", "updated": now - 4 * 86400}, ] pz.container(ui.ThreadHistory(threads=threads, selected="2"), width=320, height=260) ``` ```python pz.container(ui.ThreadHistory(threads=[]), width=320, height=110) ``` ```python import time now = time.time() threads = [ {"id": "1", "title": "Add a login form", "updated": now - 60, "detail": "Claude"}, {"id": "2", "title": "Fix the failing tests", "updated": now - 86400}, {"id": "3", "title": "Export the table to CSV", "updated": now - 4 * 86400}, ] pz.container(ui.ThreadHistory(threads=threads), width=320, height=160) ``` ```python import time now = time.time() threads = [ {"id": "1", "title": "Add a login form", "updated": now - 60, "detail": "Claude"}, {"id": "2", "title": "Fix the failing tests", "updated": now - 86400}, {"id": "3", "title": "Export the table to CSV", "updated": now - 4 * 86400}, ] pz.container(ui.ThreadHistory(threads=threads), width=320, height=110) ``` #### Eventos Historial de hilos tiene **2 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_open` | `(event)` | Se eligió un hilo (`event.value` es su id). | | `on_delete` | `(event)` | Se pulsó eliminar en un hilo. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python import time class Chats(pz.App): def __init__(self): now = time.time() self.threads = [{"id": "1", "title": "Add a login form", "updated": now - 60}, {"id": "2", "title": "Fix the tests", "updated": now - 900}] self.open_id = "1" def view(self): # event.value is the thread's id return ui.ThreadHistory(threads=self.threads, selected=self.open_id, on_open=lambda e: setattr(self, "open_id", e.value), on_delete=lambda e: self.delete(e.value)) def delete(self, ident): self.threads = [t for t in self.threads if t["id"] != ident] ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class OpenThread: ident: str @dataclass class DeleteThread: ident: str class Chats(pz.App): threads = [{"id": "1", "title": "Add a login form", "updated": 0}] open_id = "" def update(self, message): match message: case OpenThread(ident): self.open_id = ident case DeleteThread(ident): self.threads = [t for t in self.threads if t["id"] != ident] def view(self): return ui.ThreadHistory(threads=self.threads, selected=self.open_id, on_open=lambda e: OpenThread(e.value), on_delete=lambda e: DeleteThread(e.value)) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: threads: tuple = ({"id": "1", "title": "Add a login form", "updated": 0},) open_id: str = "" def view(state, set_state): return ui.ThreadHistory( threads=list(state.threads), selected=state.open_id, on_open=lambda e: set_state(open_id=e.value), on_delete=lambda e: set_state(threads=tuple(t for t in state.threads if t["id"] != e.value))) ``` #### Propiedades y API ##### `ui.ThreadHistory` ```python class ThreadHistory ``` Un componente para conversaciones anteriores: un buscador y los hilos agrupados por día (Hoy, Ayer, Esta semana, Este mes, Antes), cada uno con un botón para eliminarlo. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `threads` | `None` | Dicts u objetos con `id`, `title`, `updated` (una marca de tiempo) y un `detail` opcional. | | `selected` | `""` | El id del hilo abierto. | | `theme` | `None` | Tema. | Eventos: `on_open`, `on_delete` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#threadhistory) #### Relacionados - [Fila de lista](https://albakore.github.io/pyzed/es/library/list-item/): Una fila de lista con icono, texto y un hueco al final. - [Campo de búsqueda](https://albakore.github.io/pyzed/es/library/search-input/): Un campo con lupa, para filtrar. - [Lista virtual](https://albakore.github.io/pyzed/es/library/virtual-list/): Una lista que solo construye las filas visibles, para millones de elementos. ### Icono > Iconos de Lucide por nombre e imágenes SVG. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/icon/ :::tip[En pocas palabras] Dibujos pequeños (una carpeta, una campana, un engranaje) de una biblioteca de más de 1.500. ::: `pz.icon(name)` dibuja uno de los más de 1.500 iconos de [Lucide](https://lucide.dev); `color` toma por defecto el color del texto. `pz.svg(source)` dibuja cualquier SVG desde una ruta o bytes, teñido con `color` si quieres. `pz.icon` · Categoría: [Contenido](https://albakore.github.io/pyzed/es/library/#contenido) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#icon) **Código:** ```python pz.row([ pz.icon("sparkles", size=28, color="#e5c07b"), pz.icon("git-branch", size=28, color="#a1c181"), pz.icon("folder-open", size=28, color="#74ade8"), pz.icon("bug", size=28, color="#d07277"), pz.icon("terminal", size=28, color="#c678dd"), pz.svg("logo.svg", width=40, height=40), ], spacing=20, align="center") ``` #### Cuándo usarlo - Dentro de botones, filas y cabeceras. Ejecuta `pyzed icons` para buscar los nombres. #### Uso ```python import pyzed as pz pz.row([ pz.icon("sparkles", size=28, color="#e5c07b"), pz.icon("git-branch", size=28, color="#a1c181"), pz.icon("folder-open", size=28, color="#74ade8"), pz.icon("bug", size=28, color="#d07277"), pz.icon("terminal", size=28, color="#c678dd"), pz.svg("logo.svg", width=40, height=40), ], spacing=20, align="center") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Tamaño ```python pz.icon("sparkles", size=12) ``` ```python pz.icon("sparkles", size=16) ``` ```python pz.icon("sparkles", size=24) ``` ```python pz.icon("sparkles", size=40) ``` ##### Color ```python pz.icon("git-branch", size=24, color="#a1c181") ``` ```python pz.icon("bug", size=24, color="#d07277") ``` ```python pz.icon("folder-open", size=24, color="#74ade8") ``` ##### SVG `color` pinta cada píxel visible del SVG de un color y deja los transparentes, como el tinte de un icono. Un SVG con fondo relleno, como `logo.svg`, queda como una placa lisa. ```python pz.svg("logo.svg", width=48, height=48) ``` ```python shapes = ('' '') pz.svg(shapes, width=48, height=48, color="#e5c07b") ``` #### Eventos Icono no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.icon` ```python icon(name, *, size=16, color=None) ``` Un icono de [Lucide](https://lucide.dev). `color` por defecto es el color del texto que lo rodea. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `name` | *obligatorio* | Nombre del icono (`pyzed icons` los lista). | | `size` | `16` | Tamaño en píxeles. | | `color` | `None` | Color. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#icon) ##### `pz.svg` ```python svg( source, *, width=None, height=None, color=None, content_fit=None, opacity=1.0, rotation=0.0, ) ``` Una imagen SVG. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `source` | *obligatorio* | Ruta a un `.svg`, el código SVG como texto (empieza por `<`) o `bytes`. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `color` | `None` | Si se indica, tiñe todo el SVG de ese color (útil para iconos monocromos). | | `content_fit` | `None` | `"contain"`, `"cover"`, `"fill"`, `"none"` o `"scale-down"`. | | `opacity` | `1.0` | Opacidad de 0 a 1. | | `rotation` | `0.0` | Rotación en grados. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#svg) #### Relacionados - [Botón de icono](https://albakore.github.io/pyzed/es/library/icon-button/): Un botón cuadrado con solo un icono y un tooltip opcional. - [Imagen](https://albakore.github.io/pyzed/es/library/image/): Imágenes con ajuste, opacidad y radio; un visor con zoom. ### Imagen > Imágenes con ajuste, opacidad y radio; un visor con zoom. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/image/ :::tip[En pocas palabras] Muestra una imagen de un archivo: una foto, un logo, una captura. ::: `pz.image(source)` muestra un PNG, JPEG, GIF, WebP, BMP… desde una ruta, `bytes` o píxeles RGBA con `size=(w, h)`. Opciones: `content_fit` (`"contain"`, `"cover"`, `"fill"`, `"none"`, `"scale_down"`), `filter` (`"linear"` o `"nearest"`), `opacity`, `radius`, `rotation` y `scale`. `pz.image_viewer` añade desplazamiento (arrastrar) y zoom (rueda). `pz.image` · Categoría: [Contenido](https://albakore.github.io/pyzed/es/library/#contenido) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#image) **Código:** ```python pz.row([ pz.image("photo.png", width=300, height=180, content_fit="cover", radius=10), pz.image("photo.png", width=140, height=180, content_fit="contain", opacity=0.5), ], spacing=16, align="center") ``` #### Cuándo usarlo - Para capturas, avatares, miniaturas y visores de fotos. #### Uso ```python import pyzed as pz pz.row([ pz.image("photo.png", width=300, height=180, content_fit="cover", radius=10), pz.image("photo.png", width=140, height=180, content_fit="contain", opacity=0.5), ], spacing=16, align="center") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Ajuste ```python pz.image("photo.png", width=200, height=110, content_fit="cover") ``` ```python pz.image("photo.png", width=200, height=110, content_fit="contain") ``` ```python pz.image("photo.png", width=200, height=110, content_fit="fill") ``` ##### Aspecto `rotation` va en grados, en sentido horario; las esquinas de una imagen girada pueden salir de su caja. ```python pz.image("photo.png", width=200, height=110, content_fit="cover", radius=16) ``` ```python pz.image("photo.png", width=200, height=110, content_fit="cover", opacity=0.4) ``` ```python pz.image("photo.png", width=200, height=110, content_fit="cover", radius=10, rotation=8) ``` #### Eventos Imagen no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.image` ```python image( source, *, width=None, height=None, content_fit=None, filter='linear', opacity=1.0, radius=0.0, rotation=0.0, scale=1.0, size=None, ) ``` Una imagen de mapa de bits (PNG, JPEG, GIF, BMP, WebP…) desde una ruta o `bytes`, o píxeles RGBA crudos. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `source` | *obligatorio* | Ruta del archivo, `bytes` del archivo codificado, o píxeles RGBA crudos con `size=(ancho, alto)`. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `content_fit` | `None` | `"contain"`, `"cover"`, `"fill"`, `"none"` o `"scale-down"`. | | `filter` | `"linear"` | `"linear"` (suave) o `"nearest"` (píxeles nítidos, para pixel art). | | `opacity` | `1.0` | Opacidad de 0 a 1. | | `radius` | `0.0` | Radio de las esquinas. | | `rotation` | `0.0` | Rotación en grados. | | `scale` | `1.0` | Escala. | | `size` | `None` | `(ancho, alto)` cuando `source` son píxeles RGBA crudos. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#image) ##### `pz.image_viewer` ```python image_viewer( source, *, width=None, height=None, min_scale=0.25, max_scale=10.0, scale_step=0.1, filter='linear', size=None, ) ``` Una imagen que se puede desplazar (arrastrando) y ampliar (rueda del ratón), como el visor de Iced. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `source` | *obligatorio* | Ruta del archivo, `bytes` del archivo codificado, o píxeles RGBA crudos con `size=(ancho, alto)`. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `min_scale` | `0.25` | Zoom mínimo. | | `max_scale` | `10.0` | Zoom máximo. | | `scale_step` | `0.1` | Cuánto cambia el zoom por paso de rueda. | | `filter` | `"linear"` | `"linear"` (suave) o `"nearest"` (píxeles nítidos, para pixel art). | | `size` | `None` | `(ancho, alto)` para píxeles RGBA crudos. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#image_viewer) #### Relacionados - [Icono](https://albakore.github.io/pyzed/es/library/icon/): Iconos de Lucide por nombre e imágenes SVG. - [Código QR](https://albakore.github.io/pyzed/es/library/qr-code/): Un código QR para cualquier texto o URL. ### Indicador > Un punto de color para estado, presencia o cambios sin guardar. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/indicator/ :::tip[En pocas palabras] Un puntito de color que indica un estado de un vistazo: verde es que va bien, rojo que hay un problema. ::: Un punto de color pequeño. `color` es un token del tema o un color hex y `size` su diámetro. `ui.indicator` · Categoría: [Visualización](https://albakore.github.io/pyzed/es/library/#visualización) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#indicator) **Código:** ```python pz.column([ pz.row([ui.indicator("success"), ui.label("Server running")], spacing=8, align="center"), pz.row([ui.indicator("warning"), ui.label("Reconnecting")], spacing=8, align="center"), pz.row([ui.indicator("error"), ui.label("Build failed")], spacing=8, align="center"), pz.row([ui.indicator(size=10), ui.label("Unsaved changes")], spacing=8, align="center"), ], spacing=10) ``` #### Cuándo usarlo - Junto a un nombre para mostrar presencia (conectado, ocupado) o un estado (en marcha, falló). - En una pestaña o fila para marcar cambios sin guardar. #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ pz.row([ui.indicator("success"), ui.label("Server running")], spacing=8, align="center"), pz.row([ui.indicator("warning"), ui.label("Reconnecting")], spacing=8, align="center"), pz.row([ui.indicator("error"), ui.label("Build failed")], spacing=8, align="center"), pz.row([ui.indicator(size=10), ui.label("Unsaved changes")], spacing=8, align="center"), ], spacing=10) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Color ```python ui.indicator() ``` ```python ui.indicator("success") ``` ```python ui.indicator("warning") ``` ```python ui.indicator("error") ``` ```python ui.indicator("text_muted") ``` ```python ui.indicator("#c678dd") ``` ##### Tamaño ```python ui.indicator("success", size=6) ``` ```python ui.indicator("success", size=10) ``` ```python ui.indicator("success", size=14) ``` ##### Junto a un texto ```python pz.row([ui.indicator("success"), ui.label("Ana is online")], spacing=8, align="center") ``` ```python pz.row([ui.label("notes.py"), ui.indicator(size=8)], spacing=6, align="center") ``` #### Eventos Indicador no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.indicator` ```python indicator(color='accent', *, size=6, theme=None) ``` Un punto de color (estado, cambios sin guardar, presencia). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `color` | `"accent"` | Token o color. | | `size` | `6` | Diámetro. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#indicator) #### Relacionados - [Insignia](https://albakore.github.io/pyzed/es/library/badge/): Una pastilla pequeña para estados, versiones y contadores. - [Avatar](https://albakore.github.io/pyzed/es/library/avatar/): Un avatar redondo con iniciales en un color de colaborador. ### Insignia > Una pastilla pequeña para estados, versiones y contadores. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/badge/ :::tip[En pocas palabras] Una pastilla de color diminuta con una o dos palabras, como “beta” o “3 errores”. ::: Una pastilla pequeña con texto y un icono opcional. `tint` elige un color semántico y `variant` es `"soft"` (por defecto), `"solid"` o `"outline"`. `ui.badge` · Categoría: [Visualización](https://albakore.github.io/pyzed/es/library/#visualización) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#badge) **Código:** ```python pz.row([ ui.badge("accent"), ui.badge("success", tint="success"), ui.badge("warning", tint="warning"), ui.badge("error", tint="error", icon="circle-x"), ], spacing=8) ``` #### Cuándo usarlo - Para estados cortos de solo lectura: `beta`, `M` (modificado), `v0.4`, `3 errores`. - Para un número redondo encima de un icono usa [Contador](https://albakore.github.io/pyzed/es/library/count-badge/). #### Uso ```python import pyzed as pz from pyzed import ui pz.row([ ui.badge("accent"), ui.badge("success", tint="success"), ui.badge("warning", tint="warning"), ui.badge("error", tint="error", icon="circle-x"), ], spacing=8) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Tinte ```python ui.badge("accent") ``` ```python ui.badge("passed", tint="success") ``` ```python ui.badge("pending", tint="warning") ``` ```python ui.badge("failed", tint="error") ``` ```python ui.badge("info", tint="info") ``` ```python ui.badge("custom", tint="#c678dd") ``` ##### Variante ```python ui.badge("soft") ``` ```python ui.badge("solid", variant="solid") ``` ```python ui.badge("outline", variant="outline") ``` ##### Con icono ```python ui.badge("new", icon="sparkles") ``` ```python ui.badge("3 errors", tint="error", icon="circle-x") ``` ```python ui.badge("verified", tint="success", variant="solid", icon="check") ``` ##### Estados y eventos ```python ui.badge("beta", size="sm") ``` ```python ui.badge("Pro", size="lg", icon="star") ``` ```python ui.badge("3 issues", tint="warning", on_press=on_press) ``` #### Eventos Insignia tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_press` | `()` | La hace pulsable. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Inbox(pz.App): def __init__(self): self.unread = 3 def view(self): return ui.badge(f"{self.unread} new", tint="accent", on_press=lambda: setattr(self, "unread", 0)) # on_press makes it clickable ``` **Declarativo:** ```python class Inbox(pz.App): unread = 3 def update(self, message): if message == "read all": self.unread = 0 def view(self): return ui.badge(f"{self.unread} new", on_press="read all") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: unread: int = 3 def view(state, set_state): return ui.badge(f"{state.unread} new", on_press=lambda: set_state(unread=0)) ``` #### Propiedades y API ##### `ui.badge` ```python badge( content, *, tint='accent', variant='soft', icon=None, size='md', on_press=None, theme=None, ) ``` Una píldora pequeña para estados y contadores. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `tint` | `"accent"` | Color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` o un token. | | `variant` | `"soft"` | `"soft"`, `"solid"` o `"outline"`. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `size` | `"md"` | `"sm"`, `"md"` o `"lg"`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_press` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#badge) #### Relacionados - [Contador](https://albakore.github.io/pyzed/es/library/count-badge/): Un contador redondo que muestra 99+ por encima del máximo. - [Indicador](https://albakore.github.io/pyzed/es/library/indicator/): Un punto de color para estado, presencia o cambios sin guardar. - [Chip](https://albakore.github.io/pyzed/es/library/chip/): Una etiqueta compacta: pulsable como filtro y con ✕ para quitarla. ### Interruptor > Un interruptor para ajustes que se aplican al momento. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/switch/ :::tip[En pocas palabras] Un interruptor de encendido y apagado, como el de la luz, para ajustes que cambian al momento. ::: Un interruptor de encendido y apagado con el color de acento del tema. `on_toggle` recibe el nuevo `bool`. `ui.switch` · Categoría: [Formularios](https://albakore.github.io/pyzed/es/library/#formularios) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#switch) **Código:** ```python pz.column([ ui.switch(True, on_toggle, label="Autosave"), ui.switch(False, on_toggle, label="Vim mode"), ], spacing=12) ``` #### Cuándo usarlo - Para ajustes que se aplican en el acto (Wi-Fi, autoguardado, modo oscuro). - Ponlo a la derecha de un [Campo de formulario](https://albakore.github.io/pyzed/es/library/field/) para hacer pantallas de ajustes. #### Uso ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.autosave = True def view(self): return ui.switch(self.autosave, lambda on: setattr(self, "autosave", on), label="Autosave") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Estado ```python ui.switch(True, on_toggle, label="Autosave") ``` ```python ui.switch(False, on_toggle, label="Autosave") ``` ```python ui.switch(True, label="Sync (managed)") ``` ```python ui.switch(True, on_toggle) ``` ##### Estados y eventos ```python ui.switch(True, on_toggle, label="Locked", disabled=True) ``` ```python ui.switch(True, on_toggle, label="Small", size="sm") ``` ```python ui.switch(True, on_toggle, label="Large", size="lg") ``` ##### Widget base El widget `pz.*` de debajo: sin tema, todos los colores y tamaños son tuyos. ```python pz.toggler(True, "pz.toggler", on_toggle=on_toggle, accent="#a1c181", size=22) ``` #### Eventos Interruptor tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_toggle` | `ui.switch`, `pz.toggler` | `(checked)` | Callable que recibe el nuevo `bool` (o mensaje que se envía). | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Settings(pz.App): def __init__(self): self.autosave = True def set_autosave(self, on): # on_toggle: the new state self.autosave = on def view(self): return pz.column([ ui.switch(self.autosave, self.set_autosave, label="Autosave"), ui.label("Saving as you type" if self.autosave else "Save with Ctrl+S"), ], spacing=8) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class SetAutosave: on: bool class Settings(pz.App): autosave = True def update(self, message): match message: case SetAutosave(on): self.autosave = on def view(self): return ui.switch(self.autosave, SetAutosave, label="Autosave") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: autosave: bool = True def view(state, set_state): return ui.switch(state.autosave, lambda on: set_state(autosave=on), label="Autosave") ``` #### Consejos :::tip Al pulsarlo o llegar con Tab tiene el foco del teclado (un anillo aparece al usar las teclas): Espacio o Enter lo cambian. ::: #### Propiedades y API ##### `ui.switch` ```python switch( checked, on_toggle=None, *, label=None, size='md', disabled=False, id=None, theme=None, ) ``` Un interruptor encendido/apagado. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `checked` | *obligatorio* | Estado actual. | | `label` | `None` | Texto de la etiqueta. | | `size` | `"md"` | `"sm"`, `"md"` o `"lg"`. | | `disabled` | `False` | Lo atenúa e ignora los clics. | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_toggle` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#switch) ##### `pz.toggler` ```python toggler( checked, label=None, *, on_toggle=None, size=None, text_size=None, accent=None, bg=None, border_color=None, knob_color=None, color=None, ) ``` Un interruptor encendido/apagado. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `checked` | *obligatorio* | Estado actual. | | `label` | `None` | Texto de la etiqueta. | | `size` | `None` | Alto del interruptor. | | `text_size` | `None` | Tamaño del texto. | | `accent` | `None` | Color de acento (parte activa). | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `border_color` | `None` | Color del borde. | | `knob_color` | `None` | Color del tirador. | | `color` | `None` | Color del texto o del icono. | Eventos: `on_toggle` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#toggler) #### Relacionados - [Casilla](https://albakore.github.io/pyzed/es/library/checkbox/): Una casilla de verificación con etiqueta opcional. - [Campo de formulario](https://albakore.github.io/pyzed/es/library/field/): Una fila de ajustes: título y descripción a la izquierda, control a la derecha. - [Grupo de ajustes](https://albakore.github.io/pyzed/es/library/settings-group/): Un bloque con título de filas de ajustes, para pantallas de ajustes. ### Kbd > Atajos de teclado dibujados como teclas, con símbolos en macOS. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/kbd/ :::tip[En pocas palabras] Dibuja teclas del teclado para que la gente aprenda el atajo: Ctrl + S. ::: Dibuja un atajo escrito como texto (`"ctrl-shift-p"`, acordes como `"cmd-k cmd-s"`) como teclas. En macOS usa símbolos (⌘⇧P) y palabras en el resto; `pyzed.ui.base.KEY_STYLE` fuerza `"symbols"` o `"words"`. `ui.format_keys` devuelve las etiquetas si quieres dibujarlas tú. `ui.kbd` · Categoría: [Visualización](https://albakore.github.io/pyzed/es/library/#visualización) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#kbd) **Código:** ```python pz.column([ ui.kbd("ctrl-shift-p"), ui.kbd("ctrl-k ctrl-s"), pz.row([ui.kbd("escape", size="xs"), ui.kbd("enter", size="xs"), ui.kbd("tab", size="xs")], spacing=8), ], spacing=12, align="center") ``` #### Cuándo usarlo - En menús, tooltips, paletas y pantallas de ayuda, para enseñar atajos. - Muchos componentes ya aceptan `keys=` y lo dibujan por ti (botón, entrada de menú, tooltip). #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ ui.kbd("ctrl-shift-p"), ui.kbd("ctrl-k ctrl-s"), pz.row([ui.kbd("escape", size="xs"), ui.kbd("enter", size="xs"), ui.kbd("tab", size="xs")], spacing=8), ], spacing=12, align="center") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Teclas Escribe los atajos como `"ctrl-s"`; en macOS `ctrl` pasa a ⌃ y `cmd` a ⌘ solo. ```python ui.kbd("enter") ``` ```python ui.kbd("ctrl-shift-p") ``` ```python ui.kbd("ctrl-k ctrl-s") ``` ```python pz.row([ui.kbd("escape"), ui.kbd("tab"), ui.kbd("backspace"), ui.kbd("up")], spacing=6) ``` ##### Tamaño ```python ui.kbd("ctrl-p", size="xs") ``` ```python ui.kbd("ctrl-p", size="sm") ``` #### Eventos Kbd no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.kbd` ```python kbd(keys, *, size='sm', theme=None) ``` Dibuja un atajo como teclas: `"ctrl-shift-p"`, `"cmd-k cmd-s"`. En macOS usa símbolos (⌘⇧P) y en el resto palabras. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `keys` | *obligatorio* | El atajo. | | `size` | `"sm"` | `"xs"` o `"sm"`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#kbd) ##### `ui.format_keys` ```python format_keys(keys) ``` Separa un atajo en etiquetas por acorde: `format_keys("ctrl-shift-p") == [["Ctrl", "Shift", "P"]]`. El estilo se controla con `pyzed.ui.base.KEY_STYLE` (`"auto"`, `"symbols"` o `"words"`). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `keys` | *obligatorio* | El atajo. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#format_keys) ##### `ui.keys_text` ```python keys_text(keys) ``` Un atajo como texto plano para una etiqueta o una línea de estado: `keys_text("ctrl-shift-p") == "Ctrl+Shift+P"` (`"⌃⇧P"` con los símbolos de macOS), los acordes separados por un espacio. Los tooltips, botones y menús toman `keys=` y dibujan las teclas, así que no escribas las teclas en su texto. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `keys` | *obligatorio* | El atajo. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#keys_text) #### Relacionados - [Pista de atajo](https://albakore.github.io/pyzed/es/library/keybinding-hint/): Una pista discreta tipo “Abrir archivo Ctrl P”. - [Atajos](https://albakore.github.io/pyzed/es/library/shortcuts/): Una lista de atajos agrupada y con búsqueda. - [Tooltip](https://albakore.github.io/pyzed/es/library/tooltip/): Una pista que aparece al pasar el ratón, con su atajo opcional. ### Lienzo > Dibujo 2D con formas, trazos, texto y eventos de ratón. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/canvas/ :::tip[En pocas palabras] Una hoja en blanco para dibujar formas, líneas y texto, para gráficos y juegos. ::: Una superficie de dibujo 2D. `shapes` es una lista hecha con [`pyzed.draw`](https://albakore.github.io/pyzed/es/reference/draw/) (rectángulos, círculos, arcos, trazos, texto, imágenes…). Los callbacks del ratón reciben coordenadas relativas al lienzo; con un `id` la geometría se guarda entre fotogramas. Los gráficos de esta biblioteca están dibujados con él. `pz.canvas` · Categoría: [Contenido](https://albakore.github.io/pyzed/es/library/#contenido) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#canvas) **Código:** ```python pz.canvas([ draw.rect(10, 10, 120, 80, fill="#74ade8", radius=8), draw.circle(200, 50, 40, fill="#a1c181"), draw.arc(300, 50, 40, 0, 270, stroke="#e5c07b", width=8, cap="round"), draw.polygon([(360, 90), (400, 10), (440, 90)], fill="#d07277"), draw.line(10, 120, 440, 120, stroke="#5d636f", width=2, dash=[6, 4]), draw.text(225, 150, "pz.canvas + pyzed.draw", color="#a9afbc", size=16, align_x="center"), ], width=450, height=170) ``` #### Cuándo usarlo - Para gráficos, juegos, diagramas, apps de dibujo y widgets propios. #### Uso ```python import pyzed as pz from pyzed import draw pz.canvas([ draw.rect(10, 10, 120, 80, fill="#74ade8", radius=8), draw.circle(200, 50, 40, fill="#a1c181"), draw.arc(300, 50, 40, 0, 270, stroke="#e5c07b", width=8, cap="round"), draw.polygon([(360, 90), (400, 10), (440, 90)], fill="#d07277"), draw.line(10, 120, 440, 120, stroke="#5d636f", width=2, dash=[6, 4]), draw.text(225, 150, "pz.canvas + pyzed.draw", color="#a9afbc", size=16, align_x="center"), ], width=450, height=170) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Formas ```python pz.canvas([draw.rect(10, 10, 140, 70, fill="#74ade8", radius=10)], width=160, height=90) ``` ```python pz.canvas([draw.circle(50, 45, 36, fill="#a1c181")], width=100, height=90) ``` ```python pz.canvas([draw.arc(50, 45, 34, 0, 270, stroke="#e5c07b", width=8, cap="round")], width=100, height=90) ``` ```python pz.canvas([draw.polygon([(10, 80), (50, 10), (90, 80)], fill="#d07277")], width=100, height=90) ``` ```python pz.canvas([draw.line(10, 45, 190, 45, stroke="#c678dd", width=3, dash=[8, 5])], width=200, height=90) ``` ```python pz.canvas([draw.text(100, 45, "pyzed.draw", color="#74ade8", size=20, align_x="center", align_y="center")], width=200, height=90) ``` #### Eventos Lienzo tiene **5 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_press` | `(x, y)` | Callable que recibe `(x, y)` al pulsar. | | `on_release` | `(x, y)` | Callable que recibe `(x, y)` al soltar. | | `on_right_press` | `(x, y)` | Callable que recibe `(x, y)` con clic derecho. | | `on_move` | `(x, y)` | Se llama con `(x, y)` al mover el ratón. | | `on_scroll` | `(dx, dy)` | Callable que recibe `(dx, dy)` en líneas al usar la rueda. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Paint(pz.App): def __init__(self): self.dots, self.drawing = [], False def press(self, x, y): # on_press / on_move get the position self.drawing = True self.dots.append((x, y)) def move(self, x, y): if self.drawing: self.dots.append((x, y)) def view(self): return pz.canvas([draw.circle(x, y, 3, fill="#74ade8") for x, y in self.dots], width=420, height=200, cursor="crosshair", on_press=self.press, on_move=self.move, on_release=lambda x, y: setattr(self, "drawing", False), on_right_press=lambda x, y: self.dots.clear()) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Dot: x: float y: float class Paint(pz.App): dots = [] def update(self, message): match message: case Dot(x, y): self.dots = self.dots + [(x, y)] case "clear": self.dots = [] def view(self): return pz.canvas([draw.circle(x, y, 3, fill="#74ade8") for x, y in self.dots], width=420, height=200, on_press=Dot, on_right_press=lambda x, y: "clear") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: dots: tuple = () zoom: float = 1.0 def view(state, set_state): shapes = [draw.circle(x, y, 3 * state.zoom, fill="#74ade8") for x, y in state.dots] return pz.canvas(shapes, width=420, height=200, on_press=lambda x, y: set_state(dots=state.dots + ((x, y),)), on_scroll=lambda dx, dy: set_state(zoom=max(0.5, state.zoom + dy * 0.1))) ``` #### Propiedades y API ##### `pz.canvas` ```python canvas( shapes=None, *, width=None, height=None, id=None, on_press=None, on_release=None, on_right_press=None, on_move=None, on_scroll=None, cursor=None, ) ``` Una superficie de dibujo 2D. `shapes` es una lista creada con [`pyzed.draw`](https://albakore.github.io/pyzed/es/reference/draw/). Los callbacks del ratón reciben coordenadas relativas al canvas. Con un `id`, la geometría se cachea entre frames y solo se recalcula cuando cambian las formas. Ver la guía de [dibujo con canvas](https://albakore.github.io/pyzed/es/guides/canvas/). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `shapes` | `None` | Lista de formas de `pyzed.draw` (se aceptan listas anidadas). | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `id` | `None` | Activa la caché de geometría. | | `cursor` | `None` | Cursor del ratón (`"crosshair"`, `"pointer"`…). | Eventos: `on_press`, `on_release`, `on_right_press`, `on_move`, `on_scroll` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#canvas) #### Relacionados - [Gráfico de barras](https://albakore.github.io/pyzed/es/library/bar-chart/): Un gráfico de barras verticales con etiquetas. - [Dona](https://albakore.github.io/pyzed/es/library/donut/): Un gráfico de dona con una etiqueta en el centro. - [Pila](https://albakore.github.io/pyzed/es/library/stack/): Superpone los hijos; `pin` coloca uno en x, y. ### Límite de errores > Dibuja una parte de la vista, o una tarjeta de error en su lugar cuando falla. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/error-boundary/ :::tip[En pocas palabras] Una red de seguridad para una parte de la ventana: si esa parte se rompe, ahí aparece una cajita de error y el resto de la app sigue funcionando. ::: `ui.error_boundary(build)` devuelve `build()` o, cuando lanza una excepción, una tarjeta de error en su lugar, así una parte rota de la vista (una barra lateral, un panel) nunca tumba la ventana entera. La tarjeta muestra el error, el archivo y la línea de donde viene y un botón Copiar el error; `on_details` añade un botón Detalles y `fallback` cambia la tarjeta por tu propio widget. `on_error(info)` recibe el error cada vez que la parte no se puede dibujar. `ui.ErrorInfo` describe un error (su tipo, mensaje, traceback y origen) y `ui.error_card` es la tarjeta sola. `ui.error_boundary` · Categoría: [Estado y avisos](https://albakore.github.io/pyzed/es/library/#estado-y-avisos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#error_boundary) **Código:** ```python def totals(): values = [] return ui.label(f"Average: {sum(values) / len(values)}") pz.row([ ui.card(ui.muted("This panel keeps working."), title="Files", width=200), pz.container(ui.error_boundary(totals, title="Totals", on_details=on_details), width=380), ], spacing=12, align="center") ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Alrededor de cada panel de una ventana grande, para que un fallo en uno deje los demás funcionando. - Alrededor de partes que dibujan datos que no controlas: un plugin, un archivo que abrió el usuario, la respuesta de un servidor. - Abre un [Diálogo de error](https://albakore.github.io/pyzed/es/library/error-dialog/) desde `on_details` para enseñar el traceback completo. #### Uso ```python import pyzed as pz from pyzed import ui class Report(pz.App): def __init__(self): self.error = None # the ui.ErrorInfo shown in the dialog def totals(self): ... # may raise def show(self, info): self.error = info def close(self): self.error = None def view(self): page = pz.row([ self.files(), ui.error_boundary(self.totals, title="Totals", on_details=self.show), ]) dialog = ui.error_dialog(self.error, self.close) if self.error else None return ui.dialog_layer(page, dialog, on_dismiss=self.close) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Límite Mientras `build()` funciona, el límite devuelve su widget tal cual; solo se reemplaza una parte que lanza una excepción. ```python ui.error_boundary(lambda: ui.label("Average: 4.2")) ``` ```python def totals(): values = [] return ui.label(f"Average: {sum(values) / len(values)}") pz.container(ui.error_boundary(totals), width=360) ``` ```python def totals(): values = [] return ui.label(f"Average: {sum(values) / len(values)}") pz.container(ui.error_boundary(totals, title="Totals"), width=360) ``` ```python def totals(): values = [] return ui.label(f"Average: {sum(values) / len(values)}") pz.container(ui.error_boundary(totals, title="Totals", on_details=on_details), width=360) ``` ```python def totals(): values = [] return ui.label(f"Average: {sum(values) / len(values)}") ui.error_boundary(totals, fallback=lambda info: ui.callout( "Totals are not available", info.summary, tint="warning")) ``` ##### Tarjeta de error `ui.error_card` es la tarjeta sola, para los errores que capturas tú. ```python info = ui.ErrorInfo("KeyError", "'email'", origin=("app/forms.py", 42), where="Sign-up form") pz.container(ui.error_card(info), width=360) ``` ```python info = ui.ErrorInfo("KeyError", "'email'", origin=("app/forms.py", 42), where="Sign-up form") pz.container(ui.error_card(info, title="The form could not be loaded", actions=[ui.button("Retry", on_retry, icon="rotate-ccw", size="sm", variant="ghost")]), width=360) ``` ```python pz.container(ui.error_card(ui.ErrorInfo("TimeoutError", "the server took too long")), width=360) ``` #### Eventos Límite de errores tiene **2 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_error` | `ui.error_boundary` | `(info)` | Recibe el `ui.ErrorInfo`. | | `on_details` | `ui.error_boundary`, `ui.error_card` | `(info)` | Añade el botón Detalles; recibe el `ui.ErrorInfo` (abre un `ui.error_dialog`). | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Report(pz.App): def __init__(self): self.values = [] self.error = None # the ui.ErrorInfo shown in the dialog def totals(self): return ui.label(f"Average: {sum(self.values) / len(self.values)}") # raises on [] def view(self): page = ui.error_boundary(self.totals, title="Totals", on_error=lambda info: print(info.message), # every failure on_details=lambda info: setattr(self, "error", info)) # Details dialog = ui.error_dialog(self.error, lambda: setattr(self, "error", None)) if self.error else None return ui.dialog_layer(page, dialog) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class ShowError: info: object class Report(pz.App): error = None def totals(self): return ui.label(f"Average: {sum([]) / len([])}") def update(self, message): match message: case ShowError(info): self.error = info case "close": self.error = None def view(self): page = ui.error_boundary(self.totals, title="Totals", on_details=ShowError) return ui.dialog_layer(page, ui.error_dialog(self.error, "close") if self.error else None) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: values: tuple = () failures: int = 0 def totals(values): return ui.label(f"Average: {sum(values) / len(values)}") def view(state, set_state): return pz.column([ ui.error_boundary(lambda: totals(state.values), title="Totals", on_error=lambda info: set_state(failures=state.failures + 1)), ui.button("Add 10", lambda: set_state(values=state.values + (10,))), ], spacing=8, width=380) ``` #### Consejos :::tip El límite ejecuta `build()` en cada redibujado, así que `on_error` se llama otra vez cada vez que la parte falla. Para avisar de un error una sola vez, compáralo con el anterior: dos `ErrorInfo` con el mismo tipo, mensaje, traceback y origen son iguales (`time` no cuenta). `ErrorInfo.from_exception(err)` describe cualquier excepción que captures tú, e `info.report()` da el texto para pegar en un informe de error. ::: #### Propiedades y API ##### `ui.error_boundary` ```python error_boundary( build, *, on_error=None, title=None, where='', on_details=None, fallback=None, theme=None, ) ``` Dibuja `build()`, o una tarjeta de error si falla, para que una parte de la vista que falla nunca tumbe la ventana entera (como los error boundaries de React). `on_error(info)` se entera del error cada vez que la parte se dibuja y falla, así que regístralo una sola vez (compara `info.traceback`). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `build` | *obligatorio* | Una función que devuelve el widget de la parte. | | `title` | `None` | Título de la tarjeta (y el `where` del error). | | `where` | `""` | Qué es la parte, para el informe. | | `fallback` | `None` | Tu propio widget para el error: `fallback(info)`. | | `theme` | `None` | Tema a usar. | Eventos: `on_error`, `on_details` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#error_boundary) ##### `ui.error_card` ```python error_card(info, *, title=None, on_details=None, actions=(), theme=None) ``` La tarjeta que dibuja `ui.error_boundary` en lugar de lo que falló: el error, de dónde viene y el botón Copiar el error (y Detalles con `on_details`). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `info` | *obligatorio* | Un `ui.ErrorInfo`. | | `title` | `None` | Título. | | `actions` | `()` | Más botones tras ellos. | | `theme` | `None` | Tema a usar. | Eventos: `on_details` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#error_card) ##### `ui.ErrorInfo` ```python @dataclass class ErrorInfo(kind, message, traceback='', origin=None, where='', time=0.0) ``` Un error tal como lo muestran `ui.error_card` y `ui.error_dialog`: `kind` (el tipo de la excepción), `message`, el `traceback` completo, el `origin` `(ruta, línea)` del que viene (el último frame fuera de la librería estándar, los paquetes instalados y Pyzed), `where` pasó y `time`. `ErrorInfo.from_exception(err, where=...)` crea uno (`prefer=` elige qué frames pueden ser el origen); `ErrorInfo.current()` describe la excepción que se está manejando. `summary` es `Tipo: mensaje`, `location` es `archivo.py:12` y `report()` es todo como texto para un informe de error. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `kind` | *obligatorio* | El nombre del tipo de la excepción. | | `message` | *obligatorio* | Su mensaje. | | `traceback` | `""` | El traceback completo, como lo imprime Python. | | `origin` | `None` | `(ruta, línea)` del código del que viene, o `None`. | | `where` | `""` | Qué se estaba ejecutando. | | `time` | `0.0` | Cuándo pasó (`time.time()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#errorinfo) #### Relacionados - [Diálogo de error](https://albakore.github.io/pyzed/es/library/error-dialog/): Qué salió mal, dónde y el traceback, con Copiar e Ir al origen. - [Aviso](https://albakore.github.io/pyzed/es/library/callout/): Un aviso en línea con título, texto y acciones. - [Consola](https://albakore.github.io/pyzed/es/library/console/): Un registro estilo dev tools con niveles, filtro, copiar y limpiar. ### Lista virtual > Una lista que solo construye las filas visibles, para millones de elementos. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/virtual-list/ :::tip[En pocas palabras] Una lista que sigue siendo rápida con millones de filas, porque solo dibuja lo que ves. ::: Una lista con scroll para miles o millones de elementos. Solo se construyen las filas visibles: `render(elemento)` se llama cuando un elemento entra en la vista, así que la lista cuesta lo mismo sea cual sea su longitud. Todas las filas tienen la misma `item_height`. `pz.virtual_column` es la pieza de debajo (filas por índice, dentro de cualquier `scrollable`), y la [Tabla](https://albakore.github.io/pyzed/es/library/table/) también la usa para sus filas. `ui.virtual_list` · Categoría: [Listas y menús](https://albakore.github.io/pyzed/es/library/#listas-y-menús) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#virtual_list) **Código:** ```python files = [f"src/module_{i:06}.py" for i in range(1_000_000)] pz.container(ui.virtual_list( files, lambda f: ui.list_item(f, icon="file-code", selected=f.endswith("02.py")), height=240, ), width=420) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para logs, resultados de búsqueda, listas de archivos o cualquier lista que pueda pasar de unos cientos de filas. - Para listas cortas, una `pz.column` de [Filas](https://albakore.github.io/pyzed/es/library/list-item/) es más sencilla. #### Uso ```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") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python pz.container(ui.virtual_list([f"row {i:,}" for i in range(1_000_000)], lambda r: ui.list_item(r, icon="file"), height=150), width=260) ``` ```python pz.container(ui.virtual_list([f"[{i:06}] request handled" for i in range(100_000)], lambda line: pz.text(line, font="mono", size=12), item_height=20, height=150), width=260) ``` ```python pz.container(ui.virtual_list(list(range(10_000)), lambda i: ui.card(ui.label(f"Card {i}"), padding=8), item_height=40, spacing=6, height=150), width=260) ``` #### Ejemplos ##### Filas por índice `pz.virtual_column(count, item, item_height=...)` llama a `item(índice)`, útil cuando las filas salen de un array, un cursor de base de datos o un cálculo. Se desplaza como cualquier `scrollable`: la fila `i` empieza en `i * (item_height + spacing)`. **Código:** ```python pz.container(pz.scrollable( pz.virtual_column(10_000_000, lambda i: pz.text(f"{i:>10,} {i * i:>22,}", font="mono", size=12), item_height=22), height=150, ), width=420) ``` #### Eventos Lista virtual tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_scroll` | `(info)` | Callable que recibe el viewport del scroll, como en `pz.scrollable`. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Logs(pz.App): def __init__(self): self.lines = [f"[{i:06}] request handled" for i in range(100_000)] self.top = 0 def scrolled(self, info): # on_scroll: offsets and sizes of the viewport self.top = int(info["y"] // 20) def view(self): return pz.column([ ui.label(f"Line {self.top + 1} of {len(self.lines)}"), ui.virtual_list(self.lines, lambda line: pz.text(line, font="mono", size=12), item_height=20, height=200, on_scroll=self.scrolled), ], spacing=6) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Scrolled: info: dict class Logs(pz.App): lines = [f"[{i:06}] request handled" for i in range(100_000)] top = 0 def update(self, message): match message: case Scrolled(info): self.top = int(info["y"] // 20) def view(self): return ui.virtual_list(self.lines, lambda line: pz.text(line, size=12), item_height=20, height=200, on_scroll=Scrolled) ``` **Funcional:** ```python from dataclasses import dataclass LINES = [f"[{i:06}] request handled" for i in range(100_000)] @dataclass(frozen=True) class State: top: int = 0 def view(state, set_state): return pz.column([ ui.label(f"Line {state.top + 1}"), ui.virtual_list(LINES, lambda line: pz.text(line, size=12), item_height=20, height=200, on_scroll=lambda info: set_state(top=int(info["y"] // 20))), ], spacing=6) ``` #### Consejos :::tip - Las filas se vuelven a construir cuando vuelven a la vista, así que mantén `render` ligero o envuélvelo en `@pz.memo`. - `items` debería ser una lista u otra secuencia: otros iterables se copian a una lista en cada `view()`. ::: #### Propiedades y API ##### `ui.virtual_list` ```python virtual_list( items, render, *, item_height=26, spacing=0, width='fill', height='fill', padding=None, overscan=4, id=None, on_scroll=None, theme=None, ) ``` Una lista con scroll que solo construye las filas visibles, para miles o millones de elementos. `render(elemento)` devuelve el widget de un elemento y se llama cuando entra en la vista. Todas las filas tienen la misma altura. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `items` | *obligatorio* | Los elementos: una lista o cualquier secuencia (otros iterables se convierten en lista). | | `render` | *obligatorio* | Función que recibe un elemento y devuelve su widget. | | `item_height` | `26` | Altura de cada fila, en píxeles. | | `spacing` | `0` | Separación entre hijos, en píxeles. | | `width` | `"fill"` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `"fill"` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `padding` | `None` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `overscan` | `4` | Filas extra construidas por encima y por debajo de las visibles. | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_scroll` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#virtual_list) ##### `pz.virtual_column` ```python virtual_column(count, item, *, item_height, spacing=0.0, overscan=4, width=None) ``` Una columna de `count` filas de la misma altura que solo construye las filas visibles: `item(índice)` se llama cuando la fila entra en la vista, y las que salen se descartan. Una columna de un millón de filas cuesta lo mismo que una de cincuenta. Ponla dentro de un `scrollable` (se virtualiza contra el área visible del más cercano, o de la ventana). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `count` | *obligatorio* | Número de filas. | | `item` | *obligatorio* | Función que recibe el índice de una fila y devuelve su widget. | | `item_height` | *obligatorio* | Altura de cada fila, en píxeles. | | `spacing` | `0.0` | Espacio entre filas, en píxeles. | | `overscan` | `4` | Filas extra construidas por encima y por debajo de las visibles, para que el scroll rápido no deje huecos. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#virtual_column) #### Relacionados - [Fila de lista](https://albakore.github.io/pyzed/es/library/list-item/): Una fila de lista con icono, texto y un hueco al final. - [Tabla](https://albakore.github.io/pyzed/es/library/table/): Una tabla de datos con cabecera fija, ordenación y selección. ### Llamada a herramienta > Un paso de un agente de IA: qué hizo, su resultado y Permitir o Rechazar. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/tool-call/ :::tip[En pocas palabras] Una cosa que hizo un asistente de IA, como leer o cambiar un archivo. Púlsala para ver los detalles; si necesita tu permiso, pregunta con botones de Permitir y Rechazar. ::: `ui.ToolCall` es un componente para una acción de un agente de IA (leer un archivo, editarlo, ejecutar un comando), dibujado como en Zed: una línea con el estado, el título (`title`) y sobre qué trabaja (`detail`), y un clic despliega la entrada (`input`) y la salida (`output`), más las imágenes (`images`) si las hay. Las ediciones, los comandos (`kind` `edit`, `delete`, `move` o `execute`) y las llamadas que esperan son tarjetas. `diffs` muestra los cambios que hizo como diferencias de colores y, mientras se ejecuta, `progress` muestra las últimas líneas que imprimió un comando. Mientras `status` es `"waiting"` pregunta en la misma tarjeta: Permitir siempre, Permitir o Rechazar, que llegan a `on_allow`, `on_allow_always` y `on_deny`. El componente recuerda si está desplegado; tu app solo guarda los datos. `ui.ToolCall` · Categoría: [Agentes de IA](https://albakore.github.io/pyzed/es/library/#agentes-de-ia) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#toolcall) **Código:** ```python pz.column([ ui.ToolCall(title="Read file", detail="app/storage.py", kind="read", output="def save(path): ..."), ui.ToolCall(title="Search", detail="def save", kind="search", output="app/storage.py:12"), ui.ToolCall(title="Edit file", detail="app/storage.py", kind="edit", status="waiting", diffs=[{"path": "app/storage.py", "old": "def save(path):\n write(path)\n", "new": "def save(path, data):\n write(path, data)\n"}], on_allow=on_allow, on_allow_always=on_allow_always, on_deny=on_deny), ui.ToolCall(title="Run", detail="pytest -q", kind="execute", status="running", progress="collected 24 items\n........................"), ], spacing=4, width=560) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - En el chat de un agente, una por cada paso que da. - Con `status="waiting"`, para preguntar antes de un paso delicado como escribir archivos o ejecutar un comando. - Para un cambio suelto, fuera de una llamada a herramienta, usa la [Vista de diferencias](https://albakore.github.io/pyzed/es/library/diff-view/). #### Uso ```python import pyzed as pz from pyzed import ui class Chat(pz.App): def __init__(self): self.status = "waiting" def answer(self, allowed): self.status = "done" if allowed else "rejected" def view(self): return ui.ToolCall(title="Run", detail="pytest -q", kind="execute", status=self.status, input={"command": "pytest -q"}, output="24 passed", on_allow=lambda e: self.answer(True), on_deny=lambda e: self.answer(False)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Estado Pendiente y en marcha muestran un spinner, esperando pregunta con Permitir, Permitir siempre y Rechazar, y fallida y rechazada lo dicen en rojo. ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="pending"), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="running"), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="done"), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="waiting", on_allow=on_allow, on_allow_always=on_allow_always, on_deny=on_deny), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="failed"), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="rejected"), width=360) ``` ##### Tipo El tipo elige el icono de una llamada terminada. ```python pz.container(ui.ToolCall(title="Read file", detail="app.py", kind="read"), width=300) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit"), width=300) ``` ```python pz.container(ui.ToolCall(title="Delete file", detail="old.py", kind="delete"), width=300) ``` ```python pz.container(ui.ToolCall(title="Move file", detail="a.py → b.py", kind="move"), width=300) ``` ```python pz.container(ui.ToolCall(title="Search", detail="def save", kind="search"), width=300) ``` ```python pz.container(ui.ToolCall(title="Run", detail="pytest -q", kind="execute"), width=300) ``` ```python pz.container(ui.ToolCall(title="Think", kind="think"), width=300) ``` ```python pz.container(ui.ToolCall(title="Fetch", detail="https://example.com", kind="fetch"), width=300) ``` ```python pz.container(ui.ToolCall(title="Switch mode", detail="plan", kind="switch_mode"), width=300) ``` ```python pz.container(ui.ToolCall(title="Tool", detail="custom", kind="other"), width=300) ``` ##### Contenido Las desplegadas se dibujan como quedan tras un clic en la cabecera. ```python pz.container(ui.ToolCall(title="Run", detail="pytest -q", kind="execute", status="running", progress="collected 24 items\n............\n............"), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", status="running", diffs=[{"path": "app.py", "old": "x = 1\ny = 2\n", "new": "x = 10\ny = 2\n"}]), width=360) ``` ```python pz.container(ui.ToolCall(title="Run", detail="rm -rf build", kind="execute", status="waiting", allow_always=False, on_allow=on_allow, on_deny=on_deny), width=360) ``` ```python pz.container(ui.ToolCall(title="Read file", detail="app.py", kind="read", input={"path": "app.py"}, output="import pyzed as pz\n\npz.run(App)"), width=360) ``` ```python pz.container(ui.ToolCall(title="Edit file", detail="app.py", kind="edit", diffs=[{"path": "app.py", "old": "x = 1\ny = 2\n", "new": "x = 10\ny = 2\n"}]), width=360) ``` #### Eventos Llamada a herramienta tiene **3 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_allow` | `(event)` | Se pulsó Permitir. | | `on_allow_always` | `(event)` | Se pulsó Permitir siempre. | | `on_deny` | `(event)` | Se pulsó Rechazar. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Chat(pz.App): def __init__(self): self.status = "waiting" def answer(self, status): self.status = status def view(self): # a pz.Component: handlers get a pz.Event return ui.ToolCall(title="Run", detail="pytest -q", kind="execute", status=self.status, input={"command": "pytest -q"}, on_allow=lambda e: self.answer("done"), on_allow_always=lambda e: self.answer("done"), on_deny=lambda e: self.answer("rejected")) ``` **Declarativo:** ```python class Chat(pz.App): status = "waiting" def update(self, message): match message: case "allow": self.status = "done" case "deny": self.status = "rejected" def view(self): return ui.ToolCall(title="Run", detail="pytest -q", kind="execute", status=self.status, on_allow="allow", on_deny="deny") # messages work for components too ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: status: str = "waiting" def view(state, set_state): return ui.ToolCall(title="Run", detail="pytest -q", kind="execute", status=state.status, on_allow=lambda e: set_state(status="done"), on_deny=lambda e: set_state(status="rejected")) ``` #### Consejos :::tip `status` es tuyo: ponlo según lo que diga el agente (`pending`, luego `running`, luego `done` o `failed`; `rejected` tras un Rechazar). `kind` solo elige el icono: `read`, `edit`, `delete`, `move`, `search`, `execute`, `think`, `fetch`, `switch_mode` u `other`. La cabecera solo se despliega cuando hay algo que mostrar (`input`, `output`, `images` o `diffs`). ::: #### Propiedades y API ##### `ui.ToolCall` ```python class ToolCall ``` Un componente para una acción de un agente de IA, dibujada como en el panel Agent de Zed: una línea con su icono, lo que hace y dónde (se despliega para mostrar la entrada y la salida), o una tarjeta para las ediciones y los comandos, con el diff o la salida. Mientras `status` es `"waiting"` pregunta en la propia tarjeta: Permitir siempre, Permitir o Rechazar. Mira [Agentes de IA](https://albakore.github.io/pyzed/es/editor/ai-agents/). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | `""` | Lo que hace ("Editar archivo"). | | `detail` | `""` | Sobre qué trabaja, en letra monoespaciada. | | `kind` | `""` | El tipo de ACP: `read`, `edit`, `delete`, `move`, `search`, `execute`, `think`, `fetch` u `other` (su icono). | | `status` | `"done"` | `pending`, `running`, `waiting`, `done`, `failed`, `rejected` o `canceled`. | | `input` | `None` | Los argumentos (un dict o texto). | | `output` | `None` | La respuesta (texto o un dict). | | `images` | `None` | Bytes PNG que se muestran bajo la salida. | | `diffs` | `None` | Los cambios que hizo: dicts `{"path", "old", "new"}`, que se muestran como diffs de colores (mira `ui.diff_view`). | | `progress` | `""` | Lo que imprimió hasta ahora (la salida de un comando); sus últimas líneas se ven bajo la cabecera mientras corre. | | `allow_always` | `True` | Muestra Permitir siempre. | | `theme` | `None` | Tema. | Eventos: `on_allow`, `on_allow_always`, `on_deny` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#toolcall) #### Relacionados - [Vista de diferencias](https://albakore.github.io/pyzed/es/library/diff-view/): Un cambio como líneas de colores: lo añadido en verde, lo quitado en rojo. - [Plan](https://albakore.github.io/pyzed/es/library/plan/): El plan de un agente como una lista que se pliega. - [Barra de cambios](https://albakore.github.io/pyzed/es/library/edits-bar/): Los archivos que cambió un agente, con Conservar todo y Rechazar todo. - [Medidor de contexto](https://albakore.github.io/pyzed/es/library/context-meter/): Cuánto se llenó la ventana de contexto de un modelo: un anillo y `48k / 200k`. ### Marco de ventana > Tu propia barra de título, botones de ventana y menús. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/window-frame/ :::tip[En pocas palabras] Dibuja la barra superior de la propia ventana, con sus menús y los botones de minimizar, maximizar y cerrar. ::: Dibuja el marco completo de una ventana abierta sin decoraciones del sistema (`frame = "custom"`): una barra de título que mueve la ventana (doble clic la maximiza), los botones de minimizar, maximizar y cerrar con el estilo de una plataforma, menús a la izquierda (`ui.Menu`) y bordes que cambian el tamaño de la ventana. `ui.window_title_bar` es la barra sola, `ui.window_controls` los tres botones y `ui.system_platform()` dice qué estilo usa este sistema. `ui.window_frame` · Categoría: [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/#espacio-de-trabajo) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#window_frame) **Código:** ```python ui.window_frame( ui.editor("frame-main", "import pyzed as pz\n\npz.run(App)\n", path="main.py"), title="notes.py", menus=[ui.Menu("File", [ui.MenuItem("Open…", on_open, keys="ctrl-o")]), ui.Menu("Edit", [ui.MenuItem("Undo", on_undo, keys="ctrl-z")])], start=[ui.title_button("pyzed", on_projects, chevron=True)], style="windows", ) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para apps que quieren una ventana como la de un editor moderno: menús y selectores en la barra de título en lugar del marco del sistema. Mira [Marcos de ventana](https://albakore.github.io/pyzed/es/guides/windows/#marcos-de-ventana-tu-propia-barra-de-título). - Sin callbacks, `ui.window_title_bar` y `ui.window_controls` solo se muestran: úsalos para previsualizar una ventana con el marco de cada plataforma. #### Uso ```python import pyzed as pz from pyzed import ui class Notes(pz.App): title = "Notes" frame = "custom" # or "macos", "windows", "gnome", "ubuntu", "kde" def view(self): return ui.window_frame( self.editor(), menus=[ui.Menu("File", [ui.MenuItem("Open…", self.open, keys="ctrl-o"), ui.SEPARATOR, ui.MenuItem("Quit", self.close)])], start=[ui.title_button("pyzed", chevron=True)], end=[ui.button("Run", self.run, icon="play", size="sm")], ) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Estilo de plataforma ```python pz.container(ui.window_title_bar("notes.py", style="windows"), width=420) ``` ```python pz.container(ui.window_title_bar("notes.py", style="macos"), width=420) ``` ```python pz.container(ui.window_title_bar("notes.py", style="gnome"), width=420) ``` ```python pz.container(ui.window_title_bar("notes.py", style="kde"), width=420) ``` ```python pz.container(ui.window_title_bar("notes.py", style="ubuntu"), width=420) ``` ##### Estado ```python pz.container(ui.window_title_bar("notes.py", style="windows", focused=False), width=420) ``` ```python ui.window_controls("windows", maximized=True) ``` ```python ui.window_controls("macos") ``` #### Eventos Marco de ventana tiene **5 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_drag` | `ui.window_title_bar` | `()` | Se llama al arrastrar la parte vacía de la barra (devuelve `Command.drag_window()`). | | `on_minimize` | `ui.window_title_bar`, `ui.window_controls` | `()` | Botón de minimizar. | | `on_maximize` | `ui.window_title_bar`, `ui.window_controls` | `()` | Botón de maximizar y doble clic en la barra. | | `on_close` | `ui.window_title_bar`, `ui.window_controls` | `()` | Botón de cerrar. | | `on_menu` | `ui.window_title_bar` | `()` | Clic derecho en la barra. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Notes(pz.App): frame = "custom" # Pyzed draws the title bar def view(self): bar = ui.window_title_bar( "notes.py", on_drag=lambda: self.drag(), # the empty part of the bar on_minimize=lambda: self.minimize(), on_maximize=lambda: self.toggle_maximize(), # also a double click on the bar on_close=lambda: self.close(), on_menu=lambda: print("window menu"), # right click on the bar maximized=self.window_maximized, ) return pz.column([bar, main_view()]) ``` **Declarativo:** ```python class Notes(pz.App): frame = "custom" def update(self, message): # the commands are returned from update match message: case "drag": return self.drag() case "minimize": return self.minimize() case "maximize": return self.toggle_maximize() case "close": return self.close() def view(self): return pz.column([ui.window_title_bar("notes.py", on_drag="drag", on_minimize="minimize", on_maximize="maximize", on_close="close"), main_view()]) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: title: str = "notes.py" def view(state, set_state): # A handler can return a command: Pyzed runs it. return pz.column([ui.window_title_bar(state.title, on_drag=lambda: pz.Command.drag_window(), on_close=lambda: pz.Command.exit()), main_view()]) ``` #### Consejos :::tip Si solo necesitas menús, define `frame_menus()` en la ventana y no llames a `ui.window_frame`: el entorno de ejecución enmarca la vista por ti. `style` toma por defecto el `frame` de la ventana, o el estilo de este sistema. ::: #### Propiedades y API ##### `ui.window_frame` ```python window_frame( content, *, title=None, menus=None, menu_button=True, start=(), center=None, end=(), style=None, window=None, height=None, resize_border=6.0, resize_edges=True, bg=None, theme=None, ) ``` El marco entero de una ventana abierta con `frame="custom"`: una barra de título que mueve la ventana (doble clic maximiza, clic derecho abre el menú del sistema en Windows), los botones de minimizar, maximizar y cerrar con el estilo de una plataforma, menús a la izquierda y bordes que cambian el tamaño de la ventana. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | El contenido de la ventana, bajo la barra de título. | | `title` | `None` | Título de la barra. Por defecto el `title` de la ventana. | | `menus` | `None` | Menús a la izquierda de la barra: una lista de `ui.Menu`. | | `menu_button` | `True` | Muestra los menús tras un botón ☰ (`False` muestra siempre sus nombres). | | `start` | `()` | Elementos después de los menús (proyecto, rama…). | | `center` | `None` | Tu propio widget en el centro en lugar del título (un buscador, Run y Stop…); la barra sigue arrastrándose a ambos lados. | | `end` | `()` | Elementos antes de los botones de ventana. | | `style` | `None` | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"` o `"kde"`. Por defecto el `frame` de la ventana, o el del sistema. | | `window` | `None` | La ventana sobre la que actúan los botones. Por defecto aquella cuyo `view()` se está ejecutando. | | `height` | `None` | Alto de la barra. Por defecto el de la plataforma. | | `resize_border` | `6.0` | Ancho en píxeles de los bordes que cambian el tamaño de la ventana. | | `resize_edges` | `True` | `False` deja los bordes de redimensionar a tu cargo (`pz.window_edges`), para ponerlos encima de menús y diálogos. | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#window_frame) ##### `ui.Menu` ```python @dataclass class Menu(label, items=[]) ``` Un menú de la barra de menús de un marco de ventana: un nombre y sus elementos (`ui.MenuItem`, `ui.SEPARATOR` o un texto de cabecera). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `label` | *obligatorio* | El nombre del menú. | | `items` | `[]` | Sus elementos: `ui.MenuItem`, `ui.SEPARATOR` o un texto de cabecera. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#menu-1) ##### `ui.window_title_bar` ```python window_title_bar( title=None, *, style=None, start=(), center=None, end=(), on_drag=None, on_minimize=None, on_maximize=None, on_close=None, on_menu=None, maximized=False, focused=True, width='fill', height=None, bg=None, border=True, theme=None, ) ``` La barra de título de una ventana, sola: botones de ventana con el estilo de `style` (a la izquierda en macOS), elementos `start`, el título y elementos `end`. Sin callbacks solo se muestra, que es como Pyzed Design previsualiza un formulario con el marco de cada plataforma. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | `None` | Título (centrado, o a la izquierda en Windows). | | `style` | `None` | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"` o `"kde"` (`None`: el del sistema). | | `start` | `()` | Elementos al principio. | | `center` | `None` | Widget en el centro en lugar del título; la barra sigue arrastrándose a ambos lados. | | `end` | `()` | Elementos al final. | | `maximized` | `False` | Muestra el símbolo de restaurar. | | `focused` | `True` | `False` la atenúa como una ventana en segundo plano. | | `width` | `"fill"` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto. Por defecto el de la plataforma. | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `border` | `True` | Línea bajo la barra. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_drag`, `on_minimize`, `on_maximize`, `on_close`, `on_menu` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#window_title_bar) ##### `ui.window_controls` ```python window_controls( style=None, *, on_minimize=None, on_maximize=None, on_close=None, maximized=False, focused=True, height=None, theme=None, ) ``` Los botones de minimizar, maximizar y cerrar con el estilo de una plataforma. Un botón sin callback se dibuja pero no hace nada. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `style` | `None` | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"` o `"kde"` (`None`: el del sistema). | | `maximized` | `False` | Muestra el símbolo de restaurar. | | `focused` | `True` | `False` los atenúa. | | `height` | `None` | Alto. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_minimize`, `on_maximize`, `on_close` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#window_controls) ##### `ui.system_platform` ```python system_platform() ``` El estilo de los botones de ventana de este sistema: `"macos"`, `"windows"`, `"kde"`, `"ubuntu"` o `"gnome"` (otros escritorios Linux). En WSL es `"windows"`. [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#system_platform) #### Relacionados - [Barra de título](https://albakore.github.io/pyzed/es/library/title-bar/): La barra superior con los selectores de proyecto y rama. - [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/workspace/): La disposición de una ventana estilo editor: barra de título, docks, centro y barra de estado. - [Menú](https://albakore.github.io/pyzed/es/library/menu/): Un menú contextual con iconos, atajos y marcas. ### Markdown > Muestra Markdown con los colores del tema. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/markdown/ :::tip[En pocas palabras] Muestra texto con formato (títulos, listas, enlaces) escrito en el formato sencillo Markdown. ::: Muestra Markdown (títulos, listas, bloques de código, citas, tablas, enlaces) con los colores de texto, enlace y código del tema. `on_link(url)` recibe los enlaces pulsados. `pz.markdown` es el widget base con colores explícitos. `ui.markdown` · Categoría: [Código y herramientas](https://albakore.github.io/pyzed/es/library/#código-y-herramientas) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#markdown) **Código:** ```python ui.markdown('''## Release notes Pyzed **0.4** adds the *component designer*, `pyzed build` and [styles](https://iced.rs). - Track grids and flex - Ready-made dialogs ''', width=460, on_link=on_link) ``` #### Cuándo usarlo - Para READMEs, notas de versión, páginas de ayuda y mensajes de chat. #### Uso ```python from pyzed import ui ui.markdown('''## Release notes Pyzed **0.4** adds the *component designer*, `pyzed build` and [styles](https://iced.rs). - Track grids and flex - Ready-made dialogs ''', width=460, on_link=on_link) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Elementos ```python ui.markdown("## Title\nSome **bold** and *italic* text.", width=320) ``` ```python ui.markdown("- Fast\n- Native\n- Pythonic\n\n1. Install\n2. Run", width=320) ``` ```python ui.markdown("Run `pyzed new`:\n\n```python\nimport pyzed as pz\n```", width=320) ``` ```python ui.markdown("> Desktop UIs in Python.\n\nSee [the docs](https://albakore.github.io/pyzed/).", width=320, on_link=on_link) ``` ```python ui.markdown("| Key | Action |\n| --- | --- |\n| ctrl-s | Save |\n| ctrl-p | Open |", width=320) ``` #### Eventos Markdown tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_link` | `ui.markdown`, `pz.markdown` | `(url)` | Callable que recibe la URL al pulsar un enlace. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python import webbrowser class Notes(pz.App): def __init__(self): self.last = None def follow(self, url): # on_link: the URL of the clicked link self.last = url webbrowser.open(url) def view(self): return ui.markdown("Read the [docs](https://albakore.github.io/pyzed/) or the " "[changelog](https://github.com/albakore/pyzed).", on_link=self.follow) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Link: url: str class Notes(pz.App): def update(self, message): match message: case Link(url) if url.startswith("app:"): print("internal page", url[4:]) # links can drive the app, too case Link(url): print("open", url) def view(self): return ui.markdown("Go to [settings](app:settings) or the [docs](https://albakore.github.io/pyzed/).", on_link=Link) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: page: str = "home" PAGES = {"home": "Welcome. See [about](about).", "about": "Made with Pyzed. Back [home](home)."} def view(state, set_state): text = PAGES.get(state.page, "Not found. Back [home](home).") return ui.markdown(text, on_link=lambda url: set_state(page=url)) ``` #### Propiedades y API ##### `ui.markdown` ```python markdown(source, *, size=None, on_link=None, width=None, selectable=None, theme=None) ``` `pz.markdown` con los colores de texto, enlaces y código del tema. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `source` | *obligatorio* | El texto Markdown. | | `size` | `None` | Tamaño. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `selectable` | `None` | Igual que en `pz.markdown`; la selección usa el color `selection` del tema. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_link` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#markdown) ##### `pz.markdown` ```python markdown( source, *, size=14.0, color=None, link_color=None, code_color=None, code_bg=None, on_link=None, width=None, selectable=None, selection_color=None, ) ``` Renderiza Markdown (títulos, listas, bloques de código, citas, tablas, enlaces…). Para los colores del tema usa `ui.markdown`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `source` | *obligatorio* | El texto Markdown. | | `size` | `14.0` | Tamaño. | | `color` | `None` | Color del texto o del icono. | | `link_color` | `None` | Color de los enlaces. | | `code_color` | `None` | Color del código. | | `code_bg` | `None` | Fondo del código. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `selectable` | `None` | `True` deja seleccionar el texto con el ratón (arrastrar, doble clic en una palabra, triple clic en un bloque) y copiarlo con Ctrl+C. Una cadena nombra un grupo de selección: los textos y el Markdown de un mismo grupo forman una sola selección, en el orden en que se dibujan (las líneas de un log, los mensajes de un chat). | | `selection_color` | `None` | Color detrás del texto seleccionado. | Eventos: `on_link` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#markdown) #### Relacionados - [Texto enriquecido](https://albakore.github.io/pyzed/es/library/rich-text/): Un bloque de texto que mezcla colores, pesos y enlaces. - [Texto](https://albakore.github.io/pyzed/es/library/text/): Un texto con tamaño, color, fuente y peso. ### Medidor de contexto > Cuánto se llenó la ventana de contexto de un modelo: un anillo y `48k / 200k`. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/context-meter/ :::tip[En pocas palabras] Un anillo pequeño que se llena a medida que un chat con una IA se alarga, como el indicador de gasolina. ::: `ui.context_meter(used, limit)` muestra cuánto se llenó la ventana de contexto de un modelo de IA: un anillo pequeño y `48k / 200k`. Es gris por debajo del 80 %, amarillo desde el 80 % y rojo desde el 95 %, y su tooltip da las cifras exactas. `ui.format_tokens(count)` escribe una cifra de forma corta: `950`, `4.5k`, `48k`, `1.25M`. `ui.context_meter` · Categoría: [Agentes de IA](https://albakore.github.io/pyzed/es/library/#agentes-de-ia) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#context_meter) **Código:** ```python pz.row([ ui.context_meter(48_000, 200_000), ui.context_meter(170_000, 200_000), ui.context_meter(196_000, 200_000), ], spacing=24, align="center") ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - En la barra o en la caja de texto del chat de un agente, para que el usuario vea cuándo una conversación se hace larga. #### Uso ```python import pyzed as pz from pyzed import ui pz.row([ ui.context_meter(48_000, 200_000), ui.context_meter(170_000, 200_000), ui.context_meter(196_000, 200_000), ], spacing=24, align="center") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Nivel Dibujado tal como lo muestra el puntero: los tooltips solo se abren al pasar el ratón. ```python ui.context_meter(48_000, 200_000) ``` ```python ui.context_meter(170_000, 200_000) ``` ```python ui.context_meter(196_000, 200_000) ``` ```python ui.context_meter(48_000, 200_000) ``` ##### Cifras cortas `ui.format_tokens` devuelve el texto que muestra el medidor, para tus propias etiquetas. ```python ui.label(ui.format_tokens(950), mono=True) ``` ```python ui.label(ui.format_tokens(4_500), mono=True) ``` ```python ui.label(ui.format_tokens(48_000), mono=True) ``` ```python ui.label(ui.format_tokens(1_250_000), mono=True) ``` #### Eventos Medidor de contexto no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.context_meter` ```python context_meter(used, limit, *, theme=None) ``` Cuánto se llenó la ventana de contexto de un modelo, como el contador de tokens de Zed: un anillo pequeño y `48k / 200k`, amarillo pasado el 80 % y rojo pasado el 95 %. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `used` | *obligatorio* | Tokens en uso. | | `limit` | *obligatorio* | La ventana del modelo. | | `theme` | `None` | Tema. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#context_meter) ##### `ui.format_tokens` ```python format_tokens(count) ``` Un número de tokens corto: `950`, `4.5k`, `48k`, `1.25M`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `count` | *obligatorio* | Tokens. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#format_tokens) #### Relacionados - [Dona](https://albakore.github.io/pyzed/es/library/donut/): Un gráfico de dona con una etiqueta en el centro. - [Progreso](https://albakore.github.io/pyzed/es/library/progress/): Una barra de progreso fina en un color semántico. - [Llamada a herramienta](https://albakore.github.io/pyzed/es/library/tool-call/): Un paso de un agente de IA: qué hizo, su resultado y Permitir o Rechazar. ### Menú > Un menú contextual con iconos, atajos y marcas. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/menu/ :::tip[En pocas palabras] La lista de opciones que aparece al hacer clic derecho o pulsar un botón de “más”. ::: Un menú contextual flotante. Las entradas son `ui.MenuItem(label, on_press, icon=, keys=, checked=, danger=, disabled=)`; `ui.SEPARATOR` dibuja una línea entre grupos y una cadena es una cabecera. Muéstralo con un [Popover](https://albakore.github.io/pyzed/es/library/popover/) o una [Capa](https://albakore.github.io/pyzed/es/library/layer/). `ui.menu` · Categoría: [Listas y menús](https://albakore.github.io/pyzed/es/library/#listas-y-menús) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#menu) **Código:** ```python ui.menu([ ui.MenuItem("New file", on_new, icon="file-plus", keys="ctrl-n"), ui.MenuItem("New folder", on_new_folder, icon="folder-plus"), ui.SEPARATOR, "EDIT", ui.MenuItem("Copy path", on_copy, keys="ctrl-alt-c"), ui.MenuItem("Soft wrap", on_wrap, checked=True), ui.SEPARATOR, ui.MenuItem("Delete", on_delete, icon="trash-2", danger=True), ]) ``` #### Cuándo usarlo - Para menús de clic derecho y las opciones de un botón dividido o de puntos suspensivos. - Marca las acciones destructivas con `danger=True` y ponlas al final. #### Uso ```python from pyzed import ui ui.menu([ ui.MenuItem("New file", on_new, icon="file-plus", keys="ctrl-n"), ui.MenuItem("New folder", on_new_folder, icon="folder-plus"), ui.SEPARATOR, "EDIT", ui.MenuItem("Copy path", on_copy, keys="ctrl-alt-c"), ui.MenuItem("Soft wrap", on_wrap, checked=True), ui.SEPARATOR, ui.MenuItem("Delete", on_delete, icon="trash-2", danger=True), ]) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Entradas ```python ui.menu([ui.MenuItem("Open", on_open), ui.MenuItem("Rename", on_rename), ui.MenuItem("Duplicate", on_duplicate)], width=200) ``` ```python ui.menu([ui.MenuItem("Copy", on_copy, icon="copy", keys="ctrl-c"), ui.MenuItem("Paste", on_paste, icon="clipboard", keys="ctrl-v")], width=220) ``` ```python ui.menu([ui.MenuItem("Soft wrap", on_wrap, checked=True), ui.MenuItem("Minimap", on_minimap, checked=False)], width=200) ``` ```python ui.menu([ui.MenuItem("Rename", on_rename), ui.SEPARATOR, ui.MenuItem("Delete", on_delete, icon="trash-2", danger=True)], width=200) ``` ```python ui.menu([ui.MenuItem("Undo", on_undo, keys="ctrl-z"), ui.MenuItem("Redo", on_redo, keys="ctrl-shift-z", disabled=True)], width=220) ``` ##### Grupos ```python ui.menu(["FILE", ui.MenuItem("New", on_new, keys="ctrl-n"), ui.SEPARATOR, "EDIT", ui.MenuItem("Find", on_find, keys="ctrl-f")], width=220) ``` ##### Estados y eventos ```python ui.menu([ui.MenuItem("Share", on_press, icon="share", submenu=True), ui.MenuItem("Rename", on_press, description="F2 also works")]) ``` ```python ui.menu([ui.MenuItem(f"Recent {i}", on_press) for i in range(10)], max_height=140) ``` #### Eventos Menú tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_press` | `()` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Files(pz.App): def __init__(self): self.wrap = True self.log = [] def view(self): return ui.menu([ ui.MenuItem("New file", lambda: self.log.append("new"), icon="file-plus", keys="ctrl-n"), ui.MenuItem("Soft wrap", lambda: setattr(self, "wrap", not self.wrap), checked=self.wrap), ui.SEPARATOR, ui.MenuItem("Delete", lambda: self.log.append("delete"), icon="trash-2", danger=True), ]) ``` **Declarativo:** ```python class Files(pz.App): wrap = True def update(self, message): # each item's on_press is a message match message: case "new": print("new file") case "wrap": self.wrap = not self.wrap def view(self): return ui.menu([ui.MenuItem("New file", "new", icon="file-plus", keys="ctrl-n"), ui.MenuItem("Soft wrap", "wrap", checked=self.wrap)]) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: wrap: bool = True def view(state, set_state): return ui.menu([ ui.MenuItem("Soft wrap", lambda: set_state(wrap=not state.wrap), checked=state.wrap), ui.MenuItem("Delete", None, icon="trash-2", danger=True, disabled=True), ]) ``` #### Propiedades y API ##### `ui.menu` ```python menu(items, *, width=240, max_height=None, theme=None) ``` Un menú contextual flotante. Usa `ui.SEPARATOR` entre grupos; un texto suelto es un encabezado. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `items` | *obligatorio* | Lista de `MenuItem`, `ui.SEPARATOR` o textos. | | `width` | `240` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `max_height` | `None` | Desplaza un menú más alto que esto. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#menu) ##### `ui.MenuItem` ```python @dataclass class MenuItem(label, on_press=None, icon=None, keys=None, disabled=False, danger=False, checked=None, description=None, submenu=False) ``` Una entrada de `ui.menu`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `label` | *obligatorio* | Texto de la etiqueta. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `keys` | `None` | Atajo que se muestra al final, escrito como texto: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `disabled` | `False` | Lo deshabilita (ignora los clics). | | `danger` | `False` | En rojo (acciones destructivas). | | `checked` | `None` | Muestra una marca (`True`/`False`) o nada (`None`). | | `description` | `None` | Una segunda línea atenuada. | | `submenu` | `False` | Termina la fila con una flecha; abre el siguiente menú al lado en `on_press`. | Eventos: `on_press` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#menuitem) ##### `ui.SEPARATOR` Separador para `ui.menu`. [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#separator) #### Relacionados - [Popover](https://albakore.github.io/pyzed/es/library/popover/): Contenido flotante en un punto: menús contextuales, selectores de color. - [Botón dividido](https://albakore.github.io/pyzed/es/library/split-button/): Una acción principal más una flecha que abre más opciones. - [Fila de lista](https://albakore.github.io/pyzed/es/library/list-item/): Una fila de lista con icono, texto y un hueco al final. ### Migas de pan > `src › main.rs › fn main`, pulsables si quieres. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/breadcrumbs/ :::tip[En pocas palabras] Muestra dónde estás, paso a paso: Carpeta › Subcarpeta › Archivo. ::: La ruta hasta la ubicación actual. Las partes son cadenas o tuplas `(icono, texto)`; `on_press` recibe el índice de la parte pulsada. `ui.breadcrumbs` · Categoría: [Navegación](https://albakore.github.io/pyzed/es/library/#navegación) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#breadcrumbs) **Código:** ```python pz.column([ ui.breadcrumbs(["src", "ui", "button.rs"]), ui.breadcrumbs([("folder", "src"), ("file-code", "main.rs"), "fn main"], on_press=on_press), ui.breadcrumbs(["Settings", "Editor", "Font"], separator="/"), ], spacing=14) ``` #### Cuándo usarlo - Encima de un editor o una vista anidada, para mostrar dónde estás y saltar a un nivel superior. #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ ui.breadcrumbs(["src", "ui", "button.rs"]), ui.breadcrumbs([("folder", "src"), ("file-code", "main.rs"), "fn main"], on_press=on_press), ui.breadcrumbs(["Settings", "Editor", "Font"], separator="/"), ], spacing=14) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python ui.breadcrumbs(["src", "ui", "button.rs"]) ``` ```python ui.breadcrumbs([("folder", "src"), ("file-code", "main.rs"), "fn main"]) ``` ```python ui.breadcrumbs(["Settings", "Editor", "Font"], separator="/") ``` ```python ui.breadcrumbs(["Home", "Projects", "pyzed"], on_press=on_press) ``` ##### Estados y eventos ```python ui.breadcrumbs(["pyzed", "python", "pyzed", "ui", "lists.py"], max_items=3, on_press=on_press, on_more=on_more) ``` #### Eventos Migas de pan tiene **2 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_press` | `(index)` | Se llama con el índice de la parte pulsada. | | `on_more` | `()` | Hace pulsable el `…`. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Browser(pz.App): def __init__(self): self.path = ["home", "ada", "projects", "pyzed"] def go(self, index): # on_press: the index of the clicked part self.path = self.path[:index + 1] def view(self): return ui.breadcrumbs(self.path, on_press=self.go, max_items=3, on_more=lambda: self.go(0)) # the … of the hidden parts ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class GoTo: index: int class Browser(pz.App): path = ["home", "ada", "projects", "pyzed"] def update(self, message): match message: case GoTo(index): self.path = self.path[:index + 1] def view(self): return ui.breadcrumbs(self.path, on_press=GoTo) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: path: tuple = ("home", "ada", "projects", "pyzed") def view(state, set_state): return ui.breadcrumbs(list(state.path), on_press=lambda i: set_state(path=state.path[:i + 1])) ``` #### Propiedades y API ##### `ui.breadcrumbs` ```python breadcrumbs( parts, *, separator='›', on_press=None, max_items=None, on_more=None, theme=None, ) ``` `src › main.rs › fn main`. Las partes pueden ser textos o tuplas `(icono, texto)`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `parts` | *obligatorio* | Las partes. | | `separator` | `"›"` | Separador. | | `max_items` | `None` | Conserva la primera parte y las últimas y muestra `…` en lugar del resto. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_press`, `on_more` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#breadcrumbs) #### Relacionados - [Barra de herramientas](https://albakore.github.io/pyzed/es/library/toolbar/): La barra bajo las pestañas: migas de pan y acciones. - [Barra de pestañas](https://albakore.github.io/pyzed/es/library/tab-bar/): Pestañas de editor con iconos, puntos de cambios y botón de cerrar. ### Minigráfico > Un gráfico de líneas diminuto para tendencias. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/sparkline/ :::tip[En pocas palabras] Un gráfico de líneas diminuto que muestra si algo sube o baja. ::: Un gráfico de líneas pequeño, relleno bajo la línea por defecto, que muestra la forma de una serie. `ui.sparkline` · Categoría: [Datos](https://albakore.github.io/pyzed/es/library/#datos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#sparkline) **Código:** ```python pz.column([ ui.sparkline([1, 4, 2, 6, 3, 7, 5, 8], width=240, height=48), ui.sparkline([8, 6, 7, 4, 5, 3, 2, 1], width=240, height=48, tint="error", fill=False), ], spacing=16) ``` #### Cuándo usarlo - En línea junto a un número o en una celda, cuando importa más la tendencia que los valores. #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ ui.sparkline([1, 4, 2, 6, 3, 7, 5, 8], width=240, height=48), ui.sparkline([8, 6, 7, 4, 5, 3, 2, 1], width=240, height=48, tint="error", fill=False), ], spacing=16) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python ui.sparkline([1, 4, 2, 6, 3, 7, 5, 8], width=200, height=40) ``` ```python ui.sparkline([1, 4, 2, 6, 3, 7, 5, 8], width=200, height=40, fill=False) ``` ```python ui.sparkline([8, 6, 7, 4, 5, 3, 2, 1], width=200, height=40, tint="error") ``` ```python ui.sparkline([2, 3, 3, 5, 6, 8, 9, 12], width=200, height=40, tint="success") ``` ##### Tamaño ```python ui.sparkline([3, 5, 2, 6, 4, 7], width=80, height=20) ``` ```python ui.sparkline([3, 5, 2, 6, 4, 7, 5, 9, 6, 8], width=280, height=64) ``` #### Eventos Minigráfico no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.sparkline` ```python sparkline(values, *, width=120, height=32, tint='accent', fill=True, theme=None) ``` Un gráfico de línea diminuto para tendencias. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `values` | *obligatorio* | Valores. | | `width` | `120` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `32` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `tint` | `"accent"` | Color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` o un token. | | `fill` | `True` | Rellena el área bajo la línea. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#sparkline) #### Relacionados - [Cifra](https://albakore.github.io/pyzed/es/library/stat/): Una cifra de dashboard con variación y minigráfico. - [Gráfico de barras](https://albakore.github.io/pyzed/es/library/bar-chart/): Un gráfico de barras verticales con etiquetas. ### Modal > Una superficie de diálogo con título, contenido y botones al pie. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/modal/ :::tip[En pocas palabras] Una ventana dentro de la ventana que pide tu atención antes de seguir. ::: Una superficie de diálogo con título, botón de cerrar, tu contenido y un pie de botones. Úsala como capa de `ui.layer` o `ui.dialog_layer`. Los [diálogos](https://albakore.github.io/pyzed/es/library/confirm/) listos para usar están hechos con ella. `ui.modal` · Categoría: [Capas flotantes](https://albakore.github.io/pyzed/es/library/#capas-flotantes) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#modal) **Código:** ```python ui.modal( ui.muted("notes.py has unsaved changes. Save them before closing?"), title="Save changes?", on_close=on_close, footer=[ui.button("Don't save", on_discard, variant="ghost"), ui.button("Cancel", on_close), ui.button("Save", on_save, variant="solid")], width=440, ) ``` #### Cuándo usarlo - Para diálogos con tu propio contenido: un formulario, ajustes, una caja Acerca de. #### Uso ```python from pyzed import ui ui.modal( ui.muted("notes.py has unsaved changes. Save them before closing?"), title="Save changes?", on_close=on_close, footer=[ui.button("Don't save", on_discard, variant="ghost"), ui.button("Cancel", on_close), ui.button("Save", on_save, variant="solid")], width=440, ) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido ```python ui.modal(ui.muted("Your trial ends in 3 days."), width=320) ``` ```python ui.modal(ui.muted("Your trial ends in 3 days."), title="Trial", on_close=on_close, width=320) ``` ```python ui.modal(ui.muted("Discard the draft?"), title="Draft", on_close=on_close, width=320, footer=[ui.button("Cancel", on_close), ui.button("Discard", on_discard, variant="error")]) ``` ##### Estados y eventos `max_height=` desplaza el contenido largo, y Escape envía `on_close` mientras el diálogo tiene el foco. ```python ui.modal(ui.input("notes.md", on_input), title="Rename", description="Choose a new name", icon="pencil", on_close=on_close, width="sm", footer=[ui.button("Rename", on_press, variant="solid")]) ``` #### Ejemplos ##### Encima de la app Envuelve la vista con `ui.dialog_layer` para centrarlo sobre un fondo oscurecido. **Código:** ```python ui.dialog_layer(main_view(), ui.modal( pz.column([ui.input("", on_input, placeholder="Project name"), ui.checkbox(True, on_toggle, label="Create a git repository")], spacing=10), title="New project", on_close=on_close, footer=[ui.button("Cancel", on_close), ui.button("Create", on_create, variant="solid")], width=380, ), on_dismiss=on_close) ``` #### Eventos Modal tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_close` | `()` | El botón de cerrar; también lo envía Escape mientras el diálogo tiene el foco. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Notes(pz.App): def __init__(self): self.asking = True def save(self): self.asking = False def view(self): dialog = ui.modal(ui.muted("Save your changes before closing?"), title="Save changes?", on_close=lambda: setattr(self, "asking", False), # ✕ and Escape footer=[ui.button("Save", self.save, variant="solid")], width=420) return ui.dialog_layer(main_view(), dialog if self.asking else None) ``` **Declarativo:** ```python class Notes(pz.App): asking = True def update(self, message): if message in ("save", "close"): self.asking = False def view(self): dialog = ui.modal(ui.muted("Save your changes?"), title="Save changes?", on_close="close", footer=[ui.button("Save", "save", variant="solid")]) return ui.dialog_layer(main_view(), dialog if self.asking else None) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: asking: bool = True def view(state, set_state): close = lambda: set_state(asking=False) dialog = ui.modal(ui.muted("Save your changes?"), title="Save changes?", on_close=close, footer=[ui.button("Save", close, variant="solid")]) return ui.dialog_layer(main_view(), dialog if state.asking else None) ``` #### Propiedades y API ##### `ui.modal` ```python modal( content, *, title=None, on_close=None, width=480, footer=(), description=None, icon=None, max_height=None, theme=None, ) ``` Un diálogo. Úsalo como `overlay` de `ui.layer`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `title` | `None` | Título. | | `width` | `480` | Píxeles o `"sm"` (360), `"md"` (480), `"lg"` (640) o `"xl"` (820). | | `footer` | `()` | Botones del pie, alineados a la derecha. | | `description` | `None` | Línea atenuada bajo el título. | | `icon` | `None` | Icono antes del título. | | `max_height` | `None` | Desplaza el contenido más alto que esto, sin ocultar la cabecera ni el pie. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_close` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#modal) #### Relacionados - [Capa de diálogo](https://albakore.github.io/pyzed/es/library/dialog-layer/): Centra un diálogo encima de tu vista con el fondo oscurecido. - [Capa](https://albakore.github.io/pyzed/es/library/layer/): Dibuja una capa encima de tu vista; al pulsar fuera se cierra. - [Confirmación](https://albakore.github.io/pyzed/es/library/confirm/): Una pregunta de sí o no, en rojo para acciones destructivas. ### Notificación > Notificaciones apiladas en la esquina inferior derecha. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/toast/ :::tip[En pocas palabras] Una notificación pequeña que aparece en una esquina y se va sola. ::: `ui.toast` dibuja una notificación con título, mensaje, acciones y botón de cerrar; `kind` es `"info"`, `"success"`, `"warning"` o `"error"`. `ui.toasts(base, items)` las apila en la esquina inferior derecha encima de tu vista. `ui.toast` · Categoría: [Capas flotantes](https://albakore.github.io/pyzed/es/library/#capas-flotantes) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#toast) **Código:** ```python pz.column([ ui.toast("Saved", "main.py was saved.", kind="success", on_dismiss=on_dismiss), ui.toast("Update available", "Pyzed 0.4 is on PyPI.", kind="info", actions=[ui.button("What's new", on_open, size="sm")], on_dismiss=on_dismiss), ui.toast("Could not connect", kind="error", on_dismiss=on_dismiss), ], spacing=8) ``` #### Cuándo usarlo - Para confirmar algo que pasó en segundo plano (guardado, copiado, conectado). - Los mensajes que necesitan respuesta van en un [Diálogo](https://albakore.github.io/pyzed/es/library/confirm/). #### Uso ```python import pyzed as pz from pyzed import ui import threading class Notes(pz.App): def __init__(self): self.messages = [] def notify(self, title): self.messages.append(title) threading.Timer(3, pz.post, args=(("expire", title),)).start() # hide it after 3 s def update(self, message): if message[0] == "expire": self.messages.remove(message[1]) def view(self): items = [ui.toast(m, kind="success", on_dismiss=lambda m=m: self.messages.remove(m)) for m in self.messages] return ui.toasts(self.main_view(), items) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Tipo ```python ui.toast("Update available", "Pyzed 0.5 is on PyPI.") ``` ```python ui.toast("Saved", "main.py was saved.", kind="success") ``` ```python ui.toast("Low disk space", "2 GB left.", kind="warning") ``` ```python ui.toast("Could not connect", "Check your network.", kind="error") ``` ##### Contenido ```python ui.toast("Copied to clipboard", kind="success") ``` ```python ui.toast("Update available", "Restart to install it.", actions=[ui.button("Restart", on_restart, size="sm")], on_dismiss=on_dismiss) ``` ```python ui.toast("Now playing", "Lo-fi beats", icon="music", width=280) ``` ##### Estados y eventos ```python ui.toast("Uploading", "notes.md", kind="info", progress=0.6, on_dismiss=on_close) ``` ```python ui.toast("Build failed", "Click to see the log", kind="error", on_press=on_press) ``` #### Eventos Notificación tiene **2 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_dismiss` | `()` | Mensaje o callable que se envía al cerrar. | | `on_press` | `()` | Hace pulsable la tarjeta. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Notes(pz.App): def __init__(self): self.messages = ["Saved"] self.opened = 0 def view(self): items = [ui.toast(m, kind="success", on_dismiss=lambda m=m: self.messages.remove(m), # its ✕ on_press=lambda: setattr(self, "opened", self.opened + 1)) # the card for m in self.messages] return ui.toasts(main_view(), items) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Dismiss: text: str class Notes(pz.App): messages = ["Saved"] def update(self, message): match message: case Dismiss(text): self.messages = [m for m in self.messages if m != text] def view(self): return ui.toasts(main_view(), [ui.toast(m, kind="success", on_dismiss=Dismiss(m)) for m in self.messages]) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: messages: tuple = ("Saved",) def view(state, set_state): close = lambda m: set_state(messages=tuple(x for x in state.messages if x != m)) return ui.toasts(main_view(), [ui.toast(m, kind="success", on_dismiss=lambda m=m: close(m)) for m in state.messages]) ``` #### Propiedades y API ##### `ui.toast` ```python toast( title, message=None, *, kind='info', icon=None, on_dismiss=None, actions=(), width=340, progress=None, on_press=None, theme=None, ) ``` Una notificación. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | *obligatorio* | Título. | | `message` | `None` | Texto secundario. | | `kind` | `"info"` | `"info"`, `"success"`, `"warning"` o `"error"`. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `actions` | `()` | Widgets extra (normalmente botones) que se muestran como acciones. | | `width` | `340` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `progress` | `None` | De 0 a 1: una barra fina abajo (tiempo restante, una descarga). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_dismiss`, `on_press` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#toast) ##### `ui.toasts` ```python toasts(base, items, *, spacing=8) ``` Muestra `items` (normalmente `toast(...)`) apilados en la esquina inferior derecha sobre `base`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `base` | *obligatorio* | La interfaz de debajo. | | `items` | *obligatorio* | Lista de toasts. | | `spacing` | `8` | Separación entre hijos, en píxeles. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#toasts) #### Relacionados - [Aviso](https://albakore.github.io/pyzed/es/library/callout/): Un aviso en línea con título, texto y acciones. - [Capa](https://albakore.github.io/pyzed/es/library/layer/): Dibuja una capa encima de tu vista; al pulsar fuera se cierra. - [Alerta](https://albakore.github.io/pyzed/es/library/alert/): Un mensaje con un botón. ### Paleta de comandos > Una paleta de comandos con búsqueda difusa y atajos. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/command-palette/ :::tip[En pocas palabras] Un buscador de todas las acciones de la app: escribe lo que quieres hacer y pulsa Enter. ::: Una paleta de comandos: filtra `commands` (`ui.PaletteItem(label, on_press, keys=)`) por `query` con búsqueda difusa, resalta las letras encontradas y muestra cada atajo. La consulta y el índice `selected` son tuyos; Enter llama a `on_submit`. `ui.command_palette` · Categoría: [Capas flotantes](https://albakore.github.io/pyzed/es/library/#capas-flotantes) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#command_palette) **Código:** ```python ui.command_palette("tog", on_query, [ ui.PaletteItem("editor: toggle soft wrap", on_wrap, keys="alt-z"), ui.PaletteItem("theme selector: toggle", on_theme, keys="ctrl-k ctrl-t"), ui.PaletteItem("workspace: toggle left dock", on_left, keys="ctrl-b"), ui.PaletteItem("terminal panel: toggle", on_terminal, keys="ctrl-j"), ], selected=0, width=520) ``` #### Cuándo usarlo - Para que todas las acciones se puedan usar con el teclado (normalmente con Ctrl+Shift+P). #### Uso ```python import pyzed as pz from pyzed import ui class App(pz.App): def __init__(self): self.open, self.query, self.selected = False, "", 0 def commands(self): return [ui.PaletteItem("editor: toggle soft wrap", self.toggle_wrap, keys="alt-z"), ui.PaletteItem("workspace: toggle left dock", self.toggle_left, keys="ctrl-b")] def on_key(self, combo): if combo == "ctrl-shift-p": self.open, self.query, self.selected = True, "", 0 return pz.Command.focus("command-palette") if self.open and combo in ("up", "down"): self.selected += 1 if combo == "down" else -1 elif combo == "escape": self.open = False def run_selected(self): matches = ui.fuzzy_filter(self.query, self.commands(), key=lambda c: c.label) if matches: self.open = False return matches[self.selected % len(matches)][0].on_press() def view(self): palette = ui.command_palette(self.query, lambda q: setattr(self, "query", q), self.commands(), selected=self.selected, on_submit=self.run_selected) return ui.layer(self.main_view(), palette if self.open else None, on_dismiss=lambda: setattr(self, "open", False)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Búsqueda ```python ui.command_palette("", on_query, [ ui.PaletteItem("file: new", on_new, keys="ctrl-n"), ui.PaletteItem("file: open", on_open, keys="ctrl-o"), ui.PaletteItem("theme selector: toggle", on_theme), ], width=420) ``` ```python ui.command_palette("op", on_query, [ ui.PaletteItem("file: new", on_new, keys="ctrl-n"), ui.PaletteItem("file: open", on_open, keys="ctrl-o"), ui.PaletteItem("workspace: open recent", on_recent, keys="ctrl-r"), ], width=420) ``` ```python ui.command_palette("", on_query, [ ui.PaletteItem("file: new", on_new, keys="ctrl-n"), ui.PaletteItem("file: open", on_open, keys="ctrl-o"), ui.PaletteItem("file: save", on_save, keys="ctrl-s"), ], selected=1, width=420) ``` #### Eventos Paleta de comandos tiene **3 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_query` | `ui.command_palette` | `(text)` | Callable que recibe el texto nuevo. | | `on_submit` | `ui.command_palette` | `()` | Mensaje o callable que se envía al pulsar Enter. | | `on_press` | `ui.PaletteItem` | `()` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class App(pz.App): def __init__(self): self.open, self.query, self.wrap = True, "", False def toggle_wrap(self): self.wrap, self.open = not self.wrap, False def commands(self): # each item's on_press runs the command return [ui.PaletteItem("editor: toggle soft wrap", self.toggle_wrap, keys="alt-z")] def run_first(self): # on_submit: Enter runs the first match matches = ui.fuzzy_filter(self.query, self.commands(), key=lambda c: c.label) return matches[0][0].on_press() if matches else None def view(self): palette = ui.command_palette(self.query, lambda q: setattr(self, "query", q), # on_query self.commands(), on_submit=self.run_first) return ui.layer(main_view(), palette if self.open else None, on_dismiss=lambda: setattr(self, "open", False)) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Query: text: str class App(pz.App): open, query, wrap = True, "", False def update(self, message): match message: case Query(text): self.query = text case "toggle wrap" | "submit": self.wrap, self.open = not self.wrap, False def view(self): palette = ui.command_palette(self.query, Query, [ui.PaletteItem("editor: toggle soft wrap", "toggle wrap")], on_submit="submit") return ui.layer(main_view(), palette if self.open else None) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: query: str = "" wrap: bool = False def view(state, set_state): items = [ui.PaletteItem("editor: toggle soft wrap", lambda: set_state(wrap=not state.wrap))] return ui.command_palette(state.query, lambda q: set_state(query=q), items, on_submit=lambda: set_state(wrap=not state.wrap)) ``` #### Propiedades y API ##### `ui.command_palette` ```python command_palette( query, on_query, commands, *, selected=0, on_submit=None, placeholder='Execute a command…', input_id='command-palette', theme=None, **kwargs, ) ``` Una paleta de comandos: filtra `commands` por `query` con búsqueda difusa y muestra sus atajos. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `query` | *obligatorio* | Texto buscado. | | `commands` | *obligatorio* | Lista de `PaletteItem`. | | `selected` | `0` | Índice resaltado (para navegar con flechas). | | `placeholder` | `"Execute a command…"` | Texto de ayuda cuando está vacío. | | `input_id` | `"command-palette"` | Id del campo (para `Command.focus`). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | | `**kwargs` | — | Los de `picker`. | Eventos: `on_query`, `on_submit` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#command_palette) ##### `ui.PaletteItem` ```python @dataclass class PaletteItem(label, on_press=None, keys=None, icon=None, detail=None) ``` Una entrada de `command_palette` o `picker`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `label` | *obligatorio* | Texto de la etiqueta. | | `keys` | `None` | Atajo que se muestra al final, escrito como texto: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `icon` | `None` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `detail` | `None` | Texto secundario (por ejemplo, la ruta de un archivo). | Eventos: `on_press` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#paletteitem) #### Relacionados - [Buscador](https://albakore.github.io/pyzed/es/library/picker/): Una lista con búsqueda en una superficie flotante, con coincidencia difusa. - [Capa](https://albakore.github.io/pyzed/es/library/layer/): Dibuja una capa encima de tu vista; al pulsar fuera se cierra. - [Kbd](https://albakore.github.io/pyzed/es/library/kbd/): Atajos de teclado dibujados como teclas, con símbolos en macOS. ### Panel > Un panel lateral acoplable y redimensionable. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/panel/ :::tip[En pocas palabras] Una zona lateral de la ventana, como la lista de archivos de la izquierda, que puedes ensanchar o estrechar. ::: Un panel acoplable con una cabecera opcional. `side` decide qué borde lleva la línea; con un `width` numérico (`height` para el de abajo) ese borde se puede arrastrar para cambiar el tamaño, y `on_resize` informa del nuevo tamaño. `ui.panel` · Categoría: [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/#espacio-de-trabajo) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#panel) **Código:** ```python pz.row([ ui.panel( pz.column([ui.list_item("Inbox", on_open, icon="inbox", selected=True, inset=True), ui.list_item("Archive", on_open, icon="archive", inset=True)]), header=ui.panel_header("Mail", [ui.icon_button("plus", on_new, size="xs")], icon="mail"), width=220, side="left", ), pz.container(ui.muted("Content"), center=True, width="fill", height="fill"), ], height="fill") ``` #### Cuándo usarlo - Cuando montas tu propia disposición en lugar de `ui.workspace`: barras laterales, inspectores, esquemas. #### Uso ```python import pyzed as pz from pyzed import ui pz.row([ ui.panel( pz.column([ui.list_item("Inbox", on_open, icon="inbox", selected=True, inset=True), ui.list_item("Archive", on_open, icon="archive", inset=True)]), header=ui.panel_header("Mail", [ui.icon_button("plus", on_new, size="xs")], icon="mail"), width=220, side="left", ), pz.container(ui.muted("Content"), center=True, width="fill", height="fill"), ], height="fill") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Lado ```python pz.row([ui.panel(ui.list_item("Inbox", icon="inbox", inset=True), header=ui.panel_header("Mail"), width=160, side="left"), pz.container(width="fill")], height="fill") ``` ```python pz.row([pz.container(width="fill"), ui.panel(ui.muted("Properties"), header=ui.panel_header("Inspector"), width=160, side="right")], height="fill") ``` ```python pz.column([pz.container(height="fill"), ui.panel(ui.terminal("$ ls"), header=ui.panel_header("Terminal"), height=100, side="bottom")], width="fill") ``` #### Eventos Panel tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_resize` | `(size)` | Se llama con el nuevo tamaño al terminar de arrastrar. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Mail(pz.App): def __init__(self): self.width = 220 def view(self): return pz.row([ ui.panel(ui.muted("Inbox"), header=ui.panel_header("Mail"), width=self.width, on_resize=lambda size: setattr(self, "width", size)), # when the drag ends main_view(), ], height="fill") ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Width: size: float class Mail(pz.App): width = 220 def update(self, message): match message: case Width(size): self.width = size def view(self): return pz.row([ui.panel(ui.muted("Inbox"), width=self.width, on_resize=Width), main_view()], height="fill") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: width: float = 220 def view(state, set_state): return pz.row([ui.panel(ui.muted("Inbox"), width=state.width, on_resize=lambda s: set_state(width=s)), main_view()], height="fill") ``` #### Propiedades y API ##### `ui.panel` ```python panel( content, *, header=None, width=None, height=None, side='left', resizable=True, on_resize=None, min_size=120, theme=None, ) ``` Un panel acoplable. `side` decide en qué borde va la línea; con un `width` numérico (`height` para el inferior) ese borde se arrastra para cambiar su tamaño. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `header` | `None` | Cabecera (`ui.panel_header`). | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `side` | `"left"` | `"left"`, `"right"` o `"bottom"`. | | `resizable` | `True` | `False` mantiene el tamaño fijo. | | `min_size` | `120` | Tamaño mínimo al redimensionar. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_resize` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#panel) #### Relacionados - [Cabecera de panel](https://albakore.github.io/pyzed/es/library/panel-header/): Icono, título y acciones encima de un dock. - [Espacio de trabajo](https://albakore.github.io/pyzed/es/library/workspace/): La disposición de una ventana estilo editor: barra de título, docks, centro y barra de estado. - [División](https://albakore.github.io/pyzed/es/library/split/): Dos paneles con un divisor que se arrastra. ### Pila > Superpone los hijos; `pin` coloca uno en x, y. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/stack/ :::tip[En pocas palabras] Pone las cosas unas encima de otras, como hojas de papel. ::: Superpone sus hijos, el último encima de todos: la base de capas, modales y notificaciones. `pz.pin(child, x=, y=)` coloca un hijo en una posición dentro de la pila. `pz.stack` · Categoría: [Disposición](https://albakore.github.io/pyzed/es/library/#disposición) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#stack) **Código:** ```python t = pz.theme.current() pz.stack([ pz.container(width=320, height=180, bg=t.element, radius=10), pz.pin(pz.container(ui.label("pinned at 24, 24"), padding=10, bg=t.panel, radius=6, border_color=t.border, border_width=1), x=24, y=24), pz.pin(ui.count_badge(3), x=290, y=8), pz.pin(pz.container(ui.label("on top", color="#ffffff"), padding=10, bg=t.accent, radius=6), x=150, y=110), ], width=320, height=180) ``` #### Cuándo usarlo - Para insignias encima de iconos, capas y para colocar cosas libremente. #### Uso ```python import pyzed as pz from pyzed import ui t = pz.theme.current() pz.stack([ pz.container(width=320, height=180, bg=t.element, radius=10), pz.pin(pz.container(ui.label("pinned at 24, 24"), padding=10, bg=t.panel, radius=6, border_color=t.border, border_width=1), x=24, y=24), pz.pin(ui.count_badge(3), x=290, y=8), pz.pin(pz.container(ui.label("on top", color="#ffffff"), padding=10, bg=t.accent, radius=6), x=150, y=110), ], width=320, height=180) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python t = pz.theme.current() pz.stack([pz.container(width=180, height=100, bg=t.element, radius=10), pz.container(ui.label("on top"), center=True, width=180, height=100)]) ``` ```python t = pz.theme.current() pz.stack([pz.container(width=180, height=100, bg=t.element, radius=10), pz.pin(ui.count_badge(3), x=150, y=8)], width=180, height=100) ``` ```python pz.stack([pz.container(ui.icon_button("bell", on_press, size="lg"), padding=6), pz.pin(ui.indicator("error", size=8), x=24, y=6)], width=40, height=40) ``` #### Eventos Pila no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.stack` ```python stack(children=None, *, width=None, height=None) ``` Superpone los hijos, el último encima. Base de capas, modales y notificaciones. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `children` | `None` | Lista de hijos. Se aplanan listas anidadas y se ignoran `None` y `False`, así que puedes escribir `cond and widget`. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#stack) ##### `pz.pin` ```python pin(content, *, x=0.0, y=0.0, width=None, height=None) ``` Coloca `content` en `(x, y)` dentro de una caja. Úsalo en un `stack` para posicionar popovers y menús contextuales. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `x` | `0.0` | Posición horizontal. | | `y` | `0.0` | Posición vertical. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#pin) #### Relacionados - [Capa](https://albakore.github.io/pyzed/es/library/layer/): Dibuja una capa encima de tu vista; al pulsar fuera se cierra. - [Popover](https://albakore.github.io/pyzed/es/library/popover/): Contenido flotante en un punto: menús contextuales, selectores de color. - [Contenedor](https://albakore.github.io/pyzed/es/library/container/): Tamaño, relleno, alineación, fondo, borde y sombra para un hijo. ### Pista de atajo > Una pista discreta tipo “Abrir archivo Ctrl P”. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/keybinding-hint/ :::tip[En pocas palabras] Una línea discreta que enseña un atajo: “Abrir archivo Ctrl P”. ::: Una pista discreta que une una acción con su atajo, para pies y estados vacíos. `ui.keybinding_hint` · Categoría: [Estado y avisos](https://albakore.github.io/pyzed/es/library/#estado-y-avisos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#keybinding_hint) **Código:** ```python pz.column([ ui.keybinding_hint("Open file", "ctrl-p"), ui.keybinding_hint("Command palette", "ctrl-shift-p"), ui.keybinding_hint("Toggle terminal", "ctrl-j"), ], spacing=10, align="center") ``` #### Cuándo usarlo - En editores vacíos y pies de página, para enseñar los atajos de las acciones más útiles. #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ ui.keybinding_hint("Open file", "ctrl-p"), ui.keybinding_hint("Command palette", "ctrl-shift-p"), ui.keybinding_hint("Toggle terminal", "ctrl-j"), ], spacing=10, align="center") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Ejemplos ```python ui.keybinding_hint("Search", "/") ``` ```python ui.keybinding_hint("Command palette", "ctrl-shift-p") ``` ```python ui.keybinding_hint("Keyboard shortcuts", "ctrl-k ctrl-s") ``` #### Eventos Pista de atajo no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.keybinding_hint` ```python keybinding_hint(text, keys, *, theme=None) ``` Una pista atenuada como «Abrir archivo Ctrl P» para pies y estados vacíos. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `text` | *obligatorio* | Texto. | | `keys` | *obligatorio* | Atajo que se muestra al final, escrito como texto: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#keybinding_hint) #### Relacionados - [Kbd](https://albakore.github.io/pyzed/es/library/kbd/): Atajos de teclado dibujados como teclas, con símbolos en macOS. - [Estado vacío](https://albakore.github.io/pyzed/es/library/empty-state/): Un marcador centrado para listas y búsquedas vacías. - [Atajos](https://albakore.github.io/pyzed/es/library/shortcuts/): Una lista de atajos agrupada y con búsqueda. ### Plan > El plan de un agente como una lista que se pliega. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/plan/ :::tip[En pocas palabras] Una lista de tareas que un asistente de IA se escribe y va tachando mientras trabaja. ::: `ui.Plan` es un componente que muestra el plan de un agente de IA como una lista: cada paso está pendiente (un círculo vacío), en curso (un punto, o un spinner con `running=True`) o hecho (un check verde, con el texto atenuado), y la cabecera cuenta `hechos/total`. Un clic en la cabecera pliega y despliega los pasos. `steps` son diccionarios con `step` (el texto) y `status`: `"pending"`, `"in_progress"` o `"done"`. `ui.Plan` · Categoría: [Agentes de IA](https://albakore.github.io/pyzed/es/library/#agentes-de-ia) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#plan) **Código:** ```python pz.container(ui.Plan(steps=[ {"step": "Read the form and its handlers", "status": "done"}, {"step": "Add the email field", "status": "done"}, {"step": "Validate the email as you type", "status": "in_progress"}, {"step": "Run the tests", "status": "pending"}, ], running=True), width=420) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Encima del chat de un agente, mientras resuelve una tarea de varios pasos. - Para cualquier lista de pasos que se actualiza mientras avanza el trabajo, como una instalación o una migración. #### Uso ```python import pyzed as pz from pyzed import ui class Agent(pz.App): def __init__(self): self.steps = [{"step": "Add the form", "status": "done"}, {"step": "Wire the events", "status": "in_progress"}] self.working = True def view(self): return ui.Plan(steps=self.steps, running=self.working) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Estado Sin `running` el paso en curso muestra un punto; el plegado se dibuja como queda tras un clic en la cabecera. ```python pz.container(ui.Plan(steps=[{"step": "Add the form", "status": "done"}, {"step": "Wire the events", "status": "in_progress"}, {"step": "Run the tests", "status": "pending"}], running=True), width=300) ``` ```python pz.container(ui.Plan(steps=[{"step": "Add the form", "status": "done"}, {"step": "Wire the events", "status": "in_progress"}, {"step": "Run the tests", "status": "pending"}]), width=300) ``` ```python pz.container(ui.Plan(steps=[{"step": "Add the form", "status": "pending"}, {"step": "Run the tests", "status": "pending"}]), width=300) ``` ```python pz.container(ui.Plan(steps=[{"step": "Add the form", "status": "done"}, {"step": "Run the tests", "status": "done"}]), width=300) ``` ```python pz.container(ui.Plan(steps=[{"step": "Add the form", "status": "done"}, {"step": "Run the tests", "status": "pending"}]), width=300) ``` #### Eventos Plan no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. #### Propiedades y API ##### `ui.Plan` ```python class Plan ``` Un componente para el plan de un agente: una lista de pasos con `hechos/total` en la cabecera, que la pliega. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `steps` | `None` | Dicts `{"step": texto, "status": "pending" \| "in_progress" \| "done"}`. | | `running` | `False` | Hace girar el paso en curso. | | `theme` | `None` | Tema. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#plan) #### Relacionados - [Llamada a herramienta](https://albakore.github.io/pyzed/es/library/tool-call/): Un paso de un agente de IA: qué hizo, su resultado y Permitir o Rechazar. - [Progreso](https://albakore.github.io/pyzed/es/library/progress/): Una barra de progreso fina en un color semántico. - [Barra de cambios](https://albakore.github.io/pyzed/es/library/edits-bar/): Los archivos que cambió un agente, con Conservar todo y Rechazar todo. ### Popover > Contenido flotante en un punto: menús contextuales, selectores de color. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/popover/ :::tip[En pocas palabras] Una cajita flotante que aparece junto a donde pulsaste, como un menú de clic derecho. ::: Muestra `content` (un menú, una tarjeta…) encima de `base` en `(x, y)`. Al pulsar en otro sitio se llama a `on_dismiss`; con `content=None` solo se muestra `base`. Para un menú de clic derecho, guarda la posición del puntero con `pz.mouse_area(on_move=...)` y ábrelo en `on_right_press`. `ui.popover` · Categoría: [Capas flotantes](https://albakore.github.io/pyzed/es/library/#capas-flotantes) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#popover) **Código:** ```python ui.popover( main_view(), ui.menu([ ui.MenuItem("Copy", on_copy, icon="copy", keys="ctrl-c"), ui.MenuItem("Paste", on_paste, icon="clipboard", keys="ctrl-v"), ui.SEPARATOR, ui.MenuItem("Delete", on_delete, icon="trash-2", danger=True), ], width=220), x=180, y=70, on_dismiss=on_close, ) ``` #### Cuándo usarlo - Para menús contextuales, paneles desplegables y selectores anclados a un punto o a un botón. #### Uso ```python import pyzed as pz from pyzed import ui class Canvas(pz.App): def __init__(self): self.pointer, self.menu_at = (0, 0), None def view(self): base = pz.mouse_area(self.content(), on_move=lambda x, y: setattr(self, "pointer", (x, y)), on_right_press=lambda: setattr(self, "menu_at", self.pointer)) menu = ui.menu([ui.MenuItem("Copy", self.copy), ui.MenuItem("Paste", self.paste)]) x, y = self.menu_at or (0, 0) return ui.popover(base, menu if self.menu_at else None, x=x, y=y, on_dismiss=lambda: setattr(self, "menu_at", None)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido ```python ui.popover(main_view(), ui.menu([ui.MenuItem("Copy", on_copy, icon="copy"), ui.MenuItem("Paste", on_paste, icon="clipboard")], width=180), x=60, y=40, on_dismiss=on_close) ``` ```python ui.popover(main_view(), ui.card(ui.muted("Any widget fits here."), title="Popover", width=220), x=120, y=40, on_dismiss=on_close) ``` #### Eventos Popover tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_dismiss` | `()` | Mensaje o callable que se envía al cerrar. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Canvas(pz.App): def __init__(self): self.pointer, self.menu_at = (0, 0), None def view(self): base = pz.mouse_area(main_view(), on_move=lambda x, y: setattr(self, "pointer", (x, y)), on_right_press=lambda: setattr(self, "menu_at", self.pointer)) menu = ui.menu([ui.MenuItem("Copy", lambda: setattr(self, "menu_at", None))]) x, y = self.menu_at or (0, 0) return ui.popover(base, menu if self.menu_at else None, x=x, y=y, on_dismiss=lambda: setattr(self, "menu_at", None)) # click outside ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class MenuAt: x: float y: float class Canvas(pz.App): menu_at = None def update(self, message): match message: case MenuAt(x, y): self.menu_at = (x, y) case "close": self.menu_at = None def view(self): base = pz.canvas([], width=400, height=200, on_right_press=MenuAt) x, y = self.menu_at or (0, 0) return ui.popover(base, ui.menu([ui.MenuItem("Copy", "close")]) if self.menu_at else None, x=x, y=y, on_dismiss="close") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: menu_at: tuple = () def view(state, set_state): base = pz.canvas([], width=400, height=200, on_right_press=lambda x, y: set_state(menu_at=(x, y))) menu = ui.menu([ui.MenuItem("Copy", lambda: set_state(menu_at=()))]) if state.menu_at else None x, y = state.menu_at or (0, 0) return ui.popover(base, menu, x=x, y=y, on_dismiss=lambda: set_state(menu_at=())) ``` #### Propiedades y API ##### `ui.popover` ```python popover(base, content=None, *, x=0, y=0, on_dismiss=None, align_x='start') ``` Muestra `content` (un `ui.menu`, una tarjeta…) sobre `base` en `(x, y)`. Hacer clic en cualquier otro sitio llama a `on_dismiss`. Para un menú de clic derecho, guarda la posición del ratón con `mouse_area(on_move=...)` y abre el popover desde `on_right_press` (ver `examples/context_menu.py`). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `base` | *obligatorio* | La interfaz de debajo. | | `content` | `None` | Lo que flota (o `None`). | | `x` | `0` | Posición horizontal. | | `y` | `0` | Posición vertical. | | `align_x` | `"start"` | Alineación horizontal: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | Eventos: `on_dismiss` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#popover) #### Relacionados - [Menú](https://albakore.github.io/pyzed/es/library/menu/): Un menú contextual con iconos, atajos y marcas. - [Capa](https://albakore.github.io/pyzed/es/library/layer/): Dibuja una capa encima de tu vista; al pulsar fuera se cierra. - [Botón dividido](https://albakore.github.io/pyzed/es/library/split-button/): Una acción principal más una flecha que abre más opciones. ### Pregunta > Pide una línea de texto. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/prompt/ :::tip[En pocas palabras] Una cajita que te pide escribir algo, como un nombre nuevo para un archivo. ::: Pide una línea de texto: un campo controlado más Cancelar y Aceptar. Enter o el botón llaman a `on_submit`. `ui.prompt` · Categoría: [Diálogos](https://albakore.github.io/pyzed/es/library/#diálogos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#prompt) **Código:** ```python ui.dialog_layer(main_view(), ui.prompt( "Rename", "notes.py", on_input, on_submit, on_cancel, message="New name for the file:", width=360, )) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para nombres rápidos: renombrar un archivo, nombrar una rama, guardar una vista. #### Uso ```python import pyzed as pz from pyzed import ui class Files(pz.App): def __init__(self): self.new_name = None def view(self): dialog = None if self.new_name is not None: dialog = ui.prompt("Rename", self.new_name, lambda t: setattr(self, "new_name", t), self.rename, lambda: setattr(self, "new_name", None)) return ui.dialog_layer(self.main_view(), dialog) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido ```python ui.prompt("New branch", "", on_input, on_submit, on_cancel, placeholder="feature/…", width=340) ``` ```python ui.prompt("Rename", "notes.py", on_input, on_submit, on_cancel, message="New name for the file:", width=340) ``` ```python ui.prompt("Save view", "My view", on_input, on_submit, on_cancel, submit_label="Save", width=340) ``` #### Eventos Pregunta tiene **3 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_input` | `(text)` | Recibe cada edición. | | `on_submit` | `()` | Enter o el botón de aceptar. | | `on_cancel` | `()` | Cancelar, el icono de cerrar. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Files(pz.App): def __init__(self): self.name = "notes.py" self.new_name = "notes.py" # None while the dialog is closed def rename(self): # on_submit: Enter or the button self.name, self.new_name = self.new_name, None def view(self): dialog = None if self.new_name is not None: dialog = ui.prompt("Rename", self.new_name, lambda text: setattr(self, "new_name", text), # on_input self.rename, lambda: setattr(self, "new_name", None), # on_cancel message="New name for the file:") return ui.dialog_layer(main_view(), dialog) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Typed: text: str class Files(pz.App): name, new_name = "notes.py", "notes.py" def update(self, message): match message: case Typed(text): self.new_name = text case "rename": self.name, self.new_name = self.new_name, None case "cancel": self.new_name = None def view(self): dialog = ui.prompt("Rename", self.new_name, Typed, "rename", "cancel") \ if self.new_name is not None else None return ui.dialog_layer(main_view(), dialog) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: name: str = "notes.py" draft: str = "notes.py" renaming: bool = True def view(state, set_state): if not state.renaming: return ui.label(state.name) return ui.dialog_layer(main_view(), ui.prompt( "Rename", state.draft, lambda text: set_state(draft=text), lambda: set_state(name=state.draft, renaming=False), lambda: set_state(renaming=False))) ``` #### Propiedades y API ##### `ui.prompt` ```python prompt( title, value, on_input, on_submit, on_cancel, *, message=None, placeholder='', submit_label=None, cancel_label=None, id=None, width=440, theme=None, ) ``` Pide una línea de texto. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | *obligatorio* | Título. | | `value` | *obligatorio* | Texto actual. | | `message` | `None` | Texto sobre el campo. | | `placeholder` | `""` | Texto de ayuda cuando está vacío. | | `submit_label` | `None` | Texto del botón de aceptar. | | `cancel_label` | `None` | Texto del botón de cancelar. | | `id` | `None` | Id del campo (para `Command.focus`). | | `width` | `440` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_input`, `on_submit`, `on_cancel` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#prompt) #### Relacionados - [Confirmación](https://albakore.github.io/pyzed/es/library/confirm/): Una pregunta de sí o no, en rojo para acciones destructivas. - [Campo de texto](https://albakore.github.io/pyzed/es/library/input/): Un campo de texto controlado con icono, placeholder y modo contraseña. - [Capa de diálogo](https://albakore.github.io/pyzed/es/library/dialog-layer/): Centra un diálogo encima de tu vista con el fondo oscurecido. ### Progreso > Una barra de progreso fina en un color semántico. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/progress/ :::tip[En pocas palabras] Una barra que se llena para mostrar cuánto falta de una tarea. ::: Una barra fina que se llena de `0` a `max` (1.0 por defecto). `tint` cambia su color y `height` su grosor. `ui.progress` · Categoría: [Estado y avisos](https://albakore.github.io/pyzed/es/library/#estado-y-avisos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#progress) **Código:** ```python pz.column([ ui.progress(0.25), ui.progress(0.6, tint="success", height=6), ui.progress(37, max=40, tint="warning", height=8), ], spacing=18, width=360) ``` #### Cuándo usarlo - Para trabajos cuyo avance puedes medir: descargas, compilaciones, importaciones. - Cuando no se puede medir, muestra un [Spinner](https://albakore.github.io/pyzed/es/library/spinner/). #### Uso ```python import pyzed as pz from pyzed import ui class Download(pz.App): def __init__(self): self.done, self.total = 0, 1 def update(self, message): self.done, self.total = message # posted from a worker thread with pz.post((done, total)) def view(self): return ui.progress(self.done, max=self.total) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Valor ```python ui.progress(0.1, width=240) ``` ```python ui.progress(0.5, width=240) ``` ```python ui.progress(1.0, width=240) ``` ```python ui.progress(37, max=40, width=240) ``` ##### Tinte ```python ui.progress(0.7, tint="success", width=240) ``` ```python ui.progress(0.7, tint="warning", width=240) ``` ```python ui.progress(0.7, tint="error", width=240) ``` ##### Grosor ```python ui.progress(0.6, height=2, width=240) ``` ```python ui.progress(0.6, height=4, width=240) ``` ```python ui.progress(0.6, height=10, width=240) ``` ##### Estados y eventos ```python pz.container(ui.progress(0.42, label="Downloading", show_value=True), width=300) ``` ```python pz.container(ui.progress(None, label="Indexing"), width=300) ``` ##### Widget base El widget `pz.*` de debajo: sin tema, todos los colores y tamaños son tuyos. ```python pz.progress_bar(64, max=100, width=240, height=12, accent="#c678dd", bg="#c678dd33", radius=6) ``` #### Eventos Progreso no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.progress` ```python progress( value, *, max=1.0, tint='accent', height=4, width='fill', label=None, show_value=False, theme=None, ) ``` Una barra de progreso fina. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | *obligatorio* | Progreso, o `None` para una barra indeterminada (mantén una suscripción `pz.every(1/30, ...)` mientras se ve). | | `max` | `1.0` | Valor máximo. | | `tint` | `"accent"` | Color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` o un token. | | `height` | `4` | Grosor. | | `width` | `"fill"` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `label` | `None` | Texto sobre la barra. | | `show_value` | `False` | Añade el porcentaje a su derecha. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#progress) ##### `pz.progress_bar` ```python progress_bar( value, *, min=0.0, max=1.0, width=None, height=None, accent=None, bg=None, radius=2.0, ) ``` Una barra de progreso. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | *obligatorio* | Valor actual. | | `min` | `0.0` | Mínimo. | | `max` | `1.0` | Máximo. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `accent` | `None` | Color de acento (parte activa). | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `radius` | `2.0` | Radio de las esquinas. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#progress_bar) #### Relacionados - [Spinner](https://albakore.github.io/pyzed/es/library/spinner/): Un arco que gira para los estados de carga. - [Deslizador](https://albakore.github.io/pyzed/es/library/slider/): Elige un número de un rango arrastrando. - [Cifra](https://albakore.github.io/pyzed/es/library/stat/): Una cifra de dashboard con variación y minigráfico. ### Redimensionable > Un panel cuyo borde se arrastra para cambiar su tamaño. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/resizable/ :::tip[En pocas palabras] Un panel con un borde que puedes arrastrar para hacerlo más grande o más pequeño. ::: Un panel cuyo `edge` (`"left"`, `"right"`, `"top"` o `"bottom"`) se puede arrastrar para cambiar su tamaño, entre `min_size` y `max_size`. Conserva el tamaño que elige el usuario; `on_resize(size)` se llama al soltar para que puedas guardarlo. `pz.resizable` · Categoría: [Disposición](https://albakore.github.io/pyzed/es/library/#disposición) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#resizable) **Código:** ```python t = pz.theme.current() pz.row([ pz.resizable(pz.container(ui.label("Drag my right edge"), padding=12, width="fill", height="fill", bg=t.panel), edge="right", size=260, min_size=160, color=t.border, hover_color=t.accent), pz.container(ui.muted("Content"), center=True, width="fill", height="fill"), ], height="fill") ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para una barra lateral o un panel inferior redimensionable junto a contenido fijo. #### Uso ```python import pyzed as pz from pyzed import ui t = pz.theme.current() pz.row([ pz.resizable(pz.container(ui.label("Drag my right edge"), padding=12, width="fill", height="fill", bg=t.panel), edge="right", size=260, min_size=160, color=t.border, hover_color=t.accent), pz.container(ui.muted("Content"), center=True, width="fill", height="fill"), ], height="fill") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Borde ```python t = pz.theme.current() pz.row([pz.resizable(pz.container(ui.label("Sidebar"), padding=10, width="fill", height="fill", bg=t.panel), edge="right", size=150, color=t.border), pz.container(width="fill")], height="fill") ``` ```python t = pz.theme.current() pz.row([pz.container(width="fill"), pz.resizable(pz.container(ui.label("Inspector"), padding=10, width="fill", height="fill", bg=t.panel), edge="left", size=150, color=t.border)], height="fill") ``` ```python t = pz.theme.current() pz.column([pz.container(height="fill"), pz.resizable(pz.container(ui.label("Console"), padding=10, width="fill", height="fill", bg=t.panel), edge="top", size=80, color=t.border)], width="fill") ``` #### Eventos Redimensionable tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_resize` | `(size)` | Se llama con el nuevo tamaño al terminar de arrastrar. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Mail(pz.App): def __init__(self): self.width = 260 def view(self): return pz.row([ pz.resizable(ui.muted(f"{self.width:.0f} px"), edge="right", size=self.width, min_size=160, on_resize=lambda size: setattr(self, "width", size)), # when the drag ends main_view(), ], height="fill") ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Width: size: float class Mail(pz.App): width = 260 def update(self, message): match message: case Width(size): self.width = size def view(self): return pz.row([pz.resizable(ui.muted("Folders"), size=self.width, on_resize=Width), main_view()], height="fill") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: width: float = 260 def view(state, set_state): return pz.row([pz.resizable(ui.muted("Folders"), size=state.width, on_resize=lambda size: set_state(width=size)), main_view()], height="fill") ``` #### Propiedades y API ##### `pz.resizable` ```python resizable( content, *, edge='right', size=240.0, min_size=80.0, max_size=None, on_resize=None, color=None, hover_color=None, thickness=1.0, handle=8.0, width=None, height=None, ) ``` Un panel cuyo borde `edge` se arrastra para cambiar su tamaño. Conserva el tamaño al que lo arrastra el usuario; `on_resize(size)` se llama al terminar de arrastrar para que puedas guardarlo. Si `size` cambia después, vuelve a ese valor. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | El contenido del panel. | | `edge` | `"right"` | El borde con el asa: `"right"` (una barra lateral izquierda), `"left"`, `"top"` (un panel inferior) o `"bottom"`. | | `size` | `240.0` | Ancho inicial (alto con los bordes top/bottom). | | `min_size` | `80.0` | Tamaño mínimo. | | `max_size` | `None` | Tamaño máximo. | | `color` | `None` | Color de la línea. | | `hover_color` | `None` | Color de la línea al pasar el ratón o arrastrar. | | `thickness` | `1.0` | Grosor de la línea. | | `handle` | `8.0` | Ancho de la zona que se puede agarrar. | | `width` | `None` | Ancho con los bordes top/bottom. | | `height` | `None` | Alto con los bordes left/right. | Eventos: `on_resize` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#resizable) #### Relacionados - [División](https://albakore.github.io/pyzed/es/library/split/): Dos paneles con un divisor que se arrastra. - [Panel](https://albakore.github.io/pyzed/es/library/panel/): Un panel lateral acoplable y redimensionable. ### Responsivo > Construye su contenido según el espacio disponible. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/responsive/ :::tip[En pocas palabras] Cambia la disposición según el espacio que haya, como un móvil frente a un monitor. ::: Llama a `function(width, height)` con el espacio que recibe de verdad y muestra lo que devuelve, así la disposición se adapta: ocultar una barra lateral si la ventana es estrecha, cambiar el número de columnas… `pz.responsive` · Categoría: [Disposición](https://albakore.github.io/pyzed/es/library/#disposición) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#responsive) **Código:** ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.responsive(lambda w, h: pz.center(pz.column([ ui.muted(f"{w:.0f} × {h:.0f} px available"), pz.row([box("1"), box("2"), box("3"), box("4")], spacing=8) if w > 400 else pz.column([box("1"), box("2")], spacing=8), ], spacing=10, align="center"))), width=480, height=150) ``` #### Cuándo usarlo - Para disposiciones que deben funcionar en una ventana pequeña y en una grande. #### Uso ```python import pyzed as pz from pyzed import ui box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.responsive(lambda w, h: pz.center(pz.column([ ui.muted(f"{w:.0f} × {h:.0f} px available"), pz.row([box("1"), box("2"), box("3"), box("4")], spacing=8) if w > 400 else pz.column([box("1"), box("2")], spacing=8), ], spacing=10, align="center"))), width=480, height=150) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Ancho disponible El mismo código con dos anchos: la disposición cambia sola. ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.responsive(lambda w, h: pz.row([box("1"), box("2"), box("3")], spacing=8) if w > 300 else pz.column([box("1"), box("2")], spacing=8)), width=200, height=110) ``` ```python box = lambda text, w=90, h=44: pz.container(ui.label(text), center=True, width=w, height=h, bg=pz.theme.current().element, radius=6) pz.container(pz.responsive(lambda w, h: pz.row([box("1"), box("2"), box("3")], spacing=8) if w > 300 else pz.column([box("1"), box("2")], spacing=8)), width=400, height=60) ``` #### Eventos Responsivo no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.responsive` ```python responsive(function) ``` Construye su contenido con `function(ancho, alto)` usando el espacio realmente disponible, para que el layout se adapte (por ejemplo, ocultar una barra lateral si la ventana es estrecha). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `function` | *obligatorio* | Función que recibe `(ancho, alto)` y devuelve un widget. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#responsive) #### Relacionados - [Cuadrícula](https://albakore.github.io/pyzed/es/library/grid/): Columnas iguales, o pistas como en CSS con expansión. - [Flex](https://albakore.github.io/pyzed/es/library/flex/): Flexbox de CSS: justify, align, gap, wrap y grow. ### Resumen de cambios > `+12 −3` con bloques al estilo de GitHub. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/diff-stat/ :::tip[En pocas palabras] Cuántas líneas se añadieron (verde) y se quitaron (rojo) en un cambio. ::: Líneas añadidas y quitadas, con una fila de bloques verdes y rojos, como en el panel de git de un editor de código. `ui.diff_stat` · Categoría: [Datos](https://albakore.github.io/pyzed/es/library/#datos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#diff_stat) **Código:** ```python pz.column([ pz.row([ui.label("render.rs", mono=True), pz.hspace(), ui.diff_stat(42, 9)], align="center"), pz.row([ui.label("app.py", mono=True), pz.hspace(), ui.diff_stat(3, 27)], align="center"), pz.row([ui.label("README.md", mono=True), pz.hspace(), ui.diff_stat(12, 0)], align="center"), ], spacing=10, width=320) ``` #### Cuándo usarlo - Junto a un archivo, commit o pull request en una lista de cambios. #### Uso ```python import pyzed as pz from pyzed import ui pz.column([ pz.row([ui.label("render.rs", mono=True), pz.hspace(), ui.diff_stat(42, 9)], align="center"), pz.row([ui.label("app.py", mono=True), pz.hspace(), ui.diff_stat(3, 27)], align="center"), pz.row([ui.label("README.md", mono=True), pz.hspace(), ui.diff_stat(12, 0)], align="center"), ], spacing=10, width=320) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python ui.diff_stat(42, 3) ``` ```python ui.diff_stat(2, 57) ``` ```python ui.diff_stat(12, 0) ``` ```python ui.diff_stat(120, 40, blocks=10) ``` #### Eventos Resumen de cambios no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.diff_stat` ```python diff_stat(added, removed, *, blocks=5, theme=None) ``` `+12 −3` con bloques de colores al estilo de GitHub. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `added` | *obligatorio* | Líneas añadidas. | | `removed` | *obligatorio* | Líneas eliminadas. | | `blocks` | `5` | Número de bloques. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#diff_stat) #### Relacionados - [Insignia](https://albakore.github.io/pyzed/es/library/badge/): Una pastilla pequeña para estados, versiones y contadores. - [Árbol](https://albakore.github.io/pyzed/es/library/tree/): Un árbol de archivos plegable con iconos y colores de git. ### Segmentado > Un grupo de botones de texto excluyentes. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/segmented/ :::tip[En pocas palabras] Una fila de botones unidos en la que hay exactamente uno elegido, como Día / Semana / Mes en un calendario. ::: Una fila de botones unidos en la que uno está seleccionado. `on_select` recibe la opción elegida. `ui.segmented` · Categoría: [Formularios](https://albakore.github.io/pyzed/es/library/#formularios) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#segmented) **Código:** ```python pz.column([ ui.segmented(["Editor", "Terminal", "Both"], "Editor", on_select), ui.segmented(["Day", "Week", "Month"], "Week", on_select), ], spacing=14, align="center") ``` #### Cuándo usarlo - Para cambiar entre dos y cinco vistas o modos (Día / Semana / Mes). - Solo con iconos, usa un [Grupo de alternancia](https://albakore.github.io/pyzed/es/library/toggle-group/). #### Uso ```python import pyzed as pz from pyzed import ui class Calendar(pz.App): def __init__(self): self.mode = "Week" def view(self): return ui.segmented(["Day", "Week", "Month"], self.mode, lambda m: setattr(self, "mode", m)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python ui.segmented(["Code", "Preview"], "Code", on_select) ``` ```python ui.segmented(["Day", "Week", "Month"], "Week", on_select) ``` ```python ui.segmented(["XS", "S", "M", "L", "XL"], "M", on_select) ``` ##### Selección ```python ui.segmented(["Editor", "Terminal", "Both"], "Editor", on_select) ``` ```python ui.segmented(["Editor", "Terminal", "Both"], "Both", on_select) ``` ##### Estados y eventos ```python ui.segmented([("list", "List", "list"), ("grid", "Grid", "layout-grid")], "grid", on_select) ``` ```python ui.segmented(["Day", "Week", "Month"], "Week", on_select, width=320) ``` ```python ui.segmented(["Day", "Week", "Month"], "Day", on_select, disabled={"Month"}) ``` ```python ui.segmented(["Code", "Preview"], "Code", on_select, size="sm") ``` #### Eventos Segmentado tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_select` | `(item)` | Callable que recibe el elemento elegido. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Calendar(pz.App): def __init__(self): self.mode = "Week" def view(self): return pz.column([ ui.segmented(["Day", "Week", "Month"], self.mode, lambda m: setattr(self, "mode", m)), ui.label(f"Showing a {self.mode.lower()}"), ], spacing=8, align="center") ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Mode: name: str class Calendar(pz.App): mode = "Week" def update(self, message): match message: case Mode(name): self.mode = name def view(self): return ui.segmented(["Day", "Week", "Month"], self.mode, Mode) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: mode: str = "Week" def view(state, set_state): return ui.segmented(["Day", "Week", "Month"], state.mode, lambda m: set_state(mode=m)) ``` #### Consejos :::tip Al pulsarlo o llegar con Tab tiene el foco del teclado: ←/→ eligen la opción anterior o la siguiente (saltando las desactivadas) e Inicio/Fin la primera y la última. ::: #### Propiedades y API ##### `ui.segmented` ```python segmented( options, selected, on_select=None, *, size='md', width=None, disabled=False, id=None, theme=None, ) ``` Un grupo de botones mutuamente excluyentes. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `options` | *obligatorio* | Textos, `(valor, etiqueta)` o `(valor, etiqueta, icono)`. | | `selected` | *obligatorio* | El valor activo. | | `size` | `"md"` | `"sm"`, `"md"` o `"lg"`. | | `width` | `None` | `"fill"` reparte el ancho entre los botones. | | `disabled` | `False` | `True`, o los valores a desactivar. | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_select` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#segmented) #### Relacionados - [Grupo de alternancia](https://albakore.github.io/pyzed/es/library/toggle-group/): Un control segmentado solo con iconos. - [Grupo de radios](https://albakore.github.io/pyzed/es/library/radio-group/): Opciones excluyentes como botones de radio. - [Barra de pestañas](https://albakore.github.io/pyzed/es/library/tab-bar/): Pestañas de editor con iconos, puntos de cambios y botón de cerrar. ### Select > Un desplegable para elegir una opción. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/select/ :::tip[En pocas palabras] Una caja que abre una lista para que elijas una opción. ::: Un desplegable con el estilo del tema. Las opciones se muestran con `str()`, así que valen cadenas, enums o dataclasses, y `on_select` recibe el objeto elegido. `ui.select` · Categoría: [Formularios](https://albakore.github.io/pyzed/es/library/#formularios) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#select) **Código:** ```python pz.row([ ui.select(["Python", "Rust", "TypeScript"], "Rust", on_select, width=180), ui.select(["UTF-8", "Latin-1"], None, on_select, placeholder="Encoding…", width=180), ], spacing=12) ``` #### Cuándo usarlo - Para elegir una entre muchas opciones cuando no caben todas (idioma, tema, codificación). - Con dos a cinco opciones cortas, [Segmentado](https://albakore.github.io/pyzed/es/library/segmented/) o un [Grupo de radios](https://albakore.github.io/pyzed/es/library/radio-group/) las muestran de un vistazo. #### Uso ```python import pyzed as pz from pyzed import ui class Settings(pz.App): def __init__(self): self.language = "Python" def view(self): return ui.select(["Python", "Rust", "TypeScript"], self.language, lambda value: setattr(self, "language", value), width=180) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Estado ```python ui.select(["Python", "Rust", "TypeScript"], "Rust", on_select, width=200) ``` ```python ui.select(["UTF-8", "Latin-1"], None, on_select, placeholder="Encoding…", width=200) ``` ```python ui.select(["One Dark"], "One Dark", width=200) ``` ##### Tamaño ```python ui.select(["Python", "Rust"], "Python", on_select, size="sm", width=200) ``` ```python ui.select(["Python", "Rust"], "Python", on_select, size="md", width=200) ``` ```python ui.select(["Python", "Rust"], "Python", on_select, size="lg", width=200) ``` ##### Estados y eventos `on_open` y `on_close` se ejecutan al abrir y cerrar el menú. ```python ui.select(["One Dark"], "One Dark", on_select, disabled=True, width=200) ``` ```python ui.select(["UTF-8", "Latin-1"], None, on_select, placeholder="Encoding…", invalid=True, width=200) ``` ##### Widget base El widget `pz.*` de debajo: sin tema, todos los colores y tamaños son tuyos. ```python pz.pick_list(["Small", "Medium", "Large"], "Medium", on_select, width=200, bg="#2b2140", color="#e9d5ff", border_color="#c678dd", radius=8) ``` #### Eventos Select tiene **3 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_select` | `ui.select`, `pz.pick_list` | `(item)` | Callable que recibe el elemento elegido. | | `on_open` | `ui.select`, `pz.pick_list` | `()` | Se envía al abrir el menú. | | `on_close` | `ui.select`, `pz.pick_list` | `()` | Se envía al cerrar el menú. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Settings(pz.App): def __init__(self): self.language = "Python" self.open = False def view(self): return pz.column([ ui.select(["Python", "Rust", "TypeScript"], self.language, lambda value: setattr(self, "language", value), # on_select: the option on_open=lambda: setattr(self, "open", True), on_close=lambda: setattr(self, "open", False), width=180), ui.muted("Choosing…" if self.open else f"Language: {self.language}"), ], spacing=8) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Language: name: str class Settings(pz.App): language = "Python" def update(self, message): match message: case Language(name): self.language = name def view(self): return ui.select(["Python", "Rust", "TypeScript"], self.language, Language, width=180) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: language: str = "Python" def view(state, set_state): return ui.select(["Python", "Rust", "TypeScript"], state.language, lambda value: set_state(language=value), width=180) ``` #### Consejos :::tip Toma el foco del teclado como un campo de texto (con un clic o Tab) y lo mantiene después de elegir: entonces ↓/↑ eligen la opción siguiente o la anterior sin abrir el menú, Inicio/Fin la primera y la última, y al escribir salta a la opción que empieza por esas letras. Enter, Espacio, Alt+↓ o F4 abren el menú; ahí ↑/↓ se mueven, Enter elige y Esc lo cierra. Pasa `id=` para enfocarlo con `pz.Command.focus(id)`. ::: #### Propiedades y API ##### `ui.select` ```python select( options, selected=None, on_select=None, *, placeholder='Select…', width=None, size='md', disabled=False, invalid=False, on_open=None, on_close=None, id=None, theme=None, ) ``` Un desplegable con el estilo del tema. Las opciones se muestran con `str()`, así que sirven textos, enums, dataclasses o modelos de base de datos; `on_select` recibe el objeto elegido, no su texto. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `options` | *obligatorio* | Lista de opciones. | | `selected` | `None` | Opción actual. | | `placeholder` | `"Select…"` | Texto de ayuda cuando está vacío. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `size` | `"md"` | `"sm"`, `"md"` o `"lg"`. | | `disabled` | `False` | Lo atenúa. | | `invalid` | `False` | Pinta el borde con el color de error. | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_select`, `on_open`, `on_close` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#select) ##### `pz.pick_list` ```python pick_list( options, selected=None, on_select=None, *, on_open=None, on_close=None, placeholder=None, width=None, padding=None, text_size=None, font=None, handle=None, handle_size=12.0, menu_height=None, bg=None, color=None, placeholder_color=None, handle_color=None, border_color=None, hover_border_color=None, radius=None, menu_bg=None, menu_color=None, menu_border_color=None, selected_bg=None, selected_color=None, focus_border_color=None, id=None, ) ``` Un desplegable para elegir una de `options`. `on_select(opción)` recibe la elegida. Para el desplegable con tema usa `ui.select`. Toma el foco del teclado como un campo de texto (un clic, Tab o `Command.focus(id)`) y lo mantiene después de elegir: ↑/↓ eligen la opción anterior o la siguiente sin abrirlo, Inicio/Fin la primera y la última y al escribir salta a una opción; Enter, Espacio, Alt+↓ o F4 abren el menú, donde ↑/↓ se mueven, Enter elige y Esc cierra. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `options` | *obligatorio* | Lista de opciones. | | `selected` | `None` | Opción actual. | | `placeholder` | `None` | Texto de ayuda cuando está vacío. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `padding` | `None` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `text_size` | `None` | Tamaño del texto. | | `font` | `None` | Familia de la fuente. | | `handle` | `None` | Carácter del icono de la flecha. | | `handle_size` | `12.0` | Tamaño de la flecha. | | `menu_height` | `None` | Alto máximo del menú. | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `color` | `None` | Color del texto o del icono. | | `placeholder_color` | `None` | Color del placeholder. | | `handle_color` | `None` | Color de la flecha. | | `border_color` | `None` | Color del borde. | | `hover_border_color` | `None` | Color del borde al pasar el ratón. | | `radius` | `None` | Radio de las esquinas: un número o `(sup_izq, sup_der, inf_der, inf_izq)`. | | `menu_bg` | `None` | Fondo del menú. | | `menu_color` | `None` | Texto del menú. | | `menu_border_color` | `None` | Borde del menú. | | `selected_bg` | `None` | Fondo de la opción resaltada. | | `selected_color` | `None` | Texto de la opción resaltada. | | `focus_border_color` | `None` | Color del borde mientras tiene el foco del teclado. | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | Eventos: `on_select`, `on_open`, `on_close` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#pick_list) #### Relacionados - [Segmentado](https://albakore.github.io/pyzed/es/library/segmented/): Un grupo de botones de texto excluyentes. - [Grupo de radios](https://albakore.github.io/pyzed/es/library/radio-group/): Opciones excluyentes como botones de radio. - [Buscador](https://albakore.github.io/pyzed/es/library/picker/): Una lista con búsqueda en una superficie flotante, con coincidencia difusa. ### Selector de archivos > Un campo de ruta con un botón que abre el diálogo de archivos o carpetas del sistema. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/file-input/ :::tip[En pocas palabras] Una casilla con la ubicación de un archivo y un botón que abre la ventana de siempre para buscarlo. ::: `ui.file_input` muestra una ruta y un botón para buscar. El botón abre el diálogo del propio sistema operativo para abrir un archivo (`mode="open"`), varios (`"open_many"`), una carpeta (`"folder"`) o elegir dónde guardar (`"save"`), y `on_change` recibe lo elegido: una ruta, o una lista de rutas con `open_many`. La ruta también se puede escribir o pegar. `filters` limita los tipos de archivo (`"Imágenes: png, jpg"`). `ui.file_input` · Categoría: [Sistema](https://albakore.github.io/pyzed/es/library/#sistema) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#file_input) **Código:** ```python pz.column([ ui.field("Project folder", ui.file_input("~/projects/notes", on_change, mode="folder")), ui.field("Logo", ui.file_input("", on_change, filters="Images: png, jpg, svg", placeholder="Choose an image")), ui.field("Export to", ui.file_input("report.pdf", on_change, mode="save", button="Browse…")), ], spacing=12, width=420) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para ajustes que apuntan a un archivo o carpeta: la carpeta de un proyecto, un archivo de registro, el destino de una exportación. - `button="Examinar…"` muestra un texto en lugar del icono de carpeta. - `ui.system.pick(then, mode=...)` devuelve el mismo diálogo como un `Command`, para tus propios botones. #### Uso ```python import pyzed as pz from pyzed import ui class Export(pz.App): def __init__(self): self.target = "" def view(self): return ui.file_input(self.target, lambda path: setattr(self, "target", path), mode="save", file_name="report.pdf", filters="PDF: pdf") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Modo ```python ui.file_input("~/notes/todo.md", on_change, width=320) ``` ```python ui.file_input(["a.png", "b.png"], on_change, mode="open_many", width=320) ``` ```python ui.file_input("~/projects", on_change, mode="folder", width=320) ``` ```python ui.file_input("report.pdf", on_change, mode="save", width=320) ``` ##### Estados ```python ui.file_input("", on_change, button="Browse…", width=320) ``` ```python ui.file_input("~/notes/todo.md", on_change, editable=False, width=320) ``` ```python ui.file_input("~/missing.txt", on_change, invalid=True, width=320) ``` ```python ui.file_input("~/notes/todo.md", on_change, disabled=True, width=320) ``` #### Eventos Selector de archivos tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_change` | `ui.file_input`, `ui.path_input` | `(value)` | Recibe la ruta (una lista con `"open_many"`). | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Export(pz.App): def __init__(self): self.folder = "" def chosen(self, path): # on_change: the path (a list with mode="open_many") self.folder = path def view(self): return pz.column([ ui.file_input(self.folder, self.chosen, mode="folder", placeholder="Choose a folder"), ui.label(f"Saving to {self.folder or '…'}"), ], spacing=8, width=360) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Chosen: path: str class Export(pz.App): path = "" def update(self, message): match message: case Chosen(path): self.path = path def view(self): return ui.file_input(self.path, lambda path: Chosen(path), mode="save", file_name="report.pdf") ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: image: str = "" def view(state, set_state): return ui.file_input(state.image, lambda path: set_state(image=path), filters="Images: png, jpg") ``` #### Propiedades y API ##### `ui.file_input` ```python file_input( value, on_change=None, *, mode='open', filters=None, title=None, directory=None, file_name='', placeholder=None, button=None, editable=True, disabled=False, invalid=False, id=None, width='fill', theme=None, ) ``` Un campo de ruta con un botón que abre el diálogo de archivos del sistema, para que nadie escriba una ruta a mano. Ver la página [Selector de archivos](https://albakore.github.io/pyzed/es/library/file-input/). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | *obligatorio* | Valor actual. | | `mode` | `"open"` | `"open"` (un archivo), `"open_many"` (una lista), `"save"` o `"folder"`. | | `filters` | `None` | Archivos que se muestran: `{"Imágenes": "png jpg"}` o `"Imágenes: png, jpg"`. | | `title` | `None` | El título del diálogo. | | `directory` | `None` | Dónde se abre el diálogo (por defecto, la carpeta de la ruta actual). | | `file_name` | `""` | El nombre que propone al guardar. | | `placeholder` | `None` | Texto de ayuda cuando está vacío. | | `button` | `None` | Texto para el botón en lugar de un icono de carpeta. | | `editable` | `True` | `False` impide escribir la ruta. | | `disabled` | `False` | Lo deshabilita (ignora los clics). | | `invalid` | `False` | Pinta el borde con el color de error. | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `width` | `"fill"` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_change` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#file_input) ##### `ui.path_input` ```python path_input( value, on_change=None, *, folder=False, save=False, filters=None, kind=None, directory=None, relative_to=None, title=None, placeholder=None, size='md', mono=True, disabled=False, invalid=False, theme=None, ) ``` Un campo de texto para la ruta de un archivo o carpeta con un botón que abre el diálogo de archivos del sistema, para no tener que escribirla (aunque se puede). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | *obligatorio* | Ruta actual. | | `folder` | `False` | Elige una carpeta en vez de un archivo. | | `save` | `False` | Muestra el diálogo "Guardar como". | | `filters` | `None` | Pares `(nombre, extensiones)`: `[("Imágenes", ["png", "jpg"])]`. | | `kind` | `None` | Filtros comunes por nombre: `"image"`, `"audio"`, `"video"`, `"font"`, `"shader"` o `"data"`. | | `directory` | `None` | Carpeta donde se abre el diálogo (por defecto, la de la ruta actual). | | `relative_to` | `None` | Una ruta elegida dentro de esta carpeta se da relativa a ella. | | `title` | `None` | Título del diálogo. | | `placeholder` | `None` | Texto que se ve mientras está vacío. | | `size` | `"md"` | `"sm"`, `"md"` o `"lg"`. | | `mono` | `True` | Texto monoespaciado. | | `disabled` | `False` | Lo atenúa. | | `invalid` | `False` | Pinta el borde con el color de error. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_change` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#path_input) ##### `ui.system.pick` ```python pick(then, *, mode='open', title=None, filters=None, directory=None, file_name='') ``` El diálogo de archivos del sistema como un `Command`: devuélvelo desde un callback y `then` recibe lo que eligió el usuario (una ruta, una lista de rutas con `"open_many"`, o `None` si cancela). `ui.file_input` lo usa. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `then` | *obligatorio* | Se llama con el resultado. | | `mode` | `"open"` | `"open"`, `"open_many"`, `"save"` o `"folder"`. | | `title` | `None` | El título del diálogo. | | `filters` | `None` | Archivos que se muestran: `{"Imágenes": "png jpg"}` o `"Imágenes: png, jpg"`. | | `directory` | `None` | Dónde se abre el diálogo. | | `file_name` | `""` | El nombre que propone al guardar. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#pick) #### Relacionados - [Zona para soltar](https://albakore.github.io/pyzed/es/library/drop-zone/): Un área que recibe archivos arrastrados desde el escritorio o el explorador de archivos. - [Campo de texto](https://albakore.github.io/pyzed/es/library/input/): Un campo de texto controlado con icono, placeholder y modo contraseña. - [Campo de formulario](https://albakore.github.io/pyzed/es/library/field/): Una fila de ajustes: título y descripción a la izquierda, control a la derecha. ### Selector de color > Elige un color, un degradado o un relleno con máscara; las muestras los enseñan. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/color-picker/ :::tip[En pocas palabras] Una paleta para elegir un color, su transparencia o una mezcla de colores. ::: `ui.ColorPicker` es un componente que elige un color con alfa, un degradado lineal (paradas que añades, arrastras y quitas, más un ángulo) o un relleno con máscara de opacidad. `on_change` recibe un evento cuyo `value` es una cadena `"#rrggbb[aa]"`, un `pz.Gradient` o un `pz.Paint`, todos válidos como fondo. `ui.color_swatch` muestra cualquiera de ellos sobre un damero. `ui.ColorPicker` · Categoría: [Formularios](https://albakore.github.io/pyzed/es/library/#formularios) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#colorpicker) **Código:** ```python pz.row([ ui.ColorPicker(value="#3b82f6cc", on_change=on_change), pz.column([ ui.color_swatch("#3b82f6", size=28), ui.color_swatch("#a1c181aa", size=28), ui.color_swatch(pz.Gradient(["#f97316", "#7c3aed"], angle=90), size=28), ], spacing=8), ], spacing=16) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para editores de temas, apps de dibujo y propiedades de estilo. - Muestra un `color_swatch` y abre el selector en un [Popover](https://albakore.github.io/pyzed/es/library/popover/) al pulsarlo. #### Uso ```python import pyzed as pz from pyzed import ui class Styles(pz.App): def __init__(self): self.fill = "#3b82f6" def view(self): return pz.row([ ui.ColorPicker(value=self.fill, on_change=lambda e: setattr(self, "fill", e.value)), pz.container(width=120, height=120, bg=self.fill, radius=8), ], spacing=16) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python ui.ColorPicker(value="#3b82f6cc", on_change=on_change) ``` ```python ui.ColorPicker(value="#a1c181", alpha=False, gradients=False, masks=False, on_change=on_change) ``` ```python ui.ColorPicker(value="#e5c07b", gradients=False, masks=False, swatches=["#e06c75", "#e5c07b", "#98c379", "#61afef", "#c678dd"], on_change=on_change) ``` ##### Muestras Una muestra dibuja cualquier color, degradado o relleno sobre un damero, para que se vea la transparencia. ```python ui.color_swatch("#3b82f6", size=32) ``` ```python ui.color_swatch("#a1c181aa", size=32) ``` ```python ui.color_swatch(pz.Gradient(["#f97316", "#7c3aed"], angle=90), size=32) ``` ```python ui.checkerboard(96, 48, size=8) ``` #### Eventos Selector de color tiene **2 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_change` | `ui.ColorPicker` | `(event)` | Recibe `event.value`. | | `on_press` | `ui.color_swatch` | `()` | Lo hace pulsable. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Styles(pz.App): def __init__(self): self.fill = "#3b82f6" def picked(self, event): # a pz.Component: the color is event.value self.fill = event.value def view(self): return pz.row([ ui.ColorPicker(value=self.fill, on_change=self.picked), pz.container(width=80, height=80, bg=self.fill, radius=8), ui.color_swatch("#ef4444", on_press=lambda: setattr(self, "fill", "#ef4444")), ], spacing=16) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Fill: color: str class Styles(pz.App): fill = "#3b82f6" def update(self, message): match message: case Fill(color): self.fill = color def view(self): return pz.row([ui.ColorPicker(value=self.fill, on_change=lambda e: Fill(e.value)), ui.color_swatch("#ef4444", on_press=Fill("#ef4444"))], spacing=16) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: fill: str = "#3b82f6" def view(state, set_state): return pz.row([ui.ColorPicker(value=state.fill, on_change=lambda e: set_state(fill=e.value)), pz.container(width=80, height=80, bg=state.fill, radius=8)], spacing=16) ``` #### Propiedades y API ##### `ui.ColorPicker` ```python class ColorPicker ``` Un componente para elegir un color con alfa, un gradiente lineal (paradas que se añaden, arrastran y quitan, más un ángulo) o un relleno con máscara de opacidad. `on_change` recibe un texto `"#rrggbb[aa]"`, un `pz.Gradient` o un `pz.Paint`, todos válidos como `background`. Devuelve el valor elegido en `value`. Ver [Estilos](https://albakore.github.io/pyzed/es/theming/styles/#selector-de-color). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | `"#3b82f6"` | Un color, token del tema, `pz.Gradient` o `pz.Paint`. | | `alpha` | `True` | Muestra la barra y el campo de alfa. | | `gradients` | `True` | Muestra el cambio Sólido/Gradiente. | | `masks` | `True` | Muestra el editor de máscara. | | `swatches` | `None` | Colores rápidos (por defecto: el acento y los colores de estado del tema). | Eventos: `on_change` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#colorpicker) ##### `ui.color_swatch` ```python color_swatch(value, *, size=20, on_press=None) ``` Un cuadrito que muestra un color, gradiente o pintura sobre un damero. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | *obligatorio* | Qué mostrar. | | `size` | `20` | Lado en píxeles. | Eventos: `on_press` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#color_swatch) ##### `ui.checkerboard` ```python checkerboard(width, height, size=6) ``` Un damero claro/oscuro para dibujar bajo colores translúcidos. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `width` | *obligatorio* | Ancho. | | `height` | *obligatorio* | Alto. | | `size` | `6` | Tamaño de celda. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#checkerboard) #### Relacionados - [Popover](https://albakore.github.io/pyzed/es/library/popover/): Contenido flotante en un punto: menús contextuales, selectores de color. - [Tarjeta](https://albakore.github.io/pyzed/es/library/card/): Una superficie con borde, título y acciones opcionales. ### Selector de fecha > Un campo de fecha con un calendario desplegable, en el formato que elijas. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/date-picker/ :::tip[En pocas palabras] Una casilla para una fecha. Escríbela, o pulsa el calendarito y haz clic en un día. ::: `ui.DatePicker` es un campo de texto para una fecha. El botón del calendario (o Alt+↓) abre un [Calendario](https://albakore.github.io/pyzed/es/library/calendar/) debajo, y el usuario también puede escribir la fecha: se lee con `format` al pulsar Enter o al salir del campo. Con el foco en el campo, ↑ y ↓ mueven un día y Re Pág/Av Pág un mes, sin abrir nada. `on_change` recibe un `datetime.date` (o `None` si se vacía el campo) en `event.value`. `ui.DatePicker` · Categoría: [Fechas y horas](https://albakore.github.io/pyzed/es/library/#fechas-y-horas) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#datepicker) **Código:** ```python pz.container( ui.DatePicker(value="2026-10-22", format="DD/MM/YYYY", on_change=on_change), width=220, height=330, align_y="start") ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para fechas de nacimiento, vencimientos, reservas y cualquier campo de formulario con una fecha. - `format` acepta tokens (`DD/MM/YYYY`, `MMM D, YYYY`, `dddd D MMMM`) o un patrón de `strftime` (`%d.%m.%Y`). Los nombres de meses y días siguen el idioma de la app. - `min`, `max` y `disabled_dates` (fechas, `"weekend"` o nombres de días) desactivan días. - Para dos fechas (desde–hasta), usa [Selector de rango de fechas](https://albakore.github.io/pyzed/es/library/date-range-picker/). #### Uso ```python import pyzed as pz from pyzed import ui import datetime class Booking(pz.App): def __init__(self): self.day = datetime.date.today() def view(self): return pz.column([ ui.field("Check-in", ui.DatePicker(value=self.day, format="ddd D MMM YYYY", min=datetime.date.today(), disabled_dates=["sunday"], on_change=lambda e: setattr(self, "day", e.value))), ui.label(f"Booked for {ui.format_date(self.day, 'dddd D MMMM')}"), ], spacing=12, width=320) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Formato Tokens: `YYYY` `YY`, `MMMM` `MMM` `MM` `M`, `DD` `D`, `dddd` `ddd`; `[texto]` se copia tal cual. ```python ui.DatePicker(value="2026-10-22", on_change=on_change) ``` ```python ui.DatePicker(value="2026-10-22", format="DD/MM/YYYY", on_change=on_change) ``` ```python ui.DatePicker(value="2026-10-22", format="MMM D, YYYY", on_change=on_change) ``` ```python ui.DatePicker(value="2026-10-22", format="ddd D MMM", on_change=on_change) ``` ##### Estados ```python ui.DatePicker(placeholder="Pick a date", on_change=on_change) ``` ```python ui.DatePicker(value="2026-10-22", invalid=True, on_change=on_change) ``` ```python ui.DatePicker(value="2026-10-22", disabled=True, on_change=on_change) ``` ```python ui.DatePicker(value="2026-10-22", on_change=on_change) ``` #### Ejemplos ##### Formatos `ui.format_date` y `ui.parse_date` usan los mismos tokens que el campo, para tus propias etiquetas. **Código:** ```python pz.column([ ui.label(ui.format_date("2026-10-22", "DD/MM/YYYY"), mono=True), ui.label(ui.format_date("2026-10-22", "MMM D, YYYY"), mono=True), ui.label(ui.format_date("2026-10-22", "dddd, D MMMM YYYY"), mono=True), ui.label(ui.format_date("2026-10-22", "[week of] D/M/YY"), mono=True), ], spacing=6) ``` #### Eventos Selector de fecha tiene **4 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_change` | `ui.DatePicker` | `(event)` | Recibe `event.value`: un `datetime.date` o `None`. | | `on_open` | `ui.DatePicker` | `(event)` | Se abrió el calendario. | | `on_close` | `ui.DatePicker` | `(event)` | Se cerró el calendario. | | `on_dismiss` | `pz.popup` | `()` | Mensaje o función que se envía con un clic fuera o Escape. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python import datetime class Booking(pz.App): def __init__(self): self.day = datetime.date(2026, 10, 22) def changed(self, event): # on_change: event.value is a datetime.date (or None) self.day = event.value def view(self): return pz.column([ ui.DatePicker(value=self.day, format="DD/MM/YYYY", on_change=self.changed), ui.label(ui.format_date(self.day, "dddd D MMMM") or "No date"), ], spacing=8, width=240) ``` **Declarativo:** ```python import datetime from dataclasses import dataclass @dataclass class Picked: day: datetime.date class Booking(pz.App): day = datetime.date(2026, 10, 22) def update(self, message): match message: case Picked(day): self.day = day def view(self): # DatePicker is a pz.Component: its handler gets a pz.Event return ui.DatePicker(value=self.day, on_change=lambda e: Picked(e.value)) ``` **Funcional:** ```python import datetime from dataclasses import dataclass @dataclass(frozen=True) class State: day: datetime.date = datetime.date(2026, 10, 22) def view(state, set_state): return pz.column([ ui.DatePicker(value=state.day, format="MMM D, YYYY", on_change=lambda e: set_state(day=e.value)), ui.label(f"Week {state.day.isocalendar().week}" if state.day else "No date"), ], spacing=8, width=240) ``` #### Consejos :::tip El calendario se abre en un `pz.popup`, una capa dibujada sobre la ventana: nunca la recorta un scrollable ni un contenedor pequeño, se da la vuelta y sale por encima del campo si abajo no hay sitio, y se cierra con Escape o con un clic fuera. ::: #### Propiedades y API ##### `ui.DatePicker` ```python class DatePicker ``` Un componente: un campo de fecha que abre un calendario debajo, en una ventana emergente que ningún panel recorta. Se puede escribir en `format` (Enter o salir del campo lo lee); tras un clic, ↑/↓ mueven la fecha un día y Re Pág/Av Pág un mes sin abrir nada, Alt+↓ abre el calendario y Escape lo cierra. Ver la página [Selector de fecha](https://albakore.github.io/pyzed/es/library/date-picker/). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | `None` | La fecha (`datetime.date`, texto ISO o `None`). | | `format` | `"YYYY-MM-DD"` | Cómo se escribe: `"DD/MM/YYYY"`, `"MMM D, YYYY"`... (ver `format_date`). | | `placeholder` | `""` | Pista mientras está vacío (por defecto, el formato). | | `min` | `None` | La primera fecha. | | `max` | `None` | La última fecha. | | `disabled_dates` | `()` | Días que no se pueden elegir (como en `Calendar`). | | `first_weekday` | `"monday"` | `"monday"`, `"sunday"` o `"saturday"`. | | `week_numbers` | `False` | Muestra los números de semana en el calendario. | | `clearable` | `True` | Muestra una x que lo vacía. | | `today_button` | `True` | Añade un enlace Hoy al calendario. | | `disabled` | `False` | Lo atenúa. | | `invalid` | `False` | Pinta el borde con el color de error. | Eventos: `on_change`, `on_open`, `on_close` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#datepicker) ##### `ui.format_date` ```python format_date(value, fmt='YYYY-MM-DD', *, language=None) ``` Escribe una fecha con un patrón de marcas: `YYYY` 2026, `YY` 26, `MMMM` octubre, `MMM` oct, `MM` 10, `M` 10, `DD` 05, `D` 5, `dddd` lunes, `ddd` lun, `HH`/`hh` horas, `mm` minutos, `ss` segundos, `A` AM/PM y `[texto]` tal cual. Un patrón con `%` es de `strftime`. Los nombres de meses y días siguen el idioma de `pyzed.i18n`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | *obligatorio* | Una fecha, un datetime, texto ISO o `None` (da `""`). | | `fmt` | `"YYYY-MM-DD"` | El patrón. | | `language` | `None` | Un idioma para los nombres en lugar del actual. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#format_date) ##### `ui.parse_date` ```python parse_date(text, fmt='YYYY-MM-DD', *, language=None) ``` Lee una fecha escrita con un patrón de `format_date` (meses en el idioma actual o en inglés); el ISO `YYYY-MM-DD` siempre funciona. Devuelve `None` si el texto no es una fecha. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `text` | *obligatorio* | El texto. | | `fmt` | `"YYYY-MM-DD"` | El patrón. | | `language` | `None` | El idioma de los nombres. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#parse_date) ##### `pz.popup` ```python popup(base, content=None, *, on_dismiss=None, side='bottom', align='start', gap=4.0) ``` Muestra `content` encima de todo junto a `base`, como el menú de un desplegable: no lo recortan los paneles donde está `base`, se pasa al otro lado cuando no cabe y se queda dentro de la ventana. Un clic fuera (y fuera de `base`) o Escape llama a `on_dismiss`. Con `content=None` solo se muestra `base`. Los selectores de fecha abren su calendario con él. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `base` | *obligatorio* | El widget al que pertenece (un campo, un botón). | | `content` | `None` | Lo que se abre (un panel, un menú), o `None` mientras está cerrado. | | `side` | `"bottom"` | `"bottom"`, `"top"`, `"right"` o `"left"` de `base`. | | `align` | `"start"` | `"start"`, `"center"` o `"end"`: cómo se alinea con `base`. | | `gap` | `4.0` | Espacio entre `base` y el contenido. | Eventos: `on_dismiss` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#popup) #### Relacionados - [Calendario](https://albakore.github.io/pyzed/es/library/calendar/): Una cuadrícula del mes para elegir un día, un rango o varios días, siempre a la vista. - [Selector de rango de fechas](https://albakore.github.io/pyzed/es/library/date-range-picker/): Dos fechas en un campo, con dos meses lado a lado y rangos rápidos. - [Selector de hora](https://albakore.github.io/pyzed/es/library/time-picker/): Un campo de hora con columnas de horas y minutos, en 24 o 12 horas. - [Campo de texto](https://albakore.github.io/pyzed/es/library/input/): Un campo de texto controlado con icono, placeholder y modo contraseña. ### Selector de hora > Un campo de hora con columnas de horas y minutos, en 24 o 12 horas. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/time-picker/ :::tip[En pocas palabras] Una casilla para una hora del día, con listas de horas y minutos para elegir. ::: `ui.TimePicker` es un campo para una hora del día. Su ventana tiene una columna de horas, una de minutos (cada `minute_step` minutos), segundos si el formato tiene `ss` y AM/PM si tiene `A` o `h`. ↑ y ↓ la mueven un paso desde el teclado. `on_change` recibe un `datetime.time`. `ui.TimePicker` · Categoría: [Fechas y horas](https://albakore.github.io/pyzed/es/library/#fechas-y-horas) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#timepicker) **Código:** ```python pz.row([ pz.container(ui.TimePicker(value="14:30", on_change=on_change), width=140, height=290, align_y="start"), pz.container(ui.TimePicker(value="14:30", format="h:mm A", minute_step=15, on_change=on_change), width=140, height=290, align_y="start"), ], spacing=24) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para alarmas, horarios de apertura, reuniones y recordatorios. - `format="HH:mm"` es de 24 horas; `"h:mm A"` es de 12 horas con AM/PM; `"HH:mm:ss"` añade segundos. #### Uso ```python import pyzed as pz from pyzed import ui import datetime class Alarm(pz.App): def __init__(self): self.at = datetime.time(7, 30) def view(self): return ui.field("Wake me at", ui.TimePicker(value=self.at, format="h:mm A", on_change=lambda e: setattr(self, "at", e.value))) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Formato ```python ui.TimePicker(value="14:30", on_change=on_change) ``` ```python ui.TimePicker(value="14:30", format="h:mm A", on_change=on_change) ``` ```python ui.TimePicker(value="14:30:15", format="HH:mm:ss", on_change=on_change) ``` ##### Abierto ```python ui.TimePicker(value="06:15:30", format="HH:mm:ss", on_change=on_change) ``` ```python ui.TimePicker(value="14:30", format="h:mm A", minute_step=15, on_change=on_change) ``` #### Eventos Selector de hora tiene **3 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_change` | `(event)` | Recibe `event.value`: un `datetime.time` o `None`. | | `on_open` | `(event)` | Se abrió la lista. | | `on_close` | `(event)` | Se cerró la lista. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python import datetime class Alarm(pz.App): def __init__(self): self.at = datetime.time(7, 30) def changed(self, event): # on_change: event.value is a datetime.time self.at = event.value def view(self): return pz.column([ ui.TimePicker(value=self.at, format="h:mm A", on_change=self.changed), ui.label(f"Alarm at {ui.format_time(self.at, 'HH:mm')}"), ], spacing=8, width=200) ``` **Declarativo:** ```python import datetime from dataclasses import dataclass @dataclass class At: time: datetime.time class Alarm(pz.App): at = datetime.time(7, 30) def update(self, message): match message: case At(time): self.at = time def view(self): return ui.TimePicker(value=self.at, on_change=lambda e: At(e.value)) ``` **Funcional:** ```python import datetime from dataclasses import dataclass @dataclass(frozen=True) class State: at: datetime.time = datetime.time(7, 30) def view(state, set_state): return ui.TimePicker(value=state.at, minute_step=15, on_change=lambda e: set_state(at=e.value)) ``` #### Propiedades y API ##### `ui.TimePicker` ```python class TimePicker ``` Un componente: un campo de hora con columnas de horas y minutos (y segundos, y AM/PM con un `format` de 12 horas) en una ventana emergente. Tras un clic, ↑/↓ la cambian `minute_step` minutos. Ver la página [Selector de hora](https://albakore.github.io/pyzed/es/library/time-picker/). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | `None` | La hora (`datetime.time`, `"14:30"` o `None`). | | `format` | `"HH:mm"` | `"HH:mm"`, `"HH:mm:ss"`, `"h:mm A"`... | | `placeholder` | `""` | Pista mientras está vacío. | | `minute_step` | `5` | Minutos entre los que se ofrecen. | | `min` | `None` | La primera hora. | | `max` | `None` | La última hora. | | `clearable` | `True` | Muestra una x que lo vacía. | | `disabled` | `False` | Lo atenúa. | | `invalid` | `False` | Pinta el borde con el color de error. | Eventos: `on_change`, `on_open`, `on_close` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#timepicker) ##### `ui.format_time` ```python format_time(value, fmt='HH:mm') ``` Escribe una hora (o `"14:30"`) con un patrón como `"h:mm A"`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | *obligatorio* | Un `datetime.time` o texto. | | `fmt` | `"HH:mm"` | El patrón. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#format_time) ##### `ui.parse_time` ```python parse_time(text, fmt='HH:mm') ``` Lee una hora: `"14:30"`, `"2:30 PM"`, `"14:30:05"`, primero con `fmt`. `None` si no lo es. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `text` | *obligatorio* | El texto. | | `fmt` | `"HH:mm"` | El patrón. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#parse_time) #### Relacionados - [Selector de fecha](https://albakore.github.io/pyzed/es/library/date-picker/): Un campo de fecha con un calendario desplegable, en el formato que elijas. - [Campo numérico](https://albakore.github.io/pyzed/es/library/number-field/): Un número con botones −/+, límites, paso y sufijo. ### Selector de iconos > Elige uno de los ~2000 iconos de Lucide en una cuadrícula con buscador. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/icon-picker/ :::tip[En pocas palabras] Una cuadrícula de dibujitos con buscador para elegir un icono. ::: `ui.IconPicker` es un componente con un buscador, un desplegable de categorías y una cuadrícula con todos los iconos de Lucide incluidos. La búsqueda mira nombres y etiquetas ("person" encuentra `user`), Enter elige la primera coincidencia y `on_change` recibe el nombre del icono en `event.value`. `ui.icon_picker_dialog` lo pone en un diálogo que elige con un clic, y `ui.icon_grid` es la cuadrícula sola, que solo construye las filas visibles. `ui.IconPicker` · Categoría: [Formularios](https://albakore.github.io/pyzed/es/library/#formularios) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#iconpicker) **Código:** ```python ui.IconPicker(value="rocket", columns=10, height=200, clearable=True, on_change=on_change) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para que el usuario elija el icono de una carpeta, una etiqueta, un atajo o un botón de la barra. - Muestra el icono actual como un botón y abre `icon_picker_dialog` en una [capa de diálogo](https://albakore.github.io/pyzed/es/library/dialog-layer/) al pulsarlo. #### Uso ```python import pyzed as pz from pyzed import ui class Shortcuts(pz.App): def __init__(self): self.icon = "rocket" self.picking = False def pick(self, name): self.icon, self.picking = name or "circle-dashed", False def view(self): page = ui.button("Change icon", lambda: setattr(self, "picking", True), icon=self.icon) dialog = ui.icon_picker_dialog(self.icon, self.pick, on_close=lambda: setattr(self, "picking", False)) return ui.dialog_layer(page, dialog if self.picking else None, on_dismiss=lambda: setattr(self, "picking", False)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Selector ```python ui.IconPicker(value="rocket", columns=8, height=180, on_change=on_change) ``` ```python ui.IconPicker(value="heart", columns=8, height=180, clearable=True, on_change=on_change) ``` ##### Piezas ```python ui.icon_grid(pz.icons.search("arrow")[:24], on_pick, selected="arrow-right", columns=8, height=110) ``` ```python ui.icon_picker_dialog("rocket", on_pick, on_close=on_close, columns=8) ``` #### Eventos Selector de iconos tiene **3 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. | Evento | De | El handler recibe | Cuándo | | --- | --- | --- | --- | | `on_change` | `ui.IconPicker` | `(event)` | Recibe `event.value`. | | `on_pick` | `ui.icon_picker_dialog`, `ui.icon_grid` | `(name)` | Se llama con el nombre del icono pulsado (`""` si se quita). | | `on_close` | `ui.icon_picker_dialog` | `()` | El botón de cerrar. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Shortcut(pz.App): def __init__(self): self.icon = "rocket" self.picking = False def pick(self, name): # on_pick: the icon's name ("" when cleared) self.icon, self.picking = name or "circle-dashed", False def view(self): page = ui.button("Change icon", lambda: setattr(self, "picking", True), icon=self.icon) dialog = ui.icon_picker_dialog(self.icon, self.pick, on_close=lambda: setattr(self, "picking", False)) return ui.dialog_layer(page, dialog if self.picking else None, on_dismiss=lambda: setattr(self, "picking", False)) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Icon: name: str class Shortcut(pz.App): icon = "rocket" def update(self, message): match message: case Icon(name): self.icon = name or "circle-dashed" def view(self): # IconPicker is a pz.Component: its handler gets a pz.Event return ui.IconPicker(value=self.icon, height=200, on_change=lambda e: Icon(e.value)) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: icon: str = "rocket" def view(state, set_state): return pz.column([ ui.icon_grid(["rocket", "star", "heart", "bell"], lambda name: set_state(icon=name), selected=state.icon, columns=4, height=60), ui.button("Launch", None, icon=state.icon), ], spacing=8) ``` #### Propiedades y API ##### `ui.IconPicker` ```python class IconPicker ``` Un componente para elegir un icono de Lucide: un buscador (nombres y etiquetas), un desplegable de categorías y una cuadrícula que solo construye las filas visibles. Enter elige la primera coincidencia. `on_change` recibe el nombre del icono (`""` si se quita); devuélvelo en `value`. Ver la página [Selector de iconos](https://albakore.github.io/pyzed/es/library/icon-picker/). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | `""` | El nombre del icono elegido. | | `columns` | `8` | Iconos por fila. | | `height` | `288` | Alto de la cuadrícula. | | `clearable` | `False` | Muestra un botón que quita el icono. | | `autofocus` | `False` | Da el foco al buscador al aparecer. | Eventos: `on_change` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#iconpicker) ##### `ui.icon_picker_dialog` ```python icon_picker_dialog( value, on_pick, *, on_close=None, title=None, columns=10, clearable=True, key='icon-picker', theme=None, ) ``` Un diálogo con un `IconPicker` que elige con un clic; úsalo como diálogo de `ui.dialog_layer`. El buscador recibe el foco. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | *obligatorio* | El icono actual. | | `title` | `None` | Por defecto: "Elegir un icono", traducido. | | `columns` | `10` | Iconos por fila. | | `clearable` | `True` | Muestra el botón "Sin icono". | | `key` | `"icon-picker"` | Clave del componente, para varios selectores en una vista. | | `theme` | `None` | Tema. | Eventos: `on_pick`, `on_close` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#icon_picker_dialog) ##### `ui.icon_grid` ```python icon_grid( names, on_pick, *, selected=None, columns=8, cell=34, height=288, id=None, theme=None, ) ``` La cuadrícula de iconos sola, virtualizada: solo se construyen las filas visibles, así que miles de iconos cuestan lo mismo que unos pocos. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `names` | *obligatorio* | Nombres de iconos, en orden. | | `selected` | `None` | El nombre resaltado. | | `columns` | `8` | Iconos por fila. | | `cell` | `34` | Lado de cada celda en píxeles. | | `height` | `288` | Alto de la cuadrícula. | | `id` | `None` | Id del desplazable, para `Command.scroll_to`. | | `theme` | `None` | Tema. | Eventos: `on_pick` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#icon_grid) ##### `pz.icons.search` ```python search(query, among=None) ``` Los iconos que coinciden con una búsqueda por nombre y etiqueta, los mejores primero: nombres exactos, luego los que empiezan por la búsqueda, los que tienen todas las palabras y los encontrados por sus etiquetas (`search("person")` encuentra `user`). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `query` | *obligatorio* | Palabras que deben aparecer todas en el nombre o las etiquetas. | | `among` | `None` | Buscar solo en estos nombres (por defecto, todos los iconos). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/icons/#search) ##### `pz.icons.category` ```python category(key, among=None) ``` Los iconos de una categoría de `CATEGORIES`, ordenados. Un icono puede estar en varias. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `key` | *obligatorio* | Una clave de `CATEGORIES` (`"arrows"`, `"files"`, `"people"`...). | | `among` | `None` | Mirar solo estos nombres. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/icons/#category) #### Relacionados - [Icono](https://albakore.github.io/pyzed/es/library/icon/): Iconos de Lucide por nombre e imágenes SVG. - [Capa de diálogo](https://albakore.github.io/pyzed/es/library/dialog-layer/): Centra un diálogo encima de tu vista con el fondo oscurecido. - [Selector de color](https://albakore.github.io/pyzed/es/library/color-picker/): Elige un color, un degradado o un relleno con máscara; las muestras los enseñan. ### Selector de rango de fechas > Dos fechas en un campo, con dos meses lado a lado y rangos rápidos. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/date-range-picker/ :::tip[En pocas palabras] Una casilla para dos fechas, desde y hasta. Haces clic donde empieza el periodo y donde termina. ::: `ui.DateRangePicker` guarda una fecha de inicio y una de fin. Su ventana muestra dos meses: el primer clic elige el inicio, el puntero previsualiza el rango y el segundo clic elige el fin (en cualquier orden). La columna de la izquierda tiene rangos rápidos (hoy, últimos 7 días, esta semana, este mes, el mes pasado...). `on_change` recibe `(inicio, fin)` como dos `datetime.date`. `ui.DateRangePicker` · Categoría: [Fechas y horas](https://albakore.github.io/pyzed/es/library/#fechas-y-horas) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#daterangepicker) **Código:** ```python pz.container( ui.DateRangePicker(value=("2026-10-05", "2026-10-14"), format="DD/MM/YYYY", on_change=on_change), width=260, height=330, align_y="start") ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para informes, filtros, estancias y todo lo que dura varios días. - `months=1` muestra un solo mes y `presets=False` oculta los rangos rápidos. - El campo también se puede escribir: dos fechas en `format` con cualquier separador. #### Uso ```python import pyzed as pz from pyzed import ui class Report(pz.App): def __init__(self): self.period = (None, None) def view(self): start, end = self.period days = (end - start).days + 1 if start and end else 0 return pz.column([ ui.DateRangePicker(value=self.period, format="MMM D, YYYY", on_change=lambda e: setattr(self, "period", e.value)), ui.label(f"{days} days selected", color="text_muted"), ], spacing=8, width=300) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Ventana ```python ui.DateRangePicker(value=("2026-10-05", "2026-10-14"), on_change=on_change) ``` ```python ui.DateRangePicker(months=1, presets=False, on_change=on_change) ``` #### Eventos Selector de rango de fechas tiene **3 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_change` | `(event)` | Recibe `event.value`: `(inicio, fin)`. | | `on_open` | `(event)` | Se abrió el calendario. | | `on_close` | `(event)` | Se cerró el calendario. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Report(pz.App): def __init__(self): self.period = ("2026-10-05", "2026-10-14") def changed(self, event): # on_change: event.value is (start, end) self.period = event.value def view(self): start, end = self.period return pz.column([ ui.DateRangePicker(value=self.period, on_change=self.changed), ui.label(f"From {ui.format_date(start, 'D MMM')} to {ui.format_date(end, 'D MMM')}"), ], spacing=8, width=280) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Period: days: tuple class Report(pz.App): period = (None, None) def update(self, message): match message: case Period(days): self.period = days def view(self): return ui.DateRangePicker(value=self.period, on_change=lambda e: Period(e.value)) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: period: tuple = (None, None) def view(state, set_state): return ui.DateRangePicker(value=state.period, months=1, presets=False, on_change=lambda e: set_state(period=e.value)) ``` #### Propiedades y API ##### `ui.DateRangePicker` ```python class DateRangePicker ``` Un componente: un campo para una fecha de inicio y otra de fin, con dos meses y rangos rápidos (Hoy, Últimos 7 días, Este mes...) en su ventana emergente. El texto escrito son dos fechas separadas por `separator`. Ver la página [Selector de rango de fechas](https://albakore.github.io/pyzed/es/library/date-range-picker/). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `value` | `None` | `(inicio, fin)`: fechas, texto ISO o `None`. | | `format` | `"YYYY-MM-DD"` | Cómo se escribe cada fecha. | | `separator` | `" – "` | Texto entre las dos fechas. | | `placeholder` | `""` | Pista mientras está vacío. | | `min` | `None` | La primera fecha. | | `max` | `None` | La última fecha. | | `disabled_dates` | `()` | Días que no se pueden elegir. | | `first_weekday` | `"monday"` | `"monday"`, `"sunday"` o `"saturday"`. | | `months` | `2` | Meses que se ven uno al lado del otro. | | `presets` | `True` | Muestra los rangos rápidos. | | `clearable` | `True` | Muestra una x que lo vacía. | | `disabled` | `False` | Lo atenúa. | | `invalid` | `False` | Pinta el borde con el color de error. | Eventos: `on_change`, `on_open`, `on_close` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#daterangepicker) #### Relacionados - [Selector de fecha](https://albakore.github.io/pyzed/es/library/date-picker/): Un campo de fecha con un calendario desplegable, en el formato que elijas. - [Calendario](https://albakore.github.io/pyzed/es/library/calendar/): Una cuadrícula del mes para elegir un día, un rango o varios días, siempre a la vista. ### Shader > Un widget pintado en la GPU por tu propio código WGSL. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/shader/ :::tip[En pocas palabras] Una superficie pintada por la tarjeta gráfica, para fondos en movimiento y efectos visuales. ::: `pz.shader` ejecuta una función que escribes en WGSL, `fn shade(uv, px, size, time) -> vec4`, para cada píxel del widget, en cada fotograma, en la GPU: fondos animados, plasma, fuego o el barrido de un radar no le cuestan nada a Python. Cada entrada de `uniforms` se convierte en una función WGSL con el mismo nombre, y `mouse()` da la posición del puntero. Sin GPU el widget muestra `fallback`. Ver la [guía de shaders](https://albakore.github.io/pyzed/es/shading/shaders/) y las [demos](https://albakore.github.io/pyzed/es/showcase/). `pz.shader` · Categoría: [Contenido](https://albakore.github.io/pyzed/es/library/#contenido) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#shader) **Código:** ```python pz.shader(""" fn shade(uv: vec2, px: vec2, size: vec2, time: f32) -> vec4 { let wave = 0.5 + 0.5 * sin(uv.x * 12.0 + uv.y * 4.0 + time * speed()); return vec4(mix(vec3(0.08, 0.09, 0.2), tint().rgb, wave), 1.0); } """, uniforms={"speed": 2.0, "tint": "#c678dd"}, width=640, height=240, time=0.5, fallback=pz.Gradient(["#14172f", "#c678dd"], angle=0)) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para fondos animados detrás de paneles de cristal, menús y HUDs. - Para gráficos y efectos de juegos que serían demasiado lentos de dibujar desde Python en cada fotograma. #### Uso ```python import pyzed as pz pz.shader(""" fn shade(uv: vec2, px: vec2, size: vec2, time: f32) -> vec4 { let wave = 0.5 + 0.5 * sin(uv.x * 12.0 + uv.y * 4.0 + time * speed()); return vec4(mix(vec3(0.08, 0.09, 0.2), tint().rgb, wave), 1.0); } """, uniforms={"speed": 2.0, "tint": "#c678dd"}, width=640, height=240, time=0.5, fallback=pz.Gradient(["#14172f", "#c678dd"], angle=0)) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Uniforms Cada captura es un fotograma con `time=0.5`; ejecuta el ejemplo para verlo moverse. Sin GPU el widget dibuja su `fallback`. ```python pz.shader(""" fn shade(uv: vec2, px: vec2, size: vec2, time: f32) -> vec4 { let wave = 0.5 + 0.5 * sin(uv.x * 12.0 + uv.y * 4.0 + time * 2.0); return vec4(mix(vec3(0.08, 0.09, 0.2), tint().rgb, wave), 1.0); } """, uniforms={"tint": "#c678dd"}, width=260, height=110, time=0.5, fallback=pz.Gradient(["#14172f", "#c678dd"], angle=0)) ``` ```python pz.shader(""" fn shade(uv: vec2, px: vec2, size: vec2, time: f32) -> vec4 { let wave = 0.5 + 0.5 * sin(uv.x * 12.0 + uv.y * 4.0 + time * 2.0); return vec4(mix(vec3(0.08, 0.09, 0.2), tint().rgb, wave), 1.0); } """, uniforms={"tint": "#56b6c2"}, width=260, height=110, time=0.5, fallback=pz.Gradient(["#14172f", "#56b6c2"], angle=0)) ``` ```python pz.shader(""" fn shade(uv: vec2, px: vec2, size: vec2, time: f32) -> vec4 { let wave = 0.5 + 0.5 * sin(uv.x * 12.0 + uv.y * 4.0 + time * 2.0); return vec4(mix(vec3(0.08, 0.09, 0.2), tint().rgb, wave), 1.0); } """, uniforms={"tint": "#e5c07b"}, width=260, height=110, time=0.5, fallback=pz.Gradient(["#14172f", "#e5c07b"], angle=0)) ``` #### Eventos Shader no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Consejos :::tip Los shaders necesitan el renderizador de GPU. Pasa siempre un `fallback` que se vea bien por sí solo: es lo que dibujan el renderizador por software y los equipos sin GPU. ::: #### Propiedades y API ##### `pz.shader` ```python shader( code, *, uniforms=None, textures=None, width=None, height=None, animate=True, time=None, fallback=None, ) ``` Un widget que pinta la GPU con tu función WGSL `fn shade(uv, px, size, time) -> vec4`, que devuelve el color de cada píxel, o con un grafo de shader hecho de nodos (una ruta `.pzshad` o un `Graph` o `Shader` de [`pyzed.shading`](https://albakore.github.io/pyzed/es/reference/shading/)). Cada entrada de `uniforms` se convierte en una función con su nombre (`{"speed": 2}` -> `speed()`); `mouse()` da la posición del puntero. Sin GPU el widget muestra `fallback`. Lanza `ValueError` con el mensaje del compilador si el código no es válido. Ver la guía de [shaders](https://albakore.github.io/pyzed/es/shading/shaders/). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `code` | *obligatorio* | Código WGSL que define `shade` (y las funciones auxiliares que quieras), o una ruta `.pzshad`, un `Graph` o un `Shader`. | | `uniforms` | `None` | Hasta 32 valores con nombre: un número, de 2 a 4 números o un color. Con un `.pzshad`, las variables mutables del shader por nombre. | | `textures` | `None` | Hasta 8 imágenes, `{nombre: ruta}`: cada una se convierte en `nombre(uv)`, el color de la imagen en `uv`. | | `width` | `None` | Ancho (`"fill"` por defecto). | | `height` | `None` | Alto (`"fill"` por defecto). | | `animate` | `True` | Redibuja en cada frame para que avance `time` (`True`). | | `time` | `None` | Un `time` fijo en segundos, para un fotograma quieto. | | `fallback` | `None` | Color o degradado que se muestra sin GPU. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#shader) #### Relacionados - [Efectos](https://albakore.github.io/pyzed/es/library/effects/): Glow, bloom, glass y opacidad como propiedades de estilo. - [Lienzo](https://albakore.github.io/pyzed/es/library/canvas/): Dibujo 2D con formas, trazos, texto y eventos de ratón. - [Animación](https://albakore.github.io/pyzed/es/library/animation/): Presets, keyframes y transiciones para cualquier widget. ### Spinner > Un arco que gira para los estados de carga. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/spinner/ :::tip[En pocas palabras] Un círculo que gira y dice “espera, estoy en ello”. ::: Un arco que gira. Su ángulo sale del reloj de la app, así que mantén una suscripción como `pz.every(1 / 30, ...)` mientras se muestra o no se moverá. `ui.spinner` · Categoría: [Estado y avisos](https://albakore.github.io/pyzed/es/library/#estado-y-avisos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#spinner) **Código:** ```python pz.row([ ui.spinner(), ui.spinner(size=24, tint="success"), pz.row([ui.spinner(size=14), ui.muted("Indexing project…")], spacing=8, align="center"), ], spacing=24, align="center") ``` #### Cuándo usarlo - Para esperas cortas de duración desconocida: conectando, buscando, cargando un archivo. - Para trabajos largos con avance conocido usa [Progreso](https://albakore.github.io/pyzed/es/library/progress/). #### Uso ```python import pyzed as pz from pyzed import ui class Loader(pz.App): def __init__(self): self.loading = True def subscription(self): return pz.every(1 / 30, lambda: None) if self.loading else None def view(self): return ui.spinner() if self.loading else ui.label("Done") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Tamaño ```python ui.spinner(size=12) ``` ```python ui.spinner() ``` ```python ui.spinner(size=24) ``` ```python ui.spinner(size=36) ``` ##### Tinte ```python ui.spinner(size=24, tint="success") ``` ```python ui.spinner(size=24, tint="warning") ``` ```python ui.spinner(size=24, tint="text_muted") ``` ##### Con texto ```python pz.row([ui.spinner(size=14), ui.muted("Indexing project…")], spacing=8, align="center") ``` ```python ui.button(pz.row([ui.spinner(size=12, tint="text"), pz.text("Saving")], spacing=6, align="center")) ``` #### Eventos Spinner no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.spinner` ```python spinner(*, size=16, tint='accent', speed=1.0, theme=None) ``` Un arco que gira para estados de carga. Su ángulo sale del reloj, así que mantén una suscripción `pz.every(1/30, ...)` activa mientras se muestra. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `size` | `16` | Tamaño. | | `tint` | `"accent"` | Color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` o un token. | | `speed` | `1.0` | Vueltas por segundo. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#spinner) #### Relacionados - [Progreso](https://albakore.github.io/pyzed/es/library/progress/): Una barra de progreso fina en un color semántico. - [Estado vacío](https://albakore.github.io/pyzed/es/library/empty-state/): Un marcador centrado para listas y búsquedas vacías. ### Tabla > Una tabla de datos con cabecera fija, ordenación y selección. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/table/ :::tip[En pocas palabras] Filas y columnas de datos, como una hoja de cálculo que ordenas pulsando una columna. ::: Una tabla de datos con cabecera fija, ordenación opcional y selección de filas. Las columnas son objetos `ui.Column` cuyo `key` es una clave de dict, un atributo o una función de la fila; `render` convierte un valor en cualquier widget. La tabla ordena las filas ella misma por la columna cuyo **título** es `sort_by`: solo guardas ese título y la dirección. `ui.table` · Categoría: [Datos](https://albakore.github.io/pyzed/es/library/#datos) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#table) **Código:** ```python rows = [ {"name": "render.rs", "lang": "Rust", "lines": 1840}, {"name": "app.py", "lang": "Python", "lines": 512}, {"name": "theme.py", "lang": "Python", "lines": 730}, {"name": "docs.yml", "lang": "YAML", "lines": 64}, ] pz.container(ui.table( [ui.Column("File", "name"), ui.Column("Language", "lang", width=120), ui.Column("Lines", "lines", width=80, align="end")], rows, sort_by="Lines", descending=True, on_sort=on_sort, selected=rows[1], striped=True, ), width=520) ``` #### Cuándo usarlo - Para registros con varios campos comparables: archivos, usuarios, procesos, resultados. - Para una sola columna de elementos, una lista de [Filas](https://albakore.github.io/pyzed/es/library/list-item/) es más ligera. #### Uso ```python import pyzed as pz from pyzed import ui class Files(pz.App): def __init__(self): self.rows = [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}] self.sort_by, self.descending, self.selected = "Lines", True, None def sort(self, title): self.descending = not self.descending if self.sort_by == title else False self.sort_by = title def view(self): return ui.table( [ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], self.rows, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort, on_row_press=lambda row: setattr(self, "selected", row), selected=self.selected, ) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Opciones ```python pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}]), width=320) ``` ```python pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}], striped=True), width=320) ``` ```python rows = [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}] pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], rows, selected=rows[1]), width=320) ``` ```python pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}], sort_by="Lines", descending=True, on_sort=on_sort), width=320) ``` ```python pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}], row_height=24), width=320) ``` ##### Celdas ```python rows = [{"job": "build", "state": "passed"}, {"job": "lint", "state": "failed"}] tint = {"passed": "success", "failed": "error"} pz.container(ui.table([ui.Column("Job", "job"), ui.Column("State", "state", width=110, render=lambda s: ui.badge(s, tint=tint[s]))], rows), width=320) ``` ```python people = [("Ana", 31), ("Luis", 27)] pz.container(ui.table([ui.Column("Name", lambda p: p[0]), ui.Column("Age", lambda p: p[1], width=70, align="end")], people), width=320) ``` ##### Estados y eventos `on_row_double_press(fila)` abre una fila y `on_row_context(fila)` muestra un menú contextual. ```python rows = [{"name": "app.py", "size": 4}, {"name": "ui.py", "size": 12}, {"name": "README.md", "size": 2}] ui.table(["name", "size"], rows, selected=[rows[0], rows[2]], on_row_press=on_press, on_row_double_press=on_open, on_row_context=on_context, height=130) ``` ```python ui.table(["name", "size"], [], empty="No files yet", height=100) ``` #### Ejemplos ##### Celdas personalizadas `render` recibe el valor y devuelve un widget, aquí una insignia. **Código:** ```python rows = [{"job": "build", "state": "passed"}, {"job": "lint", "state": "failed"}, {"job": "docs", "state": "running"}] tint = {"passed": "success", "failed": "error", "running": "warning"} pz.container(ui.table( [ui.Column("Job", "job"), ui.Column("State", "state", width=120, render=lambda s: ui.badge(s, tint=tint[s]))], rows, ), width=400) ``` #### Eventos Tabla tiene **4 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_sort` | `(title)` | Callable que recibe el título pulsado. | | `on_row_press` | `(row)` | Callable que recibe la fila pulsada. | | `on_row_double_press` | `(row)` | Recibe la fila con un doble clic (abrir, editar…). | | `on_row_context` | `(row)` | Recibe la fila con un clic derecho (un menú contextual). | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Files(pz.App): def __init__(self): self.rows = [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}] self.sort_by, self.descending = "Lines", True self.selected, self.opened = None, None def sort(self, title): # on_sort: the clicked column title self.descending = not self.descending if self.sort_by == title else False self.sort_by = title def view(self): key = "name" if self.sort_by == "File" else "lines" rows = sorted(self.rows, key=lambda r: r[key], reverse=self.descending) return ui.table( [ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], rows, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort, selected=self.selected, on_row_press=lambda row: setattr(self, "selected", row), # click on_row_double_press=lambda row: setattr(self, "opened", row), # double click on_row_context=lambda row: print("menu for", row["name"]), # right click ) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Sort: title: str @dataclass class Select: row: dict class Files(pz.App): rows = [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}] sort_by, descending, selected = "Lines", True, None def update(self, message): match message: case Sort(title): self.descending = not self.descending if title == self.sort_by else False self.sort_by = title case Select(row): self.selected = row def view(self): return ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80)], self.rows, sort_by=self.sort_by, descending=self.descending, selected=self.selected, on_sort=Sort, on_row_press=Select) ``` **Funcional:** ```python from dataclasses import dataclass ROWS = ({"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}) @dataclass(frozen=True) class State: sort_by: str = "Lines" descending: bool = True selected: str = "" def view(state, set_state): key = "name" if state.sort_by == "File" else "lines" rows = sorted(ROWS, key=lambda r: r[key], reverse=state.descending) return ui.table( [ui.Column("File", "name"), ui.Column("Lines", "lines", width=80)], rows, sort_by=state.sort_by, descending=state.descending, selected=next((r for r in rows if r["name"] == state.selected), None), on_sort=lambda t: set_state(sort_by=t, descending=not state.descending if t == state.sort_by else False), on_row_press=lambda row: set_state(selected=row["name"])) ``` #### Propiedades y API ##### `ui.table` ```python table( columns, rows, *, sort_by=None, descending=False, on_sort=None, on_row_press=None, selected=None, striped=False, row_height=30, height=None, on_row_double_press=None, on_row_context=None, empty=None, theme=None, ) ``` Una tabla de datos con encabezado fijo, orden opcional y selección de filas. El orden se hace aquí cuando `sort_by` nombra una columna: pulsar un encabezado llama a `on_sort(título)` y tú cambias `sort_by`/`descending` en tu estado. Las filas están virtualizadas: solo se construyen las visibles, así que cientos de miles de filas se desplazan con fluidez. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `columns` | *obligatorio* | `ui.Column` o claves simples. | | `rows` | *obligatorio* | Dicts u objetos. | | `sort_by` | `None` | Título de la columna por la que se ordena. | | `descending` | `False` | Orden descendente. | | `selected` | `None` | La fila a resaltar, o una lista o conjunto de filas (selección múltiple). | | `striped` | `False` | Filas alternas sombreadas. | | `row_height` | `30` | Alto de cada fila. | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `empty` | `None` | Texto o widget que se muestra en lugar de las filas cuando no hay ninguna. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_sort`, `on_row_press`, `on_row_double_press`, `on_row_context` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#table) ##### `ui.Column` ```python @dataclass class Column(title, key, width='fill', align='start', render=None, sortable=True) ``` Una columna de `ui.table`. `key` es una clave de dict, un nombre de atributo o una función de la fila; `render` (opcional) convierte el valor en un widget. `width` es `"fill"`, un número o una porción como `"fill:2"`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `title` | *obligatorio* | Encabezado (y nombre para ordenar). | | `key` | *obligatorio* | Cómo obtener el valor de cada fila. | | `width` | `"fill"` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `align` | `"start"` | `"start"`, `"center"` o `"end"`. | | `render` | `None` | Función valor → widget. | | `sortable` | `True` | Si el encabezado se puede pulsar para ordenar. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#column) #### Relacionados - [Fila de lista](https://albakore.github.io/pyzed/es/library/list-item/): Una fila de lista con icono, texto y un hueco al final. - [Lista virtual](https://albakore.github.io/pyzed/es/library/virtual-list/): Una lista que solo construye las filas visibles, para millones de elementos. - [Cifra](https://albakore.github.io/pyzed/es/library/stat/): Una cifra de dashboard con variación y minigráfico. - [Grupo de ajustes](https://albakore.github.io/pyzed/es/library/settings-group/): Un bloque con título de filas de ajustes, para pantallas de ajustes. ### Tarjeta > Una superficie con borde, título y acciones opcionales. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/card/ :::tip[En pocas palabras] Una caja con borde que mantiene juntas las cosas relacionadas, como una ficha sobre una mesa. ::: Una superficie redondeada con borde que agrupa contenido. `title` y `actions` añaden una fila de cabecera; `padding` y `width` controlan su caja. `ui.card` · Categoría: [Visualización](https://albakore.github.io/pyzed/es/library/#visualización) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#card) **Código:** ```python ui.card( pz.column([ ui.muted("Pyzed draws the UI with Iced, in Rust."), pz.row([pz.hspace(), ui.button("Cancel", on_cancel, variant="ghost"), ui.button("OK", on_ok, variant="solid")], spacing=8), ], spacing=12), title="Card", actions=[ui.icon_button("ellipsis", on_menu, size="sm")], width=380, ) ``` #### Cuándo usarlo - Para agrupar contenido relacionado: un resumen, una sección de formulario, un panel de un dashboard. - Para un diálogo encima de la interfaz usa [Modal](https://albakore.github.io/pyzed/es/library/modal/). #### Uso ```python import pyzed as pz from pyzed import ui ui.card( pz.column([ ui.muted("Pyzed draws the UI with Iced, in Rust."), pz.row([pz.hspace(), ui.button("Cancel", on_cancel, variant="ghost"), ui.button("OK", on_ok, variant="solid")], spacing=8), ], spacing=12), title="Card", actions=[ui.icon_button("ellipsis", on_menu, size="sm")], width=380, ) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido ```python ui.card(ui.muted("A bordered surface."), width=260) ``` **} alt={** ```python ui.card(ui.muted("Three files changed."), title="Changes", width=260) ``` ```python ui.card(ui.muted("Three files changed."), title="Changes", actions=[ui.icon_button("ellipsis", on_menu, size="sm")], width=260) ``` ##### Relleno ```python ui.card(ui.label("Compact"), padding=8) ``` ```python ui.card(ui.label("Default")) ``` ```python ui.card(ui.label("Roomy"), padding=28) ``` ##### Estados y eventos ```python ui.card(ui.muted("Python app with a window"), title="Desktop app", on_press=on_press, selected=True, width=260) ``` ```python ui.card(ui.muted("3 files changed"), title="Commit", footer=[ui.button("Push", on_press)], elevated=True, width=260) ``` #### Eventos Tarjeta tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_press` | `()` | Hace pulsable toda la tarjeta. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Plans(pz.App): def __init__(self): self.plan = "Pro" def view(self): return pz.row([ ui.card(ui.muted(f"Plan {name}"), title=name, width=160, selected=name == self.plan, on_press=lambda name=name: setattr(self, "plan", name)) # the whole card for name in ("Free", "Pro", "Team") ], spacing=8) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Choose: plan: str class Plans(pz.App): plan = "Pro" def update(self, message): match message: case Choose(plan): self.plan = plan def view(self): return pz.row([ui.card(ui.muted(name), title=name, width=160, selected=name == self.plan, on_press=Choose(name)) for name in ("Free", "Pro", "Team")], spacing=8) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: plan: str = "Pro" def plan_card(name, selected, choose): # a small function component return ui.card(ui.muted(name), title=name, width=160, selected=selected, on_press=choose) def view(state, set_state): return pz.row([plan_card(n, n == state.plan, lambda n=n: set_state(plan=n)) for n in ("Free", "Pro", "Team")], spacing=8) ``` #### Propiedades y API ##### `ui.card` ```python card( content, *, title=None, actions=(), padding=16, width=None, footer=(), on_press=None, selected=False, elevated=False, theme=None, ) ``` Una superficie con borde para agrupar contenido, con título y acciones opcionales. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `title` | `None` | Título. | | `actions` | `()` | Widgets extra (normalmente botones) que se muestran como acciones. | | `padding` | `16` | Relleno interior. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `footer` | `()` | Widgets abajo a la derecha (botones). | | `selected` | `False` | La resalta con el color de acento. | | `elevated` | `False` | Añade una sombra. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | Eventos: `on_press` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#card) #### Relacionados - [Modal](https://albakore.github.io/pyzed/es/library/modal/): Una superficie de diálogo con título, contenido y botones al pie. - [Grupo de ajustes](https://albakore.github.io/pyzed/es/library/settings-group/): Un bloque con título de filas de ajustes, para pantallas de ajustes. - [Estado vacío](https://albakore.github.io/pyzed/es/library/empty-state/): Un marcador centrado para listas y búsquedas vacías. ### Terminal > Una vista de terminal de solo lectura con colores ANSI. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/terminal/ :::tip[En pocas palabras] Muestra el texto que imprime un programa, con sus colores, como una ventana de terminal. ::: Una vista de solo lectura que entiende los colores ANSI y se queda al final. Pásale la salida de un proceso que ejecutes (por ejemplo con `subprocess` en un hilo y `pz.post`). También acepta líneas de segmentos `(texto, color)`; `ui.ansi(text)` convierte texto ANSI en esos segmentos. `ui.terminal` · Categoría: [Código y herramientas](https://albakore.github.io/pyzed/es/library/#código-y-herramientas) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#terminal) **Código:** ```python out = ("$ pytest -q\n\x1b[32m........................\x1b[0m\n\x1b[32m24 passed\x1b[0m in 1.21s\n" "$ maturin develop --release\n\x1b[2m Compiling pyzed v0.4.0\x1b[0m\n" "\x1b[32m✓\x1b[0m Installed pyzed-0.4.0\n$ ") pz.container(ui.terminal(out), width=560, height=190) ``` #### Cuándo usarlo - Para mostrar la salida de una compilación, tests, registros de un servidor o cualquier comando. #### Uso ```python import pyzed as pz from pyzed import ui out = ("$ pytest -q\n\x1b[32m........................\x1b[0m\n\x1b[32m24 passed\x1b[0m in 1.21s\n" "$ maturin develop --release\n\x1b[2m Compiling pyzed v0.4.0\x1b[0m\n" "\x1b[32m✓\x1b[0m Installed pyzed-0.4.0\n$ ") pz.container(ui.terminal(out), width=560, height=190) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido ```python pz.container(ui.terminal("$ ls\nmain.py notes.md pyproject.toml\n$ "), width=380, height=80) ``` ```python pz.container(ui.terminal("\x1b[32m✓\x1b[0m 24 passed \x1b[33m!\x1b[0m 1 warning \x1b[31m✗\x1b[0m 0 failed"), width=380, height=50) ``` ```python pz.container(ui.terminal("$ python main.py\nHello", font_size=16), width=380, height=70) ``` ```python pz.container(ui.terminal([[("ok ", "#a1c181"), ("build", None)], [("err ", "#d07277"), ("lint", None)]]), width=380, height=60) ``` #### Eventos Terminal no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.terminal` ```python terminal(lines, *, font_size=13, id=None, theme=None) ``` Una vista de terminal de solo lectura con colores ANSI. Se mantiene desplazada al final. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `lines` | *obligatorio* | Texto con escapes ANSI o lista de líneas de segmentos `(texto, color)`. | | `font_size` | `13` | Tamaño de la fuente. | | `id` | `None` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#terminal) ##### `ui.ansi` ```python ansi(text) ``` Convierte texto con colores ANSI en líneas de segmentos `(texto, nombre_de_color)`. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `text` | *obligatorio* | Texto con escapes ANSI. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#ansi) #### Relacionados - [Consola](https://albakore.github.io/pyzed/es/library/console/): Un registro estilo dev tools con niveles, filtro, copiar y limpiar. - [Editor](https://albakore.github.io/pyzed/es/library/editor/): Un editor de código completo: resaltado, plegado, minimapa, marcas de git. ### Texto > Un texto con tamaño, color, fuente y peso. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/text/ :::tip[En pocas palabras] Texto con el tamaño, el color y la fuente que elijas. ::: El widget base de texto: `size`, `color`, `font`, `weight`, `italic`, `line_height`, alineación y ajuste de línea. En una interfaz con tema mejor usa [Etiqueta](https://albakore.github.io/pyzed/es/library/label/), que toma el color del tema. `pz.text` · Categoría: [Contenido](https://albakore.github.io/pyzed/es/library/#contenido) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#text) **Código:** ```python pz.column([ pz.text("Pyzed", size=36, weight="bold", color="#74ade8"), pz.text("Fast, modern desktop UIs in Python.", size=16, color="#a9afbc"), pz.text("IBM Plex Mono, italic", font="IBM Plex Mono", italic=True, color="#e5c07b"), ], spacing=6, align="center") ``` #### Cuándo usarlo - Para texto con estilos explícitos, o fuentes propias cargadas con `pz.load_font`. #### Uso ```python import pyzed as pz pz.column([ pz.text("Pyzed", size=36, weight="bold", color="#74ade8"), pz.text("Fast, modern desktop UIs in Python.", size=16, color="#a9afbc"), pz.text("IBM Plex Mono, italic", font="IBM Plex Mono", italic=True, color="#e5c07b"), ], spacing=6, align="center") ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Tamaño ```python pz.text("Size 12", size=12) ``` ```python pz.text("Size 18", size=18) ``` ```python pz.text("Size 32", size=32) ``` ##### Fuente y peso ```python pz.text("Bold", size=18, weight="bold") ``` ```python pz.text("Light", size=18, weight="light") ``` ```python pz.text("Italic", size=18, italic=True) ``` ```python pz.text("Monospace 0123", size=16, font="mono") ``` ##### Color y ajuste ```python pz.text("Golden text", size=18, color="#e5c07b") ``` ```python pz.container(pz.text("A long sentence that wraps over several lines, centered.", align_x="center"), width=220) ``` ```python pz.container(pz.text("Taller lines are easier to read in long paragraphs.", line_height=1.8), width=220) ``` #### Eventos Texto no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.text` ```python text( content, *, size=None, color=None, font=None, weight=None, italic=False, line_height=None, align_x=None, align_y=None, width=None, height=None, wrap=True, selectable=None, selection_color=None, ) ``` Un texto. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `size` | `None` | Tamaño de la fuente. | | `color` | `None` | Color del texto o del icono. | | `font` | `None` | `"sans"` (IBM Plex Sans), `"mono"`, `"icons"`, `"serif"` o el nombre de una familia cargada con `load_font`. | | `weight` | `None` | `"thin"`, `"light"`, `"normal"`, `"medium"`, `"semibold"`, `"bold"`, `"black"` o un número (100–900). | | `italic` | `False` | Cursiva. | | `line_height` | `None` | Alto de línea relativo al tamaño (1.0, 1.5…). | | `align_x` | `None` | Alineación horizontal: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `align_y` | `None` | Alineación vertical: `"start"`/`"top"`, `"center"`, `"end"`/`"bottom"`. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | `None` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `wrap` | `True` | Ajuste de línea. Con `False` el texto no se parte. | | `selectable` | `None` | `True` deja seleccionar el texto con el ratón (arrastrar, doble clic en una palabra, triple clic en un bloque) y copiarlo con Ctrl+C. Una cadena nombra un grupo de selección: los textos y el Markdown de un mismo grupo forman una sola selección, en el orden en que se dibujan (las líneas de un log, los mensajes de un chat). | | `selection_color` | `None` | Color detrás del texto seleccionado. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#text) #### Relacionados - [Etiqueta](https://albakore.github.io/pyzed/es/library/label/): Texto con color, tamaño y peso del tema; `muted` para texto secundario. - [Texto enriquecido](https://albakore.github.io/pyzed/es/library/rich-text/): Un bloque de texto que mezcla colores, pesos y enlaces. - [Icono](https://albakore.github.io/pyzed/es/library/icon/): Iconos de Lucide por nombre e imágenes SVG. ### Texto enriquecido > Un bloque de texto que mezcla colores, pesos y enlaces. Un componente nativo de Pyzed para GUIs de escritorio en Python, con vista previa, ejemplos y su API. Source: https://albakore.github.io/pyzed/es/library/rich-text/ :::tip[En pocas palabras] Una frase en la que algunas palabras van en negrita, de color o son enlaces. ::: Combina fragmentos `pz.span(text, color=, size=, font=, weight=, italic=, bg=, underline=, strikethrough=, link=)` en un único bloque de texto que se ajusta como un todo. Al pulsar un fragmento con `link` se envía ese valor. `pz.rich_text` · Categoría: [Contenido](https://albakore.github.io/pyzed/es/library/#contenido) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#rich_text) **Código:** ```python pz.rich_text([ pz.span("rich_text "), pz.span("mixes ", color="#e5c07b"), pz.span("styles", weight="bold", underline=True), pz.span(" on one line, with "), pz.span(" code ", font="IBM Plex Mono", bg="#3b414d", radius=4), pz.span(" and "), pz.span("links", color="#74ade8", underline=True, link="https://iced.rs"), ], size=18, width=460) ``` #### Cuándo usarlo - Para resaltados en línea, menciones, coincidencias de búsqueda y mensajes cortos con formato. #### Uso ```python import pyzed as pz pz.rich_text([ pz.span("rich_text "), pz.span("mixes ", color="#e5c07b"), pz.span("styles", weight="bold", underline=True), pz.span(" on one line, with "), pz.span(" code ", font="IBM Plex Mono", bg="#3b414d", radius=4), pz.span(" and "), pz.span("links", color="#74ade8", underline=True, link="https://iced.rs"), ], size=18, width=460) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Estilos de span ```python pz.rich_text([pz.span("Some "), pz.span("bold", weight="bold"), pz.span(" text")], size=16) ``` ```python pz.rich_text([pz.span("Some "), pz.span("italic", italic=True), pz.span(" text")], size=16) ``` ```python pz.rich_text([pz.span("Some "), pz.span("colored", color="#e5c07b"), pz.span(" text")], size=16) ``` ```python pz.rich_text([pz.span("Some "), pz.span("underlined", underline=True), pz.span(" text")], size=16) ``` ```python pz.rich_text([pz.span("Some "), pz.span("struck", strikethrough=True), pz.span(" text")], size=16) ``` ```python pz.rich_text([pz.span("Run "), pz.span(" pyzed new ", font="mono", bg="#3b414d", color="#e6e6e6", radius=4)], size=16) ``` ```python pz.rich_text([pz.span("Read "), pz.span("the docs", color="#74ade8", underline=True, link="https://iced.rs")], size=16) ``` ```python pz.rich_text([pz.span("small ", size=12), pz.span("big", size=24, weight="bold")]) ``` #### Eventos Texto enriquecido no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `pz.rich_text` ```python rich_text( spans, *, size=None, color=None, font=None, line_height=None, width=None, align_x=None, wrap=True, ) ``` Texto hecho de fragmentos con estilos distintos. Los textos sueltos son fragmentos sin estilo. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `spans` | *obligatorio* | Lista de `span(...)` o textos. | | `size` | `None` | Tamaño. | | `color` | `None` | Color del texto o del icono. | | `font` | `None` | Fuente del fragmento o del texto (nombre de familia, p. ej. `"IBM Plex Mono"`). | | `line_height` | `None` | Alto de línea. | | `width` | `None` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `align_x` | `None` | Alineación horizontal: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `wrap` | `True` | Ajuste de línea / pasa los hijos a una nueva fila cuando no caben. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#rich_text) ##### `pz.span` ```python span( text, *, color=None, size=None, font=None, weight=None, italic=False, bg=None, radius=0.0, padding=None, underline=False, strikethrough=False, link=None, ) ``` Un fragmento con estilo para `rich_text`. Con `link`, al pulsarlo se envía (o se llama) ese valor. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `text` | *obligatorio* | El texto. | | `color` | `None` | Color del texto o del icono. | | `size` | `None` | Tamaño. | | `font` | `None` | Fuente del fragmento o del texto (nombre de familia, p. ej. `"IBM Plex Mono"`). | | `weight` | `None` | Grosor de la fuente: `"normal"`, `"medium"`, `"semibold"` o `"bold"`. | | `italic` | `False` | Si es `True`, el fragmento se dibuja en cursiva. | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `radius` | `0.0` | Radio del fondo. | | `padding` | `None` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `underline` | `False` | Subrayado. | | `strikethrough` | `False` | Tachado. | | `link` | `None` | Mensaje o callable al pulsarlo. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#span) #### Relacionados - [Texto](https://albakore.github.io/pyzed/es/library/text/): Un texto con tamaño, color, fuente y peso. - [Markdown](https://albakore.github.io/pyzed/es/library/markdown/): Muestra Markdown con los colores del tema. ### Tooltip > Una pista que aparece al pasar el ratón, con su atajo opcional. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/tooltip/ :::tip[En pocas palabras] Una cajita con una pista que aparece cuando dejas el ratón encima de algo. ::: Envuelve cualquier widget y muestra una caja pequeña con el estilo del tema mientras el ratón está encima. `keys=` añade el atajo y `position` es `"top"`, `"bottom"` (por defecto), `"left"` o `"right"`. `pz.tooltip` es la versión base sin estilo. `ui.tooltip` · Categoría: [Visualización](https://albakore.github.io/pyzed/es/library/#visualización) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#tooltip) **Código:** ```python pz.row([ ui.tooltip(ui.icon_button("save", on_save), "Save", keys="ctrl-s"), ui.icon_button("settings", on_settings, tooltip="Settings"), ], spacing=8) ``` #### Cuándo usarlo - En botones de icono y controles compactos cuyo significado no es evidente. - No escondas información esencial en un tooltip: no se ve al usar el teclado. #### Uso ```python import pyzed as pz from pyzed import ui pz.row([ ui.tooltip(ui.icon_button("save", on_save), "Save", keys="ctrl-s"), ui.icon_button("settings", on_settings, tooltip="Settings"), ], spacing=8) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Posición Dibujado tal como lo muestra el puntero: los tooltips solo se abren al pasar el ratón. ```python ui.tooltip(ui.icon_button("save", on_save), "Save") ``` ```python ui.tooltip(ui.icon_button("save", on_save), "Save", position="top") ``` ```python ui.tooltip(ui.icon_button("save", on_save), "Save", position="right") ``` ```python ui.tooltip(ui.icon_button("save", on_save), "Save", position="left") ``` ##### Contenido Dibujado tal como lo muestra el puntero: los tooltips solo se abren al pasar el ratón. ```python ui.tooltip(ui.button("Save", on_save), "Save the file", keys="ctrl-s") ``` ```python ui.icon_button("settings", on_settings, tooltip="Settings") ``` #### Eventos Tooltip no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Consejos :::tip Los tooltips solo aparecen al pasar el ratón, así que la vista previa muestra los botones. `ui.icon_button(..., tooltip="Texto")` es un atajo para envolverlo en `ui.tooltip`. ::: #### Propiedades y API ##### `ui.tooltip` ```python tooltip(content, text, *, keys=None, position='bottom', theme=None) ``` Envuelve `content` con un tooltip con el estilo del tema, opcionalmente con su atajo. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `text` | *obligatorio* | Texto del tooltip. | | `keys` | `None` | Atajo que se muestra al final, escrito como texto: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `position` | `"bottom"` | `"top"`, `"bottom"`, `"left"`, `"right"` o `"cursor"`. | | `theme` | `None` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#tooltip) ##### `pz.tooltip` ```python tooltip( content, tip, *, position='bottom', gap=4.0, bg=None, color=None, border_color=None, border_width=0.0, radius=None, shadow=None, ) ``` Muestra `tip` junto a `content` al pasar el ratón. `ui.tooltip` añade el estilo del tema. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `content` | *obligatorio* | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `tip` | *obligatorio* | Contenido del tooltip. | | `position` | `"bottom"` | `"top"`, `"bottom"`, `"left"`, `"right"` o `"cursor"`. | | `gap` | `4.0` | Distancia al contenido. | | `bg` | `None` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `color` | `None` | Color del texto o del icono. | | `border_color` | `None` | Color del borde. | | `border_width` | `0.0` | Grosor del borde en píxeles. | | `radius` | `None` | Radio de las esquinas: un número o `(sup_izq, sup_der, inf_der, inf_izq)`. | | `shadow` | `None` | Sombra: `(color, (dx, dy), desenfoque)`. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/widgets/#tooltip) #### Relacionados - [Botón de icono](https://albakore.github.io/pyzed/es/library/icon-button/): Un botón cuadrado con solo un icono y un tooltip opcional. - [Kbd](https://albakore.github.io/pyzed/es/library/kbd/): Atajos de teclado dibujados como teclas, con símbolos en macOS. - [Popover](https://albakore.github.io/pyzed/es/library/popover/): Contenido flotante en un punto: menús contextuales, selectores de color. ### Visor de referencia > Documentación integrada con buscador, como la de Godot: una lista de clases junto a la página abierta. Componente de Pyzed para GUIs en Python. Source: https://albakore.github.io/pyzed/es/library/reference-browser/ :::tip[En pocas palabras] Una ventana de ayuda con la documentación dentro de la app, con buscador y sin conexión. ::: La referencia integrada: una lista de clases con buscador (la librería, tus componentes y las secciones que añadas con `reference.register`) junto a la página abierta, con atrás y adelante, enlaces internos y un botón Abrir en la web. Guarda un `pyzed.reference.Browser` en tu estado; el visor lo cambia. Los enlaces que salen de la referencia van a `on_open_url`. Ver [Referencia integrada](https://albakore.github.io/pyzed/es/editor/reference/). `ui.reference_browser` · Categoría: [Código y herramientas](https://albakore.github.io/pyzed/es/library/#código-y-herramientas) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#reference_browser) **Código:** ```python from pyzed import reference help = reference.Browser("pyzed.ui.button") ui.reference_browser(help, on_open_url=open_in_browser, sidebar_width=230) ``` #### Cuándo usarlo - En una pestaña o ventana de Ayuda de una app hecha con Pyzed, para leer la API sin conexión. - Con tus propias secciones `reference.Provider`, como la ayuda de tu app. #### Uso ```python from pyzed import ui from pyzed import reference help = reference.Browser("pyzed.ui.button") ui.reference_browser(help, on_open_url=open_in_browser, sidebar_width=230) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Páginas ```python from pyzed import reference ui.reference_browser(reference.Browser("pyzed.ui.badge"), sidebar_width=200) ``` ```python from pyzed import reference ui.reference_browser(reference.Browser("pyzed.row"), sidebar_width=200) ``` #### Eventos Visor de referencia tiene **1 evento propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_open_url` | `(url)` | Se llama con la URL de un enlace que sale de la referencia (ábrela en el navegador). | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python import webbrowser from pyzed import reference class Help(pz.App): def __init__(self): self.browser = reference.Browser("pyzed.ui.button") def view(self): return ui.reference_browser(self.browser, on_open_url=webbrowser.open) # links to the web ``` **Declarativo:** ```python from dataclasses import dataclass from pyzed import reference @dataclass class OpenUrl: url: str class Help(pz.App): browser = reference.Browser("pyzed.ui.button") def update(self, message): match message: case OpenUrl(url): print("open", url) def view(self): return ui.reference_browser(self.browser, on_open_url=OpenUrl) ``` **Funcional:** ```python from dataclasses import dataclass from pyzed import reference BROWSER = reference.Browser("pyzed.ui.button") @dataclass(frozen=True) class State: last_url: str = "" def view(state, set_state): return ui.reference_browser(BROWSER, on_open_url=lambda url: set_state(last_url=url)) ``` #### Propiedades y API ##### `ui.reference_browser` ```python reference_browser( browser, *, on_open_url=None, sidebar_width=270, width='fill', height='fill', theme=None, ) ``` La referencia integrada, como la de Godot: una lista de clases con búsqueda junto a la página abierta. Eventos: `on_open_url` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#reference_browser) #### Relacionados - [Atajos](https://albakore.github.io/pyzed/es/library/shortcuts/): Una lista de atajos agrupada y con búsqueda. - [Acerca de](https://albakore.github.io/pyzed/es/library/about/): Una caja Acerca de con icono, versión, detalles y enlaces. - [Markdown](https://albakore.github.io/pyzed/es/library/markdown/): Muestra Markdown con los colores del tema. ### Vista de diferencias > Un cambio como líneas de colores: lo añadido en verde, lo quitado en rojo. Componente de Pyzed para GUIs de escritorio en Python, con ejemplos y API. Source: https://albakore.github.io/pyzed/es/library/diff-view/ :::tip[En pocas palabras] Muestra qué cambió en un archivo: las líneas nuevas en verde y las borradas en rojo. ::: `ui.diff_view(diff)` muestra un cambio: el nombre del archivo y luego las líneas que cambiaron (las añadidas en verde, las quitadas en rojo) con dos líneas de contexto alrededor de cada cambio; los cambios alejados se separan con `⋯`. `diff` es un diccionario con `path`, `old` y `new`, los textos completos de antes y de después, y `max_lines` limita cuántas líneas se ven antes de `… N líneas más`. Es lo que muestra una [Llamada a herramienta](https://albakore.github.io/pyzed/es/library/tool-call/) para los cambios que hizo. `ui.diff_view` · Categoría: [Agentes de IA](https://albakore.github.io/pyzed/es/library/#agentes-de-ia) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#diff_view) **Código:** ```python old = "def save(path):\n data = load()\n write(path, data)\n return True\n" new = "def save(path, data):\n write(path, data)\n log(path)\n return True\n" pz.container(ui.diff_view({"path": "app/storage.py", "old": old, "new": new}), width=420) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para mostrar lo que un agente o una herramienta cambió en un archivo. - Para revisar un cambio antes de guardarlo, junto a botones de Conservar y Rechazar. - Para las cifras solas (`+12 −3`), usa el [Resumen de cambios](https://albakore.github.io/pyzed/es/library/diff-stat/). #### Uso ```python import pyzed as pz from pyzed import ui old = "def save(path):\n data = load()\n write(path, data)\n return True\n" new = "def save(path, data):\n write(path, data)\n log(path)\n return True\n" pz.container(ui.diff_view({"path": "app/storage.py", "old": old, "new": new}), width=420) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Contenido Dos líneas de contexto rodean cada cambio; los cambios más alejados se separan con `⋯`. ```python pz.container(ui.diff_view({"path": "app.py", "old": "x = 1\ny = 2\n", "new": "x = 10\ny = 2\n"}), width=300) ``` ```python pz.container(ui.diff_view({"path": "main.py", "old": "", "new": "import pyzed as pz\n\npz.run(App)\n"}), width=300) ``` ```python pz.container(ui.diff_view({"path": "todo.txt", "old": "milk\neggs\nbread\n", "new": "milk\n"}), width=300) ``` ```python old = "".join(f"line {i}\n" for i in range(1, 13)) new = old.replace("line 2\n", "line two\n").replace("line 11\n", "") pz.container(ui.diff_view({"path": "notes.txt", "old": old, "new": new}), width=300) ``` ```python new = "".join(f"item {i}\n" for i in range(1, 11)) pz.container(ui.diff_view({"path": "list.txt", "old": "", "new": new}, max_lines=4), width=300) ``` #### Eventos Vista de diferencias no tiene eventos propios: muestra lo que le das. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). #### Propiedades y API ##### `ui.diff_view` ```python diff_view(diff, *, theme=None, max_lines=120) ``` Un cambio tal como lo muestra Zed en un hilo: el nombre del archivo y las líneas cambiadas (en verde las añadidas, en rojo las quitadas) con unas pocas líneas alrededor. | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `diff` | *obligatorio* | Un dict con `path`, `old` y `new` (los textos antes y después). | | `theme` | `None` | Tema. | | `max_lines` | `120` | Líneas que se muestran antes de `… N líneas más`. | [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#diff_view) #### Relacionados - [Llamada a herramienta](https://albakore.github.io/pyzed/es/library/tool-call/): Un paso de un agente de IA: qué hizo, su resultado y Permitir o Rechazar. - [Resumen de cambios](https://albakore.github.io/pyzed/es/library/diff-stat/): `+12 −3` con bloques al estilo de GitHub. - [Barra de cambios](https://albakore.github.io/pyzed/es/library/edits-bar/): Los archivos que cambió un agente, con Conservar todo y Rechazar todo. ### Zona para soltar > Un área que recibe archivos arrastrados desde el escritorio o el explorador de archivos. Componente de Pyzed para GUIs en Python. Source: https://albakore.github.io/pyzed/es/library/drop-zone/ :::tip[En pocas palabras] Un área con borde de rayas donde arrastras archivos desde el escritorio y los sueltas para añadirlos. ::: `ui.DropZone` se ilumina cuando se arrastran archivos sobre la ventana y los recibe al soltarlos: `on_drop` recibe cada ruta y `on_files` la lista completa. `accept` limita los tipos (`["png", "jpg"]`, `["*.tar.gz"]`, `["folder"]`) y los archivos de otros tipos van a `on_reject`. Con `browse=True` (por defecto) un clic también abre el diálogo de archivos. Si una ventana tiene varias zonas, recibe los archivos la que está bajo el puntero o, si no, la que acepta su tipo. `ui.DropZone` · Categoría: [Sistema](https://albakore.github.io/pyzed/es/library/#sistema) · [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#dropzone) **Código:** ```python pz.row([ ui.DropZone(text="Drop images here", hint="PNG or JPG, up to 10 MB", accept=["png", "jpg"], on_files=on_files), ui.DropZone(icon_name="file-spreadsheet", text="Import a CSV", accept=["csv"], multiple=False, show_files=True, files=["~/data/sales-2026.csv"], on_files=on_files), ], spacing=16, width=600) ``` :::note[Nuevo] Este componente se añadió hace poco. Cuéntanos qué tal te funciona en el [gestor de incidencias](https://github.com/albakore/pyzed/issues). ::: #### Cuándo usarlo - Para importar imágenes, documentos o archivos de datos, y adjuntar archivos a un mensaje. - `show_files=True` lista lo que se soltó, con un botón para quitar cada archivo; `multiple=False` se queda solo con el último. - Los archivos soltados también llegan a `on_file_drop(path)` de la ventana, para apps que no necesitan una zona. #### Uso ```python import pyzed as pz from pyzed import ui class Gallery(pz.App): def __init__(self): self.images = [] def view(self): return pz.column([ ui.DropZone(text="Drop photos", accept=["png", "jpg", "webp"], on_files=lambda e: setattr(self, "images", list(e.value))), pz.row([pz.image(path, width=96, height=96) for path in self.images], spacing=8), ], spacing=12) ``` #### Variantes :::note[Sobre estas capturas] Cada captura la graba pyzed mismo, fuera de pantalla a 2x, en One Dark o One Light según el tema de este sitio, y recortada al componente para que se vea a su tamaño real. El puntero pasa por encima, pulsa o escribe en él; un componente que no reacciona al puntero recorre algunos temas. Con movimiento reducido ves el primer fotograma. Abre **Código** bajo una captura para ver el fragmento que la dibuja; los nombres libres como `on_save` son tus propios callbacks. ::: ##### Estados ```python ui.DropZone(text="Drop files here", on_files=on_files) ``` ```python ui.DropZone(text="Drop files here", on_files=on_files) ``` ```python ui.DropZone(text="Drop images here", accept=["png", "jpg"], on_files=on_files) ``` ```python ui.DropZone(text="Drop files here", disabled=True, on_files=on_files) ``` ##### Archivos ```python ui.DropZone(text="Attach files", show_files=True, files=["~/docs/brief.pdf", "~/img/cover.png"], on_files=on_files) ``` ```python ui.DropZone(icon_name="image", text="Drag a picture", hint="Only by dragging", browse=False, on_files=on_files) ``` #### Eventos Zona para soltar tiene **5 eventos propios**, en la tabla de abajo. Como todo widget, también tiene los **18 eventos comunes** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), que se conectan con `.on(...)`: mira [Eventos](https://albakore.github.io/pyzed/es/guides/events/) y [Eventos de cada componente](https://albakore.github.io/pyzed/es/library/events/). Es un `pz.Component`, así que también lanza `on_mount` y `on_unmount`, y sus handlers reciben un `pz.Event` cuyo `value` trae el dato. | Evento | El handler recibe | Cuándo | | --- | --- | --- | | `on_drop` | `(event)` | Recibe `event.value`: una ruta soltada (una vez por archivo). | | `on_files` | `(event)` | Recibe `event.value`: la lista de archivos. | | `on_reject` | `(event)` | Recibe una ruta que no acepta. | | `on_enter` | `(event)` | Se arrastran archivos sobre la ventana. | | `on_leave` | `(event)` | El arrastre salió sin soltar. | **El handler recibe** es lo que Pyzed le pasa a tu función: `()` es nada. Cualquier handler también puede ser un mensaje (un valor que no se puede llamar), que va a `update()`. ##### Manejar los eventos Los mismos eventos escritos de tres formas. **Imperativa**: métodos de la app que cambian `self`. **Declarativa**: los handlers producen mensajes y un único `update()` decide. **Funcional**: una función pura dibuja un estado inmutable y pide cambios con `set_state(...)`. Mira [Imperativo, declarativo y funcional](https://albakore.github.io/pyzed/es/guides/code-styles/). **Imperativo:** ```python class Attach(pz.App): def __init__(self): self.files = [] def received(self, event): # on_files: event.value is every file so far self.files = list(event.value) def view(self): return pz.column([ ui.DropZone(text="Drop files to attach", on_files=self.received), ui.label(f"{len(self.files)} files"), ], spacing=8, width=320) ``` **Declarativo:** ```python from dataclasses import dataclass @dataclass class Dropped: path: str class Attach(pz.App): files = () def update(self, message): match message: case Dropped(path): self.files = (*self.files, path) def view(self): # on_drop runs once per file return ui.DropZone(accept=["png", "jpg"], on_drop=lambda e: Dropped(e.value)) ``` **Funcional:** ```python from dataclasses import dataclass @dataclass(frozen=True) class State: rejected: str = "" def view(state, set_state): return pz.column([ ui.DropZone(text="Drop a CSV", accept=["csv"], multiple=False, on_reject=lambda e: set_state(rejected=e.value)), ui.label(f"Not a CSV: {state.rejected}" if state.rejected else ""), ], spacing=8, width=320) ``` #### Consejos :::tip Pyzed Design tiene una Zona para soltar en la sección Sistema de la caja de herramientas, junto a los ayudantes no visuales del sistema operativo (información del sistema, lanzador, monitores de red y batería). ::: #### Propiedades y API ##### `ui.DropZone` ```python class DropZone ``` Un componente que recibe archivos arrastrados desde el explorador de archivos del sistema y soltados en la ventana, y abre el diálogo de archivos con un clic. Se ilumina mientras se arrastran archivos sobre la ventana y guarda las rutas soltadas en `files`. El gancho `on_file_drop` de la ventana entrega cada archivo a la zona bajo el puntero, si no a una cuyo `accept` incluya su tipo. Ver la página [Zona para soltar](https://albakore.github.io/pyzed/es/library/drop-zone/). | Propiedad | Por defecto | Descripción | | --- | --- | --- | | `text` | `""` | El texto principal (por defecto: Suelta archivos aquí). | | `hint` | `""` | La línea de debajo. | | `icon_name` | `"upload"` | Su icono. | | `accept` | `()` | Extensiones (`["png", "jpg"]`), patrones (`"*.tar.gz"`) o `"folder"`; vacío acepta todo. | | `multiple` | `True` | `False` se queda solo con el último archivo. | | `browse` | `True` | Un clic abre el diálogo de archivos. | | `show_files` | `False` | Lista los archivos bajo el texto (cada uno con una x). | | `files` | `()` | Las rutas soltadas. | | `disabled` | `False` | No recibe nada. | | `live` | `True` | `False` lo dibuja sin recibir archivos (el lienzo del diseñador). | Eventos: `on_drop`, `on_files`, `on_reject`, `on_enter`, `on_leave` ([eventos](#eventos)). [Referencia completa →](https://albakore.github.io/pyzed/es/reference/ui/#dropzone) #### Relacionados - [Selector de archivos](https://albakore.github.io/pyzed/es/library/file-input/): Un campo de ruta con un botón que abre el diálogo de archivos o carpetas del sistema. - [Tarjeta](https://albakore.github.io/pyzed/es/library/card/): Una superficie con borde, título y acciones opcionales. ## Bloques ### Bloques > Pantallas de interfaz de escritorio en Python listas para copiar: login, dashboard, ajustes, tabla de datos, chat, IDE, asistente de bienvenida, reproductor de música y kanban, con componentes de Pyzed. Source: https://albakore.github.io/pyzed/es/blocks/ Los bloques son **pantallas completas** hechas con los [componentes](https://albakore.github.io/pyzed/es/library/) de Pyzed, en la línea de los blocks de shadcn/ui: abre uno, ejecútalo, copia el código en tu app y cambia lo que necesites. Cada bloque es un único archivo ejecutable en [`examples/blocks/`](https://github.com/albakore/pyzed/tree/main/examples/blocks), probado en CI con tema claro y oscuro, así que el código de estas páginas siempre funciona con el Pyzed actual. #### Cómo usar un bloque 1. Abre su página y mira la captura en claro y oscuro (cambia el tema del sitio). 2. Ejecútalo: `python examples/blocks/.py`, o copia la sección **Código** en un archivo. 3. Quédate con lo que necesites. Los bloques son clases `pz.App` normales: sus piezas son métodos (`sidebar()`, `header()`...) que devuelven widgets, así que puedes llevar uno a tu app tal cual. #### Hazlos tuyos - **Tema.** Pon `theme = "Gruvbox Dark"` (o cualquier [tema](https://albakore.github.io/pyzed/es/theming/themes/)) en la app, o crea uno con el color de tu marca: `pz.themes.get("One Dark").with_accent("#e11d48")`. Todos los bloques usan tokens del tema (`"accent"`, `"panel"`, `"text_muted"`...), nunca colores fijos para sus superficies. - **Datos.** Los datos de ejemplo están al principio de cada archivo. Cámbialos por una consulta a la base de datos, una llamada HTTP o archivos; el trabajo lento va en una corrutina para que la ventana nunca se bloquee (mira [Async](https://albakore.github.io/pyzed/es/guides/async/)). - **Componentes.** Cuando una pieza tenga estado propio, conviértela en un [`pz.Component`](https://albakore.github.io/pyzed/es/guides/components/) para que también aparezca en la caja de herramientas de Pyzed Design. - **IA.** Con el [servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/) conectado, pide a tu agente: *"Parte del bloque de dashboard y haz que muestre nuestros issues de GitHub"*. Puede leer cada bloque con `read_example("blocks/dashboard")`. ### Inicio de sesión > Pantalla de inicio de sesión para apps de escritorio en Python: panel de marca con degradado, botones de SSO y passkey, email y contraseña con validación. Source: https://albakore.github.io/pyzed/es/blocks/login/ Una pantalla de inicio de sesión en dos columnas: un panel de marca con degradado, ventajas y un testimonio sobre una tarjeta translúcida, y un formulario con inicio de sesión único, passkey, email y contraseña, *recordarme* y validación en línea. La captura sigue el tema del sitio: cambia entre claro y oscuro arriba de la página para ver el bloque en One Light y One Dark. #### Ejecutarlo Desde la raíz del repositorio: ```bash python examples/blocks/login.py ``` #### Qué muestra - Fondo con `pz.Gradient` y una tarjeta translúcida tipo vidrio. - Validación al enviar: un `ui.callout` muestra el error o el éxito. - Enlaces de texto hechos con `.on(click=..., cursor="pointer")`. - Enter en la contraseña inicia sesión. #### Componentes que usa | Componente | Para | | --- | --- | | [Input](https://albakore.github.io/pyzed/es/library/input/) | Email y contraseña | | [Button](https://albakore.github.io/pyzed/es/library/button/) | Iniciar sesión, SSO y passkey | | [Checkbox](https://albakore.github.io/pyzed/es/library/checkbox/) | Mantener la sesión | | [Callout](https://albakore.github.io/pyzed/es/library/callout/) | Errores y éxito | | [Avatar](https://albakore.github.io/pyzed/es/library/avatar/) | Autora del testimonio | | [Divider](https://albakore.github.io/pyzed/es/library/divider/) | El separador “o” | | [Heading](https://albakore.github.io/pyzed/es/library/heading/) | Título | | [Label](https://albakore.github.io/pyzed/es/library/label/) | Etiquetas y enlaces | #### Código El bloque completo, listo para copiar. Solo necesita `pip install pyzed-ui`. ```python title="examples/blocks/login.py" """Login block: a split sign-in screen with a brand panel, single sign-on, passkey and an email form.""" import pyzed as pz from pyzed import ui def link(text, action): """A text that behaves like a link.""" return ui.label(text, color="accent", size="sm", weight="medium").on(click=action, cursor="pointer") FEATURES = [ ("rocket", "Deploy in seconds", "Push to main and your app is live worldwide."), ("shield-check", "Secure by default", "SSO, audit logs and encrypted secrets."), ("activity", "Observability built in", "Traces, metrics and logs in one place."), ] BRAND = pz.Gradient([(0.0, "#6366f1"), (0.55, "#8b5cf6"), (1.0, "#ec4899")], angle=135) class Login(pz.App): title = "Sign in" size = (1200, 760) def __init__(self): self.email = "ada@lovelace.dev" self.password = "analytical-engine" self.remember = True self.error = "" self.signed_in = False # -- actions --------------------------------------------------------------------------- def sign_in(self): if "@" not in self.email: self.error = "Enter a valid email address." elif len(self.password) < 8: self.error = "The password needs at least 8 characters." else: self.error, self.signed_in = "", True def forgot(self): self.error = "" if "@" in self.email else "Enter your email first." def sign_up(self): self.signed_in = False def set_email(self, value): self.email, self.error = value, "" def set_password(self, value): self.password, self.error = value, "" # -- view ------------------------------------------------------------------------------ def brand(self): quote = pz.column([ pz.text("“We replaced a 4,000-line Tkinter app with 900 lines of Pyzed. " "It starts faster and finally looks like it belongs in 2026.”", size=17, color="#ffffff", line_height=1.45), pz.row([ ui.avatar("ML", color="#f59e0b", size=34), pz.column([pz.text("Maya López", size=14, weight="semibold", color="#ffffff"), pz.text("Staff engineer, Northwind Labs", size=12.5, color="#e0e7ff")], spacing=2), ], spacing=10, align="center"), ], spacing=18) return pz.container( pz.column([ pz.row([ pz.container(pz.icon("zap", size=18, color="#ffffff"), padding=7, radius=9, bg="#ffffff33"), pz.text("Acme Cloud", size=17, weight="semibold", color="#ffffff"), ], spacing=10, align="center"), pz.vspace(), pz.column([self.feature(*f) for f in FEATURES], spacing=22), pz.vspace(), pz.container(quote, padding=22, radius=12, bg="#ffffff1c", border_color="#ffffff38", border_width=1), ]), width="fill", height="fill", padding=44, ).style(background=BRAND, radius=14) def feature(self, icon, title, text): return pz.row([ pz.container(pz.icon(icon, size=18, color="#ffffff"), padding=9, radius=10, bg="#ffffff26"), pz.column([pz.text(title, size=15, weight="semibold", color="#ffffff"), pz.text(text, size=13.5, color="#e0e7ff")], spacing=3), ], spacing=14, align="center") def form(self): social = pz.row([ ui.button("Single sign-on", self.sign_in, icon="building-2", size="lg", full_width=True), ui.button("Passkey", self.sign_in, icon="fingerprint", size="lg", full_width=True), ], spacing=10) separator = pz.row([ui.divider(), ui.muted("or continue with email", size="sm", wrap=False), ui.divider()], spacing=10, align="center") password_label = pz.row([ui.label("Password", size="sm", weight="medium"), pz.hspace(), link("Forgot password?", self.forgot)], align="center") fields = pz.column([ pz.column([ui.label("Email", size="sm", weight="medium"), ui.input(self.email, self.set_email, placeholder="you@company.com", icon="mail", size="lg", id="email")], spacing=6), pz.column([password_label, ui.input(self.password, self.set_password, placeholder="••••••••", icon="lock", size="lg", secure=True, on_submit=self.sign_in, id="password")], spacing=6), ui.checkbox(self.remember, lambda v: setattr(self, "remember", v), label="Keep me signed in"), ], spacing=16) status = [] if self.error: status = [ui.callout(self.error, tint="error", icon="circle-alert")] elif self.signed_in: status = [ui.callout("Signed in", f"Welcome back, {self.email}.", tint="success", icon="circle-check")] return pz.container( pz.column([ pz.column([ui.heading("Welcome back"), ui.muted("Sign in to your workspace to continue.")], spacing=6), social, separator, fields, *status, ui.button("Sign in", self.sign_in, variant="solid", size="lg", full_width=True, icon="log-in"), pz.row([ui.muted("Don't have an account?", size="sm"), link("Create one", self.sign_up)], spacing=6, align="center"), ], spacing=22, max_width=380), width="fill", height="fill", center=True, padding=32, ) def view(self): return pz.row([ pz.container(self.brand(), width="fill", height="fill", padding=12), self.form(), ], height="fill").style(background="background") if __name__ == "__main__": pz.run(Login) ``` #### Hazlo tuyo Cambia `theme` (o `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), pon tus datos en lugar de los de ejemplo y separa piezas en [componentes](https://albakore.github.io/pyzed/es/guides/components/) cuando crezcan. Un agente de IA con el [servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/) puede leer este bloque con `read_example("blocks/login")` y adaptarlo por ti. ### Panel de control > Un dashboard de analítica en Python: navegación lateral, tarjetas KPI con sparklines, gráfico de barras de ingresos, donut de tráfico y tabla de pedidos ordenable. Source: https://albakore.github.io/pyzed/es/blocks/dashboard/ Un panel de administración: navegación lateral con una tarjeta de mejora de plan, cabecera con búsqueda y notificaciones, cuatro tarjetas KPI con tendencia, un gráfico de ingresos con selector 7 días / 30 días / 12 meses, fuentes de tráfico con donut y objetivo mensual, y una tabla ordenable de pedidos recientes. La captura sigue el tema del sitio: cambia entre claro y oscuro arriba de la página para ver el bloque en One Light y One Dark. #### Ejecutarlo Desde la raíz del repositorio: ```bash python examples/blocks/dashboard.py ``` #### Qué muestra - Tarjetas `ui.stat`: valor, variación y sparkline en una llamada. - `ui.bar_chart` y `ui.donut` reciben listas de números; los colores salen del tema. - Celdas propias en `ui.table`: avatares y badges de estado con `render=`. - Clic en una cabecera para ordenar; la búsqueda filtra los pedidos al escribir. #### Componentes que usa | Componente | Para | | --- | --- | | [Stat](https://albakore.github.io/pyzed/es/library/stat/) | Tarjetas KPI | | [Bar chart](https://albakore.github.io/pyzed/es/library/bar-chart/) | Ingresos | | [Donut](https://albakore.github.io/pyzed/es/library/donut/) | Fuentes de tráfico | | [Table](https://albakore.github.io/pyzed/es/library/table/) | Pedidos | | [Card](https://albakore.github.io/pyzed/es/library/card/) | Cada panel | | [Segmented](https://albakore.github.io/pyzed/es/library/segmented/) | Rango de fechas | | [List item](https://albakore.github.io/pyzed/es/library/list-item/) | Navegación | | [Search input](https://albakore.github.io/pyzed/es/library/search-input/) | Búsqueda | | [Progress](https://albakore.github.io/pyzed/es/library/progress/) | Objetivo mensual | | [Badge](https://albakore.github.io/pyzed/es/library/badge/) | Estados | | [Avatar](https://albakore.github.io/pyzed/es/library/avatar/) | Clientes | | [Sparkline](https://albakore.github.io/pyzed/es/library/sparkline/) | Tendencias | #### Código El bloque completo, listo para copiar. Solo necesita `pip install pyzed-ui`. ```python title="examples/blocks/dashboard.py" """Dashboard block: sidebar navigation, KPI cards, a revenue chart, traffic sources and recent orders.""" import pyzed as pz from pyzed import ui NAV = [ ("layout-dashboard", "Overview"), ("chart-line", "Analytics"), ("shopping-cart", "Orders"), ("users", "Customers"), ("box", "Products"), ("credit-card", "Billing"), ] STATS = [ ("Revenue", "$48,290", 12.4, [18, 22, 19, 25, 27, 24, 31, 35]), ("Orders", "1,284", 8.1, [40, 38, 44, 47, 45, 52, 55, 58]), ("Customers", "9,431", 3.6, [70, 72, 71, 74, 76, 77, 79, 82]), ("Refund rate", "1.9%", -0.4, [3.1, 2.8, 2.9, 2.5, 2.4, 2.2, 2.0, 1.9]), ] REVENUE = { "7 days": ([5.2, 6.8, 6.1, 7.4, 8.9, 9.6, 8.2], ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]), "30 days": ([28, 31, 26, 35, 39, 42], ["W1", "W2", "W3", "W4", "W5", "W6"]), "12 months": ([31, 29, 36, 40, 38, 45, 51, 49, 55, 58, 62, 70], ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"]), } TRAFFIC = [("Organic search", 46, "accent"), ("Direct", 24, "success"), ("Social", 18, "warning"), ("Referral", 12, "error")] ORDERS = [ {"id": "#3210", "customer": "Olivia Martin", "product": "Pro plan · yearly", "status": "Paid", "amount": 1999.00}, {"id": "#3209", "customer": "Jackson Lee", "product": "Team plan · 5 seats", "status": "Pending", "amount": 450.00}, {"id": "#3208", "customer": "Isabella Nguyen", "product": "Starter plan", "status": "Paid", "amount": 99.00}, {"id": "#3207", "customer": "William Kim", "product": "Add-on · Analytics", "status": "Refunded", "amount": 39.00}, {"id": "#3206", "customer": "Sofia Davis", "product": "Pro plan · monthly", "status": "Paid", "amount": 199.00}, ] STATUS_TINT = {"Paid": "success", "Pending": "warning", "Refunded": "error"} class Dashboard(pz.App): title = "Dashboard" size = (1200, 760) def __init__(self): self.page = "Overview" self.range = "7 days" self.query = "" self.sort_by, self.descending = "Order", True # a column title def sort(self, key): self.descending = not self.descending if key == self.sort_by else False self.sort_by = key # -- pieces ---------------------------------------------------------------------------- def sidebar(self): items = [ui.list_item(label, lambda label=label: setattr(self, "page", label), icon=icon, selected=label == self.page, height=32) for icon, label in NAV] return pz.container( pz.column([ pz.row([pz.container(pz.icon("zap", size=16, color="#ffffff"), padding=6).style( background="accent", radius=8), ui.label("Acme", size="lg", weight="semibold")], spacing=10, align="center"), pz.column(items, spacing=2), pz.vspace(), ui.card(pz.column([ ui.label("Upgrade to Pro", weight="semibold"), ui.muted("Unlock forecasting and unlimited seats.", size="sm"), ui.button("Upgrade", lambda: setattr(self, "page", "Billing"), variant="solid", size="sm", full_width=True), ], spacing=8), padding=12), ui.list_item("Settings", None, icon="settings", height=32), ], spacing=20, height="fill"), width=220, height="fill", padding=(18, 12), ).style(background="panel") def header(self): return pz.row([ pz.column([ui.heading(self.page, level=2), ui.muted("Here is what happened in your store.")], spacing=2), pz.hspace(), pz.container(ui.search_input(self.query, lambda v: setattr(self, "query", v), placeholder="Search orders…"), width=240), ui.icon_button("bell", None, tooltip="Notifications"), ui.avatar("KV", size=30), ], spacing=12, align="center") def revenue(self): values, labels = REVENUE[self.range] return ui.card( pz.column([ pz.row([pz.text("$48.3k", size=22, weight="semibold"), ui.badge("+12.4%", tint="success", icon="trending-up")], spacing=10, align="center"), ui.bar_chart(values, labels=labels, width=470, height=140), ], spacing=12), title="Revenue", actions=[ui.segmented(list(REVENUE), self.range, lambda v: setattr(self, "range", v))], width="fill", ) def traffic(self): legend = [pz.row([ui.indicator(color, size=8), ui.label(name, size="sm"), pz.hspace(), ui.label(f"{share}%", size="sm", weight="medium")], spacing=8, align="center") for name, share, color in TRAFFIC] colors = [self.resolved_theme().color(c) for _, _, c in TRAFFIC] return ui.card( pz.column([ pz.row([ui.donut([s for _, s, _ in TRAFFIC], colors=colors, size=150, thickness=20, label="12.8k"), pz.column(legend, spacing=12, width="fill")], spacing=20, align="center"), ui.divider(), pz.column([pz.row([ui.label("Monthly goal", size="sm"), pz.hspace(), ui.muted("$48.3k of $60k", size="sm")]), ui.progress(0.805, height=6)], spacing=8), ], spacing=16), title="Traffic sources", width="fill", ) def orders(self): rows = [o for o in ORDERS if self.query.lower() in (o["customer"] + o["product"]).lower()] columns = [ ui.Column("Order", "id", width=80), ui.Column("Customer", "customer", render=lambda name: pz.row( [ui.avatar("".join(p[0] for p in name.split()), size=22, index=len(name)), ui.label(name)], spacing=8, align="center")), ui.Column("Product", "product"), ui.Column("Status", "status", width=110, render=lambda s: ui.badge(s, tint=STATUS_TINT[s])), ui.Column("Amount", "amount", width=100, align="end", render=lambda a: ui.label(f"${a:,.2f}")), ] return ui.card( ui.table(columns, rows, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort, row_height=36), title="Recent orders", actions=[ui.button("View all", None, variant="ghost", size="sm", icon="arrow-right", icon_position="end")], width="fill", ) def view(self): stats = [ui.stat(title, value, delta=delta, trend=trend, width="fill") for title, value, delta, trend in STATS] body = pz.column([ self.header(), pz.row(stats, spacing=16), pz.row([pz.container(self.revenue(), width="fill:3"), pz.container(self.traffic(), width="fill:2")], spacing=16), self.orders(), ], spacing=20, padding=24) return pz.row([self.sidebar(), ui.divider(vertical=True), pz.scrollable(body, height="fill")], height="fill").style( background="background") if __name__ == "__main__": pz.run(Dashboard) ``` #### Hazlo tuyo Cambia `theme` (o `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), pon tus datos en lugar de los de ejemplo y separa piezas en [componentes](https://albakore.github.io/pyzed/es/guides/components/) cuando crezcan. Un agente de IA con el [servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/) puede leer este bloque con `read_example("blocks/dashboard")` y adaptarlo por ti. ### Ajustes > Una página de ajustes para apps de escritorio en Python: navegación por secciones, perfil, tema y color de acento, interruptores de notificaciones, barra de guardado y zona de peligro. Source: https://albakore.github.io/pyzed/es/blocks/settings/ Una pantalla de ajustes al estilo de las apps de escritorio modernas: lista de secciones a la izquierda y filas agrupadas a la derecha, con datos del perfil, idioma, tema claro / oscuro / del sistema, muestras de acento, tamaño de fuente, interruptores de notificaciones y una zona de peligro que pide confirmación. La captura sigue el tema del sitio: cambia entre claro y oscuro arriba de la página para ver el bloque en One Light y One Dark. #### Ejecutarlo Desde la raíz del repositorio: ```bash python examples/blocks/settings.py ``` #### Qué muestra - `ui.settings_group` + `ui.field`: grupos con título y filas con descripción. - Un pie fijo con *cambios sin guardar* y Guardar / Descartar. - `ui.confirm` dentro de `ui.dialog_layer` para la acción destructiva. - Muestras de acento con un anillo en la elegida. #### Componentes que usa | Componente | Para | | --- | --- | | [Settings group](https://albakore.github.io/pyzed/es/library/settings-group/) | Grupos de filas | | [Field](https://albakore.github.io/pyzed/es/library/field/) | Cada ajuste | | [Input](https://albakore.github.io/pyzed/es/library/input/) | Nombre y email | | [Select](https://albakore.github.io/pyzed/es/library/select/) | Idioma | | [Segmented](https://albakore.github.io/pyzed/es/library/segmented/) | Tema | | [Color picker](https://albakore.github.io/pyzed/es/library/color-picker/) | Muestras de acento | | [Slider](https://albakore.github.io/pyzed/es/library/slider/) | Tamaño de fuente | | [Switch](https://albakore.github.io/pyzed/es/library/switch/) | Notificaciones | | [Confirm](https://albakore.github.io/pyzed/es/library/confirm/) | Borrar la cuenta | | [Dialog layer](https://albakore.github.io/pyzed/es/library/dialog-layer/) | El diálogo sobre la página | | [List item](https://albakore.github.io/pyzed/es/library/list-item/) | Secciones | #### Código El bloque completo, listo para copiar. Solo necesita `pip install pyzed-ui`. ```python title="examples/blocks/settings.py" """Settings block: a sectioned settings page with profile, appearance, notifications and a danger zone.""" import pyzed as pz from pyzed import ui SECTIONS = [ ("user", "Profile"), ("palette", "Appearance"), ("bell", "Notifications"), ("shield", "Security"), ("credit-card", "Billing"), ("plug", "Integrations"), ] LANGUAGES = ["English", "Español", "Português", "Deutsch", "日本語"] ACCENTS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f97316", "#22c55e", "#14b8a6"] class Settings(pz.App): title = "Settings" size = (1200, 760) def __init__(self): self.section = "Profile" self.name = "Ada Lovelace" self.email = "ada@lovelace.dev" self.bio = "Writing the first programs for engines that don't exist yet." self.mode = "System" self.accent = ACCENTS[1] self.font_size = 14.0 self.language = "English" self.notify = {"mentions": True, "weekly": True, "product": False} self.saved = False self.confirm_delete = False def toggle(self, key): self.notify[key] = not self.notify[key] self.saved = False def discard(self): self.__init__() # back to the saved values def save(self): self.saved = True # -- pieces ---------------------------------------------------------------------------- def nav(self): items = [ui.list_item(label, lambda label=label: setattr(self, "section", label), icon=icon, selected=label == self.section, height=32) for icon, label in SECTIONS] return pz.column([ ui.label("Settings", size="xl", weight="semibold"), ui.search_input("", None, placeholder="Search settings…"), pz.column(items, spacing=2), ], spacing=16, width=230) def profile(self): return ui.settings_group("Profile", [ pz.row([ ui.avatar("AL", size=56), pz.column([ui.label(self.name, weight="semibold"), ui.muted(self.email, size="sm")], spacing=2, width="fill"), ui.button("Change photo", self.save, icon="upload", size="sm"), ], spacing=14, align="center"), ui.field("Display name", pz.container(ui.input(self.name, lambda v: setattr(self, "name", v)), width=320)), ui.field("Email", pz.container(ui.input(self.email, lambda v: setattr(self, "email", v), icon="mail"), width=320), description="Used for sign-in and receipts."), ui.field("Language", ui.select(LANGUAGES, self.language, lambda v: setattr(self, "language", v), width=320)), ], description="How other people see you across the workspace.") def appearance(self): swatches = pz.row([ pz.container(ui.color_swatch(color, size=22, on_press=lambda color=color: setattr(self, "accent", color)), padding=2).style(border=(2, "text" if color == self.accent else "#00000000"), radius=6) for color in ACCENTS], spacing=8) return ui.settings_group("Appearance", [ ui.field("Theme", ui.segmented(["Light", "Dark", "System"], self.mode, lambda v: setattr(self, "mode", v))), ui.field("Accent color", swatches, description="Buttons, links and focus rings."), ui.field("Interface font size", pz.row([ pz.container(ui.slider(self.font_size, lambda v: setattr(self, "font_size", round(v)), min=11, max=20, step=1), width=200), ui.label(f"{self.font_size:.0f} px", size="sm", mono=True), ], spacing=12, align="center")), ]) def notifications(self): rows = [ ("mentions", "Mentions and replies", "When someone mentions you or answers your comment."), ("weekly", "Weekly summary", "A digest of your projects every Monday."), ("product", "Product news", "New features and tips, about once a month."), ] return ui.settings_group("Notifications", [ ui.field(title, ui.switch(self.notify[key], lambda _=None, key=key: self.toggle(key)), description=text) for key, title, text in rows]) def danger(self): return ui.settings_group("Danger zone", [ ui.field("Delete account", ui.button("Delete account", lambda: setattr(self, "confirm_delete", True), variant="error", icon="trash-2"), description="Deletes your workspaces and data. This can't be undone."), ]) def view(self): footer = pz.row([ ui.muted("All changes saved" if self.saved else "You have unsaved changes", size="sm"), pz.hspace(), ui.button("Discard", self.discard, variant="ghost"), ui.button("Save changes", self.save, variant="solid", icon="check"), ], spacing=8, align="center") content = pz.column([ self.profile(), self.appearance(), self.notifications(), self.danger(), ], spacing=28, padding=(0, 24, 24, 0), max_width=760) page = pz.column([ pz.scrollable(content, height="fill"), ui.divider(), pz.container(footer, padding=(12, 0), max_width=736), ], width=780, height="fill") base = pz.row([self.nav(), page], spacing=40, padding=(28, 32, 12, 32), width="fill", height="fill").style( background="background") dialog = None if self.confirm_delete: dialog = ui.confirm("Your workspaces and data will be deleted for good.", lambda: setattr(self, "confirm_delete", False), lambda: setattr(self, "confirm_delete", False), title="Delete your account?", confirm_label="Delete", danger=True) return ui.dialog_layer(base, dialog, on_dismiss=lambda: setattr(self, "confirm_delete", False)) if __name__ == "__main__": pz.run(Settings) ``` #### Hazlo tuyo Cambia `theme` (o `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), pon tus datos en lugar de los de ejemplo y separa piezas en [componentes](https://albakore.github.io/pyzed/es/guides/components/) cuando crezcan. Un agente de IA con el [servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/) puede leer este bloque con `read_example("blocks/settings")` y adaptarlo por ti. ### Tabla de datos > Una tabla de datos con filtros en Python: búsqueda, chips de estado, filtro de rol, columnas ordenables, selección de filas, paginación, exportación CSV y diálogo de invitación. Source: https://albakore.github.io/pyzed/es/blocks/data-table/ Una tabla de miembros de un equipo: filtra por nombre o email, activa estados con chips, elige un rol, ordena por cualquier columna, selecciona filas, pasa de página, cópialas como CSV e invita gente desde un diálogo. La captura sigue el tema del sitio: cambia entre claro y oscuro arriba de la página para ver el bloque en One Light y One Dark. #### Ejecutarlo Desde la raíz del repositorio: ```bash python examples/blocks/data_table.py ``` #### Qué muestra - Filtrado, orden y paginación en Python puro: fácil de cambiar por una consulta a la base de datos. - Celdas de dos líneas (avatar, nombre y email) con `ui.Column(render=...)`. - `pz.Command.copy` deja el CSV en el portapapeles. - `ui.prompt` para invitar, con Enter para enviar. #### Componentes que usa | Componente | Para | | --- | --- | | [Table](https://albakore.github.io/pyzed/es/library/table/) | La tabla | | [Chip](https://albakore.github.io/pyzed/es/library/chip/) | Filtros de estado | | [Search input](https://albakore.github.io/pyzed/es/library/search-input/) | Filtro de texto | | [Select](https://albakore.github.io/pyzed/es/library/select/) | Filtro de rol | | [Checkbox](https://albakore.github.io/pyzed/es/library/checkbox/) | Selección de filas | | [Badge](https://albakore.github.io/pyzed/es/library/badge/) | Estados | | [Avatar](https://albakore.github.io/pyzed/es/library/avatar/) | Miembros | | [Facepile](https://albakore.github.io/pyzed/es/library/facepile/) | Equipo | | [Prompt](https://albakore.github.io/pyzed/es/library/prompt/) | Diálogo de invitación | | [Icon button](https://albakore.github.io/pyzed/es/library/icon-button/) | Paginación y menú de fila | #### Código El bloque completo, listo para copiar. Solo necesita `pip install pyzed-ui`. ```python title="examples/blocks/data_table.py" """Data table block: a team members table with search, status filters, sorting, selection and paging.""" import pyzed as pz from pyzed import ui PEOPLE = [ ("Olivia Martin", "olivia@acme.dev", "Owner", "Active", "Just now"), ("Jackson Lee", "jackson@acme.dev", "Admin", "Active", "5 min ago"), ("Isabella Nguyen", "isabella@acme.dev", "Editor", "Active", "1 hour ago"), ("William Kim", "will@acme.dev", "Viewer", "Invited", "—"), ("Sofia Davis", "sofia@acme.dev", "Editor", "Active", "Yesterday"), ("Liam Johnson", "liam@acme.dev", "Editor", "Suspended", "3 weeks ago"), ("Emma Wilson", "emma@acme.dev", "Admin", "Active", "2 hours ago"), ("Noah Brown", "noah@acme.dev", "Viewer", "Active", "4 days ago"), ("Ava Garcia", "ava@acme.dev", "Editor", "Invited", "—"), ("Lucas Rossi", "lucas@acme.dev", "Viewer", "Active", "Today"), ("Mia Fischer", "mia@acme.dev", "Editor", "Active", "Last week"), ("Ethan Clark", "ethan@acme.dev", "Viewer", "Suspended", "2 months ago"), ("Chloe Dubois", "chloe@acme.dev", "Admin", "Active", "Today"), ("Mateo Silva", "mateo@acme.dev", "Editor", "Active", "Yesterday"), ] ROWS = [{"id": i, "name": n, "email": e, "role": r, "status": s, "seen": seen} for i, (n, e, r, s, seen) in enumerate(PEOPLE)] BY_NAME = {r["name"]: r for r in ROWS} STATUS_TINT = {"Active": "success", "Invited": "info", "Suspended": "error"} ROLES = ["All roles", "Owner", "Admin", "Editor", "Viewer"] PAGE_SIZE = 9 SORT_KEYS = {"Member": "name", "Role": "role", "Status": "status", "Last active": "seen"} class DataTable(pz.App): title = "Team members" size = (1200, 760) def __init__(self): self.query = "" self.statuses = {"Active", "Invited"} self.role = "All roles" self.sort_by, self.descending = "Member", False # a column title self.selected = {1, 4} self.page = 0 self.notice = "" self.inviting, self.invite_email = False, "" # -- data ------------------------------------------------------------------------------ def visible(self): q = self.query.lower() rows = [r for r in ROWS if r["status"] in self.statuses and (self.role == "All roles" or r["role"] == self.role) and (q in r["name"].lower() or q in r["email"].lower())] return rows # -- actions --------------------------------------------------------------------------- def toggle_status(self, status): self.statuses ^= {status} self.page = 0 def toggle_row(self, row_id): self.selected ^= {row_id} def sort(self, key): self.descending = not self.descending if key == self.sort_by else False self.sort_by = key def export(self): rows = self.visible() self.notice = f"Copied {len(rows)} rows as CSV" lines = ["name,email,role,status"] + [f"{r['name']},{r['email']},{r['role']},{r['status']}" for r in rows] return pz.Command.copy("\n".join(lines)) def invite(self): if "@" in self.invite_email: self.notice = f"Invitation sent to {self.invite_email}" self.inviting, self.invite_email = False, "" def set_query(self, value): self.query, self.page = value, 0 # -- view ------------------------------------------------------------------------------ def person(self, row): return pz.row([ ui.avatar("".join(p[0] for p in row["name"].split()), size=28, index=row["id"]), pz.column([ui.label(row["name"], weight="medium"), ui.muted(row["email"], size="sm")], spacing=1), ], spacing=10, align="center") def toolbar(self): chips = [ui.chip(f"{status} {sum(r['status'] == status for r in ROWS)}", on_press=lambda status=status: self.toggle_status(status), selected=status in self.statuses, icon="circle-dot" if status in self.statuses else None) for status in STATUS_TINT] return pz.row([ pz.container(ui.search_input(self.query, self.set_query, placeholder="Filter by name or email…"), width=300), *chips, ui.select(ROLES, self.role, lambda v: setattr(self, "role", v), width=140), pz.hspace(), ui.button("Export", self.export, icon="download", variant="ghost"), ui.button("Invite member", lambda: setattr(self, "inviting", True), icon="user-plus", variant="solid"), ], spacing=8, align="center") def table(self, rows): columns = [ ui.Column("", "id", width=36, sortable=False, render=lambda i: ui.checkbox(i in self.selected, lambda _=None, i=i: self.toggle_row(i))), ui.Column("Member", "name", width=("fill", 3), render=lambda name: self.person(BY_NAME[name])), ui.Column("Role", "role", width=("fill", 1), render=lambda role: ui.label(role, color="text_muted")), ui.Column("Status", "status", width=("fill", 1), render=lambda s: ui.badge(s, tint=STATUS_TINT[s])), ui.Column("Last active", "seen", width=("fill", 1), render=lambda s: ui.muted(s)), ui.Column("", "id", width=44, sortable=False, render=lambda i: ui.icon_button("ellipsis", None, size="sm", tooltip="More")), ] return ui.table(columns, rows, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort, row_height=52) def view(self): rows = self.visible() pages = max(1, -(-len(rows) // PAGE_SIZE)) page_rows = sorted(rows, key=lambda r: r[SORT_KEYS[self.sort_by]], reverse=self.descending) page_rows = page_rows[self.page * PAGE_SIZE:(self.page + 1) * PAGE_SIZE] start = self.page * PAGE_SIZE + 1 if rows else 0 footer = pz.row([ ui.muted(self.notice or f"{len(self.selected)} selected · showing {start}–{start + len(page_rows) - 1 if rows else 0} " f"of {len(rows)}", size="sm"), pz.hspace(), ui.icon_button("chevron-left", lambda: setattr(self, "page", max(0, self.page - 1)), disabled=self.page == 0, variant="filled"), *[ui.button(str(n + 1), lambda n=n: setattr(self, "page", n), size="sm", variant="solid" if n == self.page else "ghost") for n in range(pages)], ui.icon_button("chevron-right", lambda: setattr(self, "page", min(pages - 1, self.page + 1)), disabled=self.page >= pages - 1, variant="filled"), ], spacing=6, align="center") page = pz.column([ pz.row([ pz.column([ui.heading("Team members", level=2), ui.muted("Manage who can access the Acme workspace and what they can do.")], spacing=4), pz.hspace(), ui.facepile(["OM", "JL", "IN", "WK", "SD", "LJ"], size=28), ], align="center"), self.toolbar(), ui.card(pz.column([self.table(page_rows), ui.divider(), pz.container(footer, padding=(4, 4, 0, 4))], spacing=8), padding=8), ], spacing=20, padding=28, height="fill").style(background="background") dialog = None if self.inviting: dialog = ui.prompt("Invite a member", self.invite_email, lambda v: setattr(self, "invite_email", v), self.invite, lambda: setattr(self, "inviting", False), message="They get an email with a link to join the workspace.", placeholder="name@company.com", submit_label="Send invite", id="invite") return ui.dialog_layer(page, dialog, on_dismiss=lambda: setattr(self, "inviting", False)) if __name__ == "__main__": pz.run(DataTable) ``` #### Hazlo tuyo Cambia `theme` (o `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), pon tus datos en lugar de los de ejemplo y separa piezas en [componentes](https://albakore.github.io/pyzed/es/guides/components/) cuando crezcan. Un agente de IA con el [servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/) puede leer este bloque con `read_example("blocks/data_table")` y adaptarlo por ti. ### Chat > Una interfaz de chat de equipo en Python: canales con no leídos, mensajes directos con presencia, mensajes agrupados con Markdown, adjuntos, reacciones y un compositor completo. Source: https://albakore.github.io/pyzed/es/blocks/chat/ Una mensajería al estilo de Slack o Discord: canales con contador de no leídos, mensajes directos con puntos de presencia, un hilo que agrupa mensajes seguidos, Markdown, adjuntos, reacciones y un compositor con botones de formato. Para un chat con IA que transmite la respuesta mira [Crear apps con IA](https://albakore.github.io/pyzed/es/ai/ai-apps/). La captura sigue el tema del sitio: cambia entre claro y oscuro arriba de la página para ver el bloque en One Light y One Dark. #### Ejecutarlo Desde la raíz del repositorio: ```bash python examples/blocks/chat.py ``` #### Qué muestra - Los mensajes seguidos de la misma persona se agrupan bajo un avatar. - `ui.markdown` muestra negrita, código y enlaces. - `pz.scrollable(anchor_bottom=True)` mantiene visible el último mensaje. - Enter envía y baja hasta el final con `pz.Command.scroll_to_end`. #### Componentes que usa | Componente | Para | | --- | --- | | [Markdown](https://albakore.github.io/pyzed/es/library/markdown/) | Texto de los mensajes | | [Avatar](https://albakore.github.io/pyzed/es/library/avatar/) | Personas | | [Indicator](https://albakore.github.io/pyzed/es/library/indicator/) | Presencia | | [Count badge](https://albakore.github.io/pyzed/es/library/count-badge/) | No leídos | | [List item](https://albakore.github.io/pyzed/es/library/list-item/) | Canales y personas | | [List header](https://albakore.github.io/pyzed/es/library/list-header/) | Secciones de la barra | | [Chip](https://albakore.github.io/pyzed/es/library/chip/) | Reacciones | | [Input](https://albakore.github.io/pyzed/es/library/input/) | Compositor | | [Facepile](https://albakore.github.io/pyzed/es/library/facepile/) | Miembros del canal | | [Scrollable](https://albakore.github.io/pyzed/es/library/scrollable/) | El hilo | #### Código El bloque completo, listo para copiar. Solo necesita `pip install pyzed-ui`. ```python title="examples/blocks/chat.py" """Chat block: a team messenger with channels, direct messages, a message thread and a composer.""" import pyzed as pz from pyzed import ui CHANNELS = [("general", 0), ("design", 3), ("engineering", 12), ("releases", 0), ("random", 1)] PEOPLE = [("Maya López", "online"), ("Jackson Lee", "online"), ("Isabella Nguyen", "away"), ("William Kim", "offline")] PRESENCE = {"online": "success", "away": "warning", "offline": "text_disabled"} MESSAGES = [ ("Maya López", "09:12", "Morning! The new onboarding flow is on staging. Can someone give it a spin?", None), ("Jackson Lee", "09:14", "On it. First impression: **much** faster than the old one.", None), ("Jackson Lee", "09:15", "One nit: the progress bar jumps from step 2 to 4 when you skip the team invite.", None), ("Isabella Nguyen", "09:21", "Here's the build size before and after the switch to Pyzed:", ("build-report.pdf", "2.4 MB")), ("Maya López", "09:24", "Good catch, I'll fix the skip logic. Shipping it with `v2.3.1` this afternoon.", None), ] def initials(name): return "".join(part[0] for part in name.split()) class Chat(pz.App): title = "Messages" size = (1200, 760) def __init__(self): self.channel = "design" self.draft = "Looks great! Let's demo it in Friday's review" self.messages = list(MESSAGES) self.reactions = {2: {"👍": 3, "🎯": 1}, 4: {"🚀": 4}} def send(self): if self.draft.strip(): self.messages.append(("Kevin Vargas", "09:30", self.draft.strip(), None)) self.draft = "" return pz.Command.scroll_to_end("chat-thread") # -- sidebar --------------------------------------------------------------------------- def sidebar(self): channels = [ui.list_item(f"# {name}", lambda name=name: setattr(self, "channel", name), selected=name == self.channel, height=30, end=ui.count_badge(unread) if unread else None) for name, unread in CHANNELS] people = [ui.list_item(name, None, height=30, start=pz.stack([ ui.avatar(initials(name), size=20, index=i), pz.container(ui.indicator(PRESENCE[status], size=7), width=22, height=22, align_x="end", align_y="end")])) for i, (name, status) in enumerate(PEOPLE)] return pz.column([ pz.row([ui.label("Acme", size="lg", weight="semibold"), pz.hspace(), ui.icon_button("square-pen", None, tooltip="New message")], align="center"), ui.search_input("", None, placeholder="Jump to…"), pz.column([ui.list_header("Channels", end=ui.icon_button("plus", None, size="sm")), *channels], spacing=2), pz.column([ui.list_header("Direct messages"), *people], spacing=2), ], spacing=16, width=250, height="fill", padding=(16, 12)).style(background="panel") # -- thread ---------------------------------------------------------------------------- def message(self, index, author, time, text, attachment): previous = self.messages[index - 1][0] if index else None body = [ui.markdown(text)] if attachment: name, size = attachment body.append(pz.row([ pz.container(pz.icon("file-text", size=18, color="#ffffff"), padding=8).style( background="error", radius=8), pz.column([ui.label(name, weight="medium"), ui.muted(f"PDF · {size}", size="sm")], spacing=1), pz.hspace(width=24), ui.icon_button("download", None, tooltip="Download"), ], spacing=10, align="center").style(background="surface", border=(1, "border_variant"), radius=10, padding=10, width="shrink")) reactions = self.reactions.get(index) if reactions: body.append(pz.row([ui.chip(f"{emoji} {count}", on_press=None) for emoji, count in reactions.items()], spacing=6)) if author == previous: # a follow-up message: no avatar or name return pz.row([pz.space(width=36), pz.column(body, spacing=6, width="fill")], spacing=12, padding=(2, 20)) names = [a for a, _ in PEOPLE] header = pz.row([ui.label(author, weight="semibold"), ui.muted(time, size="sm")], spacing=8, align="end") return pz.row([ ui.avatar(initials(author), size=36, index=names.index(author) if author in names else 5), pz.column([header, *body], spacing=6, width="fill"), ], spacing=12, padding=(10, 20, 2, 20)) def composer(self): return pz.container( pz.column([ ui.input(self.draft, lambda v: setattr(self, "draft", v), placeholder=f"Message #{self.channel}", on_submit=self.send, borderless=True, id="composer"), pz.row([ ui.icon_button("plus", None, tooltip="Attach"), ui.icon_button("bold", None), ui.icon_button("italic", None), ui.icon_button("code", None), ui.icon_button("smile", None, tooltip="Emoji"), ui.icon_button("at-sign", None, tooltip="Mention"), pz.hspace(), ui.muted("Enter to send", size="sm"), ui.icon_button("send-horizontal", self.send, variant="solid" if self.draft else "ghost", tooltip="Send"), ], spacing=2, align="center"), ], spacing=6), padding=10, width="fill", ).style(background="surface", border=(1, "border"), radius=12) def view(self): header = pz.row([ ui.label(f"# {self.channel}", size="lg", weight="semibold"), ui.muted("Product design, reviews and handoff", size="sm"), pz.hspace(), ui.facepile(["ML", "JL", "IN", "WK", "KV", "SD", "AB"], size=24, max_shown=4), ui.icon_button("phone", None, tooltip="Call"), ui.icon_button("pin", None, tooltip="Pinned"), ui.icon_button("panel-right", None, tooltip="Details"), ], spacing=10, align="center", padding=(12, 20)) divider = pz.row([ui.divider(), ui.badge("Today", tint="accent", variant="outline"), ui.divider()], spacing=10, align="center", padding=(8, 20)) thread = pz.scrollable( pz.column([divider, *[self.message(i, *m) for i, m in enumerate(self.messages)]], spacing=4), height="fill", id="chat-thread", anchor_bottom=True) main = pz.column([header, ui.divider(), thread, pz.container(self.composer(), padding=(8, 20, 20, 20))], width="fill", height="fill").style(background="editor") return pz.row([self.sidebar(), ui.divider(vertical=True), main], height="fill") if __name__ == "__main__": pz.run(Chat) ``` #### Hazlo tuyo Cambia `theme` (o `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), pon tus datos en lugar de los de ejemplo y separa piezas en [componentes](https://albakore.github.io/pyzed/es/guides/components/) cuando crezcan. Un agente de IA con el [servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/) puede leer este bloque con `read_example("blocks/chat")` y adaptarlo por ti. ### Explorador de archivos e IDE > Un diseño de IDE en Python, como un editor de código moderno: árbol del proyecto, pestañas, migas de pan, editor con minimapa y marcas de git, terminal, esquema y barra de estado. Source: https://albakore.github.io/pyzed/es/blocks/file-explorer/ Una ventana de editor de código hecha con `ui.workspace`: barra de título con proyecto y rama, árbol del proyecto filtrable con colores de git, pestañas y migas de pan, el editor con resaltado, marcas de git y minimapa, una terminal con pestañas, un panel de esquema y la barra de estado. La captura sigue el tema del sitio: cambia entre claro y oscuro arriba de la página para ver el bloque en One Light y One Dark. #### Ejecutarlo Desde la raíz del repositorio: ```bash python examples/blocks/file_explorer.py ``` #### Qué muestra - `ui.workspace` coloca los paneles; se redimensionan arrastrando. - `ui.TreeNode.from_paths` arma el árbol desde rutas, con estados de git. - `ui.Tab.for_file` elige el icono y el color de cada tipo de archivo. - `ui.terminal` entiende colores ANSI. #### Componentes que usa | Componente | Para | | --- | --- | | [Workspace](https://albakore.github.io/pyzed/es/library/workspace/) | El diseño de la ventana | | [Tree](https://albakore.github.io/pyzed/es/library/tree/) | Archivos del proyecto | | [Tab bar](https://albakore.github.io/pyzed/es/library/tab-bar/) | Archivos abiertos | | [Breadcrumbs](https://albakore.github.io/pyzed/es/library/breadcrumbs/) | Ruta | | [Editor](https://albakore.github.io/pyzed/es/library/editor/) | Código | | [Terminal](https://albakore.github.io/pyzed/es/library/terminal/) | Terminal | | [Title bar](https://albakore.github.io/pyzed/es/library/title-bar/) | Barra de título | | [Status bar](https://albakore.github.io/pyzed/es/library/status-bar/) | Barra de estado | | [Toolbar](https://albakore.github.io/pyzed/es/library/toolbar/) | Barra del editor | | [Panel header](https://albakore.github.io/pyzed/es/library/panel-header/) | Títulos de panel | | [List item](https://albakore.github.io/pyzed/es/library/list-item/) | Esquema | #### Código El bloque completo, listo para copiar. Solo necesita `pip install pyzed-ui`. ```python title="examples/blocks/file_explorer.py" """IDE layout block: a project tree, editor tabs with breadcrumbs, a terminal, an outline panel and a status bar.""" import pyzed as pz from pyzed import ui FILES = { "src/app/main.py": '''"""Acme Cloud desktop client.""" import pyzed as pz from pyzed import ui from .api import Client from .views import Sidebar, Deployments class AcmeApp(pz.App): title = "Acme Cloud" size = (1280, 800) def __init__(self): self.client = Client.from_env() self.deployments = [] self.selected = None def on_start(self): return self.refresh() async def refresh(self): return ("loaded", await self.client.deployments()) def update(self, message): kind, data = message if kind == "loaded": self.deployments = data def view(self): return ui.workspace( Deployments(items=self.deployments), left=Sidebar(selected=self.selected), ) if __name__ == "__main__": pz.run(AcmeApp) ''', "src/app/api.py": "import httpx\n\n\nclass Client:\n ...\n", "src/app/views.py": "import pyzed as pz\n", "src/app/__init__.py": "", "tests/test_app.py": "def test_refresh():\n ...\n", "pyproject.toml": "[project]\nname = \"acme-cloud\"\n", "README.md": "# Acme Cloud\n", ".gitignore": "__pycache__/\n", } STATUS = {"src/app/main.py": "modified", "src/app/views.py": "created", "README.md": "modified"} OUTLINE = [("class", "AcmeApp", 0), ("variable", "title", 1), ("variable", "size", 1), ("method", "__init__", 1), ("method", "on_start", 1), ("method", "refresh", 1), ("method", "update", 1), ("method", "view", 1)] OUTLINE_ICON = {"class": ("box", "warning"), "method": ("square-function", "accent"), "variable": ("variable", "info")} TERMINAL = ("$ uv run pytest -q\n" "\x1b[32m........................................\x1b[0m [100%]\n" "\x1b[32m40 passed\x1b[0m in 1.84s\n" "$ pyzed dev src/app/main.py\n" "\x1b[36m●\x1b[0m watching src/ for changes · hot reload on\n") class FileExplorer(pz.App): title = "acme-cloud — main.py" size = (1200, 760) def __init__(self): self.tree = ui.TreeNode.from_paths(list(FILES), status=STATUS) self.expanded = {"src", "src/app", "tests"} self.open = ["src/app/main.py", "src/app/api.py", "README.md"] self.active = "src/app/main.py" self.texts = dict(FILES) self.cursor = (24, 18) self.query = "" # -- actions --------------------------------------------------------------------------- def toggle(self, node_id): self.expanded ^= {node_id} def select(self, node_id): if node_id in FILES: if node_id not in self.open: self.open.append(node_id) self.active = node_id def close(self, path): self.open.remove(path) if self.active == path: self.active = self.open[-1] if self.open else None def edit(self, text): self.texts[self.active] = text # -- view ------------------------------------------------------------------------------ def project(self): return pz.column([ ui.panel_header("acme-cloud", [ui.icon_button("file-plus", None, size="xs", tooltip="New file"), ui.icon_button("folder-plus", None, size="xs", tooltip="New folder"), ui.icon_button("list-collapse", lambda: setattr(self, "expanded", set()), size="xs", tooltip="Collapse all")]), pz.container(ui.search_input(self.query, lambda v: setattr(self, "query", v), placeholder="Filter files…", size="sm"), padding=(4, 8)), ui.tree(self.tree, expanded=self.expanded, selected=self.active, on_toggle=self.toggle, on_select=self.select), ]) def outline(self): rows = [] for kind, name, depth in OUTLINE: icon, color = OUTLINE_ICON[kind] rows.append(ui.list_item(name, None, icon=icon, icon_color=self.resolved_theme().color(color), indent=depth * 14, height=26, selected=name == "refresh")) return pz.column([ui.panel_header("Outline", [ui.icon_button("arrow-down-a-z", None, size="xs")], icon="list-tree"), pz.column(rows, padding=(4, 4))]) def editor(self): if self.active is None: return ui.empty_state("No open files", "Pick a file in the project panel.", icon="file") tabs = [ui.Tab.for_file(p, dirty=p == "src/app/main.py", status=STATUS.get(p)) for p in self.open] parts = self.active.split("/") return pz.column([ ui.tab_bar(tabs, self.active, on_select=self.select, on_close=self.close, end=[ui.icon_button("columns-2", None, size="sm", tooltip="Split right")]), ui.toolbar([ui.breadcrumbs([("folder", parts[0]), *parts[1:], "AcmeApp", "refresh"])], [ui.icon_button("play", None, size="sm", tooltip="Run"), ui.icon_button("sparkles", None, size="sm", tooltip="Inline assist")]), ui.editor("ide-editor", self.texts[self.active], path=self.active, on_change=self.edit, markers={19: "modified", 20: "modified", 24: "created"}, minimap=True), ]) def view(self): line, col = self.cursor return ui.workspace( self.editor(), title_bar=ui.title_bar( [ui.title_button("acme-cloud", chevron=True), ui.title_button("feat/deployments", icon="git-branch", muted=True)], [ui.icon_button("bell", None, size="sm"), ui.avatar("KV")], center=ui.title_button("Search files…", icon="search", muted=True)), left=self.project(), right=self.outline(), bottom=pz.column([ ui.tab_bar([ui.Tab("zsh", "zsh", icon="terminal"), ui.Tab("tests", "tests", icon="flask-conical")], "zsh", end=[ui.icon_button("plus", None, size="sm"), ui.icon_button("x", None, size="sm")]), ui.terminal(TERMINAL), ]), status_bar=ui.status_bar( [ui.status_item("feat/deployments", icon="git-branch"), ui.status_item("0", icon="circle-x"), ui.status_item("2", icon="triangle-alert", icon_color=self.resolved_theme().warning)], [ui.status_item(f"Ln {line}, Col {col}"), ui.status_item("UTF-8"), ui.status_item("Python 3.13"), ui.status_item(None, icon="sparkles", tooltip="AI agent")]), left_width=250, right_width=220, bottom_height=172, ) if __name__ == "__main__": pz.run(FileExplorer) ``` #### Hazlo tuyo Cambia `theme` (o `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), pon tus datos en lugar de los de ejemplo y separa piezas en [componentes](https://albakore.github.io/pyzed/es/guides/components/) cuando crezcan. Un agente de IA con el [servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/) puede leer este bloque con `read_example("blocks/file_explorer")` y adaptarlo por ti. ### Asistente de bienvenida > Un asistente de configuración en varios pasos en Python: indicador de pasos vertical, perfil, tarjetas de plantilla, invitaciones, resumen y validación entre pasos. Source: https://albakore.github.io/pyzed/es/blocks/onboarding/ Un asistente de cuatro pasos: indicador de pasos vertical con progreso, formulario de perfil, nombre del espacio con tarjetas de plantilla seleccionables, invitaciones al equipo y un resumen antes de empezar. Cada paso valida antes de seguir. La captura sigue el tema del sitio: cambia entre claro y oscuro arriba de la página para ver el bloque en One Light y One Dark. #### Ejecutarlo Desde la raíz del repositorio: ```bash python examples/blocks/onboarding.py ``` #### Qué muestra - El paso actual es estado simple (`self.step`); cada paso es un método que devuelve su vista. - Tarjetas seleccionables con `pz.container(...).style(...).on(click=...)`. - El indicador de pasos usa iconos, `pz.rule` y tokens del tema, así sigue cualquier tema. - Botones Atrás / Continuar / Omitir y un aviso de error. #### Componentes que usa | Componente | Para | | --- | --- | | [Progress](https://albakore.github.io/pyzed/es/library/progress/) | Progreso total | | [Input](https://albakore.github.io/pyzed/es/library/input/) | Campos de texto | | [Select](https://albakore.github.io/pyzed/es/library/select/) | Rol | | [Badge](https://albakore.github.io/pyzed/es/library/badge/) | Número de paso | | [Card](https://albakore.github.io/pyzed/es/library/card/) | Invitaciones y resumen | | [List item](https://albakore.github.io/pyzed/es/library/list-item/) | Personas invitadas | | [Empty state](https://albakore.github.io/pyzed/es/library/empty-state/) | Sin invitaciones | | [Callout](https://albakore.github.io/pyzed/es/library/callout/) | Errores de validación | | [Button](https://albakore.github.io/pyzed/es/library/button/) | Navegación | #### Código El bloque completo, listo para copiar. Solo necesita `pip install pyzed-ui`. ```python title="examples/blocks/onboarding.py" """Onboarding block: a multi-step setup wizard with a stepper, choice cards, validation and a summary.""" import pyzed as pz from pyzed import ui STEPS = [ ("user", "Your profile", "Tell us who you are"), ("layout-template", "Workspace", "Pick a template"), ("users", "Invite your team", "Work together from day one"), ("rocket", "All set", "Review and launch"), ] TEMPLATES = [ ("layout-dashboard", "Dashboard", "KPIs, charts and tables for internal tools."), ("bot", "AI assistant", "A streaming chat app wired to any LLM."), ("gamepad-2", "Game", "A canvas, a game loop and keyboard input."), ("code", "Developer tool", "A code editor with docks and a terminal."), ] ROLES = ["Developer", "Designer", "Data scientist", "Student", "Founder"] class Onboarding(pz.App): title = "Welcome" size = (1200, 760) def __init__(self): self.step = 1 self.name = "Ada Lovelace" self.role = "Developer" self.template = "AI assistant" self.workspace = "analytical-engine" self.invites = ["charles@babbage.dev"] self.invite = "" self.error = "" # -- actions --------------------------------------------------------------------------- def next(self): if self.step == 0 and not self.name.strip(): self.error = "Please enter your name." return if self.step == 1 and not self.workspace.strip(): self.error = "Your workspace needs a name." return self.error = "" self.step = min(self.step + 1, len(STEPS) - 1) def back(self): self.error, self.step = "", max(self.step - 1, 0) def add_invite(self): if "@" in self.invite and self.invite not in self.invites: self.invites.append(self.invite.strip()) self.invite = "" # -- pieces ---------------------------------------------------------------------------- def stepper(self): items = [] for i, (icon, title, subtitle) in enumerate(STEPS): done, current = i < self.step, i == self.step marker = pz.container( pz.icon("check" if done else icon, size=16, color="#ffffff" if done or current else None), width=36, height=36, center=True, ).style(background="accent" if done or current else "element", radius=18, border=(2, "accent" if current else "border_variant")) items.append(pz.row([ marker, pz.column([ui.label(title, weight="semibold" if current else None, color="text" if done or current else "text_muted"), ui.muted(subtitle, size="sm")], spacing=2), ], spacing=12, align="center")) if i < len(STEPS) - 1: items.append(pz.container(pz.rule(vertical=True, thickness=2), height=28, padding=(0, 0, 0, 17))) return pz.column([ pz.row([pz.container(pz.icon("zap", size=16, color="#ffffff"), padding=6).style( background="accent", radius=8), ui.label("Acme", size="lg", weight="semibold")], spacing=10, align="center"), pz.space(height=24), pz.column(items, spacing=4), pz.vspace(), ui.muted("Step {} of {}".format(self.step + 1, len(STEPS)), size="sm"), ui.progress(self.step + 1, max=len(STEPS), height=4), ], spacing=8, width=290, height="fill", padding=32).style(background="panel") def template_card(self, icon, title, text): selected = title == self.template return pz.container( pz.column([ pz.row([pz.container(pz.icon(icon, size=20), padding=8).style( background="accent" if selected else "element", radius=10, color="#ffffff" if selected else "text"), pz.hspace(), pz.icon("circle-check", size=18) if selected else None], align="start"), ui.label(title, weight="semibold"), ui.muted(text, size="sm"), ], spacing=10), padding=16, width="fill", height=150, ).style(background="surface", radius=12, border=(2 if selected else 1, "accent" if selected else "border"), ).on(click=lambda: setattr(self, "template", title), cursor="pointer") def profile_step(self): return pz.column([ ui.field("Full name", pz.container(ui.input(self.name, lambda v: setattr(self, "name", v)), width=340)), ui.field("What do you do?", ui.select(ROLES, self.role, lambda v: setattr(self, "role", v), width=340)), ], spacing=16) def workspace_step(self): cards = [self.template_card(*t) for t in TEMPLATES] return pz.column([ pz.column([ui.label("Workspace name", size="sm", weight="medium"), ui.input(self.workspace, lambda v: setattr(self, "workspace", v), icon="at-sign", size="lg")], spacing=6), pz.column([ui.label("Start from a template", size="sm", weight="medium"), pz.row(cards[:2], spacing=14), pz.row(cards[2:], spacing=14)], spacing=10), ], spacing=22) def team_step(self): people = [ui.list_item(email, None, icon="mail", end=ui.icon_button( "x", lambda email=email: self.invites.remove(email), size="sm")) for email in self.invites] return pz.column([ pz.row([ui.input(self.invite, lambda v: setattr(self, "invite", v), placeholder="teammate@company.com", icon="mail", on_submit=self.add_invite, size="lg"), ui.button("Add", self.add_invite, size="lg")], spacing=8), ui.card(pz.column(people, spacing=2) if people else ui.empty_state("No invites yet", icon="users"), padding=6), ], spacing=14) def summary_step(self): rows = [("Name", self.name), ("Role", self.role), ("Workspace", f"@{self.workspace}"), ("Template", self.template), ("Invites", str(len(self.invites)))] return ui.card(pz.column([pz.row([ui.muted(k), pz.hspace(), ui.label(v, weight="medium")]) for k, v in rows], spacing=12)) def view(self): icon, title, subtitle = STEPS[self.step] body = [self.profile_step, self.workspace_step, self.team_step, self.summary_step][self.step]() last = self.step == len(STEPS) - 1 content = pz.column([ pz.column([ui.badge(f"Step {self.step + 1}", tint="accent"), ui.heading(["Nice to meet you", "Create your workspace", "Bring your team", "You're ready to go"][self.step]), ui.muted(["We'll tailor Acme to how you work.", "A workspace holds your projects, people and settings. You can change it later.", "Invite people now or skip this step; they'll get an email.", "Here's what we'll set up for you."][self.step])], spacing=8), body, ui.callout(self.error, tint="error", icon="circle-alert") if self.error else None, pz.vspace(), pz.row([ ui.button("Back", self.back, icon="arrow-left", variant="ghost", disabled=self.step == 0), pz.hspace(), ui.button("Skip", self.next, variant="ghost") if self.step == 2 else None, ui.button("Launch workspace" if last else "Continue", self.next, variant="solid", size="lg", icon="rocket" if last else "arrow-right", icon_position="start" if last else "end"), ], spacing=8, align="center"), ], spacing=26, max_width=640, height="fill", padding=(48, 56)) return pz.row([self.stepper(), ui.divider(vertical=True), pz.container(content, width="fill", height="fill", align_x="center")], height="fill").style(background="background") if __name__ == "__main__": pz.run(Onboarding) ``` #### Hazlo tuyo Cambia `theme` (o `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), pon tus datos en lugar de los de ejemplo y separa piezas en [componentes](https://albakore.github.io/pyzed/es/guides/components/) cuando crezcan. Un agente de IA con el [servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/) puede leer este bloque con `read_example("blocks/onboarding")` y adaptarlo por ti. ### Reproductor de música > Una interfaz de reproductor de música en Python: biblioteca, cabecera de álbum con portada en degradado, lista de canciones con ecualizador animado y barra de reproducción. Source: https://albakore.github.io/pyzed/es/blocks/music-player/ Un reproductor: biblioteca con listas, una cabecera de álbum con portada en degradado y sombra, una lista de canciones donde la que suena muestra un ecualizador animado, y una barra con aleatorio, anterior, reproducir / pausa, siguiente, repetir, barra de avance y volumen. El reloj es una suscripción real: el progreso avanza mientras suena. La captura sigue el tema del sitio: cambia entre claro y oscuro arriba de la página para ver el bloque en One Light y One Dark. #### Ejecutarlo Desde la raíz del repositorio: ```bash python examples/blocks/music_player.py ``` #### Qué muestra - `pz.every` mueve el reloj solo mientras suena (`subscriptions()`). - Portada con `pz.Gradient` y una cabecera que se desvanece con `pz.Paint(..., mask=pz.Mask([1, 0]))`. - Un ecualizador de cuatro contenedores cuya altura sigue al reloj. - Las filas son `pz.button` con fondos de hover y selección del tema. #### Componentes que usa | Componente | Para | | --- | --- | | [Slider](https://albakore.github.io/pyzed/es/library/slider/) | Avance y volumen | | [Icon button](https://albakore.github.io/pyzed/es/library/icon-button/) | Controles | | [List item](https://albakore.github.io/pyzed/es/library/list-item/) | Biblioteca y listas | | [List header](https://albakore.github.io/pyzed/es/library/list-header/) | Título de listas | | [Button](https://albakore.github.io/pyzed/es/library/button/) | Reproducir | | [Scrollable](https://albakore.github.io/pyzed/es/library/scrollable/) | Página del álbum | | [Effects](https://albakore.github.io/pyzed/es/library/effects/) | Degradados y sombras | #### Código El bloque completo, listo para copiar. Solo necesita `pip install pyzed-ui`. ```python title="examples/blocks/music_player.py" """Music player block: a library sidebar, an album page with a track list and a player bar with live progress.""" import math import pyzed as pz from pyzed import ui COVER = pz.Gradient([(0.0, "#f97316"), (0.5, "#db2777"), (1.0, "#4f46e5")], angle=135) PLAYLISTS = [("Synthwave nights", "#8b5cf6"), ("Deep focus", "#14b8a6"), ("Morning run", "#f59e0b"), ("Lo-fi beats", "#ec4899"), ("Jazz classics", "#3b82f6")] TRACKS = [ ("Neon Skyline", "The Midnight Arcade", 214), ("Afterglow", "The Midnight Arcade", 187), ("Chrome Hearts", "The Midnight Arcade feat. Vela", 242), ("Pixel Rain", "The Midnight Arcade", 201), ("Overdrive", "The Midnight Arcade", 229), ("Last Arcade", "The Midnight Arcade", 263), ("Slow Fade", "The Midnight Arcade", 198), ] def clock(seconds): return f"{int(seconds) // 60}:{int(seconds) % 60:02d}" class MusicPlayer(pz.App): title = "Music" size = (1200, 760) def __init__(self): self.current = 2 self.elapsed = 96.0 self.playing = True self.volume = 0.7 self.shuffle, self.repeat = False, True self.liked = {0, 2, 4} def subscriptions(self): return [pz.every(0.25, self.tick, key="clock")] if self.playing else [] # -- actions --------------------------------------------------------------------------- def tick(self): self.elapsed += 0.25 if self.elapsed >= TRACKS[self.current][2]: self.skip(1) def play(self, index): self.current, self.elapsed, self.playing = index, 0.0, True def skip(self, step): self.play((self.current + step) % len(TRACKS)) def toggle_like(self, index): self.liked ^= {index} # -- pieces ---------------------------------------------------------------------------- def sidebar(self): lists = [ui.list_item(name, None, height=34, start=pz.container(width=22, height=22).style( background=pz.Gradient([color, "#111827"], angle=135), radius=5)) for name, color in PLAYLISTS] return pz.column([ pz.column([ui.list_item("Home", None, icon="house", height=32), ui.list_item("Search", None, icon="search", height=32), ui.list_item("Library", None, icon="library", selected=True, height=32)], spacing=2), ui.list_header("Playlists", end=ui.icon_button("plus", None, size="sm")), pz.column(lists, spacing=2), ], spacing=12, width=230, height="fill", padding=(16, 10)).style(background="panel") def equalizer(self): """Animated bars next to the playing track (they move with the position).""" bars = [] for i in range(4): level = 0.35 + 0.65 * abs(math.sin(self.elapsed * 3.1 + i * 1.7)) if self.playing else 0.3 bars.append(pz.container(width=3, height=4 + 10 * level).style(background="accent", radius=1)) return pz.container(pz.row(bars, spacing=2, align="end"), width=24, height=16, align_y="end") def header(self): return pz.row([ pz.container(pz.icon("disc-3", size=72, color="#ffffffcc"), width=190, height=190, center=True).style( background=COVER, radius=14, shadow=("#00000066", (0, 12), 30)), pz.column([ ui.label("ALBUM", size="xs", weight="semibold", color="text_muted"), pz.text("Midnight Arcade", size=44, weight="bold"), ui.muted("The Midnight Arcade · 2026 · 7 songs, 25 min"), pz.space(height=8), pz.row([ ui.button("Play" if not self.playing else "Pause", lambda: setattr(self, "playing", not self.playing), icon="pause" if self.playing else "play", variant="solid", size="lg"), ui.icon_button("shuffle", lambda: setattr(self, "shuffle", not self.shuffle), selected=self.shuffle, tooltip="Shuffle"), ui.icon_button("heart", None, tooltip="Save to library"), ui.icon_button("ellipsis", None), ], spacing=8, align="center"), ], spacing=6), ], spacing=28, align="end") def tracks(self): rows = [] for i, (title, artist, length) in enumerate(TRACKS): playing = i == self.current number = self.equalizer() if playing else ui.muted(str(i + 1)) rows.append(pz.button( pz.row([ pz.container(number, width=28), pz.column([ui.label(title, weight="medium", color="accent" if playing else "text"), ui.muted(artist, size="sm")], spacing=1, width="fill"), ui.icon_button("heart", lambda i=i: self.toggle_like(i), size="sm", color=self.resolved_theme().accent if i in self.liked else None), pz.container(ui.muted(clock(length)), width=50, align_x="end"), ], spacing=12, align="center"), lambda i=i: self.play(i), width="fill", padding=(6, 12), radius=8, bg=self.resolved_theme().ghost_selected if playing else "#00000000", hover_bg=self.resolved_theme().ghost_hover)) return pz.column(rows, spacing=2) def player(self): title, artist, length = TRACKS[self.current] now = pz.row([ pz.container(pz.icon("disc-3", size=22, color="#ffffffcc"), width=48, height=48, center=True).style( background=COVER, radius=8), pz.column([ui.label(title, weight="semibold"), ui.muted(artist, size="sm")], spacing=2), ui.icon_button("heart", lambda: self.toggle_like(self.current), size="sm", color=self.resolved_theme().accent if self.current in self.liked else None), ], spacing=12, align="center", width=300) controls = pz.column([ pz.row([ ui.icon_button("shuffle", lambda: setattr(self, "shuffle", not self.shuffle), selected=self.shuffle), ui.icon_button("skip-back", lambda: self.skip(-1)), pz.button(pz.icon("pause" if self.playing else "play", size=18, color="#111111"), lambda: setattr(self, "playing", not self.playing), width=40, height=40, padding=11, radius=20, bg="#ffffff", hover_bg="#e5e5e5"), ui.icon_button("skip-forward", lambda: self.skip(1)), ui.icon_button("repeat", lambda: setattr(self, "repeat", not self.repeat), selected=self.repeat), ], spacing=14, align="center"), pz.row([ ui.muted(clock(self.elapsed), size="xs"), ui.slider(self.elapsed, lambda v: setattr(self, "position", v), min=0, max=length, step=1, width=340), ui.muted(clock(length), size="xs"), ], spacing=10, align="center"), ], spacing=6, align="center", width="fill") volume = pz.row([ ui.icon_button("list-music", None, size="sm"), ui.icon_button("volume-2" if self.volume else "volume-x", lambda: setattr(self, "volume", 0 if self.volume else 0.7), size="sm"), ui.slider(self.volume, lambda v: setattr(self, "volume", v), width=110), ], spacing=6, align="center", width=300) return pz.container(pz.row([now, controls, pz.row([pz.hspace(), volume])], align="center"), padding=(12, 20), width="fill").style(background="title_bar") def view(self): main = pz.scrollable(pz.column([ pz.container(self.header(), padding=32, width="fill").style( background=pz.Paint(COVER, opacity=0.28, mask=pz.Mask([1, 0]))), pz.container(self.tracks(), padding=(0, 24, 24, 24)), ], spacing=8), height="fill") return pz.column([ pz.row([self.sidebar(), ui.divider(vertical=True), pz.container(main, width="fill", height="fill").style( background="editor")], height="fill"), ui.divider(), self.player(), ], height="fill") if __name__ == "__main__": pz.run(MusicPlayer) ``` #### Hazlo tuyo Cambia `theme` (o `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), pon tus datos en lugar de los de ejemplo y separa piezas en [componentes](https://albakore.github.io/pyzed/es/guides/components/) cuando crezcan. Un agente de IA con el [servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/) puede leer este bloque con `read_example("blocks/music_player")` y adaptarlo por ti. ### Tablero kanban > Un tablero kanban en Python: columnas con contadores, tarjetas con etiquetas, prioridad, fecha, checklist y responsables, filtros y mover tarjetas entre columnas. Source: https://albakore.github.io/pyzed/es/blocks/kanban/ Un tablero de sprint: cuatro columnas con contador, tarjetas con etiquetas de color, flechas de prioridad, fecha límite (hoy en rojo), comentarios, progreso del checklist y responsables, un filtro de texto, *solo las mías*, y botones para añadir tarjetas y mover la elegida entre columnas. La captura sigue el tema del sitio: cambia entre claro y oscuro arriba de la página para ver el bloque en One Light y One Dark. #### Ejecutarlo Desde la raíz del repositorio: ```bash python examples/blocks/kanban.py ``` #### Qué muestra - Las tarjetas son dataclasses; moverlas es cambiar su campo `column`. - La tarjeta elegida tiene borde de acento y sus botones para moverla. - Cada columna se desplaza sola dentro de un `pz.scrollable`. - Una sombra suave en las tarjetas con `.style(shadow=...)`. #### Componentes que usa | Componente | Para | | --- | --- | | [Badge](https://albakore.github.io/pyzed/es/library/badge/) | Etiquetas | | [Count badge](https://albakore.github.io/pyzed/es/library/count-badge/) | Contadores | | [Progress](https://albakore.github.io/pyzed/es/library/progress/) | Checklists | | [Facepile](https://albakore.github.io/pyzed/es/library/facepile/) | Responsables | | [Breadcrumbs](https://albakore.github.io/pyzed/es/library/breadcrumbs/) | Ubicación | | [Search input](https://albakore.github.io/pyzed/es/library/search-input/) | Filtro | | [Icon button](https://albakore.github.io/pyzed/es/library/icon-button/) | Añadir y mover | | [Scrollable](https://albakore.github.io/pyzed/es/library/scrollable/) | Columnas | | [Button](https://albakore.github.io/pyzed/es/library/button/) | Solo las mías, nueva tarea | #### Código El bloque completo, listo para copiar. Solo necesita `pip install pyzed-ui`. ```python title="examples/blocks/kanban.py" """Kanban block: a project board with columns, labelled cards, assignees, progress and moving cards along.""" from dataclasses import dataclass, field from typing import List import pyzed as pz from pyzed import ui COLUMNS = [("backlog", "Backlog", "circle-dashed", "text_muted"), ("todo", "To do", "circle", "info"), ("doing", "In progress", "circle-dot", "warning"), ("done", "Done", "circle-check", "success")] TAGS = {"design": "accent", "backend": "info", "bug": "error", "docs": "success", "ai": "warning"} PRIORITY = {"high": ("chevrons-up", "error"), "medium": ("chevron-up", "warning"), "low": ("chevron-down", "info")} @dataclass class Card: id: int title: str column: str tags: List[str] = field(default_factory=list) people: List[str] = field(default_factory=list) priority: str = "medium" due: str = "" comments: int = 0 checklist: tuple = (0, 0) CARDS = [ Card(1, "Research onboarding drop-off", "backlog", ["design"], ["ML"], "low", "Nov 12", 2), Card(2, "Dark mode for the billing page", "backlog", ["design"], [], "low"), Card(3, "Rate limit the public API", "backlog", ["backend"], ["JL"], "medium", "Nov 20"), Card(4, "Streaming answers in the support bot", "todo", ["ai", "backend"], ["KV", "IN"], "high", "Nov 3", 5, (1, 4)), Card(5, "Write the MCP setup guide", "todo", ["docs", "ai"], ["SD"], "medium", "Nov 6", 1), Card(6, "Crash when exporting large CSVs", "doing", ["bug"], ["JL"], "high", "Today", 8, (2, 3)), Card(7, "New pricing table", "doing", ["design"], ["ML", "WK"], "medium", "Oct 30", 3, (3, 5)), Card(8, "Migrate jobs to SQLModel", "doing", ["backend"], ["KV"], "medium", "Nov 1", 0, (4, 6)), Card(9, "Landing page hero", "done", ["design"], ["ML"], "medium", "", 6, (4, 4)), Card(10, "Fix login redirect loop", "done", ["bug"], ["IN"], "high", "", 2, (2, 2)), ] class Kanban(pz.App): title = "Board" size = (1200, 760) def __init__(self): self.cards = [Card(**vars(c)) for c in CARDS] self.query = "" self.mine = False self.selected = 6 def move(self, card, step): keys = [c[0] for c in COLUMNS] card.column = keys[max(0, min(len(keys) - 1, keys.index(card.column) + step))] self.selected = card.id def add(self, column): new_id = max(c.id for c in self.cards) + 1 self.cards.append(Card(new_id, "Untitled task", column, [], ["KV"], "low")) self.selected = new_id # -- view ------------------------------------------------------------------------------ def card(self, card): t = self.resolved_theme() icon, tint = PRIORITY[card.priority] done, total = card.checklist meta = [] if card.due: meta.append(pz.row([pz.icon("calendar", size=13, color=t.error if card.due == "Today" else t.icon_muted), ui.label(card.due, size="xs", color="error" if card.due == "Today" else "text_muted")], spacing=4, align="center")) if card.comments: meta.append(pz.row([pz.icon("message-square", size=13, color=t.icon_muted), ui.muted(str(card.comments), size="xs")], spacing=4, align="center")) if total: meta.append(pz.row([pz.icon("list-checks", size=13, color=t.success if done == total else t.icon_muted), ui.muted(f"{done}/{total}", size="xs")], spacing=4, align="center")) selected = card.id == self.selected body = pz.column([ pz.row([*[ui.badge(tag, tint=TAGS[tag]) for tag in card.tags], pz.hspace(), pz.icon(icon, size=14, color=t.color(tint))], spacing=6, align="center"), ui.label(card.title, weight="medium"), ui.progress(done, max=total, height=3, tint="success" if done == total else "accent") if total else None, pz.row([*meta, pz.hspace(), ui.facepile(card.people, size=22) if card.people else None], spacing=12, align="center"), pz.row([ui.icon_button("arrow-left", lambda: self.move(card, -1), size="xs", tooltip="Move left"), pz.hspace(), ui.icon_button("arrow-right", lambda: self.move(card, 1), size="xs", tooltip="Move right")]) if selected else None, ], spacing=10) return pz.container(body, padding=12, width="fill").style( background="elevated", radius=10, border=(1.5 if selected else 1, "accent" if selected else "border_variant"), shadow=("#0000001f", (0, 2), 6), ).on(click=lambda: setattr(self, "selected", card.id), cursor="pointer") def column(self, key, title, icon, tint): q = self.query.lower() cards = [c for c in self.cards if c.column == key and q in c.title.lower() and (not self.mine or "KV" in c.people)] header = pz.row([ pz.icon(icon, size=15, color=self.resolved_theme().color(tint)), ui.label(title, weight="semibold"), ui.count_badge(len(cards), tint="accent" if key == "doing" else "info"), pz.hspace(), ui.icon_button("plus", lambda: self.add(key), size="sm", tooltip="Add a card"), ui.icon_button("ellipsis", None, size="sm"), ], spacing=8, align="center") return pz.container( pz.column([header, pz.scrollable(pz.column([self.card(c) for c in cards], spacing=10), height="fill")], spacing=12, height="fill"), padding=12, width="fill", height="fill", ).style(background="panel", radius=12) def view(self): top = pz.row([ pz.column([ ui.breadcrumbs([("folder", "Acme"), "Product", "Sprint 42"]), pz.row([ui.heading("Sprint 42", level=2), ui.badge("Oct 21 – Nov 3", tint="info", icon="calendar")], spacing=10, align="center"), ], spacing=6), pz.hspace(), pz.container(ui.search_input(self.query, lambda v: setattr(self, "query", v), placeholder="Filter cards…"), width=220), ui.button("Only mine", lambda: setattr(self, "mine", not self.mine), icon="user", selected=self.mine, variant="ghost"), ui.facepile(["ML", "JL", "IN", "SD", "KV", "WK"], size=28, max_shown=5), ui.button("New task", lambda: self.add("todo"), icon="plus", variant="solid"), ], spacing=10, align="center") board = pz.row([self.column(*c) for c in COLUMNS], spacing=14, height="fill") return pz.column([top, board], spacing=20, padding=24, height="fill").style(background="background") if __name__ == "__main__": pz.run(Kanban) ``` #### Hazlo tuyo Cambia `theme` (o `self.theme = pz.themes.get("One Dark").with_accent("#e11d48")`), pon tus datos en lugar de los de ejemplo y separa piezas en [componentes](https://albakore.github.io/pyzed/es/guides/components/) cuando crezcan. Un agente de IA con el [servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/) puede leer este bloque con `read_example("blocks/kanban")` y adaptarlo por ti. ## Descargar Pyzed Design ### Descargar Pyzed Design > Descarga Pyzed Design, el diseñador visual gratuito de arrastrar y soltar para GUIs de escritorio en Python, como app para Windows, macOS y Linux. Source: https://albakore.github.io/pyzed/es/download/ **Pyzed Design** es el diseñador visual de Pyzed como app independiente: dibuja ventanas y componentes, escribe su código con autocompletado y diagnósticos, gestiona las dependencias de tu proyecto y ejecútalo todo con **F5**. Elige tu plataforma; seleccionamos la que estás usando. (Image: Pyzed Design con un formulario de inicio de sesión abierto) #### Cómo se instala Pyzed Design se instala con la herramienta habitual de cada sistema y luego se mantiene al día solo: | Sistema | Instalador | Dónde queda | | --- | --- | --- | | Windows | `pyzed-design-windows-x86_64-setup.exe` | `%LOCALAPPDATA%\Programs\Pyzed Design`, solo para tu usuario (sin permisos de administrador), con entrada en el menú Inicio, acceso directo opcional en el escritorio y los archivos `.pzform` asociados. Se desinstala desde **Configuración → Aplicaciones**. | | macOS | `pyzed-design-macos-arm64.dmg` (Intel: `-x86_64.dmg`) | Arrastra la app a **Aplicaciones**. | | Linux | `curl -fsSL https://albakore.github.io/pyzed/install.sh \| sh` | `~/.local/share/pyzed-design`, con el comando `pyzed-design` en `~/.local/bin` y una entrada en el menú de aplicaciones. | El script de Linux comprueba la descarga con el `SHA256SUMS` de la release y no necesita root. Ejecútalo otra vez para reinstalar, o con `--uninstall` para quitar la app (`curl -fsSL https://albakore.github.io/pyzed/install.sh | sh -s -- --uninstall`). `PYZED_VERSION=1.3.0` instala una versión concreta. Cada release trae también un **zip** portable por plataforma (`pyzed-design--.zip`): descomprímelo donde quieras y ejecútalo. Ese zip es el que descarga el actualizador de la app. Las versiones hasta la 1.2.0 solo tienen los zips. #### Qué incluye Cada instalador y cada zip llevan todo lo que necesita Pyzed Design, así que no hay que instalar nada más: - **Python** y **pyzed-ui**, de la misma versión que la release que descargas. - El binario de **uv**, que se usa para crear proyectos (`uv init`), añadir dependencias (`uv add`) y ejecutar tus formularios en el `.venv` de cada proyecto. Consulta [Proyectos](https://albakore.github.io/pyzed/es/guides/projects/). - Las fuentes IBM Plex y los iconos Lucide. Los servidores de lenguaje del editor de código (**ty** y **ruff**) se instalan en cada proyecto con `uv add --dev ty ruff`, desde la pestaña Proyecto; consulta el [editor de código](https://albakore.github.io/pyzed/es/guides/designer/#editor-de-código). #### Actualizaciones Pyzed Design se actualiza solo, así que nunca tienes que volver a descargarlo a mano. Busca en GitHub una versión nueva al abrirse (una vez al día) y cada pocas horas mientras está abierto (desactívalo en **Ayuda → Buscar actualizaciones automáticamente**, o búscala ahora con **Ayuda → Buscar actualizaciones**). La versión nueva se descarga en segundo plano, verificada con el SHA-256 de la release, y arriba a la derecha de la barra de título aparece un botón **Reiniciar para actualizar** con un parpadeo suave: cambia la app por la versión nueva. Con la búsqueda automática desactivada, el botón dice **Actualizar a X.Y.Z** y la descarga al pulsarlo. **Ayuda → Novedades** muestra las notas de la versión cuando quieras. Tus proyectos y tu configuración viven fuera de la app, así que se conservan. Hay dos canales: **estable** (por defecto) recibe las versiones finales y **preliminar** también las pre-releases (`X.Y.Z-rc.N`) para probar lo que viene. Cámbialo con **Ayuda → Recibir versiones preliminares**. Los instaladores dejan la app en una carpeta tuya (en Windows, Configuración → Aplicaciones muestra la versión nueva tras actualizar). Si la app no puede escribir en su propia carpeta, descarga el instalador o el zip nuevo desde esta página. Las [novedades](https://albakore.github.io/pyzed/es/changelog/) cuentan qué ha cambiado y [todas las versiones](https://github.com/albakore/pyzed/releases) están en GitHub. Más en [Proyectos → Actualizaciones](https://albakore.github.io/pyzed/es/guides/projects/#actualizaciones). #### Instalar con pip Si ya usas Python (3.10 o posterior), o tu plataforma no tiene descarga precompilada (por ejemplo un Mac con chip Intel o Linux en ARM), instala la librería y abre el diseñador desde ella: El extra `designer` añade los servidores de lenguaje ty y ruff; un simple `pip install pyzed-ui` también sirve. Dentro de un proyecto, `uv run pyzed design` abre el diseñador con el entorno de ese proyecto. Consulta la [instalación](https://albakore.github.io/pyzed/es/getting-started/installation/) para más detalles. #### Compilarlo tú mismo Las descargas las genera `scripts/build_designer.py` con PyInstaller, en cada sistema operativo: -.zip uv run --with pyinstaller --with uv python scripts/build_designer.py --installer # además el setup .exe (requiere Inno Setup 6) o el .dmg `} pip={` {venv} {activate} pip install pyzed-ui pyinstaller uv python scripts/build_designer.py # dist/Pyzed Design/ y dist/pyzed-design--.zip python scripts/build_designer.py --installer # además el setup .exe (requiere Inno Setup 6) o el .dmg `} python={` {py} -m pip install pyzed-ui pyinstaller uv {py} scripts/build_designer.py # dist/Pyzed Design/ y dist/pyzed-design--.zip {py} scripts/build_designer.py --installer # además el setup .exe (requiere Inno Setup 6) o el .dmg `} /> **Siguientes pasos:** ¿Es tu primera vez con el diseñador? Lee la [guía del diseñador visual](https://albakore.github.io/pyzed/es/guides/designer/) y luego crea un widget reutilizable con el [diseñador de componentes](https://albakore.github.io/pyzed/es/guides/component-designer/). ## Manual de Pyzed Design ### Pyzed Design > El manual de Pyzed Design, el diseñador visual de GUIs para Python: dibuja ventanas y componentes, diseña shaders, conecta eventos a Python, ejecútalos y distribúyelos. Source: https://albakore.github.io/pyzed/es/editor/ **Pyzed Design** es el editor visual de Pyzed, hecho con el propio Pyzed. Dibujas una ventana con el ratón, ajustas sus propiedades en una grilla, haces doble clic en un control para escribir el código Python de su evento y pulsas F5 para ejecutarla con toda su lógica. El mismo editor crea **componentes** reutilizables, **shaders** de GPU hechos de nodos, y edita el Python de todo tu proyecto. Se apoya en herramientas que mucha gente ya conoce: el diseñador de formularios de **Visual Basic** y **Qt Designer** (dibujar, ajustar propiedades, conectar eventos), las escenas y los visual shaders de **Godot**, el panel de detalles y el editor de materiales de **Unreal Engine**, y una interfaz limpia y cómoda con el teclado inspirada en el editor de código **Zed**. El objetivo es un framework de frontend para apps de escritorio que puedas usar sin escribir código, y ampliar con código cuando quieras. #### Abrirlo Descarga la [app independiente](https://albakore.github.io/pyzed/es/download/) (trae su propio Python y uv), o arráncalo desde un entorno de Python: **uv:** ```bash uv add "pyzed-ui[designer]" # en tu proyecto: Pyzed más los servidores de lenguaje ty y ruff uv run pyzed design ``` **pip:** ```bash pip install "pyzed-ui[designer]" pyzed design ``` **python:** ```bash title="macOS / Linux" python3 -m pip install "pyzed-ui[designer]" python3 -m pyzed design ``` ```powershell title="Windows (PowerShell)" py -m pip install "pyzed-ui[designer]" py -m pyzed design ``` | Comando | Qué abre | | --- | --- | | `pyzed design` | Dentro de un proyecto: su formulario principal (`[tool.pyzed] main`). Fuera: la ventana de bienvenida o un formulario vacío. | | `pyzed design login.pzform` | Ese formulario, o uno nuevo con ese nombre que se crea al guardar por primera vez. | | `pyzed design ui/rating.pzcomp` | Un [componente](https://albakore.github.io/pyzed/es/editor/component-designer/) (se crea al guardar si no existe). | | `pyzed design fx/fuego.pzshad` | Un [shader hecho de nodos](https://albakore.github.io/pyzed/es/editor/shader-view/) (se crea al guardar si no existe). | | `python -m pyzed.designer [archivo]` | Lo mismo, sin el comando `pyzed` en el `PATH`. | #### El flujo de trabajo en una imagen ```mermaid flowchart LR TB[Herramientas] -->|arrastrar o clic| CV[Lienzo] CV -->|seleccionar| PR[Propiedades] CV -->|doble clic| EV[Eventos] EV --> CE[Editor de código] CV -- Ctrl+S --> F[(login.pzform)] CE -- Ctrl+S --> P[(login.py)] F --> RUN["F5 · pyzed run"] P --> RUN RUN --> W[Ventana nativa] RUN -. prints, eventos, errores .-> CO[Consola] ``` 1. **Coloca** controles desde las herramientas: arrástralos al formulario, o haz clic en uno y luego clic (o arrastra un rectángulo) en el formulario. 2. **Ordénalos** con el ratón, el teclado, las guías inteligentes, las herramientas de alineación o un [contenedor de disposición](https://albakore.github.io/pyzed/es/editor/layouts/). 3. **Configúralos** en [Propiedades](https://albakore.github.io/pyzed/es/editor/properties/): texto, datos, estilo, anclajes. 4. **Escribe la lógica**: doble clic en un control abre el manejador de su evento principal en el [editor de código](https://albakore.github.io/pyzed/es/editor/code-editor/). 5. **Ejecuta** con F5. La [consola](https://albakore.github.io/pyzed/es/editor/console-and-running/) muestra prints, eventos y errores; guardar actualiza la ventana en ejecución. 6. **Reutiliza y distribuye**: convierte las piezas repetidas en [componentes](https://albakore.github.io/pyzed/es/editor/component-designer/) y empaqueta la app con `pyzed build` (ver [Distribuir](https://albakore.github.io/pyzed/es/packaging/)). #### Empieza aquí #### Las vistas #### Diseñar formularios #### Ejecutar, archivos, IA y referencia **Sobre las capturas:** Cada panel y cada vista tiene su página, escrita para la versión actual. Las capturas y grabaciones cortas se hicieron en Linux con el tema One Dark (y One Light cuando el sitio está en claro), así que los botones de la barra de título y las fuentes pueden verse un poco distintos en Windows o macOS. ### La interfaz > Un recorrido por cada parte de la ventana de Pyzed Design: la barra de título y sus menús, las pestañas y ventanas de documentos, los cuatro docks y sus paneles, la barra de herramientas, el lienzo, la barra de estado y las notificaciones. Source: https://albakore.github.io/pyzed/es/editor/interface/ Pyzed Design es una ventana dividida en unas pocas zonas, y cada zona tiene una sola tarea. Cuando conozcas sus nombres, el resto del manual se lee solo. La disposición toma ideas de editores que mucha gente ya conoce: los paneles y la barra de título se inspiran en el editor de código [Zed](https://zed.dev), el lienzo del formulario y la grilla de propiedades en Visual Basic y Qt Designer, y las ideas de nodos y escenas en Godot y Unreal Engine. ```mermaid flowchart TB T["① Barra de título: menú ☰ · proyecto / archivo · Ejecutar ▶ Detener · 🔍 Buscar · Ver · Ayuda · ⚙"] subgraph W[" "] direction LR L["② Dock izquierdo
Herramientas · Proyecto · Dependencias"] subgraph C["③ Centro"] direction TB D["Pestañas de documentos"] TB["Barra de herramientas"] F["Lienzo (o el grafo de un shader)"] end R["④ Dock derecho
Propiedades · Eventos · Agente"] end B["⑤ Dock inferior: Código · Consola"] S["⑥ Barra de estado"] T --> W --> B --> S ``` #### ① La barra de título La franja de arriba del todo. De izquierda a derecha (Ejecutar, Detener y el buscador quedan en el centro): | Elemento | Qué hace | | --- | --- | | **☰** | El menú principal: Nuevo, Abrir, Ir a archivo, Guardar, Ajustes, archivos recientes y los comandos de Ejecutar. | | **Entorno** | Aparece en WSL: la distribución de Linux donde corre el proyecto (*Ubuntu*...), como el indicador remoto de Zed. | | **Nombre del proyecto ⌄** | El proyecto activo (o *Sin proyecto*). Haz clic para abrir el **selector de proyectos**: los abiertos en esta ventana y los recientes; ver [Varios proyectos en una ventana](https://albakore.github.io/pyzed/es/editor/projects/#varios-proyectos-en-una-ventana). | | **Rama** / **worktree** | La rama de git del proyecto (clic para cambiar de rama) y, en un worktree enlazado, su nombre. | | **/ nombre de archivo ⌄** | El documento que está delante. Haz clic para abrir otro formulario, componente o shader. | | **●** | Aparece cuando el documento tiene cambios sin guardar. | | Insignia **Componente** / **Shader** | Indica en qué [vista](https://albakore.github.io/pyzed/es/editor/views/) estás cuando no es un formulario. | | **▶ Ejecutar** (F5) | Ejecuta el formulario, previsualiza el componente o abre el shader en su propia ventana. | | **■ Detener** (Mayús+F5) | Cierra lo que abrió Ejecutar. Desactivado si no hay nada en marcha. | | **🔍 Buscar** (Ctrl+Mayús+P) | Abre la [paleta de comandos](https://albakore.github.io/pyzed/es/editor/menus-and-commands/#paleta-de-comandos): comandos, paneles, ajustes del editor, atajos y archivos recientes. | | **Ver** (icono de paneles) | Docks, paneles, cuadrícula y guías, vistas previas con otros temas, idiomas y marcos de ventana. | | **Ayuda** (icono ?) | Atajos, la referencia integrada, la ventana de bienvenida, actualizaciones y Acerca de. | | **⚙** (Ctrl+,) | [Ajustes](https://albakore.github.io/pyzed/es/editor/settings/). | Cada entrada de los menús aparece con sus teclas en [Menús y comandos](https://albakore.github.io/pyzed/es/editor/menus-and-commands/). Todo lo que puedes pulsar en un menú está también en la **paleta de comandos** (el buscador, o Ctrl+Mayús+P): escribe unas letras de su nombre y pulsa Enter. ##### El marco de la ventana Por defecto Pyzed Design dibuja su **propio marco de ventana**: la barra de título también contiene los botones de minimizar, maximizar y cerrar, con el estilo de tu sistema, y los bordes de la ventana sirven para cambiar su tamaño. Arrastra la barra de título para mover la ventana; haz doble clic para maximizarla. Así la ventana es compacta e igual en todas las plataformas. **¿Prefieres el marco de tu sistema?:** Desactiva **Ajustes → Apariencia → Marco de ventana propio** para usar el marco de tu sistema operativo (se aplica la próxima vez que abras Pyzed Design). Tus apps pueden usar los mismos marcos: mira `ui.window_frame` y `Window.frame` en la [referencia de la API](https://albakore.github.io/pyzed/es/reference/ui/). Marco de ventana propio desactivado, la barra conserva los menús y Ejecutar, y tu sistema dibuja el marco y sus botones alrededor." /> #### ② ④ ⑤ Docks y paneles Los paneles viven en **docks** alrededor del centro: izquierdo, derecho, inferior y uno superior que está vacío hasta que mueves un panel allí. Cada dock muestra una pestaña a la vez. Cualquier sitio acepta cualquier pestaña: un panel puede ir junto a las pestañas de formularios en el centro (por ejemplo, el editor de código al lado de tus formularios), un formulario puede ir a un dock lateral, y cualquier pestaña puede flotar en una **ventana propia**, sola o apilada con otras. | Panel | Dock | Para qué sirve | Manual | | --- | --- | --- | --- | | **Herramientas** | Izquierdo | Todo lo que puedes colocar, sobre la **Estructura** (el árbol de lo colocado); los iconos van [coloreados por rol](https://albakore.github.io/pyzed/es/editor/toolbox-and-canvas/#colores-de-los-iconos). En un shader se convierte en la barra **Shading** y las variables del shader. | [Herramientas y lienzo](https://albakore.github.io/pyzed/es/editor/toolbox-and-canvas/) | | **Proyecto** | Izquierdo | Los archivos del proyecto en árbol, con búsqueda, estado de git y un menú contextual. | [Proyectos](https://albakore.github.io/pyzed/es/editor/projects/) | | **Dependencias** | Izquierdo | Paquetes, ruff y ty del proyecto (solo con un proyecto abierto). | [Proyectos](https://albakore.github.io/pyzed/es/editor/projects/) | | **Propiedades** | Derecho | Todo sobre la selección: texto, tamaño, colores, datos, comportamiento. | [Propiedades](https://albakore.github.io/pyzed/es/editor/properties/) | | **Eventos** | Derecho | Los eventos de la selección y el manejador Python conectado a cada uno. | [Eventos](https://albakore.github.io/pyzed/es/editor/events/) | | **Agente** | Derecho | Un agente de IA que crea formularios y componentes y escribe su código. | [Agentes de IA](https://albakore.github.io/pyzed/es/editor/ai-agents/) | | **Código** | Inferior | El editor de Python, mientras hay un manejador o un archivo abierto. | [Editor de código](https://albakore.github.io/pyzed/es/editor/code-editor/) | | **Consola** | Inferior | Lo que imprime un formulario en marcha, los eventos que maneja y sus errores. | [Ejecución y consola](https://albakore.github.io/pyzed/es/editor/console-and-running/) | Qué puedes hacer con ellos: - **Cambiar** con las pestañas de arriba de cada dock. - **Arrastrar** cualquier pestaña, de un panel o de un formulario: a lo largo de su fila para reordenarla, sobre otra fila para apilarla allí, sobre un dock o el centro para moverla allí, o fuera de la ventana para abrirla en una **ventana propia**. Mientras arrastras, una copia de la pestaña sigue al puntero, el sitio de destino se ilumina y, sobre una fila, una barra marca dónde caerá. Suelta más pestañas sobre una ventana para apilarlas y arrástralas de vuelta sobre la ventana principal para devolverlas. - **Mover** un panel desde el menú: el botón del dock al final de la fila de pestañas abre **Mover *panel* a** izquierdo, derecho, inferior, superior o **Centro**, o **Ventana nueva**. La pestaña de un panel fuera de los docks tiene una **×** que lo devuelve a su dock. En una ventana propia, el mismo botón ofrece **Devolver todo a los paneles laterales**, y cerrar la ventana hace lo mismo: los formularios vuelven a la ventana principal y los paneles a sus docks. - **Cambiar el tamaño** de un dock arrastrando su borde que da al lienzo. - **Ocultar** un dock con su **×**, o alternarlo: Ctrl+B (izquierdo), Ctrl+Alt+B (derecho), Ctrl+J (inferior). - **Guardar la distribución** con **Ver → Guardar la distribución como…** y cambiar entre distribuciones desde **Ver → Distribuciones**. Pyzed trae **Por defecto**, **Programar** (el código junto a las propiedades), **Concentración** (solo el lienzo y el dock derecho), **Eventos y código** y **Código con los formularios** (el editor de código como una pestaña junto a los formularios y los eventos a la izquierda); **Eliminar esta distribución** borra una tuya. - **Restablecer** todo con **Ver → Restablecer los paneles**. Dónde está cada panel, el orden de las pestañas, el tamaño de cada dock y las ventanas de paneles (con su tamaño y posición) se guardan con tus ajustes y se reabren la próxima vez. **Una disposición para escribir código:** En un portátil, elige **Ver → Distribuciones → Programar**, u oculta el dock izquierdo (Ctrl+B) y añade controles con Ctrl+Mayús+A. Con dos pantallas, arrastra **Código** y **Consola** a la segunda y guarda el resultado como una distribución propia. #### ③ El centro: pestañas, barra de herramientas y lienzo ##### Pestañas de documentos y ventanas Cada archivo abierto es una pestaña: `Formulario · login.pzform`, `Componente · rating.pzcomp`, un shader, un archivo Python, la referencia o las notas de la versión. El final de la fila de pestañas dice qué vista está delante (**Vista de formulario**, **Vista de componente**...). - **Haz clic** en una pestaña para cambiar; cada documento conserva su selección y su historial de deshacer. - **Arrastra** una pestaña por la fila para reordenarla, o sobre un dock para trabajar en ese formulario al lado del del centro. - **Arrastra una pestaña fuera** de la ventana para darle una ventana propia, con sus pestañas, barra de herramientas y lienzo. Suéltala sobre otra ventana de Pyzed Design para apilarla allí. **Ver → Mover la pestaña a una ventana nueva** y **Mover la pestaña a la ventana principal** hacen lo mismo con el teclado (útil en Wayland, donde las apps no pueden saber dónde están sus ventanas). - Los paneles también pueden compartir esta fila: suelta aquí la pestaña **Código** o **Consola** y quedará entre los formularios. - Los paneles siguen al formulario que usaste por última vez, esté donde esté. Al cerrar una ventana de documentos, sus pestañas vuelven a la ventana principal. ##### Barra de herramientas Bajo las pestañas de un formulario o componente: **Nuevo formulario**, **Abrir**, **Guardar**, **Nuevo componente**; **Deshacer** y **Rehacer**; las herramientas de **alinear**, **distribuir**, **mismo tamaño** y **centrar**; **Traer al frente**, **Enviar al fondo** y **Eliminar**; y al final **Cuadrícula**, **Ajustar**, **Guías inteligentes**, **Redimensionar el contenido con sus anclajes**, **Ajustar la selección a la cuadrícula** y **Editar el código** (F7). Las herramientas que no se aplican a la selección aparecen en gris, y cada tooltip muestra su atajo. Un shader tiene su propia barra; ver [la vista Shader](https://albakore.github.io/pyzed/es/shading/shader-designer/). ##### Lienzo El formulario tal como se verá al ejecutarse, dentro de un marco de ventana, sobre una cuadrícula de puntos. Haz clic para seleccionar, arrastra para mover, tira de las asas para cambiar el tamaño. Todo está en [Herramientas y lienzo](https://albakore.github.io/pyzed/es/editor/toolbox-and-canvas/). **Mira tu formulario en otros sistemas:** **Ver → Vista previa del marco de ventana…** dibuja el formulario dentro del marco de macOS, Windows 11, GNOME, Ubuntu o KDE Plasma, para comprobar cómo se verá allí sin salir de tu equipo. No cambia nada del formulario. #### ⑥ La barra de estado La barra de abajo te dice dónde estás y alterna algunas cosas con un clic: | Elemento | Significado | | --- | --- | | `120, 48` | Dónde está el puntero en el formulario, en píxeles. | | `sign_in 344×32 en 48, 304` | El control seleccionado, su tamaño y posición (o los del formulario, o cuántos hay seleccionados). | | **Colocando Botón…** | Aparece mientras colocas un control desde las herramientas. | | Tema / idioma | Aparece mientras previsualizas otro tema o idioma; haz clic para cambiarlo. | | **macOS**, **Windows 11**... | El marco de ventana que muestra el lienzo; haz clic para elegir otro. | | **12 controles** | Cuántos controles tiene el documento. | | **Cuadrícula 8px** · **Ajustar** | Haz clic para activar o desactivar la cuadrícula y el ajuste. | | `Ln 12, Col 8` · **ty** · **ruff** | Mientras programas: el cursor y el estado de los servidores de lenguaje. | | **Actualizar a X.Y.Z** | Hay una versión nueva lista; ver [Actualizaciones](https://albakore.github.io/pyzed/es/download/#actualizaciones). | Ocúltala con **Ver → Barra de estado**. sign_in seleccionado." plain /> #### La ventana de bienvenida Cuando Pyzed Design arranca sin un archivo, te recibe la **ventana de bienvenida**: crea o abre un proyecto, vuelve a uno reciente o crea un proyecto a partir de un ejemplo (apps, juegos, demos, bloques e integraciones). Vuelve a abrirla desde **Ayuda → Bienvenida**. Todo sobre ella está en [Bienvenida y ejemplos](https://albakore.github.io/pyzed/es/editor/examples/). #### Notificaciones Mensajes cortos aparecen en una esquina durante unos segundos: guardado, copiado, un cable que no se puede conectar, un comando que falló... Haz clic en la **×** para cerrar uno; se muestran como mucho cuatro a la vez. #### Abrir páginas e iniciar sesión Los enlaces del editor (Ayuda → Documentación, la referencia, las páginas de inicio de sesión de los agentes de IA) se abren en tu navegador predeterminado. Cuando Pyzed Design se ejecuta dentro de **WSL** en Windows, se abren en el navegador de Windows, sin que aparezca una ventana de consola, salvo que elijas otro con la variable `BROWSER`. #### Siguiente ### Bienvenida y ejemplos > La ventana de bienvenida de Pyzed Design: empieza o abre un proyecto, vuelve a uno reciente o crea un proyecto desde uno de los ejemplos incluidos, cada uno con miniatura, título y descripción. Source: https://albakore.github.io/pyzed/es/editor/examples/ Cuando Pyzed Design se inicia sin un archivo, se abre la **ventana de bienvenida**. A la izquierda puedes empezar un proyecto nuevo, abrir uno, crear un formulario o leer la documentación, y volver a un proyecto reciente. A la derecha está la **galería de proyectos de ejemplo**: cada ejemplo tiene una miniatura, un título y una descripción. (Image: La ventana de bienvenida con la galería de proyectos de ejemplo) Ábrela de nuevo cuando quieras desde **Ayuda → Bienvenida**, o **Ayuda → Proyectos de ejemplo…** para ver todos los ejemplos. Desmarca **Mostrar al iniciar** (o desactiva **Ventana de bienvenida** en [Ajustes](https://albakore.github.io/pyzed/es/editor/settings/)) para empezar directamente con un formulario vacío. #### La galería Los botones de encima de la galería filtran los ejemplos por categoría (cada uno dice cuántos tiene) y el buscador los filtra por título y descripción. **Todos** muestra cada categoría bajo su propio título: | Categoría | Ejemplos | | --- | --- | | **Apps** | Contador, Lista de tareas, Tareas (Iced), Pomodoro, Visor de Markdown, Juego de la vida, Reloj analógico, Cronómetro, Código QR, Selector de color, Visor de imágenes, Varias ventanas, Marco de ventana propio, Galería de componentes, Mini editor, Editor estilo Zed. | | **Tutorial** | Los cinco pasos del tutorial (Hola, Estado, Diseño, Trabajo asíncrono, App de notas), Diseño básico, Listas y tablas, Navegación, Hablar con el usuario, Selector de tema, Contactos con SQLite. | | **Audio** | Sintetizador, Caja de ritmos, Rack modular, Visualizador de música, Controlador MIDI, Webcam. | | **Juegos** | Snake, Breakout, Neon Invaders, Rock Storm, Pixel Run, 2048, Coin Chase, Jump Lab, Walker, Orb Rush, Target Rush, Dodge. | | **Animación** | Animaciones, Efectos, Línea de tiempo. | | **Estilo** | Estilos, Diseño estilo web, Laboratorio de temas y los temas De marca, Oscuro de cristal, Pastel, Terminal retro, Alto contraste y Dos temas. | | **Shading** | Galería de shaders, Shader de aurora. | | **Vitrina** | Panel de cristal, HUD de juego, Menú principal de juego, Inventario. | | **Bloques** | Panel de control, Tablero Kanban, Reproductor de música, Chat, Ajustes, Tabla de datos, Diseño de IDE, Pantalla de inicio de sesión, Asistente de bienvenida. | | **Patrones** | Servicios (temporizador, trabajo en segundo plano, proceso, portapapeles, ajustes), Componentes propios, Paneles y cuadrículas, Paneles redimensionables, Menú contextual, Diálogos e idiomas, Trabajo en segundo plano, Progreso de descargas, Datos en vivo. | | **IA** | Agente de Pyzed (un agente que usa herramientas, con un modelo de demostración que no necesita clave), Chat de IA. | | **Diseñador** | Formularios hechos en Pyzed Design: Inicio de sesión, Lanzador de tareas (los componentes no visuales), Explorador de proyecto (árbol, ruta de navegación, pestañas, tabla ordenable y otros controles de la caja de herramientas), Mini IDE (un Dock con paneles en cada lado), Tarjetas de componentes, Registro con datos enlazados, Formulario sobre un shader. | | **Integraciones** | Héroes con SQLModel, Inventario con SQLModel, Registro con Pydantic, PyPI con httpx, Descargas con httpx, Contactos con SQLAlchemy, Inicio rápido de SQLAlchemy, Diario con SQLite asíncrono, Héroes con FastAPI. | (Image: La categoría Audio de la galería: el sintetizador, la caja de ritmos, el rack modular, el visualizador, el controlador MIDI y la webcam) (Image: La categoría Juegos de la galería) (Image: La categoría Vitrina de la galería: el panel de cristal, el HUD de juego, el menú principal y el inventario) #### Crear un proyecto desde un ejemplo Haz clic en un ejemplo para darle nombre al proyecto nuevo y elegir dónde va (por defecto `~/Pyzed Projects/`; **Examinar…** elige otra carpeta). **Crear proyecto** entonces: 1. copia el ejemplo en la carpeta como un [proyecto de uv](https://albakore.github.io/pyzed/es/editor/projects/) normal, con `pyzed-ui` y los paquetes que el ejemplo necesita en `pyproject.toml`; 2. lo abre en el panel Proyecto, con su script (o, en el ejemplo del Diseñador, su formulario principal) delante; 3. instala sus paquetes con `uv sync` en segundo plano. (Image: Dar nombre al proyecto creado desde un ejemplo) La copia es tuya: cámbiala a tu gusto, el ejemplo original no se toca. #### Ejecutarlo Los proyectos creados desde un ejemplo de código tienen un `script` en `[tool.pyzed]`: ```toml [tool.pyzed] ui = "ui" script = "snake.py" ``` Cuando no hay un formulario abierto, **Ejecutar** (F5) inicia ese script con `uv run` y su salida va a la [consola](https://albakore.github.io/pyzed/es/editor/console-and-running/). **Ejecutar → Ejecutar el script del proyecto** hace lo mismo en cualquier momento. Los archivos del proyecto abiertos con cambios sin guardar se guardan antes. #### De dónde salen los ejemplos La app independiente **Pyzed Design** lleva dentro todos los ejemplos y sus miniaturas (unos 5 MB), así que la galería funciona sin conexión. Un diseñador instalado con pip no los incluye: la galería ofrece **Descargar ejemplos**, que los baja una vez desde la release de la versión instalada. Crear el entorno del proyecto con `uv sync` necesita conexión la primera vez, para descargar Python y los paquetes. ### Proyectos > Crea y abre proyectos de uv en Pyzed Design, busca archivos, gestiona dependencias, ejecuta ruff y ty y crea el repositorio git desde el editor. Source: https://albakore.github.io/pyzed/es/editor/projects/ Una app de Pyzed es un proyecto de [uv](https://docs.astral.sh/uv/) normal: la interfaz vive en `ui/`, el backend en `src/` y uv gestiona las dependencias. El panel **Proyecto** (dock izquierdo, junto a **Herramientas**) muestra los archivos del proyecto, como el explorador de un editor de código, y el panel **Dependencias** de al lado gestiona paquetes, herramientas y comprobaciones, todo sin salir del editor. El contexto, la estructura de un proyecto y los comandos `pyzed` equivalentes están en [Proyectos con uv](https://albakore.github.io/pyzed/es/guides/projects/). (Image: El panel Proyecto: los archivos y carpetas del proyecto) #### Cómo encuentra el editor tu proyecto Al arrancar, el editor busca un proyecto de Pyzed (un `pyproject.toml` con una tabla `[tool.pyzed]`) en el archivo que abriste o por encima, o por encima de la carpeta actual. Si lo encuentra: - **lo abre** en el panel Proyecto, convierte su carpeta en la de trabajo y lo recuerda entre los proyectos recientes; - **lo activa**, para que formularios y manejadores puedan importar los módulos de `ui/`, el paquete de `src/` y cualquier dependencia de su `.venv`; - **descubre sus componentes**: cada `.pzcomp` del proyecto se suma a la caja de herramientas; - **comprueba sus herramientas** en segundo plano (uv, git, ruff y ty, con sus versiones); - **prepara su entorno**, como Zed al abrir un proyecto: si falta uv lo instala con su instalador oficial, y si el proyecto todavía no tiene `.venv` ejecuta `uv sync`, que también instala ruff y ty. Una notificación avisa cuando el proyecto está listo. La barra de título muestra entonces el nombre del proyecto, su rama de git y, en un [worktree de git](https://git-scm.com/docs/git-worktree) enlazado, el nombre del worktree, cada uno por separado, como en Zed. #### Varios proyectos en una ventana Haz clic en el nombre del proyecto en la barra de título (o pulsa Ctrl+Alt+O) para abrir el **selector de proyectos**: (Image: El selector de proyectos abierto bajo el nombre del proyecto, con los proyectos de esta ventana y los recientes) - **Esta ventana** lista los proyectos que abriste en esta ventana; el activo lleva una marca. Haz clic en otro para activarlo: el panel Proyecto, Ejecutar, las comprobaciones y el agente de IA pasan a trabajar sobre él. La **×** de una fila saca ese proyecto de la ventana. - **Proyectos recientes** lista los que abriste antes. Haz clic en uno para abrirlo aquí. - Escribe para filtrar las dos listas, muévete con las flechas y pulsa Enter para abrir el resaltado. - **Abrir carpeta local…** y **Nuevo proyecto…**, abajo, abren esos diálogos. Los formularios siempre pertenecen al proyecto activo: un formulario nuevo se guarda por defecto en su carpeta `ui/`. ##### El archivo de inicio Cada proyecto tiene un archivo desde el que arranca la aplicación: su formulario principal (`main` en `[tool.pyzed]`) o, si lo tiene, su `script`. El panel Proyecto lo muestra bajo el nombre del proyecto (**▶ Inicia desde** `ui/main.pzform`) y lo marca con un ▶ en el árbol. Para cambiarlo: - haz clic en ese nombre: un selector lista primero los formularios del proyecto y luego sus scripts de Python (los archivos de código de formularios y componentes quedan fuera). Escribe para filtrar, haz clic en uno o pulsa Enter; - o haz clic derecho en un formulario o archivo `.py` del árbol y elige **Usar como archivo de inicio**; - o, con un formulario abierto, usa **Usar este formulario como archivo de inicio** desde el menú Ejecutar o la paleta de comandos (la paleta también tiene **Elegir el archivo de inicio…**). `pyproject.toml` se actualiza solo: un formulario pasa a ser `main` (y se quita el `script`), un script pasa a ser `script`. **Ejecutar** (F5) sin nada abierto inicia la app desde ese archivo (un formulario se abre como ventana principal, un script se ejecuta con `uv run`), y lo mismo hacen `pyzed run` y `pyzed build`. ##### Clonar un repositorio **Clonar repositorio…** (en el panel Proyecto vacío, el selector de proyectos y la ventana de bienvenida) pide una URL de git y una carpeta, lo clona con git y lo abre, preparando su entorno como cualquier proyecto que abras. :::tip[Ramas] Haz clic en la rama, junto al nombre del proyecto, para cambiar a otra rama local (`git switch`). ::: #### Entornos y WSL El editor sabe dónde vive cada proyecto. En WSL la barra de título empieza con la distribución de Linux (*Ubuntu*, por ejemplo), y los proyectos recientes que viven en una distribución de WSL muestran su nombre al lado. uv, ruff, ty y git se ejecutan entonces en Linux, las páginas web se abren en el navegador de Windows, y el editor y las ventanas de tus formularios se abren como ventanas nativas de Windows (ver [WSL: ventanas nativas de Windows](https://albakore.github.io/pyzed/es/getting-started/installation/#wsl-ventanas-nativas-de-windows)). Pyzed Design **en Windows** también puede abrir un proyecto que vive en WSL, como los proyectos WSL de Zed: abre su carpeta a través de `\\wsl.localhost\\...` (o `\\wsl$\...`). La barra de título muestra la distribución, y uv, git, ruff, ty y Ejecutar pasan por `wsl.exe` a Linux, con rutas de Linux. Si la distribución no tiene uv, al abrir el proyecto se instala allí, y el primer `uv sync` crea un `.venv` de Linux. El agente de IA recibe lo mismo con cada mensaje: el proyecto activo, su entorno y su rama, y los demás proyectos abiertos en la ventana, para trabajar sobre el correcto. #### Sin proyecto abierto Sin proyecto, el panel ofrece **New project** y **Open…**, y lista tus proyectos recientes (clic en uno para abrirlo). (Image: El diálogo New project) ##### Nuevo proyecto | Campo | Qué hace | | --- | --- | | **Name** | El nombre del proyecto y de su carpeta. | | **Location** | La carpeta donde se crea; el diálogo muestra la ruta completa que va a crear. **Browse…** abre el explorador de carpetas: ve hasta una carpeta y pulsa **Choose this folder**. | | **Backend in src/** | `uv init --package`: un paquete `src//` que se puede importar desde la UI y desde los tests. Apagado da una estructura plana (`uv init --app`). | | **Git repository** | Ejecuta `git init` y escribe un `.gitignore`. | **Create** ejecuta uv en segundo plano y luego abre el proyecto nuevo y su formulario principal. La carpeta de destino tiene que estar vacía o ser ya un proyecto. ##### Abrir un proyecto **Open…** muestra un explorador de carpetas en lugar de un campo de texto: (Image: El diálogo Open project: lugares, proyectos recientes y las carpetas de la actual) - La barra de arriba va a la **carpeta superior** (flecha) o al **inicio**, y cada parte de la ruta se puede pulsar. El ojo muestra u oculta las carpetas ocultas. - El campo de ruta de debajo acepta una carpeta escrita o pegada; Enter va a ella. - A la izquierda, los **lugares** habituales (inicio, escritorio, documentos, descargas, los discos) y tus proyectos **recientes** (haz clic en uno para abrirlo). - A la derecha, las carpetas de la actual; haz clic en una para entrar. Las que son proyectos de uv (tienen un `pyproject.toml`) llevan la etiqueta **project** y su propio botón **Open**. **Open**, abajo, abre la carpeta en la que estás cuando es un proyecto. Si el proyecto tiene formulario principal, se abre en una pestaña. #### El panel del proyecto ##### Cabecera El nombre, la versión y la carpeta del proyecto. A la derecha, la **rama de git** actual, o **Create repository** cuando la carpeta todavía no es un repositorio (ejecuta `git init` y escribe un `.gitignore`), y un botón de carpeta que abre otro proyecto. Mientras se ejecuta un comando, una barra con un indicador dice qué está pasando (`Adding httpx`, `Syncing the environment`...). Los comandos corren en otro hilo, así que puedes seguir diseñando mientras uv descarga paquetes. ##### Archivos Debajo de la cabecera, los archivos y carpetas del proyecto en árbol. Las carpetas se abren y cierran con un clic (`ui/` y `src/` empiezan abiertas); los archivos se colorean según su estado en git (modificado, nuevo, borrado, conflicto) y las carpetas toman el color de lo que cambió dentro. Los entornos virtuales, las cachés y la salida de compilación (`.venv`, `__pycache__`, `build`, `dist`...) no se muestran; los archivos ocultos como `.gitignore`, sí. Al hacer clic en un archivo se abre donde corresponde: | Archivo | Se abre | | --- | --- | | `.pzform`, `.pzcomp` | En una pestaña del diseñador. | | El `.py` de un formulario o componente | Su formulario, con el archivo de código entero en el [panel de código](https://albakore.github.io/pyzed/es/editor/code-editor/). | | Cualquier otro archivo de texto (`.py`, `.toml`, `.md`, `.json`...) | En una pestaña del panel de código, para editarlo ahí mismo. | | Imágenes y otros archivos binarios | Con la app del sistema para ese tipo de archivo. | (Image: Un archivo del proyecto abierto en el panel de código, junto al README) Cada archivo abierto tiene una pestaña en el panel de código, después del código del formulario si está abierto. El editor resalta la sintaxis según el lenguaje del archivo y tiene buscar y reemplazar (Ctrl+F), ir a una línea (Ctrl+G) y todos los atajos de edición; el autocompletado y los diagnósticos son solo para el código del formulario. Un punto en la pestaña indica cambios sin guardar: **Save** o Ctrl+S (con el foco en el archivo) lo guarda, y al cerrarlo pregunta antes. Si renombras o borras un archivo en el árbol, su pestaña se actualiza. La barra sobre el árbol tiene un **buscador** de archivos y carpetas (busca las letras en orden, como el buscador de archivos de los editores de código, y el nombre del archivo cuenta más que las carpetas: `crt` encuentra `ui/cart.pzform`), **New file**, **New folder** (en la carpeta seleccionada), **Collapse all** y **Refresh**. El árbol se recarga solo al guardar y después de los comandos de uv. **Clic derecho** sobre un archivo o carpeta (o sobre el espacio vacío bajo el árbol, para la carpeta del proyecto) abre su menú, con las operaciones de archivo habituales de un editor de código: (Image: El menú de clic derecho de un archivo en la pestaña Proyecto) | Opción | Teclas | Qué hace | | --- | --- | --- | | **New File** / **New Folder** | Ctrl+N / Alt+N | Crea uno en esa carpeta (un archivo nuevo se abre al momento). | | **Reveal in File Manager** | Ctrl+Alt+R | Lo muestra en Archivos, Finder o el Explorador. | | **Open in Default App** | Ctrl+Shift+Enter | Abre el archivo con la app del sistema para su tipo. | | **Open in the code editor** | | Abre cualquier archivo de texto como texto (también el `.py` de un formulario por separado). | | **Open in Terminal** | | Abre la terminal del sistema en esa carpeta (en Linux, primero `$TERMINAL`). | | **Cut** / **Copy** / **Paste** | Ctrl+X / C / V | Lo mueve o lo copia a la carpeta donde pegas; si el nombre está ocupado, añade ` copy`. | | **Duplicate** | Ctrl+D | Lo copia a su lado y pide el nombre de la copia. | | **Undo** / **Redo** | Ctrl+Z / Ctrl+Shift+Z | Deshace o rehace el último archivo o carpeta nuevo, renombrado, movido o copiado aquí. | | **Copy Path** / **Copy Relative Path** | Alt+Shift+C / Ctrl+Alt+Shift+C | Copia la ruta completa o la ruta desde la carpeta del proyecto. | | **Add to .gitignore** / **Add to .git/info/exclude** | | Lo ignora en git, para todos o solo en tu copia. | | **Open File Permalink** / **Copy File Permalink** | | El enlace en el commit actual de GitHub, GitLab u otro remoto (solo si hay remoto). | | **Rename** | F2 | Lo renombra. | | **Trash** | Supr | Lo mueve a la papelera del sistema, desde donde se puede recuperar. | | **Delete** | Ctrl+Supr | Lo borra para siempre después de preguntar (una carpeta, con todo lo que tiene). | Las teclas funcionan después de hacer clic en el árbol, junto con ↑ ↓ para mover la selección, ← → para cerrar y abrir carpetas y Enter para abrir el archivo seleccionado. Un archivo abierto en el panel de código sigue sus cambios de nombre y de carpeta, y se cierra al borrarlo (salvo que tenga cambios sin guardar). ##### Ir a un archivo Ctrl+P (o **File → Go to file…**) abre una búsqueda sobre todos los archivos y carpetas del proyecto. Escribe parte de un nombre y pulsa Enter para el mejor resultado, o haz clic en uno; una carpeta se muestra en el panel Proyecto. #### El panel Dependencias Con un proyecto abierto, el dock izquierdo suma un panel **Dependencias** junto a **Proyecto** (si los tres títulos no caben, los paneles que no se muestran se quedan solo con su icono). (Image: El panel Dependencias: paquetes, herramientas y la salida de ruff) ##### Dependencias | Control | Ejecuta | | --- | --- | | Campo **Add package** + **+** (o Enter) | `uv add `. Escribe varios separados por espacios, o una restricción como `httpx>=0.28`. Marca **dev** para el grupo dev. | | **Lápiz** en una fila | Abre **Change**: escribe un requisito nuevo (`httpx==0.27.2`, `httpx>=0.28,<1`) y aplícalo. | | **Flecha arriba** en una fila | Actualiza ese paquete dentro de su restricción. | | **Papelera** en una fila | `uv remove` del grupo en el que está. | | **Recargar** en la cabecera de la sección | `uv sync`: hace que `.venv` coincida con `uv.lock`. | | **Lupa** en la cabecera de la sección | **Check for updates**: busca versiones nuevas; las filas que tienen una muestran `↑versión` en verde (pasa el ratón para ver el cambio). | | **Flecha arriba** en la cabecera de la sección | Actualiza todos los paquetes. | Las dependencias de otros grupos muestran su grupo como insignia. Cada fila muestra la versión instalada en `.venv`. Se pone amarilla cuando no es la que dice `uv.lock` (pasa el ratón para ver las dos): pulsa **Recargar** (`uv sync`). El panel vuelve a leer `pyproject.toml`, `uv.lock` y `.venv` solo cuando otra cosa los cambia, como `uv add` en una terminal o el agente. ##### Avisos Cuando un proyecto no puede ejecutarse como espera el editor, un recuadro amarillo arriba de los paneles **Proyecto** y **Dependencias** dice por qué, y al abrir el proyecto también sale como notificación: - **El proyecto usa pyzed-ui 1.2.0** y el editor es más nuevo: los formularios que usan controles o propiedades añadidos después no funcionan. **Actualizar pyzed-ui** sube su versión a la del editor (`uv add "pyzed-ui>="`, que bloquea y sincroniza); si esa versión aún no está publicada, lo actualiza hasta la última de PyPI. - **pyzed-ui no es una dependencia** de un proyecto de Pyzed: el mismo botón lo añade. - **El entorno no está creado** o hay paquetes **sin instalar como dice uv.lock**: **Sincronizar** ejecuta `uv sync`. La **×** oculta el recuadro hasta que se vuelva a abrir el proyecto. Un proyecto que toma `pyzed-ui` de una copia local (`[tool.uv.sources]`) no se compara con la versión del editor. ##### Herramientas uv, git, ruff y ty con la versión encontrada, o una insignia roja **missing**. Debajo: - **Lint** ejecuta `ruff check` en el proyecto. - **Format** ejecuta `ruff format`. - **Types** ejecuta `ty check`. La sección **Output** del final muestra el comando que se ejecutó (`$ ruff check`) y las últimas líneas de su salida, en rojo si falló. Un comando que falla muestra además una notificación con su última línea. :::tip Ruff lee sus reglas de `[tool.ruff]` en `pyproject.toml`, así que **Lint** y los avisos en línea del editor de código siguen la misma configuración. ::: #### Ejecutar formularios de un proyecto Cuando el editor corre dentro del entorno del propio proyecto (lo abriste con `uv run pyzed design`), **Ejecutar** (F5) ejecuta el formulario en el proceso del editor. Si no (la app independiente, o `pyzed design` instalado con `uv tool`), **Ejecutar** guarda el formulario y lo arranca con `uv run pyzed run --reload` en el proyecto, así tiene todas las dependencias de `.venv` y se actualiza al guardar. En los dos casos su salida llega a la [consola](https://albakore.github.io/pyzed/es/editor/console-and-running/). #### Empaquetar la app El panel Proyecto no empaqueta apps; usa la línea de comandos en la carpeta del proyecto: ```bash uv run pyzed build # una carpeta con el ejecutable para esta plataforma uv run pyzed build --onefile # un único archivo uv run pyzed build --workflow # un workflow de GitHub Actions para Linux, Windows y macOS ``` Ver [Empaquetar y distribuir](https://albakore.github.io/pyzed/es/packaging/build/). #### La app independiente **Pyzed Design** también se publica como app independiente para Linux, Windows y macOS: el editor compilado con su propio Python y el binario de `uv`, sin nada más que instalar. Los proyectos que crea son proyectos de uv normales con su propio `.venv`, y sus formularios siempre se ejecutan con `uv run`. :::tip Si el autocompletado no conoce un paquete que acabas de añadir, pulsa **Restart servers** en los [ajustes del editor](https://albakore.github.io/pyzed/es/editor/code-editor/#ajustes-del-editor) cuando termine la sincronización: los servidores de lenguaje leen el `.venv` del proyecto al arrancar. ::: ### Vistas y documentos > Los cuatro tipos de documento de Pyzed Design y la vista en que se abre cada uno: la vista de formulario para ventanas, la de componente para piezas reutilizables, la vista Shader para efectos de GPU y el editor de código para Python. Source: https://albakore.github.io/pyzed/es/editor/views/ Pyzed Design edita cuatro tipos de documento. Cada uno se abre en su propia **vista**: la misma ventana, pero el dock izquierdo, la barra de herramientas, el centro y el panel Propiedades cambian según lo que estés creando. La fila de pestañas dice qué vista está delante, y la barra de título muestra una insignia **Componente** o **Shader** cuando no es un formulario. | Documento | Archivo | Se abre en | Piénsalo como | | --- | --- | --- | --- | | **Formulario** | `login.pzform` + `login.py` | Vista de formulario | Una ventana de tu app, como un formulario de Visual Basic o un `.ui` de Qt Designer. | | **Componente** | `rating.pzcomp` + `rating.py` | Vista de componente | Una pieza reutilizable que colocas en formularios, como una escena de Godot o un widget blueprint de Unreal. | | **Shader** | `fire.pzshad` | Vista Shader | Un efecto de GPU hecho de nodos, como los visual shaders de Godot o el editor de materiales de Unreal. | | **Código y texto** | `*.py`, `*.toml`, `*.md`... | Editor de código | El Python detrás de todo, con autocompletado y diagnósticos. | Todos los archivos son texto plano (JSON o Python), así que se comparan bien en git y puedes abrirlos en cualquier otro editor. ```mermaid flowchart LR N["Nuevo (Ctrl+N)"] --> F[Formulario] & C[Componente] & S[Shader] F -->|"colócalo"| C F -->|"control Shader"| S C -->|"control Shader"| S F & C -->|"doble clic en un control"| K[Editor de código] ``` #### Vista de formulario **Para:** las ventanas de tu app: un inicio de sesión, una página de ajustes, un panel, un diálogo. - **Dock izquierdo:** las **Herramientas** con todos los controles y la **Estructura** del formulario. - **Centro:** el formulario dibujado en un marco de ventana, con cuadrícula de puntos, asas y guías inteligentes. - **Dock derecho:** **Propiedades** y **Eventos** de la selección (o del formulario si no hay nada seleccionado). - **Ejecutar** (F5) abre el formulario como una ventana real con sus manejadores Python. Lee [La vista de formulario](https://albakore.github.io/pyzed/es/editor/form-view/) para un primer formulario guiado. #### Vista de componente **Para:** piezas que usas muchas veces: una valoración con estrellas, una tarjeta de perfil, una ficha de producto. Un componente no tiene ventana; vive dentro de formularios y de otros componentes. - Las mismas herramientas y lienzo que un formulario, pero el lienzo muestra un **marco de acento con el nombre de la clase** en lugar de una ventana, y la barra de título una insignia **Componente**. - Sin nada seleccionado, **Propiedades** muestra el **panel del componente**: el nombre de la clase, cómo aparece en las herramientas, sus **propiedades** tipadas y sus **métodos**. Sus **eventos personalizados** están en la pestaña **Eventos**, debajo de los incluidos. - **Ejecutar** previsualiza el componente solo en una ventana. - Haz doble clic en un componente colocado en un formulario para abrirlo en su propia pestaña. Todo está en [La vista de componente](https://albakore.github.io/pyzed/es/editor/component-designer/). #### Vista Shader **Para:** fondos animados, brillos, transiciones y gráficos de juegos dibujados por la tarjeta gráfica. - **Dock izquierdo:** la barra **Shading** (todos los nodos, por categoría) y la **Estructura** de variables, valores con nombre y nodos. - **Centro:** tres pestañas: **Grafo** (los nodos), **Vista previa** (el resultado en grande, con un control por variable) y **WGSL** (el código generado, solo lectura). - **Propiedades:** los ajustes y entradas del nodo seleccionado, de una variable o del propio shader. - **Ejecutar** abre el shader en una ventana que sigue tus cambios. Empieza por [La vista Shader](https://albakore.github.io/pyzed/es/editor/shader-view/) de este manual, o ve directamente a la [sección Shading](https://albakore.github.io/pyzed/es/shading/). #### Editor de código **Para:** el Python de cada formulario y componente (sus manejadores de eventos y métodos) y cualquier archivo de texto del proyecto. - Se abre en el **dock inferior** cuando haces doble clic en un control o un evento, pulsas F7 para el archivo entero o haces clic en un archivo del panel Proyecto. Cada archivo es una pestaña del panel Código. - Autocompletado, diagnósticos, code lens, plegado, minimapa, cursores múltiples, buscar y reemplazar, y atajos de teclado que puedes cambiar. Ver [Editor de código](https://albakore.github.io/pyzed/es/editor/code-editor/). #### Cambiar de vista - Cada documento abierto es una pestaña; haz clic para cambiar. Cada uno conserva su selección y su historial de deshacer. - Abrir un archivo que ya está abierto cambia a su pestaña. - Arrastra una pestaña fuera de la ventana para trabajar con dos documentos lado a lado (ver [La interfaz](https://albakore.github.io/pyzed/es/editor/interface/#pestañas-de-documentos-y-ventanas)). **Código con un error de sintaxis:** Al salir de un formulario o componente se aplica el código de sus manejadores. Si el código no se puede analizar, el cambio espera a que lo corrijas, y una notificación explica por qué: no se pierde nada de lo que escribiste. #### Desde la línea de comandos Pasa un archivo para abrirlo en la vista adecuada (se crea al guardar por primera vez si no existe): **uv:** ```bash uv run pyzed design ui/login.pzform uv run pyzed design ui/rating.pzcomp uv run pyzed design shaders/fire.pzshad ``` **pip:** ```bash pyzed design ui/login.pzform pyzed design ui/rating.pzcomp pyzed design shaders/fire.pzshad ``` **python:** ```bash title="macOS / Linux" python3 -m pyzed design ui/login.pzform ``` ```powershell title="Windows (PowerShell)" py -m pyzed design ui\login.pzform ``` ### La vista de formulario > Diseña una ventana de tu app en la vista de formulario de Pyzed Design: coloca controles, ajusta sus propiedades, conecta un evento a Python y ejecútala, con cada parte de la vista explicada. Source: https://albakore.github.io/pyzed/es/editor/form-view/ Un **formulario** es una ventana de tu app. La **vista de formulario** es donde la dibujas: eliges controles de una caja, los sueltas en la ventana, cambias su aspecto en una lista de propiedades y escribes unas líneas de Python para lo que pasa cuando el usuario hace clic o escribe. Si usaste Visual Basic, el diseñador de WinForms o Qt Designer, ya conoces la idea; si no, el recorrido de abajo lleva diez minutos. #### Qué ves | Zona | En la vista de formulario | | --- | --- | | **Herramientas** (izquierda, arriba) | Todos los controles que puedes colocar, agrupados (Comunes, Presentación, Contenedores, Disposición, Navegación...), con un buscador. | | **Estructura** (izquierda, abajo) | Los controles del formulario en árbol: selecciónalos, renómbralos, anídalos, ocúltalos y bloquéalos. | | **Barra de herramientas** (centro, arriba) | Guardar, deshacer, alinear, distribuir, mismo tamaño, centrar, orden, cuadrícula, ajuste y guías. | | **Lienzo** (centro) | El formulario dentro de un marco de ventana, tal como se ejecutará. | | **Propiedades** (derecha) | El control seleccionado, o el formulario si no hay nada seleccionado. | | **Eventos** (derecha) | A qué puede reaccionar el control seleccionado y la función Python conectada a cada evento. | | **Código** (abajo) | El Python del formulario, que se abre al conectar un evento. | Cada zona se describe en detalle en su propia página: [Herramientas y lienzo](https://albakore.github.io/pyzed/es/editor/toolbox-and-canvas/), [Propiedades](https://albakore.github.io/pyzed/es/editor/properties/), [Eventos](https://albakore.github.io/pyzed/es/editor/events/) y [Editor de código](https://albakore.github.io/pyzed/es/editor/code-editor/). #### Tu primer formulario: un saludador Vas a crear una ventana con un cuadro de texto, un botón y una etiqueta. Al escribir un nombre y pulsar el botón, muestra "¡Hola, *nombre*!". 1. **Crea el formulario.** Pulsa Ctrl+N. En el diálogo **Nuevo** deja **Formulario**, escribe `hello` como **Nombre** y `Saludador` como **Título**, elige la plantilla **En blanco** y pulsa **Crear**. Aparecen dos archivos: `hello.pzform` (el diseño) y `hello.py` (el código). 2. **Coloca un cuadro de texto.** En las herramientas, arrastra **Cuadro de texto** al formulario. Queda seleccionado y el panel Propiedades lo muestra. Cambia su **Nombre** a `your_name` y su **Texto de ayuda** a `Tu nombre`. 3. **Coloca un botón.** Haz clic en **Botón** en las herramientas y arrastra un rectángulo en el formulario bajo el cuadro de texto: el botón toma ese tamaño. Llámalo `greet` y pon su **Texto** en `Saludar`. 4. **Coloca una etiqueta.** Añade una **Etiqueta de texto** bajo el botón, llámala `message` y borra su texto. 5. **Alinéalos.** Selecciona los tres controles (arrastra un rectángulo de selección alrededor) y pulsa **Alinear a la izquierda** en la barra de herramientas. Las guías inteligentes muestran líneas rosas cuando los bordes coinciden mientras arrastras. 6. **Conecta el clic.** Haz doble clic en el botón. El panel Eventos conecta su evento **click** a una función nueva, `greet_click`, y la abre en el editor de código. Escribe una línea: ```python title="hello.py" def greet_click(form, event): """Se ejecuta al pulsar greet.""" form.message.text = f"¡Hola, {form.your_name.text}!" ``` `form` es la ventana en marcha: cada control es un atributo con su nombre (`form.message`) y sus propiedades son atributos del control (`form.message.text`). Al cambiarlos se actualiza la ventana. 7. **Ejecútalo.** Pulsa F5 (guarda antes). Tu formulario se abre como una ventana real; escribe un nombre y pulsa el botón. La [consola](https://albakore.github.io/pyzed/es/editor/console-and-running/) de abajo muestra lo que imprime el formulario y cualquier error. Déjala abierta, cambia el color de la etiqueta en Propiedades y guarda: la ventana en marcha se actualiza. **Los nombres importan:** El **Nombre** de un control es cómo lo alcanza tu código (`form.your_name`), y los manejadores se llaman como él (`greet_click`). Pon nombres cortos y claros en cuanto coloques los controles; renombrar uno después también renombra los manejadores que siguen esa convención (`greet_click`), pero el código de otros sitios que use el nombre antiguo hay que actualizarlo a mano. #### Ejecuta el formulario fuera del diseñador El formulario y su código son archivos normales de tu proyecto. Ejecútalos desde una terminal: **uv:** ```bash uv run pyzed run hello.pzform uv run pyzed run hello.pzform --reload # se actualiza al guardar ``` **pip:** ```bash pyzed run hello.pzform pyzed run hello.pzform --reload ``` **python:** ```bash title="macOS / Linux" python3 -m pyzed run hello.pzform ``` ```powershell title="Windows (PowerShell)" py -m pyzed run hello.pzform ``` O desde tu propio código Python: ```python title="main.py" from pyzed import designer designer.run("hello.pzform") ``` #### Qué más hace la vista de formulario | Tarea | Cómo | Más | | --- | --- | --- | | Ordenar controles en filas, columnas y cuadrículas que se adaptan a la ventana | Selecciónalos, clic derecho → **Agrupar en una fila / columna / cuadrícula**, o usa un contenedor de las herramientas | [Disposiciones](https://albakore.github.io/pyzed/es/editor/layouts/) | | Mantener un control pegado a un borde cuando la ventana crece | **Posición → Anclajes** en Propiedades | [Propiedades](https://albakore.github.io/pyzed/es/editor/properties/#posición) | | Cambiar colores, fuentes, bordes y sombras | **Estilo** en Propiedades; la ventana entera en el **Estilo** del formulario | [Estilos y temas](https://albakore.github.io/pyzed/es/editor/styles-and-themes/) | | Animar al pasar el ratón o al hacer clic, añadir brillo, desenfoque o cristal | **Efectos** y **Movimiento** en Propiedades | [Animaciones y efectos](https://albakore.github.io/pyzed/es/editor/animations-and-effects/) | | Rellenar campos con datos y validarlos | Sección **Datos**: enlaces y reglas | [Enlace de datos](https://albakore.github.io/pyzed/es/editor/data-binding/) | | Ver el formulario con otro tema, idioma o marco de sistema | **Ver → Vista previa con tema / idioma / marco de ventana** | [Interfaz](https://albakore.github.io/pyzed/es/editor/interface/#lienzo) | | Reutilizar un grupo de controles en muchos formularios | Conviértelo en un componente | [Vista de componente](https://albakore.github.io/pyzed/es/editor/component-designer/) | | Pintar un fondo con un efecto de GPU | El control **Shader** de Presentación, con un `.pzshad` | [Shading](https://albakore.github.io/pyzed/es/shading/) | #### El archivo `.pzform` Un formulario es JSON: su tamaño y aspecto (`form`), y cada control con su tipo, posición y propiedades. Sus manejadores viven a su lado en un módulo Python con el mismo nombre. Ambos son tuyos para versionar, revisar y editar a mano. ```json title="hello.pzform (resumido)" { "format": "pyzed.form/1", "form": {"name": "hello", "title": "Saludador", "width": 360, "height": 220, "theme": "One Dark"}, "controls": [ {"name": "your_name", "type": "input", "x": 24, "y": 24, "width": 312, "height": 32, "props": {"placeholder": "Tu nombre"}, "events": {}}, {"name": "greet", "type": "button", "x": 24, "y": 72, "width": 120, "height": 32, "props": {"text": "Saludar"}, "events": {"click": "greet_click"}} ] } ``` **Para profundizar:** Esta página cubre lo esencial de la vista de formulario. Las demás páginas de esta sección describen a fondo cada control de la vista, empezando por [Herramientas y lienzo](https://albakore.github.io/pyzed/es/editor/toolbox-and-canvas/). ### Herramientas y lienzo > Coloca, selecciona, mueve, redimensiona y ordena controles en Pyzed Design: snap, guías inteligentes, alineación, orden z y la estructura del formulario. Source: https://albakore.github.io/pyzed/es/editor/toolbox-and-canvas/ El panel **Herramientas** (dock izquierdo) tiene los componentes arriba y la **Estructura** abajo; arrastra la barra entre ellas para dar más sitio a una. El **lienzo** del centro muestra el documento exactamente como se ejecutará, pero inerte: los botones no hacen clic y los campos de texto no escriben, así el ratón queda libre para editar. #### La caja de herramientas Todos los componentes que puedes colocar, por categoría: **Comunes**, **Formularios** (todo lo que el usuario escribe o elige), **Fecha y hora**, **Presentación**, **Contenedores**, **Disposición**, **Navegación**, **Sistema** (lo que habla con el sistema operativo), **Componentes** (los demás [no visuales](https://albakore.github.io/pyzed/es/editor/non-visual-components/), que van a la bandeja bajo el formulario) y una categoría por cada grupo de [componentes diseñados](https://albakore.github.io/pyzed/es/editor/component-designer/) de tu proyecto. Pasa el ratón por una fila para ver la descripción del componente. | Categoría | Componentes | | --- | --- | | **Comunes** | Etiqueta de texto, Título, Botón, Enlace, Botón con menú | | **Formularios** | Cuadro de texto, Área de texto, Casilla, Interruptor, Opciones, Desplegable, Deslizador, Número, Lista, Buscador, Segmentado, Grupo de botones, Selector de color, Selector de iconos | | **Fecha y hora** | [Selector de fecha](https://albakore.github.io/pyzed/es/library/date-picker/), [Rango de fechas](https://albakore.github.io/pyzed/es/library/date-range-picker/), [Selector de hora](https://albakore.github.io/pyzed/es/library/time-picker/), [Calendario](https://albakore.github.io/pyzed/es/library/calendar/) | | **Presentación** | Barra de progreso, Insignia, Imagen, Imagen SVG, Visor de imágenes, Shader, Icono, Markdown, Indicador de carga, Etiqueta (chip), Aviso, Avatar, Grupo de avatares, Contador, Punto de estado, Atajo, Métrica, Gráfico, Tabla, Resumen de cambios, Estado vacío, Código QR, Salida de terminal | | **Navegación** | Pestañas, Árbol, Ruta de navegación | | **Contenedores** y **Disposición** | Panel, Marco, Tarjeta, Separador, Fila flexible, Columna flexible, Cuadrícula, Dock, Panel del dock, Espaciador | | **Sistema** | [Selector de archivos](https://albakore.github.io/pyzed/es/library/file-input/), [Zona para soltar](https://albakore.github.io/pyzed/es/library/drop-zone/) y, en la bandeja: Atajo, Vigilante de archivos, Proceso, Portapapeles, Diálogo de archivos, Notificación, Información del sistema, Lanzador, Monitor de red, Monitor de batería | | **Componentes** | Temporizador, Tarea en segundo plano, Petición web, Ajustes | | **Multimedia** y **Audio** | Webcam y los [controles de audio](https://albakore.github.io/pyzed/es/audio/): perillas, faders, pads, piano, medidores, formas de onda, espectro... | El ejemplo [Explorador de proyecto](https://albakore.github.io/pyzed/es/editor/examples/) pone casi todos en un formulario, con un manejador para cada uno de sus eventos. - **Buscar componentes…** filtra la lista por nombre (en inglés o en el idioma del editor) o por tipo. - **Puntero** (la primera fila) es la herramienta normal de selección. Está resaltada cuando no hay componente elegido. ##### Colores de los iconos Cada componente tiene un **rol**, y su icono lleva el color de ese rol en todas partes: la caja de herramientas, la estructura, la bandeja de componentes no visuales, la cabecera de los paneles **Propiedades** y **Eventos** y la ficha que sigue al puntero mientras arrastras. Así la estructura de un formulario se entiende de un vistazo: las cajas azules contienen al resto, los campos verdes recogen lo que escribe el usuario y los botones amarillos ejecutan tu código. El icono de la paleta junto al buscador muestra la misma leyenda. | Color | Rol | Qué es | | --- | --- | --- | | Azul | **Disposición** | Contiene y ordena otros controles: paneles, tarjetas, marcos, filas, columnas, cuadrículas, docks, separadores, espaciadores. | | Verde | **Entrada** | Recoge un valor del usuario: cuadros de texto, casillas, interruptores, desplegables, deslizadores, números, selectores. | | Amarillo | **Acción** | Ejecuta algo al pulsarlo: botones y botones con menú. | | Cian | **Navegación** | Lleva de una vista a otra: pestañas, árboles, rutas de navegación, enlaces. | | Neutro | **Presentación** | Muestra contenido: etiquetas, títulos, imágenes, tablas, gráficos, insignias. | | Rojo | **Multimedia** | Sonido y vídeo: los controles de audio y la webcam. | | Naranja | **Sistema** | Componentes no visuales: temporizadores, procesos, vigilantes de archivos, peticiones web, el portapapeles. | | Morado | **Propio** | Tus propios componentes, hechos con el [diseñador de componentes](https://albakore.github.io/pyzed/es/editor/component-designer/) o con `pz.Component`. | Los colores salen de la paleta de terminal del tema, así que cambian con el tema y se leen bien en temas claros y oscuros. El rol se deduce del componente (si contiene controles, su categoría y sus eventos); un componente registrado desde Python puede indicar el suyo con `Component(..., role="input")`. ##### Colocar un control Hay cuatro formas; usa la que te venga mejor: | Gesto | Resultado | | --- | --- | | **Arrastrar** una fila de herramientas al formulario | El control cae donde lo sueltas, con su tamaño por defecto, dentro del contenedor que hay bajo el puntero (se ilumina mientras arrastras). | | **Clic** en una fila y luego **clic** en el formulario | Lo mismo, en el punto donde hiciste clic. | | **Clic** en una fila y luego **arrastrar un rectángulo** en el formulario | El control toma el tamaño del rectángulo (al menos 6×6 px); sus esquinas se ajustan a la cuadrícula. | | Ctrl+Shift+A (**Añadir control…**) | Una lista con buscador; Enter añade el componente resaltado centrado en el formulario. | (Image: Colocar un botón: arrastra un rectángulo con la herramienta elegida) Mientras hay un componente elegido, el puntero es una cruz y la barra de estado dice `Placing Button: click or drag on the form`. Esc (o clic en **Puntero**, o en la misma fila otra vez) suelta la herramienta. Los controles nuevos reciben un nombre libre según su tipo (`button1`, `button2`...) y quedan seleccionados, con el panel Propiedades al frente. (Image: Añadir control: una lista de componentes con búsqueda difusa) :::tip El **+** de la fila de un contenedor en la estructura, y **Add inside…** en su menú contextual, abren la misma lista pero añaden el control nuevo *dentro* de ese contenedor. ::: #### Seleccionar | Gesto | Selecciona | | --- | --- | | Clic en un control | Ese control (los demás se deseleccionan). | | Clic en un control ya seleccionado | Mantiene la selección y convierte ese control en el **principal**: la referencia para alinear e igualar tamaños, dibujado con un borde más grueso. | | Arrastrar en un hueco | Un rectángulo de selección: todos los controles que toca. Un contenedor y lo que tiene dentro cuentan una vez (el contenedor). | | Ctrl+A | Todos los controles. | | Clic en un hueco | Nada: el panel Propiedades muestra entonces el propio formulario. | | Una fila de la estructura | Ese control, aunque esté tapado por otros. | Con varios controles seleccionados, el panel Propiedades muestra las herramientas de alineación y espaciado en lugar de propiedades, y el panel Eventos pide que elijas un solo control. #### Mover y redimensionar - **Arrastra** un control seleccionado para mover toda la selección. Las posiciones se ajustan a la cuadrícula (con el snap activo) y el grupo no sale del formulario. - **Suéltalo** sobre un contenedor para meterlo dentro, o sobre el formulario vacío para sacarlo de su contenedor. El destino se ilumina con un marco de color de acento mientras arrastras. - Las **ocho asas** de un único control seleccionado lo redimensionan desde ese borde o esquina; el borde opuesto no se mueve. Los controles nunca son más pequeños que el tamaño mínimo y no salen de su contenedor. - El **tirador** bajo la esquina inferior derecha del formulario lo redimensiona (al menos 120×80, ajustado a la cuadrícula). - Esc durante cualquier arrastre lo cancela y deja todo como estaba. - Las **flechas** mueven la selección 1 px; Shift+flechas la mueven un paso de cuadrícula (1 px con el snap apagado). Varios empujones seguidos son un solo paso de deshacer. Los controles bloqueados no se mueven ni se redimensionan (muestran un candadito sobre su esquina), y las herramientas de alineación los saltan. ##### Los contenedores y su pestaña Los hijos de un contenedor lo cubren casi entero, así que un clic ahí elige un hijo. Para elegir o arrastrar el propio contenedor, usa la **pestaña** con su nombre sobre su esquina superior izquierda: aparece mientras el puntero está sobre el contenedor y mientras el contenedor, o un control suyo, está seleccionado. Clic derecho en la pestaña abre el menú contextual del contenedor. Ver [Disposición y contenedores](https://albakore.github.io/pyzed/es/editor/layouts/). #### Snap, cuadrícula y guías inteligentes | Herramienta | Dónde | Qué hace | | --- | --- | --- | | **Mostrar la cuadrícula** | Barra de herramientas, barra de estado, **Ver → Cuadrícula**, G | Muestra u oculta los puntos de la cuadrícula. | | **Ajustar a la cuadrícula** | Barra de herramientas (imán), barra de estado, **Ver** | Redondea posiciones y tamaños al paso de la cuadrícula al arrastrar, colocar y redimensionar. | | **Guías inteligentes** | Barra de herramientas (regla), **Ver** | Ajusta el control que mueves a los bordes y centros de los demás controles y del formulario, y dibuja una línea roja mientras coinciden. | | **Redimensionar el contenido con sus anclajes** | Barra de herramientas (ancla) | Al redimensionar el formulario, un componente o un contenedor libre, lo que hay dentro se mueve y se estira según sus anclajes, como en la ventana en ejecución. Activado por defecto. | | **Ajustar la selección a la cuadrícula** | Barra de herramientas | Mueve y redimensiona los controles seleccionados (todos si no hay selección) sobre la cuadrícula. | | **Tamaño de cuadrícula** | Propiedades del formulario (sección **Diseñador**) | El paso de cuadrícula de este formulario (2 a 64 px, 8 por defecto). Los formularios nuevos toman el valor de [Ajustes](https://albakore.github.io/pyzed/es/editor/settings/). | (Image: Una guía inteligente: la casilla se alinea con los centros de los demás controles) Las guías ajustan a menos de 4 px de un objetivo, en los dos ejes a la vez, así que un control puede alinearse con el borde izquierdo de uno y el centro de otro en el mismo arrastre. #### Alinear, distribuir e igualar tamaños La barra de herramientas actúa sobre la selección: | Botón | Efecto | | --- | --- | | **Alinear a la izquierda / los centros / a la derecha** | Alinea los bordes izquierdos, los centros horizontales o los bordes derechos. | | **Alinear arriba / al medio / abajo** | Alinea los bordes superiores, los centros verticales o los bordes inferiores. | | **Distribuir horizontalmente / verticalmente** | Reparte tres o más controles a la misma distancia entre el primero y el último. | | **Mismo ancho / alto** | Da a todos los seleccionados el ancho o el alto del control principal. | | **Centrar horizontalmente / verticalmente en el formulario** | Centra la selección en el formulario. | | **Traer al frente / Enviar al fondo** | Cambia el orden de dibujo (qué queda encima). | | **Eliminar** | Borra la selección (Supr o Retroceso). | Con varios controles, la alineación usa los límites de la selección; con uno solo, alinea con el formulario. El panel Propiedades muestra las mismas herramientas cuando hay varios controles seleccionados. (Image: Tres controles seleccionados, listos para alinear) #### Portapapeles e historial - Ctrl+C, Ctrl+X, Ctrl+V: copiar, cortar y pegar. Los controles pegados se desplazan un poco para que se vean; un contenedor se copia con todo lo que tiene dentro. Los controles pegados **no** están conectados: sus eventos empiezan vacíos, así no comparten manejadores con los originales. - Lo copiado va al **portapapeles del sistema**, así que puedes pegar controles en **otro formulario**: otra pestaña, otra ventana de Pyzed Design o el mismo formulario tras reiniciar. En otro formulario, un control pegado conserva su nombre si allí está libre (si no, recibe uno nuevo), junto con sus propiedades, estilo, [enlace y reglas](https://albakore.github.io/pyzed/es/editor/data-binding/). Pegar un texto que no son controles de Pyzed pega lo último que se copió en esta ventana. - Ctrl+D duplica la selección sin tocar el portapapeles. - Ctrl+Z deshace, Ctrl+Shift+Z (o Ctrl+Y) rehace. Cada documento tiene su propio historial; las ediciones repetidas de un campo (escribir un título, empujar con las flechas) se juntan en un paso. #### El menú contextual Clic derecho en un control del lienzo, en su fila de la estructura o en la pestaña de un contenedor: (Image: El menú contextual de un botón) | Entrada | Hace | | --- | --- | | **View code** | Abre el manejador del evento principal del control (o abre un componente diseñado en su pestaña). | | **Help** | Abre la página del control en la [referencia](https://albakore.github.io/pyzed/es/editor/reference/) (Shift+F1). | | **Add inside…** | Solo contenedores: la lista Añadir control, añadiendo dentro de este contenedor. | | **Group in a row / column / grid / panel** | Envuelve la selección en una fila flex, una columna flex, una grilla o un panel nuevos. | | **Move out of *contenedor*** | Saca el control de su contenedor, al padre del contenedor. | | **Cut, Copy, Paste, Duplicate** | Como los atajos. | | **Bring to front, Send to back** | Orden de dibujo. | | **Lock / Unlock** | Bloquea el control en su sitio. | | **Delete** | Borra la selección. | El menú se abre en el puntero y no sale de la ventana. Esc o un clic fuera lo cierran. #### La estructura La **Estructura** lista todos los controles del documento en árbol. La primera fila es el propio formulario (o componente): clic para seleccionarlo, **+** le añade un control. (Image: La estructura: un control bloqueado y otro oculto) - **Orden**: el de más arriba primero, como las capas de un programa de dibujo. Los hijos de una fila flex, una columna flex o una grilla aparecen en su orden de disposición. - **Flecha**: pliega o despliega los hijos de un contenedor. En un componente diseñado colocado, lista sus propiedades y métodos públicos; haz clic en uno para insertar `form.ada.rate()` (o `self.stars.pick()` en un componente) en el cursor del editor de código, o para copiarlo si no hay código abierto. - **Ojo**: oculta el control. Los controles ocultos no se ven al ejecutar el formulario y su nombre sale en gris en la estructura. Mira [Diálogos, cajones y otros controles ocultos](#diálogos-cajones-y-otros-controles-ocultos). - **Candado**: bloquea el control para no moverlo ni redimensionarlo sin querer. - **+** (contenedores): añade un control dentro. - **Arrastra una fila** para reordenarla: una línea muestra dónde va (antes o después de una fila), y un marco cuando iría *dentro* de un contenedor. Suéltala en el lienzo para meterla en el contenedor bajo el puntero, o en el formulario. - **Clic derecho** en una fila abre el menú contextual. :::tip La estructura es la forma más fácil de elegir un control tapado por otros, o un [componente no visual](https://albakore.github.io/pyzed/es/editor/non-visual-components/) como un temporizador, y de mover controles entre contenedores con precisión. ::: #### Diálogos, cajones y otros controles ocultos Un diálogo, un cajón o una página que solo aparece al pulsar un botón es un contenedor con **Visible** apagado, que el código muestra con `form.status_dialog.show()` (y `.hide()` para cerrarlo). El lienzo no dibuja los controles ocultos, así un formulario con muchos no se convierte en un montón de paneles superpuestos: 1. **Selecciónalo en la estructura** (o algo de dentro). Aparece delante del formulario, que queda oscurecido detrás, tal como se verá al ejecutarlo. Un aviso arriba muestra su nombre. 2. **Edítalo como siempre**: muévelo, cámbiale el tamaño, suelta controles nuevos dentro desde las herramientas o cambia sus propiedades. 3. **Vuelve** con Esc, la **×** del aviso o un clic en el formulario oscurecido. Para ver todos los controles ocultos a la vez, atenuados en su sitio, activa **Ver → Mostrar todos los controles ocultos**. ##### Ocultar un control solo en el editor Si prefieres tener todos los controles en el lienzo y ocultar algunos solo mientras trabajas, como las capas de un programa de dibujo, pon **Ajustes → Lienzo del formulario → Ojo de la estructura** en **Oculto solo en el editor**. Así el ojo oculta el control (y lo que tiene dentro) solo en el lienzo: sigue oculto aunque lo selecciones, al ejecutar el formulario se ve, y **Visible** se cambia en el panel Propiedades. En este modo los controles con Visible apagado se dibujan normal. Lo que oculta el ojo se guarda con el formulario. #### La barra de estado De izquierda a derecha: la posición del puntero en el formulario, lo seleccionado (`sign_in 344×32 at 48, 304`, `3 selected`, o el formulario y su tamaño) y, con un componente elegido, la pista para colocarlo. A la derecha: el marco de ventana que se muestra alrededor del formulario (**GNOME**, **macOS**...: clic para ver el de otra plataforma), el número de controles, **Grid 8px** (clic para mostrar u ocultar la cuadrícula) y **Snap** (clic para activar o desactivar el ajuste). Con el editor de código abierto muestra además la línea y columna del cursor y el estado de los servidores de lenguaje. Ocúltala con **Ver → Barra de estado**. ### Propiedades > La grilla de propiedades de Pyzed Design sección por sección: el formulario, un control, varios controles a la vez y los editores de listas y tablas. Source: https://albakore.github.io/pyzed/es/editor/properties/ El panel **Propiedades** (dock derecho) edita lo que esté seleccionado. Su contenido cambia con la selección: | Selección | El panel muestra | | --- | --- | | Nada | El propio **formulario**: ventana, aspecto, estilo global y ajustes del diseñador. En un componente, el [panel Component](https://albakore.github.io/pyzed/es/editor/component-designer/#el-panel-component). | | Un control | Su nombre, **Posición** primero, luego sus propiedades por grupo, **Comportamiento**, su estilo (**Texto**, **Relleno**, **Borde**, **Espaciado**), **Efectos** y **Animación**. | | Varios controles | Herramientas para alinear, distribuir e igualar tamaños. | Cada sección se pliega: haz clic en su cabecera. Las secciones plegadas se recuerdan mientras el editor está abierto, así que puedes dejar solo las que usas. Cada cambio es un paso de deshacer, y escribir en un campo se junta en un solo paso. ##### Campos numéricos Todos los números del panel funcionan como en Figma: - **Arrástralo de lado**: pulsa sobre el número y mueve el ratón a la izquierda o a la derecha para bajarlo o subirlo. Los pasos pequeños (opacidad, interlineado) avanzan despacio, para que puedas afinar. - **Doble clic** para escribir un valor; Enter lo confirma. Un solo clic pone el cursor en el campo, y ↑ / ↓ suman o restan un paso. - La letra o unidad dentro del campo (**X**, **W**, **px**, **°**) dice qué es el número. En tus propias apps es el mismo [`ui.number_field`](https://albakore.github.io/pyzed/es/library/number-field/): `drag=False` desactiva el arrastre y `steppers=False` oculta los botones `−`/`+`. #### Un control (Image: Las propiedades de una tabla: el editor de filas) ##### Cabecera y nombre Arriba del panel están el icono del componente, su tipo y una descripción de una línea. Debajo, **Nombre** es el identificador del control: el atributo que usas en el código (`form.email`) y el prefijo de sus manejadores. Escribe un nombre nuevo y pulsa Enter para renombrarlo; los manejadores que siguen la convención de nombres (`email_change`) se renombran también en el archivo de código. El editor ajusta los nombres para que sean válidos y únicos. El botón del **libro** a la derecha de la cabecera (Shift+F1) abre la página del control en la [referencia integrada](https://albakore.github.io/pyzed/es/editor/reference/): cada propiedad con su tipo y valor por defecto, sus eventos y sus métodos. Pasa el ratón por la etiqueta de una propiedad para ver una descripción de una línea. ##### Las secciones propias del componente Cada componente agrupa sus propiedades en secciones **Apariencia**, **Datos**, **Disposición** o **Comportamiento** (una sección con el mismo nombre que una del propio panel, como **Comportamiento**, se junta con ella). El tipo de una propiedad decide su editor: | Tipo | Editor | Ejemplo | | --- | --- | --- | | Texto | Un campo de texto. | El `text` de un botón, el `placeholder` de un campo. | | Multilínea | Un pequeño editor de código con ajuste de línea. | El `text` de un área de texto o de un markdown. | | Número | Un campo numérico que se arrastra o se escribe, con límites. | `min`, `max`, `step` de un deslizador. | | Archivo, carpeta | Un campo y un botón de **carpeta** que abre el diálogo de archivos del sistema (ver abajo). | El `source` de una imagen, el `file` de un shader, el `path` de un vigilante de archivos. | | Interruptor | Un switch de encendido y apagado. | `checked`, `secure`, `wrap`. | | Opción | Hasta cuatro opciones cortas lado a lado como un control segmentado; las listas más largas, como desplegable. | El `variant` de un botón, el `size` de una etiqueta. | | Color | Una muestra y un campo para un token del tema (`accent`, `text_muted`) o `#hex`. | El `color` de una etiqueta. | | Icono | Una vista previa, un campo para un nombre de icono de [Lucide](https://lucide.dev) y un botón **…**: pulsa la vista previa o **…** para elegirlo en el diálogo de iconos (ver abajo). | El `icon` de un botón. | | Lista | Un campo por elemento (ver abajo). | Las `options` de un desplegable, los `items` de una lista, las `tabs` de una barra de pestañas. | | Filas | Una pequeña hoja de cálculo (ver abajo). | Los `data` de una tabla. | Pasa el ratón por el nombre de una propiedad para leer qué hace, cuando tiene ayuda. La [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/) documenta cada propiedad de cada componente. Los controles que guardan un valor muestran además **Enlazar a** y sus reglas de validación (**Obligatorio**, **Longitud mínima**, **Patrón**...) en la sección **Datos**: consulta [Enlace de datos y validación](https://albakore.github.io/pyzed/es/editor/data-binding/). ##### Archivos y carpetas Toda propiedad que guarda un archivo o una carpeta tiene un botón de **carpeta** al final. Abre el diálogo del sistema (filtrado por tipo: imágenes, sonidos, fuentes, shaders, archivos de datos...) y escribe la ruta por ti. Un archivo dentro de tu proyecto se escribe **relativo al proyecto** (`assets/logo.png`), así el proyecto sigue funcionando si lo mueves. También puedes escribir la ruta a mano. Vale para las propiedades de los componentes (`source`, `file`, `path`, `directory`...), para cualquier propiedad de texto cuyo nombre indique una ruta (`icon_path`, `output_dir`, `cwd`...), para las [propiedades de tus componentes](https://albakore.github.io/pyzed/es/editor/component-designer/) de tipo `pz.FilePath` o `pz.FolderPath`, y para los campos de carpeta de los diálogos del editor. En tus propias apps el mismo campo es `ui.path_input`. ##### El diálogo de iconos Al pulsar la vista previa de una propiedad de icono o su botón **…** se abre **Elegir un icono**: un buscador, un desplegable de categorías y una cuadrícula con todos los iconos incluidos. La búsqueda mira los nombres y las etiquetas de Lucide (en inglés: "person" encuentra `user`, "trash" encuentra `trash-2`), Enter elige la primera coincidencia y un clic en un icono fija la propiedad y cierra el diálogo. El icono en uso aparece resaltado y abajo, junto al número de coincidencias; **×** deja la propiedad sin icono. (Image: El diálogo de iconos abierto desde la propiedad Icono de un botón) El mismo diálogo elige el **Icon** de un [componente diseñado](https://albakore.github.io/pyzed/es/editor/component-designer/) y el valor por defecto de sus propiedades de icono. En tus propias apps es [`ui.IconPicker`](https://albakore.github.io/pyzed/es/library/icon-picker/). ##### El editor de listas (Image: Los elementos de una lista, un campo por elemento) Las listas tienen un campo numerado por elemento, con botones para **subirlo**, **bajarlo** y **quitarlo**, y **Añadir elemento** al final. El icono de página a la derecha del título de la propiedad cambia a **Edit as text**: un elemento por línea en un pequeño editor, cómodo para pegar una lista larga. El icono de lista vuelve (**Edit item by item**). ##### El editor de filas Los datos de una tabla son una pequeña hoja de cálculo. La primera fila (marcada con **H**) es la cabecera; las demás están numeradas. **Añadir fila** y **Añadir columna** la amplían, la **×** al final de una fila la quita y la **×** sobre una columna quita la columna (una tabla conserva al menos una). Se desplaza de lado cuando la tabla es más ancha que el panel. Como con las listas, el icono de página cambia a texto plano: una fila por línea y las celdas separadas por comas. ##### Estilo: Texto, Relleno, Borde, Espaciado El estilo del control se reparte en cuatro secciones cortas, con las mismas propiedades que [`pz.Style`](https://albakore.github.io/pyzed/es/theming/styles/). Los campos vacíos mantienen el aspecto propio del componente, y **Restablecer el estilo** al final los borra todos. Ver [Estilos y temas](https://albakore.github.io/pyzed/es/editor/styles-and-themes/). | Sección | Campos | | --- | --- | | **Texto** | **Fuente** (familia y peso), **Tamaño** (tamaño de letra e interlineado), **Color**, **Cursiva**. | | **Relleno** | El fondo: un color, un degradado o una máscara. | | **Borde** | **Color**, **Ancho** y **Esquinas** (un radio, o uno por esquina con el botón de **enlace**). | | **Espaciado** | El **modelo de caja**, **Alinear contenido**, **Tamaño máximo** y **Recortar contenido**. | El modelo de caja se dibuja como en el navegador: un anillo exterior ámbar para el **margen** (espacio fuera del control) y uno interior verde para el **relleno** o padding (espacio entre su borde y su contenido), con un número por lado. Arrastra o haz doble clic en cualquiera. **Alinear contenido** es una caja de 3×3: haz clic en un punto para llevar el contenido a esa esquina, a ese lado o al centro. La sombra está en **Efectos**, junto a la opacidad, el resplandor, el bloom y el cristal; **Efectos** y **Animación** se explican en [Animaciones y efectos](https://albakore.github.io/pyzed/es/editor/animations-and-effects/). ##### Posición **Posición** es siempre la primera sección. Para un control colocado libremente (en el formulario, o en un panel, un group box o una tarjeta): | Campo | Significado | | --- | --- | | **Dentro de** | Solo dentro de un contenedor: el nombre del contenedor (clic para seleccionarlo) y un botón para **sacarlo**. | | **Alineación** | Seis botones que llevan el control al borde izquierdo, al centro, al borde derecho, arriba, al medio o abajo de su contenedor (o del formulario). | | **Posición** | **X** e **Y**, respecto al formulario o al contenedor. | | **Tamaño** | **W** (ancho) y **H** (alto) en píxeles. | | **L · T**, **R · B** | Aparecen en Posición y Tamaño en lugar de X y W (o Y y H) cuando el control se estira en ese eje: sus distancias a los lados del contenedor. Al cambiar una, el lado opuesto se queda donde está. | | **Rotación** | Grados en sentido horario, alrededor del pivote (el mismo valor que el `rotate` del estilo). | | **Anclaje** | El preset de anclaje, dibujado como una caja en un marco, y su nombre (`Arriba · Izquierda`). Púlsalo para abrir la cuadrícula de 4×4 presets (ver abajo). | **Más opciones** al final muestra los campos que se usan menos: | Campo | Significado | | --- | --- | | **Tamaño mínimo**, **Tamaño máximo** | Límites del ancho y el alto cuando el control se estira (`0`: sin límite), para que un panel estirado se detenga en un ancho legible. | | **Pivote** | El punto alrededor del que el control gira y escala, y que queda fijo cuando un límite de tamaño frena el estiramiento: `0 0` es la esquina de arriba a la izquierda, `0.5 0.5` (por defecto) el centro, `1 1` la esquina de abajo a la derecha. | (Image: La sección Posición con los presets de anclaje abiertos) Para un hijo de un **auto layout** (una fila o columna flex, o una grilla) la sección cambia, porque lo coloca su contenedor: **Ancho** y **Alto** dicen cada uno **Fijo** (mantiene ese tamaño) o **Llenar** (ocupa el sitio que deja el contenedor), **Crecer** reparte el espacio libre entre los hijos que llenan, y en una grilla **Columnas** dice cuántas celdas ocupa. Estos hijos no tienen X ni Y: arrástralos para reordenarlos. Ver [Disposición y contenedores](https://albakore.github.io/pyzed/es/editor/layouts/). ##### Auto layout Una fila, columna flex o grilla muestra una sección **Auto layout**, como en Figma: | Campo | Significado | | --- | --- | | **Dirección** | Uno al lado del otro (fila) o uno debajo del otro (columna). Una grilla muestra **Columnas** en su lugar. | | **Alineación** | Una caja de 3×3: haz clic donde quieres los hijos dentro del contenedor (izquierda, centro o derecha; arriba, medio o abajo). | | **Separación** | Espacio entre los hijos, o **Espacio entre**, **Espacio alrededor** o **Espacio uniforme** para repartirlos en el sitio libre. Una grilla tiene separación y alto de fila. | | **Relleno** | Espacio entre el borde del contenedor y sus hijos: horizontal y vertical, o uno por lado con el botón del **cuadrado**. | | **Llenar a lo ancho** | Los hijos se estiran al alto del contenedor (en una fila) o a su ancho (en una columna). | Selecciona algunos controles y pulsa Shift+A (**Añadir auto layout**) para envolverlos en una fila o una columna, deducida de cómo están colocados. Ctrl+G los agrupa en un panel invisible sin moverlos, y Ctrl+Shift+G saca los hijos de un grupo o layout y lo quita, manteniendo su sitio y su tamaño. Los presets son los anclajes de Unity, Unreal y Godot. Las columnas dicen cómo sigue el control al ancho (**Izquierda**, **Centro**, **Derecha** o **Estirar**) y las filas cómo sigue al alto (**Arriba**, **Centro**, **Abajo** o **Estirar**); la última celda, **Estirar · Estirar**, llena el contenedor. Elegir un preset solo cambia cómo sigue el control a un cambio de tamaño; activa **Moverlo también allí** para pegarlo además a los bordes a los que está anclado (centrado, o llenando el contenedor en los ejes estirados). Por dentro, un preset es un conjunto de bordes, como en WinForms: | Anclajes | Cuando la ventana crece | | --- | --- | | Izquierda + Arriba (por defecto) | El control se queda donde está. | | Izquierda + Derecha | Se estira en horizontal. | | Solo derecha | Se mueve con el borde derecho. | | Ni izquierda ni derecha | Se queda centrado en horizontal. | | Arriba + Abajo, solo abajo, ninguno | Lo mismo, en vertical. | ##### Comportamiento | Campo | Significado | | --- | --- | | **Visible** | Los controles ocultos no se muestran al ejecutar (`form.status.show()` los vuelve a mostrar). | | **Habilitado** | Los controles deshabilitados se dibujan en gris e ignoran al usuario. | | **Bloqueado** | Solo en diseño: el control no se puede mover ni redimensionar en el lienzo. | #### Varios controles Con dos o más controles seleccionados, el panel muestra **Align** (bordes izquierdos, centros, bordes derechos, bordes superiores, medios, bordes inferiores) y **Spacing and size** (distribuir en horizontal o en vertical, mismo ancho, mismo alto). Funcionan como los [botones de la barra de herramientas](https://albakore.github.io/pyzed/es/editor/toolbox-and-canvas/#alinear-distribuir-e-igualar-tamaños). #### El formulario Haz clic en un hueco del lienzo, o en la primera fila de la estructura, para editar el propio formulario. | Sección | Campo | Significado | | --- | --- | --- | | **Ventana** | **Nombre** | El nombre del formulario: el nombre del archivo y el prefijo de los manejadores del formulario. | | | **Título** | El título de la ventana. | | | **Ancho**, **Alto** | El tamaño de la ventana (también con el tirador bajo el lienzo). | | | **Redimensionable** | Si el usuario puede cambiar el tamaño de la ventana en ejecución. | | **Aspecto** | **Tema** | Cualquier tema registrado: One Dark, One Light, Ayu, Gruvbox... o el tuyo. | | | **Acento** | Siete colores de acento predefinidos y **×** para el del tema. | | | **Acento propio** | Cualquier acento `#rrggbb`. | | | **Fondo** | Un color para el fondo de la ventana (`theme default` si está vacío). | | | **Radio de esquina** | El radio de todos los controles (0 a 24). | | | **Tamaño de letra** | El tamaño base del texto de la ventana. | | **Estilo** | | El **estilo global** de la ventana: fuente, color del texto, peso, tamaño e interlineado para todo texto que no fije los suyos; fondo, color del borde, radio y relleno de la ventana. | | **Diseñador** | **Tamaño de cuadrícula** | El paso de cuadrícula de este formulario. | El aspecto del formulario es independiente del tema del propio editor: un formulario puede ser claro mientras diseñas en un editor oscuro. ### Disposición y contenedores > Paneles, group boxes y tarjetas, filas y columnas flex, grillas, espaciadores y anclajes: organiza formularios de escritorio en Python con Pyzed Design. Source: https://albakore.github.io/pyzed/es/editor/layouts/ Los controles pueden estar directamente en el formulario, en posiciones fijas, o dentro de **contenedores** que los colocan por ti. El mismo código de disposición se ejecuta en el editor y en la app, así que lo que ves al diseñar es lo que obtienes. (Image: Un formulario con barra lateral: una columna flex con sus hijos) #### Los tres tipos de contenedor | Tipo | Componentes | Cómo coloca a sus hijos | | --- | --- | --- | | **Libre** | Panel, Marco (group box), Tarjeta | Donde los sueltas, en coordenadas relativas al contenedor. Sus anclajes siguen los bordes del contenedor. | | **Flex** | Fila flexible, Columna flexible | Uno tras otro, como flexbox de CSS. | | **Grilla** | Grid | En celdas, de izquierda a derecha y de arriba abajo. | | **Dock** | Dock | Solo paneles del dock, cada uno en su lado bajo una fila de pestañas (mira [Dock](#dock)). | ```mermaid flowchart TB F[Formulario] --> P["Panel (libre)"] F --> R["Fila flex"] F --> G["Grilla"] P --> A["botón en x=16, y=8"] R --> B1["botón · crecer 1"] R --> S["espaciador"] R --> B2["botón"] G --> L1["etiqueta"] & I1["campo de texto · 2 columnas"] ``` #### Meter controles en un contenedor - **Suelta** un control sobre un contenedor (se ilumina con un marco de acento mientras arrastras): desde la caja de herramientas, desde el lienzo o desde la estructura. - **Dibuja** un control nuevo dentro: elige un componente y haz clic o arrastra dentro del contenedor. - **Add inside…**: el **+** de la fila del contenedor en la estructura, o su menú contextual, abre la lista Añadir control. - **Agrupa**: selecciona algunos controles y pulsa Ctrl+G para meterlos en un panel invisible que los envuelve justo, sin moverlos. Shift+A (**Añadir auto layout**) los envuelve en una fila o columna flex, deducida de cómo están colocados. **Editar → Agrupar en una fila / columna / cuadrícula** (paleta) y el menú contextual ofrecen todos los tipos, también **Group in a panel**. - **Desagrupa**: Ctrl+Shift+G sobre un contenedor saca sus hijos y lo quita. Conservan el sitio y el tamaño que tenían en el lienzo, incluso los que una fila flex estiraba. - **Saca**: arrastra el control al formulario vacío, usa **Move out of *contenedor*** en su menú contextual o la flecha junto a **Dentro de** en su sección Posición. Mover o redimensionar un contenedor mueve a sus hijos con él, y copiar, cortar, pegar, duplicar y borrar se los llevan. Un contenedor no se puede soltar dentro de sí mismo ni de sus hijos. #### Fila flex y columna flex | Propiedad | Significado | | --- | --- | | **Dirección** | `row` (de izquierda a derecha) o `column` (de arriba abajo). Una fila flex y una columna flex son el mismo contenedor con otro valor por defecto. | | **Separación** | Espacio entre hijos, en píxeles. | | **Relleno** | Espacio entre el borde del contenedor y sus hijos: un número para todos los lados, dos (vertical y horizontal) o cuatro (arriba, derecha, abajo, izquierda). | | **Justificar** | Dónde van los hijos en el eje principal cuando sobra espacio: `start`, `center`, `end`, `space-between`, `space-around`, `space-evenly`. | | **Alineación** | En el eje cruzado: `stretch` (llenar), `start`, `center`, `end`. | | **Fondo** | Un color detrás de los hijos. | La sección **Auto layout** del panel Propiedades edita todo esto como Figma: una dirección, una caja de 3×3 de **Alineación** (haz clic donde quieres los hijos), la separación (o una forma de repartirlos), el relleno y **Llenar a lo ancho**. Ver [Propiedades](https://albakore.github.io/pyzed/es/editor/properties/#auto-layout). Cada hijo dice en su sección Posición si su **Ancho** y su **Alto** son **Fijo** (su propio tamaño) o **Llenar** (el sitio que deja el contenedor); un hijo que llena a lo ancho ignora la **Alineación** del contenedor para sí mismo (`align_self` en el `.pzform`). Cada hijo recibe además un campo **Crecer** en su sección Posición: la parte del espacio libre que ocupa (0 mantiene su tamaño). Dos hijos con crecer 1 se reparten el espacio libre a medias; 2 y 1, en tercios. Cuando los hijos no caben, todos encogen en proporción a su tamaño, como `flex-shrink` de CSS. Un **Espaciador** (Spacer) es un control vacío con crecer 1: ponlo entre dos botones y el segundo queda empujado al final de la fila. #### Grilla | Propiedad | Significado | | --- | --- | | **Columnas** | Número de columnas del mismo ancho. | | **Separación** | Espacio entre celdas, en los dos sentidos. | | **Relleno** | Espacio por dentro del borde del contenedor. | | **Alto de fila** | Alto de cada fila; **0** reparte el alto del contenedor entre las filas. | | **Alineación** | Cómo se coloca un hijo en su celda: `stretch` la llena; si no, `start`, `center` o `end` con su propio tamaño. | | **Fondo** | Un color detrás de las celdas. | Cada hijo recibe **Columnas**: cuántas columnas ocupa (1 a 24). Un hijo que no cabe en lo que queda de fila empieza una nueva. #### Dock Un **Dock** (categoría Disposición) es la disposición de Zed y del propio Pyzed Design: un área central con paneles a la izquierda, a la derecha, arriba y abajo. Un Dock nuevo trae tres **Paneles del dock**: Explorer a la izquierda, Terminal abajo y Editor en el centro. Suelta controles en un panel como en cualquier contenedor libre (los anclajes funcionan dentro). | Dónde | Propiedad | Significado | | --- | --- | --- | | Panel del dock | **Título**, **Icono** | Lo que muestra su pestaña. | | Panel del dock | **Lado** | `left`, `right`, `top`, `bottom` o `center` (sin pestaña, entre los demás). | | Dock | **Ancho izquierdo**, **Ancho derecho**, **Alto superior**, **Alto inferior** | El tamaño de cada lado, con las pestañas. | | Dock | **Distribución guardada** | Dónde dejó el usuario los paneles (se rellena al ejecutar; guárdala con un componente Ajustes). | Los paneles del mismo lado comparten su sitio como pestañas; el de más arriba (el último en el orden del esquema) es la pestaña visible, así que usa **Traer al frente** o el esquema para editar los demás. Al ejecutar el formulario, las pestañas cambian de panel, arrastrar una pestaña a otro lado la mueve allí, el clic derecho en una pestaña ofrece los mismos movimientos y arrastrar el borde interior de un lado cambia su tamaño. Cada cambio lanza el evento `layout` del dock con la distribución nueva. ```python def dock1_layout(form, event): """Se ejecuta al mover, redimensionar o cambiar un panel de dock1.""" form.status.text = f"Lado izquierdo: {event.value['sizes'].get('left', 220):.0f} px" ``` #### El orden de los hijos En un contenedor flex o grilla importa el orden, no la posición: los hijos no tienen X ni Y. Para reordenarlos, arrastra un hijo dentro del contenedor en el lienzo (cae en el sitio que hay bajo el puntero) o arrastra su fila en la estructura, que los lista en orden de disposición. #### Anclajes: disposiciones libres que se adaptan En el formulario y en los contenedores libres, los **anclajes** deciden qué pasa cuando la ventana (o el contenedor) cambia de tamaño al ejecutarse, como en WinForms. Anclado a izquierda y derecha, un control se estira; anclado solo a la derecha, sigue ese borde; sin anclar a ninguno, se queda centrado. Lo mismo en vertical con arriba y abajo. Se eligen en la sección **Posición** de [Propiedades](https://albakore.github.io/pyzed/es/editor/properties/#posición) en una cuadrícula de 16 presets, como en Unity, Unreal o Godot, y a un control estirado se le puede dar un **tamaño mínimo** y **máximo** para que deje de crecer en un ancho legible (su **pivote** dice qué lado conserva entonces). El formulario en ejecución los aplica todos; en código sin el diseñador, [`pz.anchor_layout`](https://albakore.github.io/pyzed/es/guides/layout/#disposiciones-ancladas) hace lo mismo. Los anclajes también funcionan mientras diseñas: al cambiar el tamaño del formulario (o de un componente) con su esquina o con **Ancho** y **Alto**, o al redimensionar un contenedor libre, lo que hay dentro se mueve y se estira igual que en la ventana en ejecución, con límites de tamaño y pivote incluidos. Desactívalo con el botón del ancla de la barra de herramientas (**Redimensionar el contenido con sus anclajes**) para cambiar el tamaño sin tocar los controles. Una receta habitual para una ventana redimensionable: 1. Una **columna flex** anclada por los cuatro lados llena la ventana. 2. Dentro, filas con **crecer** en la parte que debe quedarse con el espacio extra (un área de texto, una lista). 3. Una **fila flex** abajo con un **espaciador** y los botones, para que queden a la derecha. :::tip Las plantillas de **Nuevo formulario** usan estas recetas: *dialog* pone sus botones en una fila anclada a izquierda, derecha y abajo; *sidebar* usa una columna anclada a izquierda, arriba y abajo junto a otra anclada por todos lados; la plantilla *settings* es una grilla de dos columnas. Empieza desde una y mira sus propiedades. ::: ### Estilos y temas > Da estilo a los controles en Pyzed Design: la sección Estilo, un selector de color con degradados y máscaras, el estilo global y los temas. Source: https://albakore.github.io/pyzed/es/editor/styles-and-themes/ El aspecto de un formulario sale de tres capas, de la más general a la más concreta: ```mermaid flowchart LR T["Tema
(One Dark, Gruvbox...)"] --> L["Aspecto del formulario
acento · fondo · radio · tamaño de letra"] L --> G["Estilo global
la sección Estilo del formulario"] G --> C["Estilo del control
la sección Estilo de cada control"] ``` Cada capa solo cambia lo que fija; los campos vacíos heredan de la capa de arriba. #### Temas Elige el tema del formulario en **Propiedades → Aspecto → Tema** (antes haz clic en un hueco del lienzo). Aparecen todos los temas registrados: las familias One, Ayu y Gruvbox incluidas y cualquier tema que registre tu proyecto. Junto a él: - **Acento**: siete colores predefinidos, o **×** para el acento propio del tema. **Acento propio** acepta cualquier `#rrggbb`. - **Fondo**: un color de fondo para la ventana (vacío mantiene el del tema). - **Radio de esquina**: el radio que usan todos los controles (0 para esquinas rectas). - **Tamaño de letra**: el tamaño base de todo el texto. (Image: Un formulario con el tema Gruvbox Light y un acento propio) El tema del formulario es independiente del tema del **editor** (Ajustes → Apariencia → Tema) y del tema del **editor de código** (Editor settings → Theme): puedes diseñar un formulario claro en un editor oscuro. #### La sección Estilo Cada control tiene una sección **Estilo** en Propiedades, como CSS para ese control (ver [Estilos](https://albakore.github.io/pyzed/es/theming/styles/)): | Campo | Acepta | | --- | --- | | **Fondo** | Un color, un degradado lineal o un relleno con máscara: haz clic en la muestra o en el resumen para abrir el selector; **×** lo quita. | | **Color del texto** | Un token del tema (`accent`, `text_muted`, `success`...) o `#rrggbb` / `#rrggbbaa`. | | **Fuente** | Por defecto, sans, serif o mono. | | **Peso** | Por defecto, fina, ligera, normal, media, seminegrita, negrita, gruesa. | | **Cursiva** | Por defecto, sí o no. | | **Tamaño de letra** | Píxeles; 0 mantiene el tamaño propio del componente. | | **Interlineado** | Un múltiplo del tamaño de letra; 0 lo mantiene. | | **Color del borde**, **Grosor del borde** | El borde del control. | | **Radio de esquina** | Un número, o cuatro: arriba a la izquierda, arriba a la derecha, abajo a la derecha, abajo a la izquierda (dos números se repiten). | | **Sombra** | Un color; entonces aparecen **Desplazamiento** (x e y, de −64 a 64) y **Desenfoque** (de 0 a 96). | | **Relleno** | Espacio interior: un número, dos (vertical horizontal) o cuatro (arriba derecha abajo izquierda). | | **Margen** | Espacio exterior, con el mismo formato. | | **Ancho máximo**, **Alto máximo** | 0 significa sin límite. | | **Alinear contenido**, **Alinear en vertical** | Dónde queda el contenido dentro de la caja del control: inicio, centro o final. | | **Recortar** | Corta lo que se sale de la caja. | Los campos vacíos mantienen el aspecto propio del componente. **Restablecer el estilo** (al final de la sección, cuando hay algo fijado) borra todos los cambios de ese control. Los valores a medio escribir (`#3b`) se ignoran al dibujar en lugar de fallar, así que puedes escribir con calma. (Image: Un botón con un degradado con máscara, una fuente global serif y el selector de color abierto) #### El selector de color Haz clic en la muestra de un color para abrir el selector, flotando junto al panel; haz clic fuera para cerrarlo. - **Cuadro de saturación y valor**, **barra de tono** y **barra de alfa**, el color actual con un campo hex (escribe `#rrggbb` o `#rrggbbaa`) y su opacidad en porcentaje. - **Muestras** del tema del formulario: acento, éxito, aviso, error, información, texto, texto atenuado y fondo. - En un **fondo**, **Solid / Gradient** cambia a un degradado lineal: haz clic en la barra de paradas para añadir una con el color que tiene el degradado en ese punto, arrastra una parada para moverla, selecciónala para cambiar su color, fija el **Angle** (de 15° en 15° con los botones) y quita la parada seleccionada (un degradado conserva al menos dos). - En un fondo, **Mask** desvanece el relleno de opaco a transparente: su propia barra de paradas con una **Opacity** por parada (hasta 8 paradas) y un ángulo. :::tip El selector trabaja con colores concretos: al elegir, un token del tema se convierte en su valor hex. Para mantener un color que siga al tema (y que el formulario se vea bien en un tema claro y en uno oscuro), escribe el token (`accent`, `text_muted`, `surface`...) en el campo junto a la muestra. ::: #### El estilo global del formulario Sin nada seleccionado, la sección **Estilo** del formulario es el estilo global de la ventana: - **Color del texto, fuente, peso, cursiva, tamaño de letra e interlineado** se aplican a todo texto que no fije los suyos. - **Fondo, color del borde, radio de esquina y relleno** pasan a ser los de la ventana. Funciona igual al ejecutar (`Window.style`), y también en los componentes hechos en el editor. Una fuente global serif, por ejemplo, cambia de golpe cada etiqueta, botón y campo del formulario. #### Colores como propiedades Algunos componentes tienen además sus propias propiedades de color (el **Color** de una etiqueta, el **Fondo** de un panel, el **Color** de un icono). Aceptan los mismos valores: un token del tema o un color hex, con una muestra que lo previsualiza en el tema del formulario. ### Animaciones y efectos > Da a los controles sombra, desenfoque, glow, bloom, glass y opacidad en Pyzed Design, y anímalos al aparecer, con el ratón, al hacer clic, con un valor nuevo, una regla que falla o el evento de otro control. Source: https://albakore.github.io/pyzed/es/editor/animations-and-effects/ Cada control visual tiene dos secciones al final del panel **Propiedades**, después de su estilo: **Efectos** y **Animación**. Lo que fijes ahí se guarda en el `.pzform` y se reproduce cuando el formulario se ejecuta. Es el mismo sistema que [`widget.animate()` y los efectos de estilo](https://albakore.github.io/pyzed/es/animation/) de la biblioteca. (Image: Las secciones Efectos y Animación de un botón) #### Efectos | Campo | Qué hace | | --- | --- | | **Opacidad** | Cuánto se ve el control y todo lo que tiene dentro (1 es opaco). | | **Sombra** | Una sombra proyectada: elige un color y luego **Desplazamiento** (X e Y) y **Desenfoque**. | | **Desenfoque** | Desenfoca el control y todo lo que contiene, en píxeles. | | **Desenfoque del fondo** | Desenfoca lo que hay detrás de la caja, en píxeles. | | **Resplandor** | Un halo suave alrededor de la caja. Elige un color (o un token del tema como `accent`); **Tamaño del resplandor** fija cuánto se extiende. | | **Bloom** | Luz que desborda de las partes brillantes del control. **Intensidad** va de 0 a 1. | | **Cristal** | Un panel translúcido que desenfoca lo que tiene detrás, con un borde claro. **Matiz** lo colorea e **Intensidad del tinte** fija cuánto se nota el tinte. | La rotación está en la sección **Posición**. Los subcampos solo aparecen mientras el efecto está activo. Borra el color (o apaga **Cristal**) para quitarlo. Los efectos son propiedades de estilo (`opacity`, `shadow`, `blur`, `backdrop_blur`, `glow`, `bloom`, `glass`), así que en el código aparecen en `control.style` y **Restablecer el estilo** también los quita. El cristal luce mejor sobre un fondo colorido, como un panel con degradado. #### Animación Una animación son dos decisiones: **cuándo** se reproduce y **qué** se reproduce (un preset como *Aparecer subiendo*, *Latido* o *Sacudida*). Un control sin animaciones muestra algunas listas para añadir con un clic (**Aparecer al mostrarse**, **Elevar al pasar el ratón**, **Latir al hacer clic**, **Sacudir si el valor no es válido**...), solo las que encajan con el control, y **Añadir animación** para cualquier otra. Cada fila es una animación: un icono de lo que la reproduce, un resumen (**Aparecer subiendo · 280 ms**, con **+retardo** y **×repeticiones** si las tiene) y el disparador debajo. El botón **▶** la previsualiza y la papelera la quita. Haz clic en una fila para abrir su editor: | Campo | Significado | | --- | --- | | **Cuándo** | Qué la reproduce (ver la tabla de abajo). Debajo del campo, una línea explica cómo empieza la elegida. | | **Control**, **Evento** | Solo con **Con un evento de otro control**: qué control (o el formulario) y cuál de sus eventos. | | **Animación** | El preset. Primero salen los que encajan con el disparador (entradas para *al aparecer*, interacciones para encima y pulsar, de atención para clics y eventos); el tooltip describe el elegido. | | **Duración**, **Retardo** | Milisegundos. Vacío mantiene el timing propio del preset. | | **Curva** | Cómo cambia la velocidad por el camino (suave, lineal, frenar cúbica, muelle...). | | **Repetir** | 1, 2, 3, 5 o infinito. | | Cuándo | Se reproduce | | --- | --- | | **Al aparecer** | Una vez, al abrir el formulario o al mostrarse el control. | | **Con el ratón encima**, **Mientras se pulsa**, **Mientras tiene el foco** | Hacia delante mientras se cumple la condición, y hacia atrás cuando termina. | | **Al hacer clic**, **Al hacer doble clic**, **Al hacer clic derecho** | Una vez en cada clic. | | **Cuando entra el puntero**, **Cuando sale el puntero** | Una vez cada vez que el ratón entra o sale. | | **Cuando cambia su valor** | Una vez con cada valor nuevo (al escribir, al elegir otra opción, al cambiar un interruptor...). | | **Al pulsar Enter** | Una vez cuando el usuario envía un campo de texto. | | **Cuando su valor no es válido** | Una vez cada vez que falla una [regla de validación](https://albakore.github.io/pyzed/es/editor/data-binding/): la clásica *sacudida* con una contraseña mal escrita. | | **Con un evento de otro control** | Una vez con cada evento de otro control o del formulario: una etiqueta que late al pulsar **Guardar**, una tarjeta que aparece al cargar el formulario. | | **Al reproducirla desde el código** | Solo cuando un manejador llama a `play_animation()` (ver abajo). | Los disparadores por evento funcionan aunque el control no tenga manejador para ese evento, y se reproducen antes de que corra el manejador. **Añadir animación** añade una fila (un *aparecer subiendo* al aparecer) y la abre. Elegir otro preset restablece el timing de la fila al del nuevo preset. Todos los cambios se pueden deshacer. ##### Reproducirla desde tu código Elige **Al reproducirla desde el código** y la animación espera a tus manejadores: no se reproduce al abrir el formulario. La fila abierta muestra la línea que la reproduce, con un botón para copiarla. Pégala en cualquier manejador (el clic de un botón, el tick de un temporizador, una validación que falla): ```python def sign_in_click(form, event): if not form.password.validate(): form.password.play_animation() # reproduce las animaciones "desde el código" del campo ``` `play_animation()` también repite las animaciones **Al aparecer** del control. Pásale un nombre de preset, un `pz.Animation` o valores de propiedades para reproducir algo que el control no tiene en sus filas, y usa `stop_animation()` para parar lo que inició (un bucle, por ejemplo): | Llamada | Qué se reproduce | | --- | --- | | `form.title.play_animation()` | Sus filas **Al reproducirla desde el código** (y repite las de **Al aparecer**). | | `form.title.play_animation("tada")` | Ese preset, una vez. | | `form.badge.play_animation("pulse", repeat=3, duration=400)` | Un preset con otro tiempo (cualquier [opción de `animate()`](https://albakore.github.io/pyzed/es/animation/animate/#opciones-de-tiempo)). | | `form.card.play_animation(scale=1.05, duration=200)` | Hasta esos valores y de vuelta. | | `form.spinner.stop_animation()` | Para lo que inició `play_animation`. | [Empieza aquí: aplicar y reproducir](https://albakore.github.io/pyzed/es/animation/start/) lo compara con `pz.Trigger`, la forma de hacer lo mismo en una app solo con código. ##### Vista previa El lienzo se queda quieto mientras editas, para que los controles no se muevan bajo el ratón. **Vista previa** (al final de la sección, o **▶** en una fila) reproduce una vez en el lienzo las animaciones del control, como si acabara de aparecer; **Detener** termina la previsualización. Ejecuta el formulario (**F5**) para ver cada animación con su disparador real. #### En código Las animaciones se guardan como datos simples en el control: ```json "animations": [ {"preset": "fade_in_up"}, {"preset": "lift", "on": "hover", "duration": 180} ] ``` Los valores de `"on"` son `mount` (por defecto), `hover`, `press`, `focus`, `click`, `double_click`, `right_click`, `enter`, `leave`, `change`, `submit`, `invalid`, `event` y `manual` (**Al reproducirla desde el código**). Una entrada `event` nombra además su origen y su evento: `{"preset": "tada", "on": "event", "source": "save", "event": "click"}` (un `source` vacío es el formulario). `Document.set_animations(name, animations)` las cambia desde un script y `Control.animations` las lee. Para reproducir una desde un manejador, llama a `form..play_animation(...)` (ver arriba); para crear tus propios presets, usa `pz.Animation` y `define_animation` de la biblioteca (ver la [lista de presets](https://albakore.github.io/pyzed/es/animation/presets/)). #### Animaciones con línea de tiempo Para animaciones que mueven varios controles a la vez (una entrada, una transición entre estados), abre el panel **Animation** desde **View → Animation** (`Ctrl+Shift+M`). Edita **clips**: líneas de tiempo guardadas en el `.pzform` que se reproducen igual que con [`pz.AnimationPlayer`](https://albakore.github.io/pyzed/es/animation/timeline/) en código. (Image: El panel Animation: al arrastrar el cabezal por un clip intro, el título baja, el subtítulo aparece y el botón Sign in crece en el formulario de arriba) Con el panel vacío se ven los tres pasos (añadir una pista, añadir keys, reproducirlo) y un botón **Nueva animación**. | Acción | Resultado | | --- | --- | | **Añadir pista para …** | Añade una pista para una propiedad del control seleccionado (opacidad, posición, escala, colores, tamaño…) o cualquiera de sus propiedades, como el texto de una etiqueta. | | **Clic** en cualquier punto de la regla o las pistas, o **arrastrar** | Mueve el cabezal; el lienzo muestra el formulario en ese tiempo. Se pega a las keys cercanas. | | **Doble clic** en una pista | Añade una key en ese tiempo con el valor que la pista tiene ahí. | | **Arrastrar** una key | La mueve en el tiempo. Puedes agarrarla desde unos píxeles de distancia (el puntero se vuelve una mano) y se pega a las keys de las otras pistas, al principio y al final. Una etiqueta muestra su tiempo y su valor mientras la arrastras o pasas por encima. | | **Clic** en una key | La selecciona: edita su tiempo, su valor y su curva debajo de las pistas. | | **Clic derecho** en una key | La borra. | | Pista **Llamadas** | Escribe el nombre de un manejador y pulsa Intro (o haz doble clic en la pista) para llamar a ese método del archivo de código del formulario en el cabezal. Arrastra una llamada para moverla; clic derecho para quitarla. | | **Duración**, **bucle**, **autoplay** | Indican cuánto dura el clip, si se repite y si se reproduce al abrir el formulario. | Con el puntero sobre el panel, el teclado edita la línea de tiempo: | Tecla | Hace | | --- | --- | | `Supr` / `Retroceso` | Borra la key seleccionada. | | `Espacio` | Reproduce o pausa la vista previa. | | `←` / `→` | Mueve la key seleccionada (o el cabezal) 0,01 s; con `Mayús`, 0,1 s. | | `Inicio` / `Fin` | Salta al principio o al final. | (Image: Añadir una pista de escala al título desde el panel, hacer doble clic en su pista para añadir una key, arrastrarla antes y elegirle Ease out back) Los controles dentro de un contenedor animado siguen su opacidad, posición y desenfoque. Todos los cambios se pueden deshacer. ##### Cómo empieza un clip Un clip se reproduce de una de estas dos formas, y la barra bajo las pistas dice cuál se aplica al clip que editas: - **⚡ Reproducir al abrir el formulario** (en la barra de herramientas) lo convierte en el clip de autoplay del formulario: "Se reproduce al abrir el formulario." - Si no, espera a un manejador. La barra muestra la línea a llamar, con un botón para copiarla: ```python def replay_click(form, event): form.animations.play("intro", restart=True) # desde el principio, aunque esté sonando # también: form.animations.pause(), .resume(), .seek(0.5), .play_backwards("intro"), .queue("outro") ``` `form.animations` es el [`pz.AnimationPlayer`](https://albakore.github.io/pyzed/es/animation/player/) del formulario: también pausa, salta a un tiempo, encola clips y avisa cuando uno termina. La [sección Animación](https://albakore.github.io/pyzed/es/animation/) explica a fondo los clips, las pistas, las keys, las curvas y el reproductor. ### Eventos y manejadores > El panel Eventos de Pyzed Design: todos los eventos de cada componente y formulario, y cómo escribir manejadores en Python con un doble clic. Source: https://albakore.github.io/pyzed/es/editor/events/ Los controles lanzan **eventos** (un clic, un cambio de texto, un tic del temporizador) y cada evento se puede **conectar** a una función de Python, su **manejador**, en el archivo de código del formulario (`login.py` junto a `login.pzform`). #### El panel Eventos El panel **Eventos** (dock derecho, junto a Propiedades) lista los eventos del control seleccionado, o del formulario cuando no hay nada seleccionado, con el manejador conectado a cada uno (`—` si no tiene). (Image: El panel Eventos de un cuadro de texto: submit está conectado a password_submit) | Acción | Resultado | | --- | --- | | **Doble clic** en un evento, o su botón **`<>`** (**Crear el manejador** / **Editar el código**) | Lo conecta si hace falta (creando el esqueleto del manejador) y abre el manejador en el editor de código. | | Botón **Desconectar** (solo eventos conectados) | Desconecta el evento **y borra el código de su manejador**. | | Pasar el ratón por un evento | Un tooltip dice cuándo se ejecuta y qué contiene `event.value`. | | El botón del **libro** de la cabecera | Abre la página del control en la [referencia](https://albakore.github.io/pyzed/es/editor/reference/), con cada evento y el nombre de su manejador (Shift+F1). | El evento que estás editando aparece resaltado. Con varios controles seleccionados, el panel pide que elijas uno: los eventos pertenecen a un solo control. ##### Atajos para llegar - **Doble clic en un control** del lienzo: abre el manejador de su **evento principal** (el primero: `click` en un botón, `change` en un cuadro de texto, `tick` en un temporizador). - Enter con un control seleccionado hace lo mismo. - **View code** en el menú contextual del control. - F7 (**Editar el código**) abre el archivo entero. Los manejadores se llaman `_` (`sign_in_click`), o `form_` para el formulario, y el esqueleto lleva un docstring que dice cuándo se ejecuta. Al renombrar un control se renombran los manejadores que siguen esta convención. #### Todos los eventos Cada control visible tiene tres eventos comunes además de los suyos: | Evento | Se ejecuta cuando | `event.value` | | --- | --- | --- | | `mouse_enter` | El puntero entra en el control. | `None` | | `mouse_leave` | El puntero sale del control. | `None` | | `right_click` | Se hace clic derecho en el control. | `None` | | Componente | Eventos propios | | --- | --- | | Etiqueta de texto, Imagen, Icono, Avatar, Contador, Punto de estado, Grupo de avatares, Código QR, Imagen SVG | `click`, `double_click` | | Botón, Enlace | `click` | | Etiqueta (chip) | `click`, `remove` (su x, con **Se puede quitar** activado) | | Botón con menú | `click` (la parte principal), `choose` (una opción de su menú: su texto), `menu` (se abre el menú) | | Cuadro de texto, Buscador | `change` (el texto nuevo), `submit` (se pulsó Enter) | | Área de texto | `change` (el texto nuevo) | | Casilla, Interruptor | `change` (`True` o `False`) | | Opciones, Desplegable, Segmentado | `change` (la opción elegida) | | Pestañas | `change` (la pestaña elegida), `close` (una pestaña cerrada con su x con **Se puede cerrar** activado: su nombre) | | Grupo de botones | `change` (el valor pulsado, o la lista de valores pulsados con **Varios** activado) | | Deslizador | `change` (el número nuevo mientras se arrastra), `release` (se soltó: guarda o aplica aquí) | | Número | `change` (el número nuevo) | | Lista | `change` (el elemento elegido), `open` (doble clic en un elemento) | | Tabla | `change` (el índice de la fila elegida), `open` (doble clic en una fila: su índice), `sort` (clic en un encabezado con **Ordenable** activado: su título) | | Árbol | `change` (la ruta del elemento elegido, como `"Docs/README.md"`), `toggle` (se pliega o despliega un elemento), `open` (doble clic en un elemento sin hijos) | | Ruta de navegación | `click` (el índice de la parte, 0 para la primera) | | Markdown | `link` (la dirección del enlace pulsado; sin manejador, el enlace se abre en el navegador) | | Selector de color, Selector de iconos | `change` (el color o el nombre de icono nuevo) | | Dock | `layout` (se movió, redimensionó o cambió un panel: el dict de la distribución, que también queda en **Distribución guardada**) | | Temporizador, Atajo, Vigilante de archivos, Proceso, Tarea en segundo plano, Petición web, Portapapeles, Ajustes, Diálogo de archivos, Notificación | Los suyos (`tick`, `activate`, `change`, `output`, `done`, `response`...): mira [Componentes no visuales](https://albakore.github.io/pyzed/es/editor/non-visual-components/). No tienen eventos de ratón. | | Título, Barra de progreso, Insignia, Indicador de carga, Aviso, Atajo, Métrica, Gráfico, Estado vacío, Resumen de cambios, Visor de imágenes, Salida de terminal, contenedores, Espaciador, Separador | Solo los comunes | El propio formulario: | Evento | Se ejecuta cuando | `event.value` | | --- | --- | --- | | `load` | Una vez, al abrirse la ventana. | `None` | | `close` | La ventana se está cerrando. Devuelve `False` para mantenerla abierta. | `None` | | `key` | En cada pulsación de tecla. | La combinación, como `"ctrl-s"` | | `resize` | La ventana cambia de tamaño. | `(ancho, alto)` | Los [componentes diseñados](https://albakore.github.io/pyzed/es/editor/component-designer/) y las clases `pz.Component` añaden sus propios **eventos personalizados** cuando los colocas en un formulario. La pestaña Eventos los muestra en una sección **Eventos personalizados**, aparte de los **Eventos incluidos** del control. #### Escribir manejadores Un manejador recibe el `form` en ejecución y un `event`. Los controles son atributos del formulario y sus propiedades son atributos del control; al cambiarlas se actualiza la ventana. ```python def sign_in_click(form, event): if "@" not in form.email.text: form.status.text = "Escribe un correo válido." return form.email.focus() # los manejadores pueden devolver comandos form.sign_in.enabled = False form.progress.show() form.ticker.running = True # un Temporizador empieza a hacer tic ``` | Nombre | Qué es | | --- | --- | | `event.sender` | El control (o formulario) que lanzó el evento. | | `event.name` | El evento (`"click"`, `"change"`...). | | `event.value` | Su valor: texto nuevo, estado marcado, combinación de teclas... | | `form.` | Un control; lee y escribe sus propiedades (`form.email.text`). | | `form..visible`, `.enabled`, `.x`, `.y`, `.width`, `.height`, `.anchor` | Sus campos de disposición y comportamiento. | | `form..show()`, `.hide()` | Lo muestra o lo oculta. | | `form..focus()` | Un comando que le da el foco (cuadros y áreas de texto). | | `form.title`, `form.name`, `form.size` | El título de la ventana (asignarlo la renombra), el nombre del formulario y el tamaño actual. | | `form.close()` | Un comando que cierra la ventana. | | `form.open("details", data=...)` | Un comando que abre otro formulario en una ventana nueva; recibe `data` como `form.data`. | | `form.window` | La ventana de Pyzed que ejecuta este formulario. | Los manejadores pueden devolver lo mismo que cualquier callback de Pyzed: un `Command`, una lista de comandos, una corrutina (para trabajo asíncrono) o un generador. Las funciones auxiliares al principio del archivo, los imports y los demás módulos del proyecto funcionan como siempre. ```mermaid sequenceDiagram actor Usuario participant Iced as Iced (Rust) participant RT as FormRuntime participant H as login.py Usuario->>Iced: hace clic en "Sign in" Iced->>RT: callback de sign_in.click RT->>H: sign_in_click(form, event) H->>RT: form.status.text = "..." RT->>Iced: view() con las propiedades nuevas Iced->>Usuario: redibuja la ventana ``` ##### Cuando un manejador falla Si un manejador lanza una excepción, la ventana en ejecución no se cierra: aparece un aviso **Event handler error** abajo con la última línea del error (ciérralo con su ×), y la traza completa va a la [consola](https://albakore.github.io/pyzed/es/editor/console-and-running/). Si un evento está conectado a una función que no está en el archivo de código, el aviso lo dice. :::tip Escribe `form.` en un manejador y la lista de sugerencias ofrece los controles del formulario; `form.email.` lista las propiedades de ese cuadro de texto, y `event.` los campos del evento. Ver [Editor de código](https://albakore.github.io/pyzed/es/editor/code-editor/#autocompletado). ::: #### Abrir otras ventanas ```python def details_button_click(form, event): return form.open("details", data=form.order_id.text) # details.py def form_load(form, event): form.title = f"Pedido {form.data}" ``` El nombre se busca junto al formulario actual y luego en la carpeta `ui/` del proyecto. Crea el segundo formulario con **Nuevo formulario** (Ctrl+N); la plantilla *Diálogo* trae **Aceptar** y **Cancelar** ya conectados a `form.close()`. ### Componentes no visuales > Temporizadores, atajos de teclado, vigilantes de archivos, procesos, tareas en segundo plano, peticiones web, el portapapeles, ajustes, diálogos de archivos, notificaciones, iconos de bandeja, información del sistema, el lanzador y los monitores de red y batería en Pyzed Design: componentes con propiedades y eventos que no dibujan nada. Source: https://albakore.github.io/pyzed/es/editor/non-visual-components/ Algunas partes de una app tienen **propiedades y eventos pero nada que mostrar**: un temporizador que hace tick, una carpeta que vigilas, un comando que corre en segundo plano. Visual Basic y Qt Designer las guardaban en una **bandeja bajo el formulario**, y Pyzed Design hace lo mismo. No se dibujan en el formulario, ni al diseñar ni al ejecutar: solo aparecen en la bandeja y en el **Outline**, donde pertenecen al formulario. (Image: Añadiendo un Temporizador, un Vigilante de archivos y una Petición web desde la caja de herramientas: cada uno va a la bandeja bajo el formulario y un doble clic abre su handler de tick) #### Añadir uno 1. Búscalo en la caja de herramientas, en **Componentes** o **Sistema** (o escribe su nombre en **Buscar componentes…**). 2. **Haz clic**: va directo a la bandeja, seleccionado y con sus propiedades a la derecha. Arrastrarlo al formulario hace lo mismo; nunca queda dentro de un contenedor. 3. Ajusta sus propiedades en el panel **Propiedades** y haz **doble clic** en su ficha para escribir el handler de su evento principal (`tick` en un temporizador, `output` en un proceso...). El panel **Eventos** los lista todos. | En la bandeja | Resultado | | --- | --- | | Clic en una ficha | Selecciona el componente. | | Doble clic en una ficha | Abre el handler de su evento principal. | | Clic derecho en una ficha | Ver código, Ayuda, Cortar, Copiar, Duplicar, Activar o Desactivar, Eliminar. | En el **Outline**, los componentes no visuales aparecen después de los controles, bajo **No visuales**. No tienen posición, tamaño, estilo ni animaciones: solo sus propias propiedades y **Habilitado** (un componente desactivado no lanza eventos). Copiar, pegar, deshacer y renombrar funcionan como con cualquier control, y al renombrar uno se renombran sus handlers. #### Los componentes | Componente | Qué hace | Eventos | | --- | --- | --- | | **Temporizador** | Lanza `tick` cada `interval` milisegundos mientras `running` está activo. Desactiva `repeat` para un solo tick (un retardo). | `tick` (la cuenta) | | **Atajo** | Un atajo de teclado para toda la ventana: `keys` como `ctrl-s`, varios separados por comas. `mod-s` es Cmd+S en macOS y Ctrl+S en el resto. | `activate` (la combinación) | | **Vigilante de archivos** | Vigila una carpeta (`path`), los archivos que encajan con `pattern` (`*.csv`, varios separados por `;`), con `recursive` para las subcarpetas. | `change` (las rutas cambiadas), `created`, `modified`, `deleted` (una ruta) | | **Proceso** | Ejecuta `command` en `directory` y entrega lo que imprime, línea a línea. `autostart` lo inicia al abrir el formulario. | `output`, `error_output` (una línea), `finished` (código de salida), `started` (id del proceso), `failed` | | **Tarea en segundo plano** | Ejecuta trabajo lento en un hilo aparte para que la ventana siga respondiendo. | `work` (en el hilo), `progress`, `done` (el resultado), `failed` (la excepción) | | **Petición web** | Llama a `url` con `method`, `headers` (una línea `Nombre: valor` por cabecera) y `body`, en segundo plano. | `response` (la respuesta), `failed` (sin respuesta) | | **Portapapeles** | Copia y pega texto; con `watch` se entera de lo que se copia, en esta app o en otra. | `change`, `paste` (el texto) | | **Ajustes** | Valores que se guardan entre ejecuciones (un JSON por usuario) y el valor de los controles de `remember`. | `change` (`(clave, valor)`) | | **Diálogo de archivos** | El diálogo del sistema: `mode` es `open`, `open_many`, `save` o `folder`; `filters` lleva una línea `Imágenes: png, jpg` por filtro. | `chosen` (la ruta o una lista), `cancelled` | | **Notificación** | Una notificación de escritorio con `title` y `text`. | `shown`, `failed` | | **Icono de bandeja** | Un icono en la bandeja del sistema con `icon`, `tooltip` y `menu`: un elemento por línea, `-` para un separador, `[x]` o `[ ]` para una casilla y líneas con sangría para un submenú. Aparece al abrir el formulario salvo que desactives **Mostrar al abrir el formulario**. En GNOME necesita la extensión AppIndicator. | `click`, `double_click` (Windows), `menu` (el texto del elemento), `failed` | | **Información del sistema** | El ordenador: `os`, `os_version`, `machine`, `hostname`, `user`, `home`, `cpu_count`, `language`, la memoria y el disco libre (de `disk_path`). Con `interval` vuelve a mirar cada cierto tiempo. | `update` (un diccionario con todo) | | **Lanzador** | Abre `target` con el programa que usa el sistema para eso: un archivo, una carpeta, una página web o un enlace `mailto:`. | `opened`, `failed` (el destino) | | **Monitor de red** | Si se llega a internet, probando `host`:`port` cada `interval` ms mientras `running`. | `change` (`True`/`False`), `online`, `offline` | | **Monitor de batería** | La carga de la batería y si está enchufada, cada `interval` ms. | `change` (`(porcentaje, enchufada)`), `low` (bajo `low_level`), `plugged`, `unplugged` | La página de cada componente en la referencia integrada (Shift+F1 con él seleccionado) lista todas sus propiedades y eventos con el nombre de su handler. #### Usarlos desde los handlers Son atributos del formulario como cualquier control. Sus propiedades se leen y escriben como atributos, y tienen **métodos**: ```python def run_click(form, event): if form.proc.running: form.proc.kill() else: form.proc.start(form.command.text) # o el comando puesto en el diseñador def proc_output(form, event): form.output.text += event.value + "\n" # una línea de lo que imprimió el programa def proc_finished(form, event): form.status.text = "Hecho." if event.value == 0 else f"Terminó con el código {event.value}." ``` | Componente | Métodos y estado | | --- | --- | | Temporizador | `start()`, `stop()`, `running`, `count` | | Atajo | `matches(combo)` | | Vigilante de archivos | `start()`, `stop()`, `running`, `check()` | | Proceso | `start(command=None)`, `write(text)`, `kill()`, `running`, `exit_code`, `pid` | | Tarea en segundo plano | `run(value)`, `cancel()`, `report(progress)`, `busy`, `cancelled`, `result` | | Petición web | `send(url=None, json=None, body=None)`, `busy`, `response` | | Portapapeles | `copy(text)` y `paste()` (comandos: devuélvelos), `text` | | Ajustes | `settings1["clave"]`, `settings1["clave"] = valor`, `get(clave, defecto)`, `set()`, `delete()`, `keys()` | | Diálogo de archivos | `show()` (un comando: devuélvelo), `path`, `paths` | | Notificación | `show(text=None)` | | Icono de bandeja | `show()` (devuelve si pudo), `hide()`, `visible`, `checked(item)`, `set_checked(item, value)` | | Información del sistema | `check()`, `env(name)`, `folder("documents")`, `memory_available`, `disk_free`, `is_windows`, `is_mac`, `is_linux` | | Lanzador | `open(target=None)`, `reveal(path)` (lo muestra en el explorador de archivos), `email(to, subject, body)` | | Monitor de red | `check()`, `online`, `start()`, `stop()` | | Monitor de batería | `check()`, `percent`, `plugged`, `has_battery` | Los eventos que ocurren en segundo plano (una línea de salida, un archivo que cambia, una respuesta web) llegan a tus handlers en el hilo de la ventana, así que pueden cambiar controles sin problema. La única excepción es el evento `work` de la tarea en segundo plano: ```python def count_click(form, event): form.primes.run(200_000) # empieza el trabajo en un hilo def primes_work(form, event): # corre en el hilo: no toques controles aquí found = 0 for n in range(2, event.value): if event.sender.cancelled: # se llamó a form.primes.cancel() return None if all(n % d for d in range(2, int(n ** 0.5) + 1)): found += 1 if n % 2000 == 0: event.sender.report(n * 100 // event.value) return found # llega a done def primes_progress(form, event): form.progress.value = event.value def primes_done(form, event): form.status.text = f"{event.value:,} primos" ``` Los comandos se devuelven, como en cualquier handler: ```python def open_click(form, event): return form.file_dialog1.show() # abre el diálogo del sistema def file_dialog1_chosen(form, event): form.editor.text = Path(event.value).read_text() def copy_click(form, event): return form.clipboard1.copy(form.editor.text) def http1_response(form, event): if event.value.ok: form.stars.text = str(event.value.json()["stargazers_count"]) ``` (Image: El ejemplo Task runner en marcha: el proceso entrega su salida, el temporizador mueve el reloj y la tarea en segundo plano llena la barra de progreso) El ejemplo **Task runner** (`examples/designer/tasks.pzform`) usa siete a la vez: un Temporizador para el reloj, un Proceso para el comando, una Tarea en segundo plano que cuenta primos, un Vigilante de archivos, unos Ajustes que recuerdan el comando, un Atajo (Ctrl+R) y una Notificación. Ábrelo con `pyzed design examples/designer/tasks.pzform`. #### Recordar lo que dejó el usuario Pon un componente **Ajustes** en el formulario y escribe nombres de controles en **Recordar**, uno por línea: al cerrar el formulario se guardan sus valores (el texto de una caja, el estado de una casilla, el valor de un deslizador...) y la próxima vez que se abra vuelven a estar ahí. El archivo es `..json` en la carpeta de ajustes de Pyzed de cada usuario, o el nombre de **Archivo**. Tus propios valores van en el mismo archivo: ```python def form_load(form, event): form.volume.value = form.prefs.get("volume", 50) def volume_change(form, event): form.prefs["volume"] = event.value ``` #### En código, sin el diseñador Cada componente es una clase de [`pyzed.services`](https://albakore.github.io/pyzed/es/guides/services/) (`pz.Timer`, `pz.Process`, `pz.FileWatcher`...), así que los mismos objetos funcionan en un `pz.App` escrito a mano o en un script sin ventana. :::tip Un **Proceso** o un **Vigilante de archivos** muy activos pueden llenar la consola de eventos: `output`, `error_output`, `progress` y `tick` nunca se registran, y **Ajustes → Ejecución** desactiva el registro de eventos para el resto. ::: ### Enlace de datos y validación > Enlaza controles a los datos en Pyzed Design, valida lo que escribe el usuario, traduce textos con @claves y previsualiza con otro tema o idioma. Source: https://albakore.github.io/pyzed/es/editor/data-binding/ Un formulario puede mostrar y editar **datos** sin una línea de código de pegamento: enlaza cada control a un campo de `form.data`, dale **reglas** a los controles de entrada y llama a `form.validate()` antes de usar los datos. #### Enlazar un control Selecciona un control y rellena **Enlazar a** en la sección **Datos** de [Propiedades](https://albakore.github.io/pyzed/es/editor/properties/). El campo es un nombre (`email`) o una ruta dentro de datos anidados (`customer.address.city`, `items.0.title`). (Image: La sección Datos de un cuadro de texto: enlazado a email, obligatorio, con patrón y mensaje) - Los **controles de entrada** se enlazan en los dos sentidos: muestran el campo y escriben de vuelta lo que el usuario teclea o elige. Los cuadros de texto, áreas de texto y búsquedas enlazan `text`; las casillas e interruptores `checked`; los grupos de opciones, desplegables, controles segmentados, barras de pestañas y listas `selected`; los deslizadores y cuadros numéricos `value`. - Los **controles de visualización** solo lo muestran: etiquetas, encabezados, insignias, enlaces, chips, markdown y avisos enlazan `text`, las estadísticas y barras de progreso `value`, las imágenes `source`, los gráficos `values` y las tablas `data`. - En un control de visualización, **Enlazar a** también puede ser una **expresión de Python** sobre los campos de los datos, que se vuelve a evaluar en cada renderizado: ```python f"Iniciando sesión como {email}" if email else "" f"{len(items)} artículos, {sum(i.price for i in items):.2f} €" ``` Los diccionarios anidados se leen con puntos (`customer.city`); `data` es el objeto entero y `form` el formulario en ejecución. Una ruta a un campo que todavía no existe deja el valor diseñado del control; una expresión que lanza una excepción también lo deja y muestra el error en la ventana. #### Los datos del formulario `form.data` es lo que pasó el formulario que abrió este, o un diccionario que el formulario crea por su cuenta: ```python def edit_click(form): return form.open("customer", data=selected_customer) # un dict, una dataclass, cualquier objeto ``` | Abierto con | `form.data` es | | --- | --- | | `data=` un dict o un objeto | Ese mismo objeto: lo que edita el usuario lo cambia en el sitio. | | Nada, con controles enlazados | Un dict nuevo, rellenado con los valores diseñados de los controles (`{"email": "", "remember": False}`). | Asignar `form.data = {...}` lo reemplaza y refresca todos los controles enlazados; así se carga un registro: ```python def form_load(form): form.data = database.load_customer(42) ``` #### Reglas de validación La sección **Datos** también tiene las reglas de los controles de dos sentidos. Las reglas dependen del tipo de control: | Regla | Controles | Falla cuando | | --- | --- | --- | | **Obligatorio** | Todos los controles de entrada | El valor está vacío (una casilla sin marcar, en casillas e interruptores). | | **Longitud mínima**, **Longitud máxima** | Cuadros de texto, áreas de texto, búsquedas | El texto es más corto o más largo. | | **Patrón** | Cuadros de texto, áreas de texto, búsquedas | El texto entero no cumple la expresión regular. | | **Mínimo**, **Máximo** | Deslizadores, cuadros numéricos | El número es menor o mayor. | | **Mensaje** | Todos los controles de entrada | Reemplaza el mensaje predeterminado de cualquier regla (`@clave` lo traduce). | Un valor vacío solo falla **Obligatorio**, así que un correo opcional con patrón acepta el campo en blanco. Los mensajes predeterminados ("Este campo es obligatorio", "Al menos 8 caracteres") siguen el idioma de la app. El formulario en ejecución comprueba las reglas de un control **mientras el usuario escribe** y muestra el mensaje en una burbuja bajo el control, que se marca con un borde rojo: (Image: El formulario de inicio de sesión en ejecución: el correo no cumple su patrón y la contraseña vacía es obligatoria) #### Validar desde el código ```python def sign_in_click(form): if not form.validate(): # comprueba cada control visible y habilitado y muestra los errores return auth.sign_in(form.data["email"], form.data["password"]) ``` | Código | Hace | | --- | --- | | `form.validate()` | Comprueba las reglas de cada control enlazado (se saltan los ocultos y deshabilitados) y devuelve `True` si todos pasan. | | `form.valid` | `True` mientras ningún control muestre un error. | | `form.errors` | `{"email": "Enter a valid email", ...}`: los controles que muestran un error. | | `form.email.validate()` | Comprueba un solo control; `True` si pasa. | | `form.email.error` | El mensaje del control, o `None`. Asigna un texto para mostrar tu propio error (`form.email.error = "Ya registrado"`) o `None` para quitarlo. | Así, una comprobación en el servidor se ve como cualquier otra: ```python async def sign_in_click(form): if form.validate() and not await api.email_free(form.data["email"]): form.email.error = "Ese correo ya está registrado" ``` #### Textos traducibles Cualquier propiedad de texto que empiece por `@` es una **clave de traducción**: una etiqueta cuyo texto es `@login.title` muestra `pz.t("login.title")` en el idioma de la app. `@@` escribe una `@` literal. Los elementos de una lista (las opciones de un desplegable, las pestañas de una barra) se traducen uno a uno, igual que el título del formulario y el mensaje de una regla. Los catálogos viven en una carpeta `locales/` junto al formulario, o en cualquier carpeta superior hasta la raíz del proyecto (`pyproject.toml` o `.git`), un archivo por idioma: ```json title="locales/es.json" {"login": {"title": "Iniciar sesión", "email": "Correo", "sign_in": "Entrar"}} ``` El diseñador y el formulario en ejecución los cargan solos. Consulta [Traducciones](https://albakore.github.io/pyzed/es/guides/i18n/) para plurales, idiomas de respaldo y cambiar el idioma en tiempo de ejecución. #### Vista previa con otro tema o idioma **Ver → Vista previa con tema…** y **Ver → Vista previa con idioma…** eligen un tema y un idioma para el lienzo sin tocar el formulario: el formulario conserva su tema y la app su idioma, y la primera entrada de cada selector vuelve a ellos. (Image: El formulario de inicio de sesión con One Light y en español, con el selector de idioma abierto) Mientras hay una vista previa activa, la barra de estado la muestra (haz clic para elegir otra) y **Ejecutar** (F5) arranca el formulario con el mismo tema e idioma, para probar todo el flujo en español o con un tema claro. El formulario en ejecución los lee de las variables de entorno `PYZED_PREVIEW_THEME` y `PYZED_PREVIEW_LANGUAGE`, que también funcionan desde una terminal: ```bash PYZED_PREVIEW_LANGUAGE=es PYZED_PREVIEW_THEME="One Light" uv run python main.py ``` #### Copiar controles entre formularios Copiar (Ctrl+C) pone los controles seleccionados en el **portapapeles del sistema**, con sus enlaces y reglas, para pegarlos en otro formulario, en otra pestaña u otra ventana de Pyzed Design. Consulta [Caja de herramientas y lienzo](https://albakore.github.io/pyzed/es/editor/toolbox-and-canvas/#portapapeles-e-historial). ### Editor de código > El editor de Python de Pyzed Design: autocompletado, información al pasar el ratón, ir a la definición, renombrar, arreglos rápidos, diagnósticos, code lens, sticky scroll, plegado, minimapa, servidores de lenguaje ty y ruff, teclas y ajustes. Source: https://albakore.github.io/pyzed/es/editor/code-editor/ El panel **Código** (dock inferior) es un editor de Python respaldado por dos servidores de lenguaje de Astral: **ty** (tipos, autocompletado, referencias) y **ruff** (lint y formato). Aparece cuando abres un manejador o un archivo del proyecto y se va cuando lo cierras. (Image: Escribiendo el código del evento click de un botón) #### Qué edita El editor muestra **un manejador** o **el archivo de código entero**: | Modo | Cómo llegar | Título | | --- | --- | --- | | Un manejador | Doble clic en un control o en un evento, Enter con un control seleccionado, **View code**, un método del [panel Component](https://albakore.github.io/pyzed/es/editor/component-designer/#el-panel-component). | `login.py › sign_in_click()` | | Archivo entero | F7, **Editar el código** (barra de herramientas, menú ☰, paleta), o **Whole file** en la cabecera. | `login.py` | | Un archivo del proyecto | Un clic en un archivo de texto de la [pestaña Proyecto](https://albakore.github.io/pyzed/es/editor/projects/#archivos), o Ctrl+P. | `src/shop/cart.py` | **Whole file** y **This handler** en la cabecera cambian entre los dos. En modo manejador solo ves la función, pero los servidores de lenguaje siempre ven el módulo entero con ese manejador dentro, así que el autocompletado y los diagnósticos conocen los imports y las funciones auxiliares del principio del archivo. #### La cabecera La cabecera del panel está en inglés: | Elemento | Hace | | --- | --- | | Título | El archivo de código y, en modo manejador, la función. | | **Problems** (`2 · 1`) | Errores y avisos del código; clic para mostrar u ocultar la lista de problemas. Una marca de verificación indica que no hay ninguno. | | **Engranaje** | Abre los [ajustes del editor](#ajustes-del-editor). | | **Whole file / This handler** | Cambia de modo. | | **Apply** | Comprueba la sintaxis, escribe el código en el documento y, con *Format with ruff on Apply* activo, lo formatea. | | **×** | Cierra el editor (antes aplica el código). | Tu código se aplica solo al documento cada vez que cambias de manejador, de modo o de pestaña, guardas o ejecutas; **Apply** sirve para comprobarlo (y formatearlo) en el momento. Si el código no se puede analizar, una línea roja bajo el editor dice dónde (`Line 3: invalid syntax`), y guardar, ejecutar o cambiar esperan a que lo arregles. #### Escribir código - **Resaltado de sintaxis** de Python, con el tema del editor (o uno propio, ver los ajustes). - **Sangría**: Tab inserta espacios (el *Tamaño de tabulación*), o una tabulación con *Sangrar con tabulaciones*; Enter mantiene la sangría de la línea (sus espacios y tabulaciones) y añade un nivel después de `:`; Retroceso dentro de la sangría quita un nivel entero (un espacio si se apaga *Retroceso borra un nivel entero*). Ver [Tabulaciones o espacios](#tabulaciones-o-espacios). - **Paréntesis y comillas**: escribir `(`, `[`, `{`, `"` o `'` inserta su pareja, envuelve la selección si la hay, y escribir el carácter de cierre lo salta en lugar de duplicarlo. - **Margen**: números de línea (siguen a las líneas ajustadas) y la línea actual resaltada. - **Plegado**: los chevrons del margen pliegan y despliegan bloques (funciones, clases, `if`...); Ctrl+Shift+[ pliega el bloque más interior alrededor del cursor y Ctrl+Shift+] lo despliega. - **Minimapa**: el código en miniatura a la derecha; haz clic o arrastra en él para desplazarte. - **Métodos de entrada**: los métodos de entrada (IME) de chino, japonés y coreano funcionan: el texto que se está componiendo se dibuja en el cursor y el texto confirmado se inserta en cada cursor. - **Espacios en blanco**: opcionalmente dibuja los espacios como `·` y los tabuladores como `→`. - **Zoom**: Ctrl+=, Ctrl+- o Ctrl+rueda cambian el tamaño de letra; Ctrl+0 lo restablece. El tamaño se guarda. - **Desplazamiento**: la rueda del ratón desplaza el código (Mayús+rueda, de lado) y las barras de desplazamiento quedan a la derecha del minimapa. La vista también sigue al cursor. - **Desplazamiento fijo** (sticky scroll): mientras te desplazas, las líneas que abren los bloques en los que estás (`class`, `def`, `for`, `if`, `with`...) quedan fijas arriba del editor, la más interna al final, así siempre sabes dónde estás. Haz clic en una para ir a ella. Se fijan hasta cinco líneas; se desactiva con *Desplazamiento fijo* en los ajustes. - **Líneas largas**: por defecto se desplazan de lado; Alt+Z (o *Ajustar líneas largas* en los ajustes) las ajusta al ancho del editor. (Image: El archivo entero con dos manejadores plegados y el minimapa) ##### Teclas en el editor | Teclas | Acción | | --- | --- | | Flechas, Inicio, Fin | Mover el cursor. | | Ctrl+← / → | Mover por palabras. | | Ctrl+Inicio / Fin | Principio o final del código. | | RePág / AvPág | Una página arriba o abajo. | | Shift + cualquiera de las anteriores | Ampliar la selección. | | Ctrl+A | Seleccionar todo. | | Ctrl+C / X / V | Copiar, cortar, pegar. | | Tab | Sangrar (insertar espacios). | | Ctrl+Espacio | Abrir la lista de sugerencias. | | F12, Ctrl+clic | Ir a la definición. | | Alt+← / → | Volver atrás / adelante después de un salto. | | Shift+F12 | Buscar todas las referencias. | | F2 | Renombrar el símbolo. | | Ctrl+. | Arreglos rápidos y refactorizaciones. | | Ctrl+Shift+[ / ] | Plegar / desplegar en el cursor. | | Ctrl+= / - / 0 | Letra más grande, más pequeña, por defecto. | | Ctrl+Z / Ctrl+Shift+Z | Deshacer, rehacer (lo escrito seguido es un solo paso). | | Ctrl+F / H / G | Buscar, reemplazar, ir a la línea (ver abajo). | | Esc | Volver a un cursor, cerrar la lista de sugerencias, o salir del editor. | En macOS, Cmd funciona donde aquí pone Ctrl. Los comandos globales del editor (F5, Ctrl+S, Ctrl+Shift+P...) funcionan mientras escribes; los atajos del lienzo (Supr, flechas, Ctrl+D) no, así que escribir nunca mueve un control. #### Tabulaciones o espacios Python rechaza un bloque que mezcla tabulaciones y espacios, así que el editor se queda con uno de los dos: - **Sangrar con tabulaciones** (Ajustes del editor → Edición) hace que Tab, Enter y sangrar (Ctrl+]) escriban tabulaciones. Se dibujan con el ancho del *Tamaño de tabulación*, y Enter copia tal cual los espacios y tabulaciones de la línea de arriba, en los dos modos. - **Convertir la sangría a tabulaciones** y **Convertir la sangría a espacios**, en la paleta de comandos (Ctrl+Shift+P), reescriben la sangría del archivo que muestra el editor de código. El texto dentro de cadenas de varias líneas queda como está. - **Convertir la sangría al guardar** lo hace cada vez que se guarda un archivo de Python (o el código del formulario), así el código pegado nunca rompe el archivo. - El código que el diseñador escribe por ti (un manejador o método nuevo) sigue al archivo: en un archivo sangrado con tabulaciones se escribe con tabulaciones. Mantener Retroceso o Supr borra al ritmo de **Borrado**: la primera pulsación siempre borra; las siguientes esperan la *Espera antes de repetir* y luego llegan como mucho *Borrados por segundo* veces por segundo. Pueden ir más lentas que la repetición de teclas del sistema, no más rápidas. #### Buscar, reemplazar y varios cursores (Image: Buscar (Ctrl+F) con cada form.status seleccionado como cursor) - Ctrl+F abre la barra de búsqueda con la palabra bajo el cursor; las coincidencias se resaltan en el código y la barra las cuenta. Enter o F3 van a la siguiente, Shift+F3 a la anterior y Alt+Enter pone un cursor en cada una. Los botones distinguen mayúsculas, buscan palabras completas o usan una expresión regular. - Ctrl+H muestra también el campo de reemplazo: **Reemplazar** cambia la coincidencia actual y **Reemplazar todo** todas, en un solo paso de deshacer. Con una expresión regular, `\1` en el reemplazo inserta un grupo. - Ctrl+G va a una línea (`42`) o a una línea y columna (`42:8`). - El editor tiene cursores múltiples y los atajos de edición habituales de los editores modernos: Alt+clic, Ctrl+Alt+↑/↓, Ctrl+D, Alt+Shift+↑/↓ para duplicar líneas en cada cursor, Ctrl+Shift+L, Alt+↑/↓ para mover líneas, Ctrl+/ para comentar, Ctrl+Z / Ctrl+Shift+Z y más. La lista completa está en [el componente editor de código](https://albakore.github.io/pyzed/es/components/code/#varios-cursores-y-los-atajos-de-zed) o con F1 en el diseñador. #### Autocompletado (Image: Completando una función del módulo re, con su firma y su docstring) - La lista se abre mientras escribes un nombre o después de un `.` (no en comentarios ni cadenas), y Ctrl+Espacio la abre en cualquier sitio. Desactiva *Autocomplete while typing* para abrirla solo con las teclas. - ↑/↓ se mueven (dando la vuelta), RePág/AvPág saltan nueve filas, Enter o Tab insertan, Esc cierra; un clic también inserta. El pie muestra la posición (`3/42`). - La rueda del ratón desplaza la lista sin mover la entrada resaltada, y una barra a su derecha indica dónde estás. Los nombres y firmas largos se recortan con `…` para que quepan; el panel junto a la lista los muestra completos. - Al seguir escribiendo la lista se filtra: primero los nombres que empiezan por lo escrito, luego los que contienen las letras en orden (`gtx` encuentra `get_text`). - Cada entrada tiene un icono según su tipo y, junto a la lista, un panel con su **firma** y su **documentación** (el docstring, como Markdown). | Color del icono | Tipo | | --- | --- | | Morado | Funciones y métodos | | Amarillo | Clases, enums, parámetros de tipo | | Azul | Variables, atributos y propiedades | | Cian | Constantes | | Rojo | Módulos | | Acento | Controles del formulario | - Los nombres **privados** (`_cache`) salen atenuados, con un candadito, después de los públicos; los dunders (`__init__`) van al final. - **Auto-import**: elegir un nombre que aún no está importado añade también su `import` al principio del archivo, incluso en modo manejador. - La lista se abre bajo la palabra, o encima cuando hay más sitio ahí. ##### Los nombres propios del formulario El `form` y el `event` de un manejador no tienen un tipo que los servidores puedan leer, así que el editor los completa por su cuenta: | Escribes | Obtienes | | --- | --- | | `form.` | Todos los controles del formulario, más `title`, `name`, `size`, `window` y `close()`. | | `form.email.` | Las propiedades de ese control, con sus tipos, más `name`, `type`, `visible`, `enabled`, `show()`, `hide()` y `focus()`. | | `event.` | `sender`, `name` y `value`. | (Image: form. completa los controles del propio formulario) #### Información, navegación y refactorización (Image: La tarjeta de información de una función: su firma y su docstring) Funcionan como en VS Code o Zed: responde ty donde puede y el editor completa lo que solo sabe el diseñador (los controles del formulario). - **Información al pasar el ratón**: deja el ratón medio segundo sobre un nombre y una tarjeta muestra su tipo o su firma y su docstring (como Markdown), junto con los problemas de ese punto. `form.email` y `form.email.text` describen el control y su propiedad. La tarjeta se queda mientras el ratón está encima, así puedes desplazarla y seleccionar su texto. Se desactiva con *Información al pasar el ratón* en los ajustes. - **Ir a la definición**: F12 o Ctrl+clic en un nombre abre donde está definido: más abajo en el mismo código, otro manejador del formulario (el editor cambia a él), un archivo de tu proyecto o una librería del `.venv` (se abre de solo lectura en una pestaña). Ctrl+F12 va a la definición de su **tipo**. En `form.email` selecciona el control en el lienzo. Sin ty sigue encontrando los `def` y `class` del archivo. - **Atrás y adelante**: Alt+← vuelve a donde estabas antes del salto y Alt+→ avanza de nuevo. - **Buscar referencias**: Shift+F12 lista bajo el editor cada uso del nombre, con el archivo, la línea y el texto; haz clic en uno para ir allí. - **Renombrar**: F2 abre un campo sobre el nombre; escribe el nuevo y pulsa Enter. Cambia cada uso, en este archivo y en los demás archivos del proyecto que lo usan (se abren como pestañas sin guardar para que los revises y guardes). Al renombrar un manejador sus eventos siguen conectados a él (`on sign_in.click`), y un Ctrl+Z deshace el cambio. - **Arreglos rápidos** (Ctrl+., o **Arreglo rápido…** en la tarjeta de información): los arreglos que ofrecen ruff y ty para la línea, como quitar un import sin usar, y además: - **Importar** un nombre que no está definido, desde el módulo que lo tiene. - **Instalar un paquete que falta**: en `import httpx` sin httpx instalado, *Instalar httpx en el proyecto (uv add)* lo añade a `pyproject.toml` y al `.venv`, y el error desaparece en cuanto se instala. Los nombres conocidos se traducen a su paquete (`cv2` → `opencv-python`, `PIL` → `pillow`, `yaml` → `pyyaml`). - **Organizar imports** (Alt+Shift+O) ordena y agrupa los imports con ruff, y **Arreglar todos los problemas que se arreglan solos** (paleta de comandos) aplica todos los arreglos seguros de ruff. #### Ayuda contextual Shift+F1 abre la [referencia integrada](https://albakore.github.io/pyzed/es/editor/reference/) en el nombre bajo el cursor: `form.email` o `form.email.text` abre la página del tipo del control, `form` y `event` abren las páginas del código del formulario, y los nombres de la librería (`pz.text`, `ui.button`, `Theme`) abren su página de la API. Si no hay coincidencia, busca el nombre en la referencia. #### Diagnósticos y code lens (Image: Errores en línea, la lista de problemas y code lens) - **Diagnósticos en línea** (error lens): el problema más grave de cada línea se escribe al final de ella, en rojo los errores, en amarillo los avisos y en azul la información. - **Lista de problemas**: bajo el editor, cada problema con su línea y columna, mensaje y origen (`ruff (F401)`, `ty`). Haz clic en uno para saltar a él. El botón de la cabecera muestra los recuentos y oculta o muestra la lista. - **Code lens** al final de cada línea `def` y `class`: cuántas **referencias** tiene (de ty, o contadas por nombre con ty apagado), cuántas clases **implementan** una clase y qué eventos **llaman** a un manejador (`on sign_in.click`). #### Servidores de lenguaje Los servidores arrancan en la carpeta del marcador de proyecto más cercano por encima del archivo de código (`pyproject.toml`, `uv.lock`, `setup.py`, `.git`) y usan su `.venv` (donde `uv sync` instala tus dependencias), o `$VIRTUAL_ENV`. La carpeta del código y `src/` se añaden como raíces de import, así que una UI en su propia carpeta resuelve las dos cosas: ``` myapp/ ├── pyproject.toml ← uv add httpx ├── .venv/ ← uv sync ├── src/myapp/ └── ui/ ├── login.pzform ├── login.py ← import httpx / from helpers import greet / from myapp import api └── helpers.py ``` Si el proyecto aún no tiene `.venv` (un formulario que acabas de abrir, o un proyecto antes de su primer `uv sync`), ty usa el Python con el que corre el diseñador, así `import pyzed` y la librería estándar se resuelven en vez de marcarse como que faltan. En cuanto aparece el `.venv`, o cambian `pyproject.toml` o `uv.lock` (al añadir una dependencia en la [pestaña Proyecto](https://albakore.github.io/pyzed/es/editor/projects/)), los servidores se reinician con él, así los paquetes nuevos se resuelven sin volver a abrir el proyecto. El editor busca `ty` y `ruff` primero en el `.venv` del proyecto, luego junto al editor y luego en el `PATH`. Instálalos con `uv add --dev ty ruff` en el proyecto, o con `pip install "pyzed-ui[designer]"` junto al editor. Ruff lee sus reglas de `[tool.ruff]` en `pyproject.toml`. Con el editor de código abierto, la barra de estado muestra un punto por servidor: verde cuando está **listo**, amarillo mientras **arranca**, rojo si **falta** o ha fallado. Pasa el ratón para ver los detalles; haz clic para abrir los ajustes del editor. ```mermaid sequenceDiagram participant E as Editor de código participant A as CodeAssist participant I as CodeIntel participant T as servidor ty participant R as servidor ruff E->>A: on_change(text), on_cursor(line, col) A->>I: update(module, handler, text) I->>T: didChange (el módulo entero, con el manejador dentro) I->>R: didChange T-->>I: publishDiagnostics R-->>I: publishDiagnostics A->>I: complete(line, col) I->>T: textDocument/completion T-->>A: items (vía pyzed.post, en el hilo de la UI) A->>E: lista, errores en línea, code lens ``` #### Ajustes del editor Los abren el **engranaje** de la cabecera del panel de código y los puntos de los servidores en la barra de estado: son las páginas *Editor de código* del [diálogo de Ajustes](https://albakore.github.io/pyzed/es/editor/settings/), con su buscador, sus perfiles, **↺** para devolver un ajuste a su valor por defecto y el ámbito *Proyecto* para que un proyecto tenga los suyos (su tabulación, por ejemplo). (Image: Ajustes del editor con el índice de secciones) | Sección | Ajuste | Qué hace | | --- | --- | --- | | Apariencia | **Tema** | *Igual que el diseñador*, o cualquier tema solo para el editor. | | | **Fuente** | *Por defecto* (IBM Plex Mono) o cualquier fuente instalada; también en Ajustes → Fuentes y tamaño. | | | **Tamaño de letra** | De 8 a 32 (también Ctrl+=/-/0 y Ctrl+rueda). | | | **Interlineado** | Compacto, Normal o Amplio. | | | **Números de línea**, **Resaltar la línea actual** | El margen y la franja de la línea actual. | | | **Mostrar espacios** | Espacios como `·`, tabulaciones como `→`. | | Cursor | **Forma** | Barra, bloque, subrayado o hueco. | | | **Parpadeo** | Si el cursor parpadea. | | | **Ancho** | De 1 a 4 píxeles, para el cursor de barra. | | Edición | **Tamaño de tabulación** | De 1 a 8 columnas por nivel, y el ancho con que se dibujan las tabulaciones. | | | **Sangrar con tabulaciones** | Tab, Enter y Ctrl+] sangran con tabulaciones en vez de espacios. | | | **Convertir la sangría al guardar** | Al guardar un archivo de Python su sangría se reescribe con tabulaciones o espacios, según lo de arriba. | | | **Retroceso borra un nivel entero** | Apagado: Retroceso en la sangría borra un espacio cada vez. | | | **Autocompletar al escribir** | Apagado: la lista solo se abre con Ctrl+Espacio. | | | **Cerrar paréntesis y comillas** | Pares, envolviendo la selección y saltando el de cierre. | | | **Plegado de código** | Los chevrones y las teclas de plegado. | | | **Remarcar corchetes** | Marca el paréntesis que hace pareja con el del cursor (Ctrl+M salta a él). | | | **Resaltar coincidencias de la selección** | Resalta los otros sitios con el texto seleccionado. | | | **Formatear con ruff al aplicar** | Ejecuta `ruff format` sobre el código al pulsar Aplicar. | | Borrado | **Espera antes de repetir** | Milisegundos que esperan Retroceso y Supr, mantenidos, antes de repetirse (500 por defecto; 0 deja los del sistema). | | | **Borrados por segundo** | Qué tan rápido borran mientras se mantienen (20 por defecto; 0 deja el ritmo del sistema). | | Desplazamiento y ajuste de línea | **Ajustar líneas largas** | Apagado por defecto: las líneas largas se desplazan de lado. Alt+Z lo alterna. | | | **Barras de desplazamiento** | Muestra u oculta las barras. | | | **Minimapa** | El código en miniatura a la derecha. | | | **Desplazamiento fijo** | Mantiene fijas arriba las líneas de los bloques que te rodean mientras te desplazas. | | Code lens y diagnósticos | **Code lens** | Referencias, implementaciones y eventos conectados al final de las definiciones. | | | **Diagnósticos en línea** | Los problemas al final de la línea. | | | **Información al pasar el ratón** | La tarjeta con el tipo, la documentación y los problemas del nombre bajo el ratón. | | Servidores de lenguaje | **ty**, **ruff** | Enciende o apaga cada servidor. Cada fila muestra su estado y versión. Debajo: la raíz del proyecto y el Python en uso. | | | **Reiniciar servidores** | Reinicia los dos; úsalo después de `uv sync` o de instalarlos. | (Image: La sección Servidores de lenguaje de los ajustes del editor: ty y ruff con su estado y versión, la raíz del proyecto y el Python en uso) ##### Atajos de teclado **Atajos de teclado** en el índice abre el [editor de keymap](https://albakore.github.io/pyzed/es/editor/keymap/), donde puedes cambiar las teclas de cada acción del editor, del panel de código y del resto del diseñador. Los cambios se guardan en `keymap.json`, en el formato de Zed. En tus propias apps, `ui.editor(keymap=...)` usa teclas → acción (el mismo formato que los keymaps de Zed, `None` para quitar una tecla por defecto), y `pyzed.ui.editor.keymap_for` convierte en ella cambios por acción. ### Ejecutar y la consola > Ejecuta formularios y componentes con F5 en Pyzed Design, lee salida, eventos y errores en la consola y mira los cambios en vivo al guardar. Source: https://albakore.github.io/pyzed/es/editor/console-and-running/ #### Ejecutar y Detener **Ejecutar** (F5, el botón verde de la barra de título, el menú ☰ o la consola) abre el documento en una ventana nueva con toda su lógica; **Detener** (Shift+F5, el cuadrado junto a Ejecutar) la cierra. (Image: El formulario de inicio de sesión en ejecución) Lo que hace Ejecutar depende del documento y de dónde corre el editor: ```mermaid flowchart TD F5([F5]) --> P{¿El código se analiza?} P -- no --> E[Notificación: arregla el código antes] P -- sí --> K{¿Componente?} K -- sí --> CP["Ventana de vista previa del componente
con un registro de eventos"] K -- no --> U{¿Editor dentro del
entorno del proyecto?} U -- sí --> IN["Se ejecuta en el proceso del editor
el documento actual, guardado o no"] U -- no --> UV["Guarda y luego
uv run pyzed run --reload"] IN --> C[Consola] UV --> C ``` - **En el proceso del editor** (arrancaste el editor con `uv run pyzed design`, o no hay proyecto): el formulario se ejecuta tal como está en el editor, **con los cambios sin guardar**, con su archivo de código recién compilado. Ejecutar otra vez cierra antes la ventana anterior. - **Con `uv run`** (la app independiente Pyzed Design, o un editor instalado con `uv tool`, que no tiene las dependencias del proyecto): el formulario se guarda y se arranca en su propio proceso con `uv run pyzed run --reload`, así tiene disponibles todos los paquetes de `.venv`. - **Un componente** se abre en una [ventana de vista previa](https://albakore.github.io/pyzed/es/editor/component-designer/#ejecutarlo-y-usarlo) que lista los eventos personalizados que lanza. Con **Guardar antes de ejecutar** activo (Ajustes → Ejecución), Ejecutar siempre guarda primero. :::tip Si el archivo de código falla al cargarse (un error de import, una errata en el nivel superior), Ejecutar se detiene y la consola muestra la traza, con una notificación que nombra el error. ::: #### La consola El panel **Consola** (dock inferior, Ctrl+Shift+Y o **Ver → Consola**) muestra lo que hace el formulario en ejecución sin salir del editor: (Image: La consola después de unos clics y un error) | Línea | Viene de | | --- | --- | | ▶ `login started` / ■ `login stopped` | Una ejecución que empieza y termina (con `(uv run)` cuando corre en su propio proceso). | | Evento (acento, icono de rayo) | Cada evento que atiende el formulario, con su valor y el manejador al que llama: `sign_in.click → sign_in_click()`. `tick`, `mouse_enter` y `mouse_leave` se omiten para no inundarla. | | Salida | Lo que los manejadores imprimen con `print`. | | Salida de error (amarillo) | Lo que escriben en `stderr`. | | Error (rojo) | La traza completa de un manejador que lanzó una excepción, empezando por el manejador. | La barra de la consola: - Campo de **filtro**: muestra solo las líneas que contienen el texto. - **Interruptores de nivel**, uno por cada tipo de línea presente, con su recuento: haz clic en uno para ocultar o mostrar esas líneas. - **Copy** pone todo el registro en el portapapeles; **Clear** lo vacía. - Una insignia **Running** mientras corre un formulario, **Detener** y **Ejecutar**. La lista sigue desplazándose a la línea más nueva mientras estás abajo del todo; sube para leer líneas anteriores con calma. La salida sigue llegando también a la terminal desde la que arrancaste el editor. Si la consola se abre al ejecutar lo decide **Ajustes → Ejecución → Abrir la consola al ejecutar**, y si registra los eventos, **Registrar los eventos en la consola**. Una vez que tiene algo que mostrar, o la abriste desde el menú Ver, la consola se queda en su dock hasta que la ocultes. ##### Seleccionar y copiar líneas Haz clic en una línea para seleccionarla, Shift+clic para seleccionar un rango y Ctrl+clic (Cmd en macOS) para añadir o quitar una; Ctrl+A selecciona todas las líneas visibles, Ctrl+C copia la selección y Escape la quita. Un error seleccionado muestra su traceback completo debajo. Un **clic derecho** en una línea abre su menú: | Opción | Qué hace | | --- | --- | | **Copiar línea** / **Copiar N líneas** | Las líneas seleccionadas, con su hora y nivel. | | **Copiar solo el texto** | Solo el texto de las líneas (y su traceback). | | **Copiar todo** / **Seleccionar todo** | Todas las líneas que dejan ver los filtros. | | **Ir a archivo.py:12** | Abre el archivo de una línea de traceback en el panel Código, en esa línea (también con doble clic). | | **Ver el error…** | El error en el [diálogo de error](#cuando-falla-el-propio-editor), con el traceback seleccionable. | | **Ocultar las líneas error** | Apaga el interruptor de ese nivel. | | **Ver en Logs** / **Limpiar** | Abre el panel Logs, o vacía la consola. | #### El panel Logs **Ver → Logs** (Ctrl+Shift+U) abre el panel **Logs**, el registro del propio editor: todo lo que hace Pyzed Design y cada error que encuentra, mientras que la consola es para tu formulario en ejecución. Como cualquier panel, puede ir a otro dock o a una ventana propia. Cada línea lleva su hora, un nivel y un **tipo**: | Tipo | Qué registra | | --- | --- | | Diseñador | El editor al arrancar y al cerrarse, los ajustes cambiados y sus errores inesperados con su traceback. | | Comandos | Cada comando ejecutado desde un menú, la paleta de comandos o un atajo. | | Documentos | Formularios y componentes abiertos y guardados. | | Proyecto | El proyecto y sus comandos de `uv`. | | Ejecutar | Las ejecuciones al empezar y al parar, y los eventos y errores del formulario en ejecución. | | Código, Agente, Disposición, Actualizaciones | El editor de código, los agentes de IA, los paneles movidos o mostrados y las actualizaciones. | | Sistema | Los avisos de Python. | La barra filtra por texto, por **tipo** (el desplegable *Todos los tipos*) y por **nivel** (debug, info, event, warning, error: las líneas debug están ocultas hasta que enciendes su interruptor). Las líneas se seleccionan, se copian y se abren desde su menú igual que en la consola, y el menú añade **Solo Diseñador** (o el tipo de la línea). El botón de la carpeta abre la carpeta de los archivos de log. Las mismas líneas (de event para arriba) se escriben en `logs/pyzed-design.log`, en la carpeta de configuración de Pyzed (el archivo se renueva al pasar de 1 MB y se guardan los tres últimos): adjúntalo a un informe de error. **Ajustes → Logs y errores → Escribir un archivo de log** lo desactiva. Cuando se han registrado errores o avisos, la barra de estado muestra su número a la izquierda; haz clic para abrir el panel. #### Cuando falla el propio editor Un fallo de Pyzed Design nunca lo cierra: - Una acción que falla (un clic, una tecla, un comando de menú, una tarea en segundo plano) abre un **diálogo de error** con el error, dónde pasó y el traceback, que puedes seleccionar y copiar. Sus botones: **Copiar el error** (todo, listo para pegar en un issue), **Ir a archivo.py:12** (abre la línea de la que viene, cuando es un archivo que se puede abrir), **Ver en Logs** y **Cerrar**. Si llegan más errores mientras está abierto, los cuenta. - Un panel que no se puede dibujar muestra una **tarjeta de error** en su lugar, con **Copiar el error** y **Detalles**, y el resto de la ventana sigue funcionando; el lienzo, los diálogos y la ventana entera están protegidos igual. Todos quedan en el panel Logs con su traceback. **Ajustes → Logs y errores → Mostrar un diálogo con los errores** desactiva el diálogo (los errores se siguen registrando). Para tus propias apps, las mismas piezas están en la librería: [`ui.error_boundary`, `ui.error_dialog` y `ui.ErrorInfo`](https://albakore.github.io/pyzed/es/reference/ui/#consola-errores-y-ayuda). #### Errores en la ventana en ejecución Un manejador que lanza una excepción no cierra la ventana: aparece abajo un aviso **Event handler error** con la última línea del error, y la traza va a la consola. Arregla el código, guarda y vuelve a probar (ver abajo). #### Actualización en vivo al guardar Mientras un formulario se ejecuta, **Guardar** (Ctrl+S) actualiza la ventana abierta sin reiniciarla: se sustituyen la disposición, las propiedades y el código, y se conserva lo que el usuario escribió o eligió en cada control (textos, casillas, selecciones, valores de los deslizadores). Si la versión nueva no carga, la ventana sigue con la anterior y muestra el error. Un formulario arrancado con `uv run` (o con `pyzed run app.pzform --reload` desde una terminal) vigila sus archivos, así que guardar desde cualquier editor también lo actualiza. Desde código, usa `designer.run(path, reload=True)` o define `PYZED_RELOAD=1`. ```bash pyzed run login.pzform # ejecutar un formulario sin el editor pyzed run login.pzform --reload # ...y actualizarlo en cada guardado pyzed run ui/rating.pzcomp # vista previa de un componente diseñado ``` ### La vista de componente > Dibuja componentes reutilizables en Pyzed Design con propiedades, estado, métodos y eventos propios, y anídalos como escenas de Godot. Source: https://albakore.github.io/pyzed/es/editor/component-designer/ El diseñador tiene un segundo modo para crear **componentes** en vez de ventanas. Dibujas el componente como un formulario, declaras sus propiedades y eventos en un panel, escribes la lógica de sus métodos y luego lo colocas en formularios o dentro de otros componentes. La idea es la de las escenas de Godot: cada componente tiene sus propias propiedades, métodos y eventos, y puede contener otros componentes. (Image: Un componente ProfileCard que contiene un componente Rating) Un componente diseñado **es** un [`pz.Component`](https://albakore.github.io/pyzed/es/guides/components/). El diseñador escribe una clase normal, así que dibujarlo y escribirlo a mano son lo mismo, y puedes pasar de una forma a la otra cuando quieras. #### Crear uno Pulsa **Nuevo componente…** al final de la caja de herramientas o en el menú ☰ (Ctrl+Shift+N), o **Nuevo…** (Ctrl+N, el primer botón de la barra) y elige la tarjeta **Component**. Ponle un nombre y una carpeta; **Inherits from** puede partir de otro componente (mira [Herencia](#herencia)). **Create component** guarda los dos archivos y lo abre. También puedes abrir un `.pzcomp`: (Image: El diálogo New component) ```bash pyzed design ui/rating.pzcomp # se crea al guardar si no existe ``` Un componente no es un formulario: no tiene ventana, título ni menú, y solo existe dentro de un formulario o de otro componente. El diseñador lo muestra así: una etiqueta **Component** en la barra de título, y un marco de color con el nombre de la clase en el lienzo en vez de los botones de ventana. #### Vista de formulario y vista de componente Cada archivo abierto es una pestaña sobre el lienzo. Un `.pzform` se abre en la **vista de formulario** y un `.pzcomp` en la **vista de componente**, donde se trabaja sobre la clase: sus propiedades, eventos, métodos y componentes hijos. Haz doble clic en un componente colocado en un formulario, o en otro componente, para abrirlo en su propia pestaña de vista de componente: (Image: Un formulario y el componente que usa, cada uno en su pestaña) Al cambiar de pestaña cada documento queda como estaba, con su historial de deshacer. Al cerrar una pestaña con cambios sin guardar se pregunta si guardarlos antes. Un componente son dos archivos juntos: | Archivo | Qué contiene | | --- | --- | | `rating.pzcomp` | El diseño: los controles, su tamaño, cómo aparece en la biblioteca y sus enlaces (JSON). | | `rating.py` | La clase: sus propiedades, eventos propios y métodos. | ```python from typing import Annotated import pyzed as pz from pyzed.designer import SceneComponent class Rating(SceneComponent, scene="rating.pzcomp"): """Five stars the user can click.""" value: Annotated[int, pz.Range(0, 5)] = 3 # How many stars are lit readonly: bool = False clicks: int = pz.state(0, doc="Times the user picked a rating") events = {"change": "A star was picked (event.value is the rating)"} def star1_click(self, event): return self.pick(1) def pick(self, stars): """Lights ``stars`` stars and tells the parent.""" if self.readonly: return None self.value = stars self.clicks += 1 return self.emit("change", stars) ``` #### El panel Component Sin nada seleccionado, la pestaña Propiedades muestra el propio componente, en secciones plegables: | Sección | Campos | | --- | --- | | **Class** | **Name** (el nombre de la clase de Python, en CamelCase), **Description** (el docstring de la clase, que la caja de herramientas muestra como tooltip) e **Inherits** (un componente diseñado del que heredar, ver [Herencia](#herencia)). | | **Toolbox** | **Label**, **Icon** (un nombre de Lucide, o elígelo en el [diálogo de iconos](https://albakore.github.io/pyzed/es/editor/properties/#el-diálogo-de-iconos)) y **Category**: cómo aparece el componente en la caja de herramientas de los formularios y de otros componentes. | | **Layout** | **Width**, **Height**, **Background** (transparente si está vacío) y **Grid size** de la superficie de diseño. | | **Inherited** | Solo con una base: lo que viene de cada clase base, de solo lectura. | | **Properties** | Una fila por propiedad (icono del tipo, nombre y valor), y los botones **Public** / **Private** para añadir una. | | **Methods** | El código de la clase, agrupado, un campo para añadir un método y **Edit the class**. | Los **eventos personalizados** están en la pestaña **Eventos**, en una sección **Eventos personalizados** debajo de los **Eventos incluidos** (`setup`, `mount`, `unmount`): una tarjeta por evento (nombre y descripción), y **Evento** para añadir uno. Son los eventos que el componente lanza con `self.emit("change", valor)`. El padre los recibe con `on_change=...` en código, o en la pestaña Eventos de un formulario, donde aparecen aparte de los eventos incluidos del control. - **Properties**: como en el panel de detalles de Unreal, cada propiedad es una fila compacta: el icono de su tipo, su nombre (un candado marca una privada) y su valor por defecto. Las listas, diccionarios e instancias muestran sus elementos debajo. La flecha antes del nombre abre sus ajustes: **Name**, **Type**, **Annotation**, **Access**, **Description**, mover y **Remove**. Una propiedad **pública** es una prop que pasa el padre (`Rating(value=4)`); una **privada** es estado, declarado con `pz.state`. Las dos son atributos en el código: `self.value`, `self.clicks`. El tipo decide el editor de la propiedad, aquí y en la pestaña Propiedades de los formularios que usan el componente (ver [Tipos de propiedad](#tipos-de-propiedad)). - **Methods**: todos los métodos de la clase, agrupados en **Lifecycle** (`setup()`, `on_mount()`, `on_unmount()`, que se añaden al hacer clic en ellos), **Event handlers** (el código de los eventos de los controles) y tus propios **Methods** (un nombre que empieza por `_` se marca como privado). Haz clic en uno para abrirlo en el editor de código. Escribe un nombre y pulsa **+ Method** (o Enter) para añadir un método reutilizable; **Edit the class** abre el archivo entero. Cada cambio se escribe en la clase en el momento, y lo que escribas en la clase aparece en el panel. Si el código tiene un error de sintaxis, el panel muestra la última versión válida, con un aviso, hasta que lo corrijas. #### Tipos de propiedad Como en el inspector de Godot y el panel de detalles de Unreal, el tipo de una propiedad elige su editor, y cada tipo tiene su propio icono de color. Elige un tipo común en la lista **Type** de los ajustes de la propiedad, o escribe cualquier anotación en **Annotation**: | Tipo | Anotación en la clase | Editor | | --- | --- | --- | | str, int, float, bool | `str`, `int`... | Texto, campo numérico o interruptor. | | Color | `pz.Color` | Muestra de color con un token del tema o `#rrggbb`. | | Icon | `pz.Icon` | Selector de icono. | | Multiline | `pz.Multiline` | Área de texto. | | Archivo, carpeta | `pz.FilePath`, `pz.FolderPath` (o `pathlib.Path`) | Un campo de ruta con un botón que abre el diálogo de archivo o de carpeta. | | Choice | `Literal["fast", "slow"]` | Desplegable; la tarjeta tiene un campo **Options**. | | Range | `Annotated[int, pz.Range(0, 5)]` | Campo numérico con límites; la tarjeta tiene **Min**, **Max** y **Step**. | | Optional | `float \| None`, `Optional[float]` | **None** con **+** para darle un valor, o el editor del valor con **×** para vaciarlo. | | Unión | `int \| str` | Una lista de los tipos y el editor del elegido. | | list, set, tuple | `list[str]`, `set[int]`, `tuple[int, int]` | El número de elementos con **+**, y una fila por elemento (`[0]`, `[1]`...) con su editor. | | dict | `dict[str, int]` | El número de entradas con **+**, y una fila por entrada: su clave y su valor. | | Clase | `Point`, `Point \| None` | **New Point** crea una instancia con sus valores por defecto; luego una fila por campo. **×** la vacía. | | Any | `Any`, cualquier otra | Una expresión de Python. | Los tipos se anidan: `list[Point]`, `dict[str, list[int]]` u `Optional[tuple[int, int]]` muestran sus elementos unos dentro de otros. Una clase puede ser una dataclass o cualquier clase del módulo con campos anotados, u otro componente. (Image: Propiedades de varios tipos) ```python @dataclass class Point: x: int = 0 y: int = 0 class Gadget(SceneComponent, scene="gadget.pzcomp"): mixed: int | str | list[str] = 0 maybe: Optional[float] = None scores: dict[str, int] = {"ada": 5} origin: Point = Point() ``` El panel Properties y el código son lo mismo: lo que cambias en el panel se escribe en la clase al instante, aunque la clase esté abierta en el editor de código (lo que escribiste allí y no aplicaste se conserva), y lo que escribes en la clase aparece en el panel. El diseñador añade los imports que necesitan estos tipos (`import pyzed as pz`, `from typing import Annotated, Optional`). ```python class Rating(SceneComponent, scene="rating.pzcomp"): value: Annotated[int, pz.Range(0, 5)] = 3 # How many stars are lit class ProfileCard(SceneComponent, scene="profile_card.pzcomp"): tint: pz.Color = "text_muted" # Color of the role line ``` #### Código reutilizable La clase de un componente es Python normal, así que puede tener cualquier código y no solo manejadores de eventos: métodos auxiliares, estado privado, valores calculados, imports. Un método público forma parte de la API del componente, y los formularios y componentes padre pueden llamarlo: ```python class ProfileCard(SceneComponent, scene="profile_card.pzcomp"): def rate(self, stars): """Rates the person from code, as if the user clicked the stars.""" return self.stars.pick(stars) ``` (Image: El método pick() de Rating abierto en el editor de código) #### Eventos de los controles Haz doble clic en un control, o usa la pestaña Events, para escribir el código de sus eventos, como en un formulario. En un componente son métodos: `def star1_click(self, event)`. Los controles son atributos del componente y sus propiedades se leen y escriben como atributos: ```python def clear_click(self, event): self.query.text = "" self.results.items = [] ``` Cada instancia tiene su propia copia de los controles, así que dos ratings en un formulario guardan su propio estado. #### Enlaces Selecciona un control y la sección **Bindings** enlaza sus propiedades con las del componente. Elige una propiedad en la lista, o elige **Expression…** y escribe cualquier expresión de Python sobre ellas: (Image: El título sigue a la propiedad person) | Propiedad del control | Enlace | | --- | --- | | `headline.text` | `person` | | `caption.text` | `f"{value} / 5"` | | `star3.variant` | `"solid" if value >= 3 else "ghost"` | Los enlaces se evalúan en cada render, así que los controles siguen siempre a las propiedades. #### Componentes anidados Los componentes guardados aparecen en la biblioteca bajo su categoría, en formularios y en otros componentes. El diseñador encuentra todos los `.pzcomp` del proyecto al arrancar, y recarga un componente al guardarlo. Coloca un `Rating` dentro de un `ProfileCard`, enlaza su `value` con el `score` de la tarjeta y reenvía su evento: ```python class ProfileCard(SceneComponent, scene="profile_card.pzcomp"): """A person with a rating.""" person: str = "Ada Lovelace" job: str = "Mathematician" score: int = 4 events = {"rated": "The rating changed (event.value is the new score)"} def stars_change(self, event): """The inner Rating raised change.""" self.score = event.value return self.emit("rated", event.value) ``` (Image: Dos tarjetas en un formulario) ##### Componentes hijos en el esquema Un componente hijo es un atributo de su padre, y sus propiedades y métodos se alcanzan desde él: `self.stars.value`, `self.stars.pick(5)`. Desde un formulario funciona igual: `form.ada.rate(5)`. En el **Outline**, la flecha junto a un componente colocado lista sus propiedades y métodos públicos. Haz clic en uno para insertarlo en el editor de código, en el cursor (`self.stars.pick()` en un componente, `form.ada.rate()` en un formulario): (Image: El esquema lista las propiedades y métodos de un componente colocado) Una propiedad que el propio componente cambia, como `self.score` arriba, conserva su valor hasta que el padre pasa otro distinto, como una variable exportada en Godot. Un handler del formulario también puede cambiarla: ```python def ada_rated(form, event): event.sender.score = event.value form.status.text = f"{event.sender.person} now has {event.value} stars" ``` #### Componentes responsive Un componente colocado en un formulario, o dentro de otro componente, toma el tamaño de su rectángulo ahí, y sus controles siguen sus **anclas** como en un formulario: anclado a izquierda y derecha, un control se estira con el componente; anclado a la derecha, se mueve con el borde derecho. Las filas, columnas y rejillas flex dentro del componente también se reparten el nuevo tamaño. En el ejemplo, el marco de `ProfileCard` está anclado a los cuatro lados y el título, el puesto y la valoración a izquierda, arriba y derecha. `Reviews` pone sus dos tarjetas en una **fila flex** con `grow` 1 cada una, así que se reparten el ancho de la ventana y se encogen con ella: (Image: Las dos tarjetas se encogen con una ventana estrecha) Desde código, un componente ocupa el ancho que haya, hasta su ancho de diseño, y la altura de su diseño. #### Herencia Un componente puede heredar de otro para reutilizar su diseño y su lógica. Elige la base en **Inherits from** al crearlo, o después en el campo **Inherits** de la sección Class. La clase pasa a derivar de la clase base: ```python from pyzed.designer import import_component ProfileCard = import_component("profile_card.pzcomp") class TeamCard(ProfileCard, scene="team_card.pzcomp"): """A profile card that also shows the team.""" team: str = "Analytical Engine" def stars_change(self, event): """Does what a ProfileCard does, and the badge turns green with five stars.""" result = super().stars_change(event) self.team_label.tint = "success" if event.value == 5 else "accent" return result ``` `import_component` importa la clase de un `.pzcomp` relativo al módulo, así que la base y el nuevo componente pueden estar en cualquier carpeta. Lo que recibe el nuevo componente: - **Controles**: los controles de la base aparecen en el lienzo, con los tuyos encima. Puedes mover o cambiar un control heredado (el cambio solo se guarda en tu `.pzcomp`), pero no renombrarlo ni borrarlo, porque el código de la base lo usa. Al guardar se escriben solo tus controles y los heredados que cambiaste. - **Propiedades, eventos propios y métodos**: todo lo de la clase base, como en cualquier subclase de Python. Los enlaces pueden usar propiedades heredadas. Define de nuevo un método para sobrescribirlo y llama a `super()` para ejecutar el de la base. - **Cambios en la base**: al guardar la base se recargan los componentes que heredan de ella, así que reciben sus nuevos controles y su código. La sección **Inherited** del panel lista lo que viene de cada base: (Image: TeamCard hereda de ProfileCard) #### Ejecutarlo y usarlo Pulsa **F5** para abrir el componente en una ventana de vista previa. La ventana lista los eventos propios que lanza, así que puedes probarlo antes de usarlo. `pyzed run rating.pzcomp` hace lo mismo desde una terminal. (Image: La ventana de vista previa) En código, importa la clase y úsala como cualquier componente: ```python from ui.rating import Rating class Review(pz.App): stars = 3 def view(self): return Rating(value=self.stars, on_change=lambda e: setattr(self, "stars", e.value)) ``` Los formularios encuentran solos los componentes de su proyecto al ejecutarse (`pyzed run`, `designer.run`). #### Componentes desde código Todo lo que hace el panel está disponible desde Python: ```python from pyzed import designer doc = designer.new_component("Rating", "ui/rating.pzcomp", width=232, height=32, icon="star") doc.add("button", 0, 0, 28, 28) doc.add_member("value", "int", 3) doc.add_member("clicks", "int", public=False) doc.add_event("change", "A star was picked") doc.set_binding("button1", "variant", '"solid" if value >= 1 else "ghost"') doc.wire("button1", "click") doc.save() big = designer.new_component("BigRating", "ui/big_rating.pzcomp", extends="ui/rating.pzcomp") big.save() designer.discover("ui") # importa todos los componentes diseñados de una carpeta ``` Mira [`examples/designer/components`](https://github.com/albakore/pyzed/tree/main/examples/designer/components) para el ejemplo de Rating, ProfileCard, TeamCard y Reviews. :::tip Los componentes diseñados se descubren al abrir el editor y se recargan al guardarlos, así que ten el componente y un formulario que lo use abiertos en dos pestañas: edita el componente, pulsa Ctrl+S, cambia a la pestaña del formulario y pulsa F5. ::: ### La vista Shader > Cómo encaja la vista Shader de Pyzed Design en el editor: la barra Shading, las pestañas Grafo, Vista previa y WGSL, su barra de herramientas y su barra de estado, y dónde aprender cada nodo. Source: https://albakore.github.io/pyzed/es/editor/shader-view/ El tercer tipo de documento de Pyzed Design es un **shader**: un efecto de GPU hecho conectando nodos y guardado como archivo `.pzshad`. Se abre en la **vista Shader**, un editor de nodos con el espíritu de los visual shaders de Godot y del editor de materiales de Unreal Engine. Esta página muestra cómo encaja la vista en el editor; la [sección Shading](https://albakore.github.io/pyzed/es/shading/) enseña los shaders en sí y lista cada nodo. #### Cómo cambia el editor con un shader | Zona | En la vista Shader | | --- | --- | | **Barra de título** | Una insignia **Shader** tras el nombre del archivo. **Ejecutar** abre el shader en una ventana que sigue tus cambios. | | **Dock izquierdo: Diseño** | La barra **Shading** (cada nodo por categoría, con buscador) y la **Estructura**: las **variables** del shader, sus **valores con nombre** y sus **nodos**. | | **Centro** | Tres pestañas: **Grafo**, **Vista previa** y **WGSL**, con la barra de herramientas propia del shader. | | **Dock derecho: Propiedades** | El nodo seleccionado (sus ajustes, entradas y salidas), la variable seleccionada, o el propio shader si no hay nada seleccionado. | | **Eventos** | Indica que los shaders no tienen eventos y cómo usar este: `pz.shader("fire.pzshad")` o un control Shader en un formulario. Sus variables son lo que cambia tu código. | | **Barra de estado** | La posición del puntero en el grafo, cuántos nodos y cables hay (o cuántos están seleccionados) y el zoom (clic para 100 %). | Las tres pestañas del centro: **Grafo:** Grafo: los nodos, cada uno con su vista previa en vivo, y la pequeña vista previa de la Salida en la esquina." /> **Vista previa:** Vista previa: el shader terminado, en grande, con un control por variable." /> **WGSL:** WGSL: el código en el que se compila el grafo, solo lectura." /> #### La barra de herramientas | Botón | Teclas | Hace | | --- | --- | --- | | **Nuevo shader**, **Abrir**, **Guardar** | Ctrl+O, Ctrl+S | Comandos de archivo. | | **Deshacer**, **Rehacer** | Ctrl+Z, Ctrl+Shift+Z | Cualquier cambio en el grafo. | | **Grafo**, **Vista previa**, **WGSL** | | Las tres pestañas de la vista. | | **Añadir un nodo** | Shift+A | Abre el buscador de nodos donde está el puntero. | | **Eliminar** | Supr | Quita los nodos seleccionados. | | **Ordenar nodos en columnas** | Ctrl+L | Ordena el grafo de las entradas (izquierda) a la Salida (derecha). | | **Encajar todo** | F | Ajusta el zoom para ver el grafo entero. | | **Detener el tiempo** / **Reanudar el tiempo** | | Congela todas las vistas previas en un instante, para estudiar un fotograma, y lo reanuda. | | **Vista previa pequeña** | | Muestra u oculta la vista previa de la Salida en la esquina del grafo. | | **Compila** / *N problemas* | | Pasa el ratón para leer los problemas; el nodo culpable se pone rojo. | | **Ejecutar** | F5 | El shader en su propia ventana. | #### Tres gestos rápidos - **Añadir un nodo:** doble clic en el lienzo y escribe su nombre, o arrástralo desde la barra Shading. - **Conectar:** arrastra desde una salida (lado derecho de un nodo) hasta una entrada (lado izquierdo). Suelta un cable en un espacio vacío para añadir un nodo ya conectado a él. - **Cambiar un valor:** una entrada sin cable muestra su valor en el nodo; escribe un número o elige un color ahí. Todos los gestos y atajos están en la [referencia de la vista Shader](https://albakore.github.io/pyzed/es/shading/shader-designer/). **Shaders en formularios:** El control **Shader** de las herramientas (en **Presentación**) dibuja un `.pzshad` en un formulario o un componente; ajusta su **archivo de shader** y sus **variables** en Propiedades. Un formulario en ejecución recarga el shader cuando lo guardas aquí. ### Archivos, plantillas y scripting > Pestañas de documentos, formularios nuevos y plantillas, abrir y guardar, el formato .pzform, componentes propios y la API de scripting de Pyzed Design. Source: https://albakore.github.io/pyzed/es/editor/files-and-scripting/ #### Pestañas de documentos Cada archivo abierto es una pestaña sobre la barra de herramientas: `Formulario · login.pzform` o `Componente · rating.pzcomp` (con un icono de acento). Un punto marca los cambios sin guardar, y el final de la barra de pestañas dice en qué vista estás: **Vista de formulario** o **Vista de componente**. - Haz clic en una pestaña para cambiar. Cada documento conserva su selección y su historial de deshacer; al dejar una pestaña se aplica y se cierra el editor de código (si el código no se puede analizar, el cambio espera a que lo arregles). - La **×** de una pestaña la cierra. Con cambios sin guardar, un diálogo ofrece **Save**, **Don't save** y **Cancel**. - Abrir un archivo que ya está abierto cambia a su pestaña y conserva sus cambios sin guardar. Un formulario vacío sin título que no has tocado se sustituye en lugar de acumular pestañas. - Doble clic en un [componente diseñado](https://albakore.github.io/pyzed/es/editor/component-designer/) colocado abre su archivo en una pestaña. - Arrastra una pestaña a lo largo de la fila para reordenarla. Sácala de la ventana para abrirla en una **ventana propia**, con sus pestañas, su barra de herramientas y su lienzo; arrastra una pestaña sobre otra ventana de Pyzed Design para apilarla entre sus pestañas. **Mover la pestaña a una ventana nueva** y **Mover la pestaña a la ventana principal** (menú Ver, paleta de comandos) hacen lo mismo sin ratón, también en Wayland, donde las apps no saben dónde están sus ventanas. - Los paneles (Propiedades, Esquema, Código, Eventos...) se quedan en la ventana principal y siguen al documento que usaste por última vez. Una ventana que no es la activa muestra su formulario tal cual; su primer clic solo la activa, así que un clic nunca cambia un formulario que no estabas mirando. Cerrar una ventana de documento devuelve sus pestañas a la ventana principal. El título de la ventana muestra el documento activo, tras un `●` cuando tiene cambios sin guardar. #### Nuevo formulario **Nuevo formulario** (Ctrl+N, el botón **Nuevo…** de la barra de herramientas o el menú ☰) abre el diálogo Nuevo. Arriba, **Formulario** o **Componente** elige qué crear (**Nuevo componente**, Ctrl+Shift+N, lo abre en Componente). (Image: El diálogo Nuevo formulario con sus plantillas) | Campo | Significado | | --- | --- | | **Nombre** | El nombre del archivo y el prefijo de los manejadores del formulario. Se propone uno libre (`form1`, `form2`...). | | **Título** | El título de la ventana (el nombre si está vacío). | | **Carpeta** | Dónde van los archivos: la carpeta del documento actual o la carpeta `ui/` del proyecto. El diálogo muestra la ruta que va a crear y avisa si ya existe. | | **Plantilla** | El punto de partida (abajo). | | Plantilla | Te da | | --- | --- | | **En blanco** | Una ventana vacía. | | **Diálogo** | Un título, un mensaje y una fila con los botones **Cancelar** y **Aceptar** anclada abajo, los dos ya conectados para cerrar la ventana. | | **Inicio de sesión** | Correo, contraseña, "recordarme", un botón para entrar y una etiqueta de estado en una columna centrada. | | **Barra lateral** | Botones de navegación en una columna a la izquierda y una columna de contenido a la derecha. | | **Ajustes** | Opciones con etiqueta (cuadro de texto, desplegable, interruptor, deslizador) en una grilla de dos columnas, con un botón Guardar. | **Crear** escribe el `.pzform` y su `.py` y los abre. **Formulario sin guardar** abre la plantilla sin escribir nada: se guarda (pidiendo una ruta) la primera vez que guardes. Los formularios nuevos toman el tamaño de cuadrícula de los [Ajustes](https://albakore.github.io/pyzed/es/editor/settings/). #### Abrir, guardar y guardar como - **Abrir…** (Ctrl+O, o clic en el nombre del archivo en la barra de título) lista todos los archivos de Pyzed del proyecto (o de la carpeta de trabajo), saltando `.git`, `.venv`, `node_modules`, `build`...: formularios (`.pzform`), componentes (`.pzcomp`), shaders (`.pzshad`) y bibliotecas de animaciones (`.pzanim`). Escribe para filtrar la lista, escribe una ruta y pulsa Enter, o pulsa **Examinar…** para el diálogo de archivos del sistema. Formularios, componentes y shaders se abren en su vista; una biblioteca de animaciones se abre como JSON en el editor de código, con un aviso que lista sus clips. - Los instaladores hacen que el sistema abra estos cuatro tipos de archivo en Pyzed Design con doble clic (y `pyzed design ruta/al/archivo` abre cualquiera de ellos, o un script en el editor de código). - Los archivos **recientes** aparecen al final del menú ☰ (los cinco últimos). - **Guardar** (Ctrl+S) escribe el documento y su archivo de código. Un documento sin título pide antes una ruta. Guardar también refresca el formulario que se ejecuta desde F5. - **Guardar como…** (Ctrl+Shift+S) pide una ruta nueva, relativa a la carpeta de trabajo; la extensión se añade si la omites. #### Los archivos Un formulario son dos archivos juntos, pensados para guardarse en git: ``` login.pzform ← disposición: JSON, con diffs legibles login.py ← manejadores de eventos: Python normal ``` El archivo `.pzform` guarda los ajustes del formulario y, para cada control, su tipo, posición, tamaño, propiedades, anclajes y qué manejador llama cada evento: ```json { "format": "pyzed.form/1", "form": { "name": "login", "title": "Sign in", "width": 440, "height": 400, "theme": "One Dark", "events": { "load": "form_load" } }, "controls": [ { "name": "sign_in", "type": "button", "x": 48, "y": 304, "width": 344, "height": 32, "props": { "text": "Sign in", "variant": "solid", "icon": "log-in" }, "events": { "click": "sign_in_click" }, "anchor": ["left", "top"] } ] } ``` El editor modifica el archivo `.py` con `ast`: encuentra, añade, renombra y sustituye funciones manejadoras sin tocar el resto del archivo, así que tus imports, funciones auxiliares y comentarios quedan como los escribiste. También puedes editar el archivo de código en cualquier otro editor; el formulario solo necesita que existan las funciones manejadoras. Un componente diseñado también son dos archivos, `rating.pzcomp` (disposición) y `rating.py` (la clase); ver [Vista de componente](https://albakore.github.io/pyzed/es/editor/component-designer/). #### Añadir tus propios componentes Registra un componente y aparece en la caja de herramientas, con sus propiedades en la grilla y sus eventos en el panel Eventos: ```python from pyzed import ui from pyzed.designer import Component, Prop, register register(Component( type="rating", label="Rating", icon="star", category="Custom", size=(120, 24), props=(Prop("value", "number", 3, min=0, max=5),), events=("change",), render=lambda control, ctx: ui.label("★" * int(control.props["value"])), )) ``` `render` se usa tanto en el editor (inerte) como al ejecutar; usa `ctx.on(control, "change", "value")` para los callbacks y el mismo código funciona en los dos. Todas las subclases de `pz.Component` de tu proyecto aparecen también en la caja de herramientas (ver [Componentes](https://albakore.github.io/pyzed/es/guides/components/)), igual que los componentes que dibujas en el [diseñador de componentes](https://albakore.github.io/pyzed/es/editor/component-designer/). Se cargan cada vez que se abre un proyecto (al arrancar, desde el selector de proyectos o desde una carpeta reciente) y cuando abres un formulario de otra carpeta. Pyzed importa un módulo por sus componentes solo si su nivel superior se limita a definir cosas: un módulo que llama a `pz.run(...)` fuera de `if __name__ == "__main__":` se omite. Un control cuyo componente no se encuentra (su módulo falla al importarse, o el formulario viene de un Pyzed más nuevo) se ve como un recuadro amarillo **Component not found** con su tipo: el resto del formulario se abre igual y sus propiedades y eventos se conservan al guardar. #### Tus propias plantillas Las plantillas de Nuevo formulario son datos, no código. Cada una es una carpeta: ```text toolbar/ ├── template.json {"label": "Toolbar", "icon": "panel-top", "description": "A row of buttons on top.", "order": 10} ├── form.pzform el formulario de partida: dibújalo en el diseñador y guárdalo aquí └── handlers.py.tmpl opcional: el código de los eventos que conecta el formulario (Python) ``` `{title}` en los textos del formulario se cambia por el título del formulario nuevo, y un tamaño pedido en el diálogo redimensiona el formulario siguiendo los anclajes. Las incluidas están en `pyzed/designer/templates/`. Carga una carpeta tuya con: ```python from pyzed.designer import new new.load_templates("mis_plantillas") # cada subcarpeta con un template.json ``` Una plantilla también puede ser una función de Python que rellena un formulario vacío: ```python def toolbar(form): ... # añade controles al formulario vacío new.register_template(new.Template("toolbar", "Toolbar", "panel-top", "A row of buttons on top.", toolbar)) ``` Las plantillas registradas aparecen en el diálogo Nuevo formulario después de las incluidas. #### Formularios desde código Todo lo que hace el editor está disponible desde Python, algo útil para generar formularios o para tests: ```python from pyzed import designer from pyzed.designer import Document doc = Document(designer.Form(name="hello", title="Hello", width=320, height=140)) doc.add("input", 16, 16, 200, 28) doc.add("button", 224, 16) doc.select_all() doc.align("middle") handler = doc.wire("button1", "click") doc.set_handler_source(handler, "def button1_click(form, event):\n form.button1.text = form.input1.text\n") doc.save("hello.pzform") designer.run("hello.pzform") ``` `designer.new_form(path, template="dialog")` crea un formulario y su módulo de manejadores a partir de una plantilla (en un proyecto, `project.find().new_form("settings")` lo pone en `ui/`). `designer.open_window(form_or_path, data=...)` devuelve el comando que abre un formulario desde cualquier app, y también acepta un `designer.Form` construido en código (con `handlers={"name": function}`). La API completa está en la [referencia del diseñador](https://albakore.github.io/pyzed/es/reference/designer/). #### Cómo está hecho ```mermaid classDiagram class Form { controls; events; theme; grid } class Control { name; type; x; y; width; height; props; events; anchor } class Component { type; props; events; render() } class Document { form; code; selection; history; add(); align(); wire() } class History { push(); undo(); redo() } class Designer { pyzed App: docks and gestures } class FormRuntime { dispatch(); view() } Form "1" *-- "*" Control Control ..> Component : type Document --> Form Document --> History Designer --> Document FormRuntime --> Form ``` - `model.py`: el formulario y sus controles, guardados como JSON. - `components.py`: la biblioteca de componentes y el `render` que comparten el editor y el runtime. - `geometry.py`: snap, alineación, distribución, redimensionado, anclajes y guías inteligentes (funciones puras). - `layout.py`: dónde va cada control, incluidos los contenedores flex y grilla. - `history.py`: deshacer/rehacer como instantáneas; las ediciones repetidas de un campo se juntan en un paso. - `code.py`: encuentra, añade, renombra y sustituye funciones manejadoras con `ast`. - `document.py`: cada comando de edición, sin UI, así que se puede probar entero. - `commands.py`: todos los comandos en una tabla, que leen los menús, la paleta, el teclado y la lista de atajos. - `docks.py`, `panels.py`, `surface.py`, `dialogs.py`, `console.py`, `project_panel.py`: las vistas. - `app.py`: la app `Designer`, que conecta clics y teclas con el documento. - `scene.py`, `scene_code.py`, `scene_runtime.py`, `scene_panel.py`: el diseñador de componentes. - `runtime.py`: `FormApp` y `FormWindow`, que dibujan un formulario y llaman a sus manejadores. - `lsp.py`, `intellisense.py`, `code_panel.py`, `settings.py`: el editor de código y sus servidores de lenguaje. - `preferences.py`, `locale.py`: los ajustes del editor y sus traducciones, leídas de `locales/.json`. - `new.py`: las plantillas de Nuevo formulario, leídas de las carpetas de `templates/`. ### Exportar tu app > Exporta un proyecto de Pyzed desde Pyzed Design como app para Windows, macOS y Linux en x64 y ARM: PyInstaller o Nuitka, icono de la app, pantalla de inicio, datos del archivo y un flujo de GitHub Actions para cada plataforma. Source: https://albakore.github.io/pyzed/es/editor/exporting/ **Archivo → Exportar aplicación…** (Ctrl+Shift+E) convierte el [proyecto](https://albakore.github.io/pyzed/es/editor/projects/) abierto en una app que la gente ejecuta sin instalar Python: un `.exe` en Windows, una `.app` en macOS, un ejecutable en Linux. Como la ventana de exportación de Godot, un solo diálogo lo reúne todo: el nombre y los datos de la app, las plataformas, el empaquetador, el icono y la pantalla de inicio. #### Exportar en tres pasos 1. Abre un proyecto (**Proyecto → Abrir**) y pulsa Ctrl+Shift+E. 2. Rellena **General** (al menos el nombre) y, si quieres, el **Icono** y la **Pantalla de inicio**. 3. Pulsa **Compilar para …** (el nombre de tu sistema). La salida de la compilación aparece en la [Consola](https://albakore.github.io/pyzed/es/editor/console-and-running/); al terminar, una notificación dice dónde está la app y cuánto ocupa, y **Mostrar en la carpeta** la abre. Las compilaciones siguientes no necesitan el diálogo: **Ejecutar → Compilar la aplicación para este equipo** (Ctrl+Shift+B) compila con los mismos ajustes. El resultado va a `dist/` en la carpeta del proyecto (`dist/Shop/`, `dist/Shop.exe` o `dist/Shop.app`). **Los ajustes viven en pyproject.toml:** El diálogo guarda en la tabla `[tool.pyzed.build]` de `pyproject.toml` al cerrarlo o al compilar. `uv run pyzed build` en una terminal y el flujo de CI usan exactamente los mismos ajustes; mira [Cómo funciona la compilación](https://albakore.github.io/pyzed/es/packaging/build/) para ver cada clave. #### General | Campo | Para qué sirve | | --- | --- | | **Nombre** | El ejecutable, la `.app` y la carpeta en `dist/`. Vacío: el nombre del proyecto. | | **Versión** | La versión del archivo en Windows y la del paquete en macOS. Vacío: la versión de `[project]`. | | **Editor** | La empresa en las propiedades del archivo en Windows. | | **Identificador** | Un dominio invertido como `com.acme.shop`: el id del paquete en macOS y el id de la app en Linux. macOS guarda los ajustes y permisos de la app bajo él, así que no lo cambies entre versiones. | | **Descripción**, **Copyright** | Las propiedades del archivo en Windows y el `Info.plist` de macOS. | #### Plataformas y arquitecturas PyInstaller y Nuitka compilan para el sistema en el que se ejecutan: un ordenador con Windows hace la app de Windows, un Mac la de macOS. La lista muestra los seis destinos que admite Pyzed, con **Este equipo** en el tuyo: | Destino | Sistema | Máquina de GitHub que lo compila | | --- | --- | --- | | `windows-x64` | Windows 10 y 11 en Intel y AMD | `windows-latest` | | `windows-arm64` | Windows 11 en ARM (Snapdragon) | `windows-11-arm` | | `macos-arm64` | Macs con Apple silicon (M1 y posteriores) | `macos-latest` | | `macos-x64` | Macs con Intel | `macos-15-intel` | | `linux-x64` | Linux en Intel y AMD | `ubuntu-22.04` | | `linux-arm64` | Linux en ARM (Raspberry Pi 4/5, servidores ARM) | `ubuntu-22.04-arm` | Marca las plataformas que quieras y pulsa **Escribir workflow de CI**: escribe `.github/workflows/build.yml`, que compila cada destino marcado en las máquinas de GitHub cada vez que subes una etiqueta de versión (`git tag v1.0.0 && git push --tags`). Mira [Compilar en CI](https://albakore.github.io/pyzed/es/packaging/ci/). Las máquinas Linux son a propósito el Ubuntu más antiguo que ofrece GitHub: una app compilada en un sistema más antiguo funciona en más distribuciones. **Archivo comprimido** además empaqueta la app en un `zip` (o `tar.gz`) con un nombre como `shop-windows-x86_64.zip`, el que busca el [actualizador](https://albakore.github.io/pyzed/es/packaging/updates/), listo para adjuntarlo a una Release de GitHub. **Un proyecto en WSL compila la app de Linux:** En Windows, un proyecto que vive en WSL se compila dentro de WSL: **Este equipo** es entonces `linux-x64` y obtienes la app de Linux sin salir de Windows. #### Empaquetador: PyInstaller o Nuitka | | PyInstaller | Nuitka | | --- | --- | --- | | Cómo | Empaqueta tus `.py` tal cual, junto a Python | Compila tu código y sus dependencias a C y luego a código máquina | | Tiempo de compilación | Segundos | Minutos (más rápido con [ccache](https://ccache.dev) instalado) | | Velocidad | Python normal | Compilado: código Python puro más rápido | | Tamaño de un archivo único | Mayor | Alrededor de un 15 % menor (mejor compresión) | | Tamaño de una carpeta | Menor | Mayor (el código compilado ocupa más que los `.pyc`) | | Necesita | Nada (uv lo instala) | Un compilador de C | Los dos se ejecutan dentro del entorno del proyecto con `uv run --with …`, así que no instalas ninguno. Nuitka necesita un compilador de C: `gcc` en Linux (`sudo apt install gcc`), las herramientas de línea de comandos de Xcode en macOS (`xcode-select --install`) y, en Windows, las Build Tools de Visual Studio, o Nuitka descarga uno la primera vez. | Opción | Qué hace | | --- | --- | | **Un solo archivo** | Un ejecutable que se descomprime en una carpeta temporal al arrancar. Una carpeta arranca más rápido y puede [actualizarse sola](https://albakore.github.io/pyzed/es/packaging/updates/). | | **Solo ventana** | Sin consola junto a la app. Desactívalo mientras buscas un error: la consola muestra los print y los tracebacks. | | **Optimización** | El `-O` de Python (quita los `assert`) o `-OO` (también los docstrings). Déjalo en *Ninguno* si alguna biblioteca usa docstrings al ejecutarse. | | **Ligera** | Deja fuera módulos estándar que las apps no usan (`tkinter`, `test`, `idlelib`, `lib2to3`, `ensurepip`...). | | **Quitar símbolos** | Quita los símbolos de depuración de las bibliotecas: ocupa menos en Linux y macOS. | | **Comprimir con UPX** | Comprime los binarios con [UPX](https://upx.github.io/) si está instalado. Algunos antivirus desconfían de los archivos comprimidos con UPX, así que viene desactivado. | | **Más opciones** | Cualquier otra cosa, pasada al empaquetador tal cual (por ejemplo `--lto=yes` para Nuitka). | #### Icono Elige una imagen cuadrada, idealmente un PNG de 1024 × 1024 con fondo transparente. La compilación crea a partir de ella todos los archivos que piden los sistemas, sin herramientas extra: - `app-icon.ico` con imágenes de 16 a 256 píxeles, para el `.exe` de Windows, el Explorador y la barra de tareas; - `app-icon.icns` hasta 1024 píxeles (Retina), para el paquete de macOS, el Finder y el Dock; - `app-icon.png` (256 píxeles), incluido en la app como icono de sus ventanas: la barra de título y la barra de tareas en Windows y Linux. La vista previa muestra cómo se ve en pequeño: si la versión de 16 píxeles es un borrón, simplifica el dibujo. Un archivo `.ico` o `.icns` se usa tal cual, para su sistema. En código, cualquier ventana puede tener su propio icono con `Window.icon` (mira [Iconos y metadatos](https://albakore.github.io/pyzed/es/packaging/icons-metadata/)). #### Pantalla de inicio Una pantalla de inicio (splash screen) es una imagen en una ventana sin marco, centrada en la pantalla, mientras arranca la app. Se queda al menos **Duración** segundos; luego se abre el formulario de inicio y la pantalla se cierra. Si el formulario tarda más en cargar (una base de datos grande, un evento `load` lento), la pantalla sigue hasta que está listo, así nadie se queda mirando un escritorio vacío. **Texto** se escribe sobre la parte inferior de la imagen (por ejemplo *Cargando…*). **Pantalla de inicio temprana** también pide al empaquetador su propia pantalla, que el cargador muestra incluso antes de que arranque Python, mientras una app de un solo archivo se descomprime. PyInstaller tiene una en Windows y Linux (dibuja con Tk, así que el Python del proyecto necesita `tkinter`; los Python de uv lo traen), y Nuitka para archivos únicos en Windows. Si las dos están activas, la temprana deja paso a la de Pyzed en cuanto arranca la app. Haz la imagen del tamaño que quieras para la ventana (si es mayor, se reduce para caber en 900 × 600); un PNG o JPEG de unos 600 × 360 va bien. #### Archivos y módulos La compilación ya encuentra la carpeta de la interfaz, el backend en `src/` y cada módulo que importa el código de los formularios. Añade aquí lo que no puede ver: | Lista | Para | | --- | --- | | **Archivos y carpetas** | Datos que abre la app: imágenes, una base de datos, un archivo de configuración. Misma ruta desde la raíz del proyecto, dentro de la app. | | **Importaciones ocultas** | Módulos cargados por nombre (`importlib.import_module("plugins.csv")`). | | **Paquetes completos** | Paquetes con archivos de datos o plugins que hay que incluir enteros (`babel`, `certifi`...). | | **Excluir** | Módulos que no incluir, para que la app ocupe menos (`numpy.tests`). | #### Línea de comandos La última sección muestra la misma compilación como comando de terminal, la tabla que escribe en `pyproject.toml` y un botón que copia la línea de comandos completa de PyInstaller o Nuitka, para adaptarla o informar de un problema. #### Siguiente - [Cómo funciona la compilación](https://albakore.github.io/pyzed/es/packaging/build/): cada ajuste y lo que acaba dentro de la app. - [Compilar en CI](https://albakore.github.io/pyzed/es/packaging/ci/) para las plataformas que no tienes. - Firma e instaladores: [Windows](https://albakore.github.io/pyzed/es/packaging/windows/), [macOS](https://albakore.github.io/pyzed/es/packaging/macos/), [Linux](https://albakore.github.io/pyzed/es/packaging/linux/). - ¿Algo falla? [Solución de problemas](https://albakore.github.io/pyzed/es/packaging/troubleshooting/). ### Agentes de IA > El panel del agente de Pyzed Design: un agente de IA que crea formularios y componentes y escribe su código Python, con asistente en línea y varios modelos. Source: https://albakore.github.io/pyzed/es/editor/ai-agents/ El panel **Agente** (dock derecho, junto a Propiedades y Eventos) es un agente de IA. Describe lo que quieres y lee el formulario abierto, añade y ordena controles, conecta eventos, escribe los manejadores, crea formularios y componentes nuevos y revisa su propio código. Cada cambio que hace es un paso normal de deshacer, y puedes rechazar toda la ejecución de una vez. (Image: El agente añadió un interruptor y conectó su evento) #### Primeros pasos 1. Abre el panel: la pestaña **Agente** del dock derecho, **Ver → Panel del agente** o Ctrl+Alt+A. 2. La primera vez el panel te pide **conectar un modelo** y muestra los proveedores principales. Pulsa uno para [iniciar sesión con el navegador](#iniciar-sesión), o **Todos los proveedores…** para el resto. 3. Escribe lo que quieres y pulsa Intro. Claude necesita las dependencias opcionales (`anthropic` y `keyring`); el Pyzed Design compilado ya las trae: ```bash pip install "pyzed-ui[agents]" # o: uv add "pyzed-ui[agents]" ``` (Image: Una conversación vacía: todo el panel es el editor de mensajes) ##### Iniciar sesión El inicio de sesión ocurre dentro de Pyzed Design; no hace falta ningún otro programa. - **Anthropic (Claude), OpenAI, Gemini, xAI, Mistral, DeepSeek, Groq**: estos proveedores no permiten que otras apps inicien sesión con OAuth (el inicio de sesión con suscripción de Anthropic es solo para sus propias apps), así que Pyzed inicia sesión con una página propia. El navegador abre una página **Connect … to Pyzed Design** que Pyzed sirve en este equipo (`127.0.0.1`, una dirección de un solo uso) y que enlaza a la página de claves de API del proveedor. Crea una clave allí y pégala en la página de Pyzed, o simplemente pulsa el botón **Copiar** de la clave: Pyzed también detecta la clave copiada. Pyzed la comprueba con el proveedor, la guarda en el llavero del sistema, carga los modelos del proveedor y vuelve al frente, así que puedes cerrar la pestaña y seguir trabajando. Mientras espera, una tarjeta **Iniciando sesión** ofrece **Pegar la clave** (leer el portapapeles ahora), **Abrir la página de claves** y **Cancelar**. El portapapeles solo se lee mientras se ve esa tarjeta (cinco minutos como mucho), y solo se usa un texto que parezca una clave de ese proveedor. La página local solo acepta una clave enviada desde ella misma: otros sitios y otros nombres de host no pueden usarla. Para usar tu plan Claude Pro o Max en lugar de una clave, mira [Tu suscripción de Claude](#tu-suscripción-de-claude). - **OpenRouter**: OAuth real con PKCE. Apruebas a Pyzed en la página de OpenRouter y la clave que emite vuelve a Pyzed directamente; no hay nada que copiar. - **GitHub Copilot**: **Iniciar sesión con GitHub** usa el flujo de dispositivo de GitHub. Pyzed abre `github.com/login/device` y muestra un código corto; escríbelo allí, aprueba a Pyzed y el token vuelve solo (la tarjeta tiene un botón para copiar el código y **Cancelar**). Da acceso a los modelos de tu cuenta de GitHub y tu plan de Copilot a través de GitHub Models. Iniciar sesión necesita el client ID de una GitHub App en los ajustes del proveedor; sin ella, **Iniciar sesión con un token** acepta un token de acceso personal detallado con el permiso **Models**. - **Google Vertex AI**: **Iniciar sesión con Google** abre la página de consentimiento de Google (OAuth con PKCE) y el token vuelve a Pyzed; se renueva solo. Si ya ejecutaste `gcloud auth application-default login`, se usan esas credenciales y no hace falta nada más. Elige el **Proyecto de Google Cloud** (con Vertex AI activado) en los ajustes del proveedor, o pulsa **Listar mis proyectos**. - **Anthropic sin clave**: la [federación de identidades](#claude-sin-clave-de-api) cambia un token de tu proveedor de identidad por un token de Claude de corta duración. - **Ollama, LM Studio**: no hay que iniciar sesión. Al pulsarlos se piden los modelos al servidor local. Siempre puedes pegar una clave a mano en los [ajustes](#ajustes) o definir la variable de entorno del proveedor. (Image: Iniciando sesión en Anthropic: la página de claves está abierta y Pyzed espera la clave copiada) #### El panel El panel se ve y funciona como el Agent Panel de Zed. La misma vista de conversación sirve para Pyzed Agent y para los [agentes externos](#agentes-externos) como Claude Code, Codex o Gemini CLI. | Parte | Qué hace | | --- | --- | | **Barra superior** | El icono del agente y el título de la conversación (**Hilo nuevo con Pyzed Agent** mientras está vacía). Pulsa el título para renombrarla. A la derecha: **+** (una conversación nueva con Pyzed Agent o con cualquier [agente externo](#agentes-externos), y **Añadir más agentes**), **pantalla completa** (el panel ocupa toda la ventana; púlsalo otra vez para devolverlo a su dock) y **⋯**. | | **Menú ⋯** | **Hilo actual** (**Renombrar hilo**, **Copiar el hilo como Markdown**, **Abrir el hilo como Markdown**), **Servidores MCP** (**Añadir servidor…**), **Contexto** (**Skills** y **Abrir las reglas del proyecto** / **Crear reglas del proyecto**, el `AGENTS.md` del proyecto), **Ajustes**, **Historial de hilos** y **Mostrar el razonamiento**. Con un agente externo, también **Volver a iniciar sesión** y **Reiniciar el agente** (su programa arranca de nuevo). | | **Tus mensajes** | Tarjetas del color del editor. Pulsa una para editarla: Intro (o el botón ↵) la envía otra vez desde ese punto y reemplaza lo que venía después; Esc o ✕ cancela. Ver [Editar un mensaje y puntos de control](#editar-un-mensaje-y-puntos-de-control). Los agentes externos guardan su propio historial, así que sus mensajes aún no se pueden editar, como en Zed. | | **La respuesta del agente** | Markdown. Su razonamiento queda plegado bajo **Pensamiento** (púlsalo para leerlo). | | **Llamadas a herramientas** | Una línea por cada paso, en el momento en que ocurre: el icono del tipo de paso, qué hace el agente (**Añadiendo controles**, **Read file**…) y sobre qué trabaja. Pulsa una línea para ver su entrada y su salida. Las ediciones y los comandos son tarjetas: el diff, o la salida del comando mientras corre. Una llamada que espera tu permiso lo pide en su tarjeta: **Permitir siempre**, **Permitir** o **Rechazar**. Una llamada fallida muestra una ✕ roja. | | **Fin de un turno** | Cuánto tardó el agente y cuántos tokens escribió (`42s • 3.8k tokens`), **Copiar esta respuesta** y, en el último turno, **Ir arriba**. Mientras trabaja, un indicador con el tiempo y los tokens hasta ahora. | | **Barra de actividad** | Pegada encima del editor de mensajes, lo que necesita tu atención: una llamada **Esperando confirmación**, el **Plan** (`Actual: … quedan 2`; púlsalo para ver todos los pasos), las **Ediciones** (`2 archivos • +12 -3`, con **Revisar los cambios**, **Rechazar todo** y **Mantener todo**; despliégalo para **Conservar** o **Rechazar** cada archivo) y los [mensajes en cola](#mensajes-en-cola). | | **Editor de mensajes** | Crece con el texto. Intro envía y Mayús+Intro empieza otra línea. Escribe @ para [añadir contexto](#añadir-contexto-con-) y / para [comandos](#comandos-con-); en su lista ↑ ↓ te mueven, Intro elige y Esc la cierra. Las flechas de arriba a la derecha lo hacen más alto. Una conversación vacía es todo editor de mensajes, como en Zed. | | **Izquierda de la fila inferior** | **+** (**Añadir contexto**: archivos, el formulario, su código, controles, **Adjuntar imágenes…**, **Pegar imagen** y **Captura del formulario**; ver [Imágenes](#imágenes)), **Seguir al agente** (la mira: ver [Ver trabajar al agente](#ver-trabajar-al-agente)), **Modo rápido** (en Claude Opus 5.5 y Opus 5 a través de la API de Claude: respuestas más rápidas a mayor precio), **Pensamiento** (el cerebro: deja que Claude piense antes de responder) y su **esfuerzo** (**Bajo**, **Medio**, **Alto**, **Muy alto**, **Máximo**). Los tres últimos se ven en los modelos que los tienen. | | **Derecha de la fila inferior** | Un pequeño anillo con cuánto ocupa la ventana de contexto del modelo (`48k / 1M`; amarillo pasado el 80 % y rojo pasado el 95 %), el **perfil**, el **modelo** (todos los proveedores con sus modelos; uno sin configurar ofrece **Conectar…**) y **Enviar**. Mientras el agente trabaja, **Enviar** pasa a ser **Detener**, o **Poner en cola** cuando hay texto. | | **Opciones del agente** | Con un agente externo, las opciones que ofrece ocupan el lugar del perfil y el modelo, como en Zed: sus modos (los de Claude Code: **Default**, **Accept edits**, **Plan mode**…), sus modelos (**Fable 5.1**, **Opus 5.5**, **Sonnet 5.5**, **Haiku 5.5**), su esfuerzo o nivel de razonamiento y todo lo demás que liste. Claude Code y Codex usan la última versión de su adaptador, así que los modelos nuevos aparecen solos. Pyzed recuerda lo que eliges para las próximas conversaciones con ese agente. | El perfil dice qué puede hacer Pyzed Agent, como los perfiles de Zed: | Perfil | Qué puede hacer el agente | | --- | --- | | **Escribir** | Edita formularios, código y archivos y ejecuta la app. Las ediciones del formulario abierto nunca preguntan (cada cambio se puede deshacer); con **Preguntar antes de escribir archivos** activado (por defecto, en el mismo menú) pregunta antes de crear documentos o escribir archivos. | | **Preguntar** | Solo lee: responde preguntas y sugiere código sin cambiar nada. | | **Mínimo** | Sin herramientas: una conversación simple con el modelo. | ##### Editar un mensaje y puntos de control Pulsa uno de tus mensajes para cambiarlo y enviarlo otra vez: la conversación vuelve a ese punto (lo que venía después se reemplaza por la nueva respuesta), y también los formularios y el código que el agente cambió después. Todo lo que sigue al mensaje que editas se ve atenuado hasta que lo envías o cancelas. Cuando el agente cambió documentos después de un mensaje, encima de él aparece **Restaurar punto de control**. Deja esos formularios y su código como estaban cuando enviaste ese mensaje, sin tocar la conversación. Los puntos de control duran mientras Pyzed Design está abierto. ##### Mensajes en cola Puedes seguir escribiendo mientras el agente trabaja: Intro pone el mensaje en una cola, que se ve en la barra de actividad (**1 mensaje en cola**), y se envía cuando el agente termina. Despliégala para editar un mensaje (vuelve al editor de mensajes) o quitarlo. **Enviar ya** detiene al agente para que el siguiente mensaje salga en el momento; **Vaciar** borra la cola. Una ejecución que se para con un error o que espera un inicio de sesión deja la cola esperando. (Image: Todos los proveedores en el selector de modelos) Cada mensaje lleva una nota breve de dónde estás (el proyecto y su entorno, el documento abierto con sus controles y su código, los controles seleccionados, el manejador del editor de código), así que "alinea estos" o "valida este campo" se refieren a lo que tienes delante. Pyzed Agent además empieza sabiendo la caja de herramientas (cada tipo con sus props y eventos), los temas, los componentes de `pz.ui` y las buenas prácticas de Pyzed, así que construye enseguida en lugar de buscarlos primero. ##### Añadir contexto con @ Escribe @ (o pulsa **+**) y elige qué adjuntar: la **selección**, el **formulario** abierto, su **código**, uno de sus **controles**, un **archivo** del proyecto (un archivo de imagen va como imagen) o una **captura** del formulario abierto. Sigue escribiendo para filtrar la lista; Intro elige la fila resaltada. Lo que elijas aparece como una etiqueta sobre el mensaje y su contenido va con el mensaje, así el agente no tiene que buscarlo. Los archivos que pueden tener secretos (`.env`, claves…) nunca se ofrecen. (Image: Añadir contexto con @) ##### Comandos con / Escribe / al principio del mensaje: | Comando | Hace | | --- | --- | | `/new` | Empieza una conversación nueva. | | `/app` | Empieza una petición para [crear una aplicación completa](#crear-una-aplicación). | | `/history` | Muestra las conversaciones pasadas. | | `/settings` | Abre los ajustes de los agentes de IA. | | `/form`, `/component` | Empieza una petición para generar un formulario o un componente. | | `/explain` | Explica el formulario abierto y sus manejadores. | | `/fix` | Revisa el código del formulario con `validate_code` y corrige lo que encuentre. | | `/tidy` | Alinea los controles e iguala los márgenes. | | `/look` | Hace una captura del formulario, la mira y mejora su distribución y su diseño. | ##### Qué puede hacer el agente | Herramienta | Hace | | --- | --- | | `get_document` | Lee el formulario o componente abierto: ajustes, cada control con su rect, props, eventos y estilo, la selección y el código. | | `add_controls`, `add_control`, `update_control`, `remove_controls` | Coloca un diseño entero de una vez, o un control; cambia, mueve a un contenedor, renombra y borra controles. Tipos y props se comprueban contra la caja de herramientas. | | `set_form`, `select_controls` | Cambia el título, tamaño, tema o estilo del formulario; selecciona controles para que los veas. | | `wire_event`, `set_handler`, `set_code` | Conecta eventos y escribe un manejador o el módulo entero (debe ser Python válido). | | `new_document`, `save_document` | Crea un formulario o componente junto al abierto y lo abre en una pestaña; guarda. | | `list_components`, `describe_component`, `search_icons`, `list_themes`, `read_docs`… | El mismo conocimiento que el [servidor MCP](https://albakore.github.io/pyzed/es/guides/mcp/): componentes, iconos, temas y esta documentación. | | `validate_code`, `validate_form` | Revisan código y formularios antes de devolverlos. | | `list_files`, `read_file`, `search_text`, `write_file` | Trabajan con archivos del proyecto, nunca fuera de su carpeta. | | `screenshot_form` | Dibuja el formulario abierto en una imagen y la mira, para revisar la distribución tal como la ves. | | `run_app` | Ejecuta la app (el formulario principal del proyecto u otro) en el entorno del propio proyecto sin mostrar ninguna ventana, y devuelve lo que imprimió (errores y trazas incluidos) y una imagen de su ventana. | | `update_plan` | Muestra el plan del agente como una lista sobre el cuadro de mensaje y lo mantiene al día. | | `get_project`, `create_project`, `add_dependency`, `check_project`, `open_document` | Leen el proyecto abierto (formularios, componentes, dependencias, punto de entrada), crean uno nuevo y lo abren, añaden un paquete con uv, ejecutan las comprobaciones del proyecto y abren cualquiera de sus archivos en una pestaña. | | `list_examples`, `read_example`, `new_form` | Los ejemplos de Pyzed para aprender de ellos, y un formulario inicial escrito a partir de una descripción. | | `mcp__` | Las herramientas de los [servidores MCP](#servidores-mcp) que añadiste. | Con **Preguntar antes de escribir archivos** activado, las herramientas que **crean o escriben archivos** preguntan antes: su tarjeta muestra **Permitir siempre**, **Permitir** y **Rechazar**. **Permitir siempre** se recuerda: **Ajustes → Permisos** lista esas herramientas y **Volver a preguntar** lo deshace. Si lo desactivas, no pregunta. Los cambios al documento abierto nunca preguntan, porque se pueden deshacer. #### Imágenes El agente puede mirar imágenes: una maqueta que reconstruir, una captura de un error, el formulario tal como se ve ahora. Añádelas con **+**: - **Adjuntar imágenes…** abre el diálogo de archivos del sistema (PNG, JPEG, GIF o WebP, hasta 5 MB cada una). - **Pegar imagen** toma la imagen que copiaste (una captura, una imagen del navegador). - **Captura del formulario** dibuja el formulario abierto y la adjunta. Aparecen como miniaturas sobre el mensaje y en la conversación. Las herramientas también pueden responder con imágenes: `screenshot_form` y las de los servidores MCP que devuelven imágenes. El modelo tiene que admitir imágenes (Claude, GPT-4.1 y posteriores, Gemini, la mayoría de modelos de visión de Ollama…). #### Crear una aplicación `/app` o una petición normal ("Crea una app para llevar mis gastos") empiezan una aplicación entera, no solo un formulario. El agente conoce el editor (proyectos, formularios, componentes, estilos, enlace de datos, el [servidor MCP](https://albakore.github.io/pyzed/es/guides/mcp/) y esta documentación) y, en cada mensaje, lo que está abierto: el proyecto y sus archivos, el formulario, la selección y las demás pestañas. Luego: 1. Hace un plan con `update_plan`: una tarjeta **Plan** sobre el cuadro de mensaje muestra cada paso, lo marca al terminarlo y se pliega cuando todo está hecho (pulsa su cabecera para plegarla tú). 2. Crea un [proyecto](https://albakore.github.io/pyzed/es/editor/projects/) con `create_project` (o usa el abierto) y añade los paquetes que necesita con `add_dependency`. 3. Escribe los formularios y componentes, coloca sus controles, conecta los eventos y escribe el código, leyendo los ejemplos y la documentación de Pyzed cuando los necesita, y los guarda. 4. Mira cada formulario con `screenshot_form`, y ejecuta `check_project` y `validate_code` hasta que pasan. 5. Ejecuta la app con `run_app`, lee sus errores y la imagen de su ventana, arregla lo que falla y la vuelve a ejecutar hasta que arranca sin errores. Luego te dice qué construyó y cómo ejecutarla (F5). Trabaja solo de principio a fin, con hasta 150 respuestas del modelo por mensaje. Para que no se interrumpa, desactiva **Preguntar antes de escribir archivos** en el menú de perfil; mientras está activado, pregunta antes de escribir archivos y antes de ejecutar la app. #### Generar formularios y componentes **Ver → Generar un formulario con IA…** y **Generar un componente con IA…** (también en la paleta de comandos) abren el panel con el principio de la petición ya escrito: termina la frase y pulsa Intro. Por ejemplo: > Crea un formulario nuevo para un cliente: nombre, correo, un desplegable de país y botones Guardar / Cancelar; > Guardar valida el correo y muestra un aviso. El agente crea `customer.pzform` y `customer.py`, los abre, coloca y alinea los controles, conecta los botones y escribe los manejadores. Con un componente crea el `.pzcomp` y su clase, que luego aparece en la caja de herramientas como cualquier [componente](https://albakore.github.io/pyzed/es/editor/component-designer/). #### Asistente en línea En el editor de código, Ctrl+Alt+I (o el botón ✦ de su cabecera) abre una barra sobre el código. Describe el cambio y pulsa Intro: el modelo reescribe el manejador (o el archivo entero, en modo archivo completo) y el editor muestra el resultado. **Deshacer** devuelve el código anterior y **Conservar** cierra la barra. Nada se aplica al formulario hasta que pulses **Aplicar**, como con tus propias ediciones. (Image: Asistente en línea sobre un manejador) #### Ajustes Los ajustes se abren dentro del panel, como en Zed: **⋯ → Ajustes del agente**, **Ajustes → Agentes de IA → Ajustes de los agentes de IA…** o **Ajustes de los agentes de IA…** en la paleta de comandos. La flecha de la cabecera vuelve a la conversación. Primero están los de cada día: el modelo por defecto (con el **Esfuerzo**, el **Pensamiento** y el **Modo rápido** de Claude cuando Pyzed Agent responde con Claude; las opciones propias de un agente externo están en la caja de mensajes), los proveedores de modelos y los permisos. **Avanzado** despliega los agentes externos, los servidores MCP, las skills, las reglas, el MCP de Pyzed en tus agentes y más opciones. (Image: Proveedores, claves y opciones del agente) ##### Proveedores | Proveedor | Cómo conectarlo | Notas | | --- | --- | --- | | **Anthropic** | **Iniciar sesión con el navegador**, clave de API, `ANTHROPIC_API_KEY` o [federación de identidades](#claude-sin-clave-de-api) | Los modelos Claude transmiten resúmenes de su razonamiento, usan pensamiento adaptativo y admiten esfuerzo y modo rápido. Si existe un perfil de la CLI de Anthropic (`ant auth login`) también se usa, pero la CLI no hace falta. | | **OpenAI** | **Iniciar sesión con el navegador**, clave de API u `OPENAI_API_KEY` | | | **Google Gemini** | **Iniciar sesión con el navegador**, clave de AI Studio o `GEMINI_API_KEY` | A través del endpoint compatible con OpenAI de Gemini. | | **OpenRouter** | **Iniciar sesión con el navegador** (OAuth), clave de API u `OPENROUTER_API_KEY` | OAuth con PKCE: la clave que OpenRouter emite para Pyzed va al llavero. | | **xAI**, **Mistral**, **DeepSeek**, **Groq** | **Iniciar sesión con el navegador**, clave de API o `XAI_API_KEY`, `MISTRAL_API_KEY`, `DEEPSEEK_API_KEY`, `GROQ_API_KEY` | Sus modelos aparecen tras iniciar sesión. | | **GitHub Copilot** | **Iniciar sesión con GitHub** (flujo de dispositivo), un token detallado con el permiso **Models** o `GITHUB_TOKEN` | Los modelos de GitHub Models (`openai/gpt-4.1`, …) con los límites de tu cuenta. Iniciar sesión necesita el **Client ID de la GitHub App** (o `PYZED_GITHUB_CLIENT_ID`). | | **Google Vertex AI** | **Iniciar sesión con Google**, o las credenciales de Google Cloud de este equipo (`gcloud auth application-default login`) | Gemini a través del endpoint compatible con OpenAI de Vertex AI, sin clave. Necesita un **Proyecto de Google Cloud** y una **Ubicación** (`global` por defecto); iniciar sesión con Google necesita un **Cliente OAuth** (app de escritorio) cuyo secreto va al llavero. | | **Ollama**, **LM Studio** | Nada: corren en tu equipo | Cambia la **URL del servidor** si corren en otra máquina. | | **Compatible con OpenAI** | URL del servidor y una clave si la necesita | vLLM, llama.cpp, una pasarela de empresa… | Pulsa un proveedor para abrirlo: **Iniciar sesión con el navegador** (ver [Iniciar sesión](#iniciar-sesión)), pega una clave y **Guardar**, **Obtener una clave** (abre la página de claves del proveedor), **Quitar la clave** y **Actualizar modelos**, que pide al proveedor su lista de modelos para que aparezcan en el selector. (Image: El proveedor Anthropic abierto en los ajustes de agentes de IA: el campo de la clave, Iniciar sesión con el navegador, Obtener una clave y los campos de federación de identidades) **Probar la conexión** comprueba el proveedor con lo que esté configurado y muestra de dónde salen las credenciales. ##### Claude sin clave de API Abre **Anthropic** y rellena **Federación de identidad de cargas de trabajo**. Un token de tu proveedor de identidad (GitHub Actions, Google Cloud, Azure, Okta, Kubernetes…) se cambia por un token de Claude de corta duración, así que no se guarda ninguna clave de API. Primero crea el emisor y la regla de federación en la Claude Console y luego rellena: | Campo | Qué va ahí | | --- | --- | | **Regla de federación** | El ID de la regla, `fdrl_…`. | | **ID de la organización** | El UUID de tu organización. | | **Cuenta de servicio** | `svac_…`, cuando la regla no nombra una. Opcional. | | **Espacio de trabajo** | `wrkspc_…` o `default`. Opcional. | | **Token de identidad** | De dónde sale el token: un **Archivo** (la ruta del JWT, o `ANTHROPIC_IDENTITY_TOKEN_FILE`), un **Comando** que imprime uno nuevo (por ejemplo `gcloud auth print-identity-token`) o una **Variable** (`ANTHROPIC_IDENTITY_TOKEN`). | Una clave de API guardada tiene prioridad sobre la federación; quítala para usar la federación. También sirven las variables `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID` y `ANTHROPIC_IDENTITY_TOKEN_FILE`. ##### Servidores MCP **Servidores MCP** suma las herramientas de cualquier servidor del [Model Context Protocol](https://modelcontextprotocol.io) a las del agente. **Añadir un servidor MCP** pide: - un **Nombre**; - el **Comando**, como lo escribirías en una terminal (`npx -y @modelcontextprotocol/server-github`, `uvx mcp-server-fetch`…), **o la URL** de un servidor remoto (Streamable HTTP); - **Secretos**: pares `NOMBRE=valor` separados por `;`, que se pasan como variables de entorno (o como cabeceras para una URL). Van al llavero del sistema, nunca a `agents.json`. **Probar** arranca el servidor y lista sus herramientas; el interruptor lo apaga sin quitarlo. Sus herramientas aparecen como `mcp__`. Las que el servidor no marca como de solo lectura preguntan antes en **Preguntar antes de escribir**, como al escribir archivos; en **Solo lectura** se dejan fuera. ##### Agentes externos **Agentes externos** ejecuta Claude Code, Gemini CLI, Codex o cualquier agente que hable el [Agent Client Protocol](https://agentclientprotocol.com) dentro del panel, como en Zed: - **Instalar desde el registro** lista los agentes del [registro de ACP](https://agentclientprotocol.com/registry) con un buscador. **Instalar** descarga el agente para tu sistema (un programa, o un paquete que se ejecuta con `npx` o `uvx`) en la carpeta de ajustes de Pyzed y lo añade. Cuando el registro tiene una versión más nueva, su fila ofrece **Actualizar**; **Desinstalar** lo quita junto con sus archivos. - **Añadir agente propio** acepta cualquier comando, sus argumentos y sus secretos. Empieza una conversación con un agente desde el menú **+**, o elígelo en el selector de modelos en **Agentes externos**. El interruptor de cada fila lo oculta sin quitarlo. El agente corre como su propio programa en la carpeta del proyecto e inicia sesión a su manera. Cuando necesita iniciar sesión, la conversación muestra **Inicia sesión en …** con un botón por cada forma que ofrece el agente, como en Zed: - **En el navegador** (un globo): el agente abre su página de inicio de sesión y el panel espera; cuando lo apruebas, Pyzed vuelve al frente y envía tu mensaje. - **En una terminal**: el inicio de sesión del agente (por ejemplo `claude /login`, o `gemini` con su opción de inicio de sesión) se abre en una terminal; el panel comprueba cada pocos segundos y sigue solo cuando ya has iniciado sesión. - **Con una clave**: el agente lee una clave de una variable; Pyzed abre la página para conseguirla y los **Secretos** del agente, donde se guarda en el llavero. En WSL, el navegador y la terminal son los de Windows. Pyzed responde a sus peticiones: lee y escribe archivos solo dentro del proyecto y nunca los que pueden tener secretos, y pregunta antes de actuar según el perfil (**Preguntar** solo permite leer, **Mínimo** rechaza todas las herramientas). Con **Dejar que los agentes externos controlen el editor** activado (por defecto), el agente recibe por MCP las herramientas del agente integrado: lee y cambia el formulario abierto y su código, crea formularios y componentes, ejecuta la app con `run_app`, lleva un plan y conoce los componentes y la documentación de Pyzed. Sus cambios se ven en vivo en el editor, y **Conservar** y **Rechazar** funcionan como con el agente integrado. Pyzed sirve estas herramientas en `127.0.0.1`, en una dirección de un solo uso y con un token que solo recibe ese agente (los agentes que solo arrancan servidores locales reciben un pequeño programa puente). Sus propias herramientas de archivos y de terminal siguen siendo suyas. El plan que sigue el agente se ve en la tarjeta **Plan**. ##### El MCP de Pyzed en tus agentes Al arrancar, Pyzed Design busca los agentes de IA instalados en este equipo (Claude Code, Claude Desktop, Cursor, Windsurf, Gemini CLI, Codex, VS Code y Zed) y añade el [servidor MCP de Pyzed](https://albakore.github.io/pyzed/es/guides/mcp/) a los que todavía no lo tienen, así conocen los componentes, la documentación y las comprobaciones de Pyzed allí donde los uses. Luego un mensaje te pide reiniciarlos: un agente lee sus servidores MCP al arrancar. **Reiniciar agentes** reinicia los agentes externos que corren en Pyzed; los demás, ciérralos y ábrelos. **Ajustes → El MCP de Pyzed en tus agentes** lista cada agente con **Añadir** o **Quitar**. Pyzed añade el servidor una sola vez a cada agente: si lo quitas, Pyzed no lo vuelve a poner. Solo cambia la entrada `pyzed` de los ajustes de cada agente, se guarda una copia del archivo al lado (`.pyzed-backup`) y se conservan los comentarios de los archivos de ajustes (los de Zed y VS Code los tienen). Apaga **Añadirlo a los agentes nuevos al iniciar** para que no lo haga. Los agentes arrancan el servidor con [uv](https://docs.astral.sh/uv/) (`uvx --from "pyzed-ui[mcp]" pyzed mcp`; Pyzed Design trae su propio `uv`). ##### Ver trabajar al agente Con **Seguir al agente** activado (la mira abajo en el editor de mensajes, activada por defecto), los archivos que escribe el agente se abren mientras trabaja: los formularios y componentes en su pestaña, el código y otros archivos de texto en el editor de código y, con un agente externo, el archivo que está editando. Un archivo que ya está abierto muestra el texto nuevo al instante, salvo que tenga cambios tuyos sin guardar. El cuadro de mensaje conserva el teclado, así que puedes seguir escribiendo. Cuando el agente termina, se detiene por un error, pide tu confirmación o espera un inicio de sesión mientras Pyzed Design está en segundo plano, te avisa una notificación del escritorio. Desactívala con **Avisar cuando el agente termine** en los ajustes. ##### Tu suscripción de Claude **Usar mi suscripción de Claude** (lo primero en un panel vacío, y en el selector de modelos en **Agentes externos**) usa tu plan Claude Pro o Max a través de Claude Code, el agente de Anthropic, ya que Anthropic solo deja que sus propias apps inicien sesión con una suscripción: 1. Necesita [Node.js](https://nodejs.org/en/download) 22 o posterior; si no está, Pyzed abre la página de descarga. Pyzed encuentra Node también si se instaló con nvm, volta, fnm o en `~/.local/bin`, aunque Pyzed Design se haya abierto desde el menú del escritorio. Si una respuesta falla porque falta Node.js, **Instalar Node.js** abre la página de descarga y **Usar Pyzed Agent** envía el mensaje al agente propio de Pyzed. 2. Añade Claude Code como agente externo, lo elige y le deja controlar el editor. 3. Si Claude Code todavía no inició sesión, se abre en una terminal: elige iniciar sesión con tu cuenta de Claude y termina en el navegador. Pyzed detecta el inicio de sesión, vuelve al frente y queda listo. Claude Code guarda su propio inicio de sesión; Pyzed nunca lo ve. El uso cuenta para tu plan. Si una respuesta falla porque Claude Code cerró la sesión, **Iniciar sesión en Claude** vuelve a abrir la terminal. ##### Skills Una skill es una carpeta con un archivo `SKILL.md` (un `name` y una `description` en su cabecera, y después instrucciones) y los archivos que necesite, el mismo formato que usan Claude Code y otros agentes. El agente ve el nombre y la descripción de cada skill y lee el resto cuando una tarea lo necesita. Pyzed busca skills en el proyecto (`.claude/skills/` y `.agents/skills/`), en tu carpeta personal (`~/.claude/skills/` y `~/.agents/skills/`) y en su propia carpeta de ajustes. **Skills** las lista con un interruptor para desactivar cada una. Pega la dirección de un repositorio Git y pulsa **Instalar**, o **Desde una carpeta…**, para añadir skills; las instaladas desde Git ofrecen **Actualizar**. **Mostrar en la carpeta** abre la carpeta de la skill. Claude Code lee sus propias skills; los interruptores se aplican a Pyzed Agent. ##### Dónde se guardan las claves Las claves y tokens van al **llavero del sistema operativo**: Llavero en macOS, Administrador de credenciales en Windows, Secret Service (GNOME Keyring, KWallet) en Linux, bajo el servicio `pyzed`. Nunca se escriben en los archivos de ajustes de Pyzed, en las conversaciones ni en el proyecto. Si no hay llavero, una clave que pegues se conserva en memoria hasta que cierres el diseñador, y los ajustes lo indican; en ese caso usa una variable de entorno. Las variables de entorno, las apps abiertas desde el Finder o el menú Inicio, los secretos de MCP y las buenas prácticas están en [Claves de API y credenciales](https://albakore.github.io/pyzed/es/ai/api-keys/). El resto también se queda en tu equipo: los ajustes en `agents.json` y las conversaciones en `agents/threads/`, ambos en la [carpeta de ajustes de Pyzed](https://albakore.github.io/pyzed/es/editor/settings/#dónde-se-guardan). No se envía nada a ningún sitio salvo las peticiones al proveedor que elijas. ##### Opciones del agente | Ajuste | Qué hace | | --- | --- | | **Modelo predeterminado** | El modelo con el que empiezan las conversaciones nuevas. | | **Esfuerzo** | Cuánto piensa el modelo antes de responder (`low` … `max`). Solo Claude; `high` por defecto. | | **Pensamiento** | Deja que Claude piense antes de responder. Apagado, responde antes. También en el cuadro de mensaje. | | **Modo rápido** | Respuestas más rápidas a mayor precio (Claude Opus 5.5 y Opus 5). También en el cuadro de mensaje. | | **Preguntar antes de escribir archivos** | Crear formularios y componentes y guardar archivos esperan tu permiso. Activado por defecto; también en el menú de perfil. | | **Mostrar el razonamiento** | Muestra los resúmenes del razonamiento del modelo en la conversación. | | **Seguir al agente** | Abre los archivos que escribe el agente mientras trabaja. | | **Avisar cuando el agente termine** | Una notificación del escritorio mientras la ventana está en segundo plano. | | **Reglas** | Instrucciones añadidas a cada conversación: "Responde en español", "Nombra los botones con el sufijo `_button`"… | | **Reglas del proyecto** | El archivo de reglas propio del proyecto, como en Zed: el primero de `.rules`, `.cursorrules`, `.windsurfrules`, `.clinerules`, `.github/copilot-instructions.md`, `AGENT.md`, `AGENTS.md`, `CLAUDE.md` o `GEMINI.md` en su raíz. Pyzed Agent lo lee en cada hilo de ese proyecto; **Abrir** lo edita y **Crear AGENTS.md** empieza uno. Los agentes externos leen el suyo (Claude Code lee `CLAUDE.md`). | #### Cómo obtener buenos resultados - **Empieza por la pantalla, no por los widgets.** *"Un formulario de cliente: nombre, email, un desplegable de país y Guardar / Cancelar; Guardar valida el email y muestra un aviso"* le da al agente el objetivo y las reglas a la vez. - **Selecciona y luego pide.** Selecciona un grupo de controles y di *"ponlos en una fila con 8 px de separación"*. - **Usa @ para ser preciso.** `@email_input` más *"acepta solo direcciones de empresa"* no deja dudas sobre el control. - **Enséñale una imagen.** Adjunta un boceto o una captura del error, o pide `/look` para que revise el formulario. - **Deja que se compruebe solo.** Termina las peticiones con *"y luego ejecuta /fix"*, o pídele que valide antes de responder. - **Guarda las reglas en los ajustes.** Las *Reglas* se añaden a cada conversación: *"Responde en español"*, *"Usa el tema Gruvbox Dark"*. - **Revisa y luego conserva.** Tras una ejecución, **Cambios del agente** ofrece **Conservar** o **Rechazar** para toda la ejecución. Hay más prompts que funcionan bien en la [guía de prompts](https://albakore.github.io/pyzed/es/ai/prompting/). #### Historial **Historial de hilos** en el menú **⋯** (o `/history`) lista tus conversaciones agrupadas por fecha (hoy, ayer, esta semana…), con un buscador y el agente de cada una. Pulsa una para continuarla (el agente recuerda toda la conversación) o bórrala. Se guardan las últimas 200. #### Trabajar sobre el proyecto activo Los agentes trabajan sobre el proyecto que tienes abierto: su carpeta es donde leen y escriben archivos, donde arrancan los agentes externos y los servidores MCP, y donde se buscan las skills y las reglas del proyecto. Cada mensaje les dice además dónde vive el proyecto (este equipo, o una distribución de WSL como Ubuntu, donde los comandos corren en Linux), su rama de Git y los otros proyectos de la ventana, así que un proyecto en WSL se trata como Linux desde el principio. Cada proyecto tiene sus propios hilos, como en Zed: al cambiar a otro proyecto (el selector de proyectos de la barra de título) se abre un hilo nuevo allí y el historial muestra los hilos de ese proyecto. El hilo en el que estabas queda en el historial de su propio proyecto. Una ejecución que sigue en marcha termina en el proyecto donde empezó. #### Usar agentes en tu propia app El agente no está atado al diseñador: `pyzed.agents` da a cualquier app los mismos proveedores, el llavero y el bucle del agente. Consulta [Agentes de IA en tu app](https://albakore.github.io/pyzed/es/guides/agents/). ### Menús y comandos > La barra de título, cada menú, la barra de herramientas, la paleta de comandos, Añadir control y los paneles acoplables de Pyzed Design. Source: https://albakore.github.io/pyzed/es/editor/menus-and-commands/ Todos los comandos del editor viven en una sola tabla, así que los menús, la paleta de comandos, el teclado y la lista de atajos siempre coinciden. Esta página los recoge todos. #### La barra de título De izquierda a derecha: el menú **☰**, el entorno en WSL, el nombre del proyecto (clic para abrir el selector de proyectos: los de esta ventana y los recientes), la rama de git (clic para cambiarla), el nombre del archivo (clic para abrir otro), un punto cuando hay cambios sin guardar y una insignia **Componente** o **Shader** en esas vistas. En el centro: **Ejecutar** (F5), **Detener** y el buscador, que abre la [paleta de comandos](#paleta-de-comandos). A la derecha: el menú **Ver**, el menú **Ayuda** y el engranaje de **Ajustes**, seguidos de los botones de la ventana cuando Pyzed Design dibuja su propio marco (ver [La interfaz](https://albakore.github.io/pyzed/es/editor/interface/#el-marco-de-la-ventana)). #### Menú ☰ Archivo (Image: El menú ☰ con archivos recientes) | Entrada | Teclas | Hace | | --- | --- | --- | | Nuevo formulario… | Ctrl+N | El diálogo Nuevo, en Formulario. | | Nuevo componente… | Ctrl+Shift+N | El diálogo Nuevo, en Componente. | | Abrir… | Ctrl+O | Lista los formularios, componentes, shaders y bibliotecas de animaciones del proyecto; **Examinar…** para cualquier archivo. | | Abrir proyecto reciente… | Ctrl+Alt+O | El selector de proyectos de la barra de título; ver [Varios proyectos en una ventana](https://albakore.github.io/pyzed/es/editor/projects/#varios-proyectos-en-una-ventana). | | Ir a un archivo… | Ctrl+P | Busca un archivo del proyecto por su nombre y lo abre; ver [Ir a un archivo](https://albakore.github.io/pyzed/es/editor/projects/#ir-a-un-archivo). | | Guardar | Ctrl+S | Guarda el documento y su código. | | Guardar como… | Ctrl+Shift+S | Guarda en una ruta nueva. | | Ajustes… | Ctrl+, | Los [ajustes](https://albakore.github.io/pyzed/es/editor/settings/). | | *Recientes* | | Los cinco últimos archivos abiertos. | | Ejecutar | F5 | [Ejecuta](https://albakore.github.io/pyzed/es/editor/console-and-running/) el formulario o muestra la vista previa del componente. | | Detener | Shift+F5 | Cierra el formulario en ejecución. | | Ejecutar el script del proyecto | | Inicia el `[tool.pyzed] script` del proyecto con `uv run` (ver [Bienvenida y ejemplos](https://albakore.github.io/pyzed/es/editor/examples/#ejecutarlo)). | | Elegir el archivo de inicio… | | Elige el formulario o script desde el que inicia la app (ver [El archivo de inicio](https://albakore.github.io/pyzed/es/editor/projects/#el-archivo-de-inicio)). | | Usar este formulario como archivo de inicio | | Hace del formulario abierto el archivo de inicio. | | Editar el código | F7 | Abre el archivo de código entero en el editor de código. | #### Comandos de edición Los comandos de Editar no tienen menú propio: están en la barra de herramientas, en el menú contextual del control, en el teclado y en la paleta de comandos. | Comando | Teclas | | --- | --- | | Deshacer / Rehacer | Ctrl+Z / Ctrl+Shift+Z | | Cortar / Copiar / Pegar | Ctrl+X / Ctrl+C / Ctrl+V | | Duplicar | Ctrl+D | | Eliminar | Supr | | Seleccionar todo | Ctrl+A | | Añadir control… | Ctrl+Shift+A | | Agrupar en una fila / en una columna / en una cuadrícula | | | Convertir la sangría a tabulaciones / a espacios | (paleta) el archivo del editor de código; ver [Tabulaciones o espacios](https://albakore.github.io/pyzed/es/editor/code-editor/#tabulaciones-o-espacios) | #### Menú Ver (Image: El menú Ver) | Entrada | Teclas | Hace | | --- | --- | --- | | Paleta de comandos… | Ctrl+Shift+P | Ejecuta cualquier comando por su nombre. | | Panel izquierdo / Panel derecho / Panel inferior | Ctrl+B / Ctrl+Alt+B / Ctrl+J | Muestra u oculta cada dock (marcado cuando se ve). | | Consola | Ctrl+Shift+Y | Muestra la consola y la mantiene abierta. | | Logs | Ctrl+Shift+U | Muestra el panel Logs: los eventos y errores del editor. | | Panel del agente | Ctrl+Alt+A | Muestra u oculta el [agente de IA](https://albakore.github.io/pyzed/es/editor/ai-agents/) (marcado cuando se ve). | | Generar un formulario con IA… / Generar un componente con IA… | | Abre el agente con el comienzo de una petición ("Crea un formulario nuevo para…") para que la completes. | | Mover la pestaña a una ventana nueva / Mover la pestaña a la ventana principal | | Abre el documento que está delante en una ventana propia, o lo devuelve a las pestañas de la ventana principal. Ver [Pestañas de documentos](https://albakore.github.io/pyzed/es/editor/files-and-scripting/#pestañas-de-documentos). | | Barra de herramientas, Barra de estado | | Las muestra u oculta. | | Cuadrícula | G | Muestra u oculta la cuadrícula de puntos. | | Ajustar a la cuadrícula | | Activa o desactiva el snap. | | Guías inteligentes | | Activa o desactiva las guías de alineación. | | Mostrar todos los controles ocultos | | Dibuja atenuados en el lienzo todos los controles ocultos. Apagado, uno oculto solo aparece mientras está seleccionado. | | Vista previa del marco de ventana… | | Muestra el formulario en el lienzo con el marco de ventana de otra plataforma: macOS, Windows 11, GNOME, Ubuntu o KDE Plasma (marcada mientras no es el de este sistema). | | Vista previa con tema…, Vista previa con idioma… | | Muestra el lienzo (y la próxima ejecución) con otro tema o idioma sin cambiar el formulario (marcada mientras está activa). Consulta [Vista previa con otro tema o idioma](https://albakore.github.io/pyzed/es/editor/data-binding/#vista-previa-con-otro-tema-o-idioma). | | Restablecer los paneles | | Devuelve cada panel a su dock y tamaño iniciales y cierra las ventanas de paneles. | | Guardar la distribución como… | | Guarda con un nombre dónde está cada panel, el tamaño de los docks y las ventanas de paneles. | | Eliminar esta distribución | | Borra la distribución guardada que está en uso. | | *Paneles* | | Una entrada por panel, con el dock en el que está (`Propiedades · Derecha`, o `Ventana` si flota): clic para traerlo al frente. | | *Distribuciones* | | Las distribuciones de trabajo (Por defecto, Programar, Concentración, Eventos y código, Código con los formularios, y las tuyas); la que está en uso aparece marcada. | Los interruptores se recuerdan entre sesiones. #### Menú Ayuda (Image: El menú Ayuda abierto desde el signo de interrogación de la barra de título) | Entrada | Teclas | Hace | | --- | --- | --- | | Atajos de teclado | F1 | Una lista con buscador de todos los atajos. | | Referencia | Shift+F1 | La [referencia integrada](https://albakore.github.io/pyzed/es/editor/reference/), en la página del control seleccionado o del nombre bajo el cursor. | | Bienvenida | | La [ventana de bienvenida](https://albakore.github.io/pyzed/es/editor/examples/): empezar o abrir un proyecto, proyectos recientes y la galería de ejemplos. | | Proyectos de ejemplo… | | La ventana de bienvenida con todos los ejemplos. | | Documentación | | Abre esta documentación en el navegador. | | Informar un problema | | Abre el gestor de incidencias. | | Novedades | | Una pestaña con las notas de esta versión (o de la actualización pendiente de instalar). | | Buscar actualizaciones | | Busca ahora una versión nueva en GitHub; consulta [Actualizaciones](https://albakore.github.io/pyzed/es/download/#actualizaciones). | | Buscar actualizaciones automáticamente | | Activa o desactiva la búsqueda diaria (activada por defecto). | | Recibir versiones preliminares | | Ofrece también las pre-versiones (`X.Y.Z-rc.N`): el canal preview. Ver [Actualizaciones](https://albakore.github.io/pyzed/es/download/#actualizaciones). | | Acerca de Pyzed Design | | Las versiones de Pyzed, Python e Iced, la plataforma y enlaces. | (Image: El diálogo Acerca de) #### La barra de herramientas Bajo las pestañas de documentos, en grupos de izquierda a derecha: 1. **Nuevo…** (un formulario, un componente o un shader, a elegir en el diálogo Nuevo), **Deshacer** y **Rehacer**. 2. Con algo seleccionado: **Alinear a la izquierda, los centros, a la derecha, arriba, al medio, abajo**; **Centrar horizontal y verticalmente en el formulario**, luego **Distribuir** (tres o más controles) y **Mismo ancho, alto** (dos o más); **Traer al frente**, **Enviar al fondo** y **Eliminar**. Sin selección, un aviso ocupa su lugar. 3. Al final, cómo se comporta el lienzo: **Mostrar la cuadrícula**, **Ajustar a la cuadrícula**, **Guías inteligentes**, **Redimensionar el contenido con sus anclajes**, **Ajustar la selección a la cuadrícula** y **Editar el código**. **Abrir** y **Guardar** están en el menú ☰ y en Ctrl+O y Ctrl+S. Cada botón tiene un tooltip con su atajo, y las herramientas de edición se desplazan de lado cuando la ventana es estrecha. Ocúltala con **Ver → Barra de herramientas**. Ver [Herramientas y lienzo](https://albakore.github.io/pyzed/es/editor/toolbox-and-canvas/#alinear-distribuir-e-igualar-tamaños). #### Paleta de comandos El buscador del centro de la barra de título, o Ctrl+Shift+P, abre un buscador sobre todos los comandos que se pueden ejecutar en ese momento, como `Grupo: Comando` con su atajo, y sobre los lugares del editor: `Panel: Propiedades` muestra un panel, `Ajustes del editor: Cursor` abre esa página de los [ajustes del editor](https://albakore.github.io/pyzed/es/editor/code-editor/#ajustes-del-editor) (las teclas están en `Archivo: Editor de keymap`), y `Recientes: login.pzform` abre un archivo reciente. Escribe unas letras (la búsqueda es difusa: `vcu` encuentra *Ver: Cuadrícula*), ↑/↓ para moverte, Enter para ejecutar, Esc para cerrar. También llega a los comandos que no tienen menú, como los de Editar. (Image: La paleta de comandos) #### Añadir control Ctrl+Shift+A es la paleta de componentes: una lista con búsqueda difusa de todos los componentes con su categoría. Enter añade el resaltado, centrado en el formulario, y lo selecciona. Desde un contenedor (su **+** en la estructura, o **Add inside…**), lo añade dentro de ese contenedor. #### Diálogo de atajos de teclado F1 lista todos los atajos por grupo (Archivo, Editar, Ver, Ejecutar, Ayuda, Lienzo, Código) con un buscador. Ver la [tabla completa](https://albakore.github.io/pyzed/es/editor/keyboard-shortcuts/). (Image: El diálogo de atajos de teclado) #### Paneles y docks Ocho paneles viven en cuatro docks alrededor del lienzo: | Panel | Dock por defecto | Muestra | | --- | --- | --- | | **Herramientas** | Izquierdo | La caja de herramientas y la estructura. | | **Proyecto** | Izquierdo | Los archivos del proyecto. | | **Dependencias** | Izquierdo | Dependencias, ruff y ty (solo con un proyecto abierto). | | **Propiedades** | Derecho | La grilla de propiedades. | | **Eventos** | Derecho | Los eventos de la selección. | | **Agente** | Derecho | El [agente de IA](https://albakore.github.io/pyzed/es/editor/ai-agents/). | | **Código** | Inferior | El editor de código (solo mientras hay un manejador o el archivo abierto). | | **Consola** | Inferior | La consola (cuando tiene algo que mostrar). | Cada dock muestra una pestaña a la vez. Al final de la barra de pestañas: - el **botón del dock** abre un menú para **Mover *panel* a** el dock izquierdo, derecho, inferior o superior, al **Centro** (junto a las pestañas de formularios), a una **Ventana nueva**, u **Ocultar el panel**; - la **×** oculta el dock (vuelve a mostrarlo con su atajo o con el menú Ver). Cualquier pestaña va a cualquier sitio: arrastra la pestaña de un panel o de un formulario para reordenarla, sobre otra fila o sobre un dock o el centro para moverla allí, o fuera de la ventana para que flote en una ventana propia; suelta más pestañas sobre una ventana para apilarlas. El sitio de destino se ilumina mientras arrastras. En una ventana propia, el botón del dock ofrece **Devolver todo a los paneles laterales** y la **×** hace lo mismo. (Image: Moviendo el panel Propiedades a otro dock) Arrastra el borde de un dock que da al lienzo para cambiar su tamaño (cada dock tiene un tamaño mínimo). Dónde está cada panel, qué docks están ocultos, sus tamaños y las ventanas de paneles se guardan con tus ajustes, y **Ver → Guardar la distribución como…** los conserva como una distribución de trabajo a la que volver; **Ver → Restablecer los paneles** (o **Ajustes → Paneles → Restablecer**) vuelve a la disposición inicial. ```mermaid flowchart LR subgraph Izquierdo Herramientas Proyecto Dependencias end subgraph Centro Superior["Dock superior (vacío por defecto)"] Lienzo subgraph Inferior Código Consola end end subgraph Derecho Propiedades Eventos Agente end ``` :::tip En una pantalla ancha, mueve **Eventos** al dock izquierdo (o al superior) para ver Propiedades y Eventos a la vez; en una pequeña, oculta el dock izquierdo con Ctrl+B mientras programas y usa Ctrl+Shift+A para añadir controles sin la caja de herramientas. ::: #### Notificaciones Aparecen mensajes breves en una esquina durante unos segundos: guardado, copiado, abierto, un comando que falló... Haz clic en la × para descartar uno; se muestran como mucho cuatro a la vez. ### Referencia integrada > La referencia de clases con buscador dentro de Pyzed Design, la ayuda contextual con Shift+F1 y el mismo navegador de referencia en tus propias apps. Source: https://albakore.github.io/pyzed/es/editor/reference/ Como Godot, Pyzed Design trae su propia documentación: cada control de formulario, cada función y clase de la librería y cada componente de tu proyecto, con firmas, parámetros, propiedades, eventos y ejemplos. Funciona sin conexión y en el idioma del editor. (Image: La referencia abierta en la página de Button, con la lista de clases a la izquierda) #### Abrirla | Cómo | Abre | | --- | --- | | Shift+F1 con un control seleccionado | La página del tipo de ese control (Botón, Tabla…). | | Shift+F1 en el editor de código | La página del nombre bajo el cursor: `form.email.text` abre Caja de texto, `ui.button` y `pz.text` abren sus páginas de la librería. Si no tiene página, se busca el nombre. | | El botón del **libro** en las cabeceras de Propiedades y Eventos | La página del control seleccionado. | | **Ayuda** en el menú contextual de un control | La página de ese control. | | **Ayuda → Referencia** | La referencia, donde la dejaste. | La referencia se abre como una pestaña **Referencia** junto a tus documentos. Ciérrala como cualquier pestaña; recuerda la página, la búsqueda y el historial. #### Encontrar una página La columna izquierda es la **lista de clases**, por secciones: | Sección | Páginas | | --- | --- | | **Controles del formulario** | Cada control de la caja de herramientas, en su orden: descripción, un manejador de ejemplo, sus propiedades (con tipo y valor por defecto), sus eventos (con el nombre del manejador) y los métodos que tiene todo control. | | **Código del formulario** | Cómo funciona el código de un formulario: el objeto `form`, un control desde código (`ControlProxy`) y el `event` que recibe un manejador. | | **App y ejecución**, **Widgets**, **Componentes (pyzed.ui)**… | La librería, una sección por página de la [referencia de la API](https://albakore.github.io/pyzed/es/reference/app/), con firmas en vivo. | | **Tus componentes** | Cada subclase de `pz.Component` de tu proyecto, con sus props, estado, eventos y métodos sacados de tu código y tus docstrings. | Haz clic en una sección para plegarla. Escribe en **Buscar en la referencia** para filtrar todo: primero salen los nombres exactos, cada palabra tiene que coincidir (`tab bar` encuentra `ui.tab_bar`) y cada resultado muestra su primera frase. (Image: Buscando "tab") #### Leer una página - La cabecera muestra el icono de la página, su nombre, su tipo (control, función, clase…) y dónde está en la lista. - Los enlaces a otros nombres abren su página dentro de la referencia; los botones ← y → van atrás y adelante. - **Abrir en la web** abre la misma página en este sitio. - Las descripciones de la librería salen de la misma fuente que la web, y las firmas y miembros se leen del código instalado, así que la referencia siempre coincide con la versión que ejecutas. #### Documentar tus componentes Escribe docstrings: el docstring de la clase es la descripción de la página, `pz.state(..., doc="…")` describe una variable de estado y los docstrings de los métodos públicos los describen. La página se actualiza en cuanto el código se vuelve a cargar (hot reload incluido). ```python class Counter(pz.Component): """Un número con botones + y −.""" step: int = 1 value = pz.state(0, doc="La cuenta actual.") def reset(self): """Vuelve la cuenta a cero.""" self.value = 0 ``` #### En tus apps La referencia es parte de la librería, no solo del editor, así que puedes incluirla en tu app, probarla o añadir tus propias secciones: ```python import pyzed as pz from pyzed import reference, ui class Docs(pz.App): def __init__(self): self.help = reference.Browser("pyzed.ui.button") def view(self): return ui.reference_browser(self.help, on_open_url=print) reference.search("tab bar")[0].id # "pyzed.ui.tab_bar" reference.lookup("ui.button") # "pyzed.ui.button" reference.page("pyzed.text").body # Markdown ``` `reference.register(reference.Provider(...))` añade una sección propia (la ayuda de tu app, la API de un plugin). Ver [Referencia integrada](https://albakore.github.io/pyzed/es/reference/app/#referencia-integrada) y [`ui.reference_browser`](https://albakore.github.io/pyzed/es/reference/ui/#reference_browser) en la referencia de la API. ### Atajos de teclado > Todos los atajos de teclado de Pyzed Design, el diseñador de GUIs para Python: comandos globales, el lienzo, los diálogos y el editor de código. Source: https://albakore.github.io/pyzed/es/editor/keyboard-shortcuts/ Pulsa F1 en el editor para ver esta lista con buscador. En macOS, Cmd funciona donde un atajo global dice Ctrl. :::tip Estas son las teclas de fábrica. Cambia cualquiera en el [editor de keymap](https://albakore.github.io/pyzed/es/editor/keymap/) (Ctrl+K Ctrl+S); F1 siempre muestra las teclas vigentes. ::: #### Globales Funcionan en cualquier sitio, incluso mientras escribes en el editor de código. | Teclas | Comando | | --- | --- | | Ctrl+N | Nuevo formulario | | Ctrl+Shift+N | Nuevo componente | | Ctrl+O | Abrir | | Ctrl+P | Ir a un archivo | | Ctrl+Alt+O | Abrir proyecto reciente (el selector de proyectos) | | Ctrl+S | Guardar | | Ctrl+Shift+S | Guardar como | | Ctrl+, | Ajustes | | Ctrl+K Ctrl+S | [Editor de keymap](https://albakore.github.io/pyzed/es/editor/keymap/) | | Ctrl+Shift+P | Paleta de comandos | | Ctrl+Shift+A | Añadir control | | Ctrl+B | Mostrar u ocultar el dock izquierdo | | Ctrl+Alt+B | Mostrar u ocultar el dock derecho | | Ctrl+J | Mostrar u ocultar el dock inferior | | Ctrl+Shift+Y | Consola | | Ctrl+Shift+U | Logs | | Ctrl+Alt+A | Mostrar el [panel del agente](https://albakore.github.io/pyzed/es/editor/ai-agents/) | | Ctrl+Alt+I | [Asistente en línea](https://albakore.github.io/pyzed/es/editor/ai-agents/#asistente-en-línea) en el editor de código | | F5 | Ejecutar | | Shift+F5 | Detener | | F7 | Editar el código | | F1 | Atajos de teclado | | Shift+F1 | [Referencia](https://albakore.github.io/pyzed/es/editor/reference/) del control seleccionado o del nombre bajo el cursor | Con un diálogo abierto solo actúan las teclas de la paleta, Ajustes, Atajos de teclado y Editor de keymap; las demás esperan a que se cierre. #### Lienzo Actúan cuando el lienzo tiene el teclado: después de hacer clic en el formulario, en un control, en una fila de la caja de herramientas o en una fila de la estructura. Llevar el puntero a un dock le da el teclado a ese panel (así escribir en un campo nunca mueve un control), y el editor de código lo conserva mientras escribes; haz clic otra vez en el lienzo para recuperar sus atajos. | Teclas | Acción | | --- | --- | | ← → ↑ ↓ | Mover la selección 1 px | | Shift+flechas | Moverla un paso de cuadrícula (1 px con el snap apagado) | | Supr, Retroceso | Eliminar la selección | | Ctrl+C | Copiar | | Ctrl+X | Cortar | | Ctrl+V | Pegar | | Ctrl+D | Duplicar | | Ctrl+A | Seleccionar todos los controles | | Ctrl+G | Agrupar la selección en un panel invisible | | Ctrl+Shift+G | Desagrupar: sacar los hijos del contenedor seleccionado | | Shift+A | Añadir auto layout: envolver la selección en una fila o columna flex | | Ctrl+Z | Deshacer | | Ctrl+Shift+Z, Ctrl+Y | Rehacer | | Enter | Abrir el código del evento principal del control seleccionado | | G | Mostrar u ocultar la cuadrícula | | Esc | Cancelar un arrastre, cerrar un menú o soltar la herramienta de colocar y volver al puntero | #### Ratón | Gesto | Acción | | --- | --- | | Clic | Seleccionar un control (en uno ya seleccionado: hacerlo el principal) | | Arrastrar un control | Mover la selección; soltarla sobre un contenedor la mete dentro | | Arrastrar un asa | Redimensionar | | Arrastrar en un hueco | Selección con rectángulo | | Doble clic en un control | Abrir el código de su evento principal, o un componente diseñado en su pestaña | | Clic derecho | Menú contextual | | Arrastrar un número de lado (Propiedades) | Bajarlo o subirlo; doble clic para escribirlo | | Arrastrar desde la caja de herramientas | Colocar un control donde lo sueltas | | Arrastrar una fila de la estructura | Reordenar, o mover a otro contenedor | | Arrastrar el tirador bajo el formulario | Redimensionar el formulario | | Ctrl+rueda en el editor de código | Tamaño de letra | | Ctrl+clic en el editor de código | Ir a la definición | | Dejar el ratón sobre un nombre en el editor de código | Su tipo, documentación y problemas | #### Listas, paletas y diálogos | Teclas | Acción | | --- | --- | | ↑ / ↓ | Moverse por la paleta de comandos y Añadir control | | Enter | Ejecutar el comando resaltado, añadir el control resaltado, enviar el campo de un diálogo | | Esc | Cerrar el diálogo, la paleta o el menú | #### Árbol del proyecto Después de hacer clic en el árbol de la [pestaña Proyecto](https://albakore.github.io/pyzed/es/editor/projects/#archivos): | Teclas | Acción | | --- | --- | | ↑ ↓ | Mover la selección | | ← → | Cerrar / abrir la carpeta | | Enter | Abrir el archivo | | Ctrl+N / Alt+N | Nuevo archivo / nueva carpeta | | F2 | Renombrar | | Ctrl+X / Ctrl+C / Ctrl+V | Cortar / copiar / pegar | | Ctrl+D | Duplicar | | Ctrl+Z / Ctrl+Shift+Z | Deshacer / rehacer un cambio de archivos | | Supr / Ctrl+Supr | Mover a la papelera / borrar para siempre | | Ctrl+Alt+R | Mostrar en el gestor de archivos | | Ctrl+Shift+Enter | Abrir con la app predeterminada | | Alt+Shift+C / Ctrl+Alt+Shift+C | Copiar la ruta / la ruta relativa | #### Editor de código | Teclas | Acción | | --- | --- | | Ctrl+Espacio | Mostrar sugerencias | | ↑ / ↓ | Moverse por la lista de sugerencias (da la vuelta) | | RePág / AvPág | Nueve filas en la lista, o una página en el código | | Enter, Tab | Aceptar la sugerencia | | Esc | Cerrar la lista de sugerencias | | F12 / Ctrl+F12 | Ir a la definición / a la definición del tipo | | Alt+← / → | Atrás / adelante | | Shift+F12 | Buscar todas las referencias | | F2 | Renombrar el símbolo | | Ctrl+. | Arreglos rápidos y refactorizaciones | | Alt+Shift+O | Organizar imports | | Tab | Sangrar con espacios | | Enter | Línea nueva manteniendo la sangría (un nivel más después de `:`) | | Retroceso | Dentro de la sangría, quitar un nivel | | Ctrl+Shift+[ | Plegar el bloque alrededor del cursor | | Ctrl+Shift+] | Desplegarlo | | Ctrl+= / Ctrl+- / Ctrl+0 | Letra más grande, más pequeña, por defecto | | Ctrl+← / → | Moverse por palabras | | Ctrl+Inicio / Fin | Principio o final del código | | Shift+movimiento | Seleccionar | | Ctrl+A, Ctrl+C, Ctrl+X, Ctrl+V | Seleccionar todo, copiar, cortar, pegar | | Ctrl+F / Ctrl+H | Buscar / buscar y reemplazar | | F3 / Shift+F3 | Coincidencia siguiente / anterior | | Ctrl+G | Ir a la línea | | Alt+Z | Activar o desactivar el ajuste de líneas largas | | Alt+clic | Añadir un cursor | | Ctrl+Alt+↑ / ↓ | Añadir un cursor arriba / abajo | | Ctrl+D | Seleccionar la siguiente coincidencia como otro cursor | | Ctrl+Shift+L | Seleccionar todas las coincidencias | | Alt+↑ / ↓ | Subir / bajar la línea | | Alt+Shift+↑ / ↓ | Duplicar la línea arriba / abajo, en cada cursor (también Ctrl+Alt+Shift+↑ / ↓) | | Ctrl+Shift+D | Duplicar la selección de cada cursor (la línea si no hay) | | Ctrl+Shift+K | Borrar la línea | | Ctrl+/ | Comentar o descomentar | | Ctrl+Z / Ctrl+Shift+Z | Deshacer / rehacer | | Ctrl+K Ctrl+0 / J | Plegar / desplegar todo | La lista completa está en [el componente editor de código](https://albakore.github.io/pyzed/es/components/code/#varios-cursores-y-los-atajos-de-zed). Todas estas teclas se pueden cambiar en el [editor de keymap](https://albakore.github.io/pyzed/es/editor/keymap/). ### Keymap > Cambia cualquier atajo de teclado de Pyzed Design, como el keymap de Zed: un editor de keymap con búsqueda y conflictos, y un archivo keymap.json con contextos. Source: https://albakore.github.io/pyzed/es/editor/keymap/ Todas las teclas de Pyzed Design salen de un solo keymap, como en Zed: los menús, la paleta de comandos, el lienzo, el árbol del proyecto, el panel de código y el propio editor de código. Puedes cambiar cualquiera, dar más teclas a una acción, quitarlas y volver a las de fábrica, desde una ventana o editando un archivo. #### El editor de keymap Ábrelo con **Archivo → Editor de keymap**, Ctrl+K Ctrl+S (una tras otra), **Ajustes → Atajos de teclado** o **Ajustes del editor → Atajos de teclado**. (Image: El editor de keymap buscando "save": Guardar con sus teclas de fábrica y Guardar como con teclas cambiadas) Cada fila es una acción con una de sus teclas: - **Acción**: su nombre y el que tiene en `keymap.json` (`workspace::SaveAs`). - **Teclas**: las teclas que la ejecutan, o un guion si no tiene. - **Contexto**: dónde funcionan las teclas (mira [Contextos](#contextos)). - **Origen**: **Predeterminada**, o **Usuario** para las teclas que pusiste tú. Escribe en el buscador para filtrar por nombre, teclas o contexto. **Cambiadas** muestra solo las acciones que cambiaste y **Conflictos** las teclas que ejecutan dos acciones en el mismo lugar. ##### Cambiar teclas 1. Haz clic en el lápiz de una fila (o en **+** para darle otra tecla más a la acción). 2. Pulsa las teclas nuevas. Para un acorde, pulsa dos una tras otra, como Ctrl+K y luego Ctrl+G. 3. Elige el contexto si deben funcionar en otro lugar. 4. Pulsa Intro (o la marca) para guardar, o Escape para cancelar. (Image: Grabando Ctrl+K Ctrl+G para Ir a la línea en el panel de código) Mientras grabas, la línea sobre la lista avisa si esas teclas ya ejecutan otra acción en ese contexto. :::tip La papelera quita las teclas, la flecha devuelve las teclas de fábrica de la acción y **Restablecer todo** devuelve todas. ::: ##### Conflictos Cuando las mismas teclas ejecutan dos acciones en el mismo contexto, las dos filas lo dicen. Gana la última: la tuya sobre la de fábrica. La fila de la acción que se ejecuta dice **Reemplaza a**, y la otra **Tapada por**. (Image: Ctrl+D asignado a Cuadrícula en el lienzo, tapando a Duplicar) #### Contextos Un contexto es la parte del diseñador que tiene el teclado. Las teclas se buscan desde el contexto más interno hasta **Workspace**, así que las mismas teclas pueden hacer cosas distintas en sitios distintos: Ctrl+N es **Nuevo formulario** en cualquier lado, pero **Nuevo archivo** en el árbol del proyecto. | Contexto | Dónde | Dentro de | | --- | --- | --- | | `Workspace` | En todo el diseñador | (el más externo) | | `Canvas` | El lienzo del formulario, tras hacer clic en el formulario o en un control | Workspace | | `ProjectPanel` | El árbol del proyecto | Workspace | | `CodePanel` | El panel de código: autocompletado, buscar, ir a la línea, ajuste de línea | Workspace | | `Editor` | El texto del editor de código | CodePanel | :::note En el contexto `Editor` un acorde debe empezar con Ctrl+K, como los acordes del propio editor. Dentro del editor, Ctrl+K espera la segunda tecla del editor, así que desde ahí abre el editor de keymap con el menú. ::: #### El archivo keymap.json Tus cambios se guardan en `keymap.json`, junto a los ajustes del diseñador: | Sistema | Carpeta | | --- | --- | | Windows | `%APPDATA%\pyzed` | | macOS | `~/Library/Application Support/pyzed` | | Linux | `~/.config/pyzed` (o `$XDG_CONFIG_HOME/pyzed`) | `PYZED_CONFIG_DIR` cambia la carpeta. **Editar keymap.json** en el editor de keymap lo abre en el panel de código, y se aplica en cuanto lo guardas. Usa el formato de Zed: una lista de bloques, cada uno con un `context` y sus `bindings` de teclas a una acción. `null` quita una tecla de fábrica. Se admiten comentarios (`//`) y comas finales. ```json // Atajos de Pyzed Design [ { "context": "Workspace", "bindings": { "ctrl-alt-s": "workspace::SaveAs", "ctrl-shift-s": null } }, { "context": "Editor", "bindings": { "ctrl-alt-d": "editor::DuplicateLineDown", "ctrl-d": null } }, { "context": "Canvas", "bindings": { "ctrl-k ctrl-g": "workspace::Grid" } } ] ``` - **Las teclas** se escriben como en Zed: modificadores `ctrl`, `alt`, `shift` (`cmd` y `super` cuentan como `ctrl`) unidos con `-` a una tecla (`a`, `f2`, `enter`, `space`, `up`, `pageup`...). Un acorde son dos teclas separadas por un espacio. - **Las acciones** se escriben `espacio::Acción`: `workspace::` para los comandos de los menús, `canvas::`, `project_panel::`, `code_panel::` y `editor::`. El editor de keymap muestra el nombre de cada acción. :::caution Si el archivo tiene un error (una acción, teclas o contexto desconocidos), el resto sigue funcionando y el editor de keymap muestra los problemas arriba. ::: En versiones anteriores, las teclas del editor de código se guardaban en `"keymap"` dentro de `designer.json`. El diseñador las pasa a `keymap.json` la primera vez que arranca. #### En tus propias apps El componente editor de código acepta el mismo tipo de keymap: `ui.editor(keymap={"ctrl-alt-d": "duplicate_line_down", "ctrl-d": None})`. Mira [el componente editor de código](https://albakore.github.io/pyzed/es/components/code/#varios-cursores-y-los-atajos-de-zed). ### Ajustes > Las preferencias de Pyzed Design y de su editor de código: buscador, perfiles, ajustes por proyecto, restaurar valores por defecto, el renderizador del proyecto (GPU o CPU), dónde se guardan los ajustes y cómo traducir el diseñador visual. Source: https://albakore.github.io/pyzed/es/editor/settings/ **Ajustes** (Ctrl+,, el engranaje a la derecha de la barra de título, o el menú ☰) reúne en un solo diálogo todas las preferencias del diseñador y de su editor de código. Los cambios se aplican al momento y se guardan enseguida; **Listo** cierra el diálogo. (Image: El diálogo de ajustes) - **El índice** de la izquierda agrupa las páginas: *Diseñador* (General, Fuentes y tamaño, Lienzo del formulario, Ejecución, Logs y errores, Telemetría), *Editor de código* (Apariencia, Cursor, Edición, Borrado, Desplazamiento y ajuste de línea, Code lens y diagnósticos, Servidores de lenguaje) y, con un proyecto abierto, *Proyecto* (Renderizado). Un número junto a una página cuenta sus ajustes cambiados. - **Buscar ajustes…** (tiene el foco al abrir el diálogo) encuentra un ajuste por su nombre o su ayuda, en inglés o en tu idioma, en todas las páginas. - **Un punto** tras el nombre de un ajuste indica que no tiene su valor por defecto; el botón **↺** a su derecha lo restaura. - **Restablecer todo** devuelve todos los ajustes del perfil a su valor por defecto. Pide una segunda pulsación, así un clic no te hace perder tus ajustes. - **Abrir el archivo de ajustes** abre el JSON donde escribe la página. (Image: Buscar en los ajustes) #### Perfiles Un perfil es un conjunto completo de preferencias entre los que puedes cambiar, como los perfiles de VS Code: uno para el día a día, otro con fuentes grandes para presentar, otro para la pantalla pequeña del portátil... La lista **Perfil** de arriba a la derecha cambia de uno a otro y lo aplica al momento (tema, fuentes, tamaños, opciones del editor). | Botón | Qué hace | | --- | --- | | **+** | Un perfil nuevo, copia del que usas, con el nombre que escribas. | | **✎** | Renombra el perfil en uso. | | **🗑** | Elimina el perfil en uso y vuelve a *Por defecto*, que no se puede renombrar ni eliminar. | Los perfiles solo guardan preferencias: la disposición de los paneles, los archivos recientes y el [keymap](https://albakore.github.io/pyzed/es/editor/keymap/) son los mismos en todos. (Image: Un perfil con dos ajustes cambiados) #### Ajustes del proyecto Con un proyecto abierto, **Usuario | Proyecto** arriba dice adónde van tus cambios: - **Usuario**: tus ajustes, en el perfil activo. Valen para todos los proyectos. - **Proyecto**: los ajustes propios del proyecto, guardados en `.pyzed/settings.json` dentro del proyecto, como el `.zed/settings.json` de Zed. Mandan sobre los del usuario mientras el proyecto está abierto y viajan con él (sube el archivo al repositorio para que los tenga cualquiera que lo abra). En el ámbito Proyecto el punto marca los ajustes que define el proyecto y **↺** los quita para que vuelva a valer el del usuario; **Quitar los ajustes del proyecto** los quita todos. En el ámbito Usuario, un ajuste que define el proyecto muestra *El proyecto define el suyo*. Algunos ajustes son solo del usuario (la ventana de bienvenida, el marco de ventana, las actualizaciones, el archivo de logs). (Image: El ámbito Proyecto: el proyecto usa tabulación de 2) ```json { "editor": { "tab_size": 2, "hard_tabs": false, "format_on_apply": true }, "designer": { "grid": 4, "snap": true } } ``` #### Renderizado **Proyecto → Renderizado** elige cómo dibuja sus ventanas la app del proyecto. Se guarda en el `pyproject.toml` del proyecto, así que vale cuando el diseñador ejecuta el proyecto, cuando lo ejecutas con `pz.run` o `pyzed run`, y en la [app exportada](https://albakore.github.io/pyzed/es/editor/exporting/). | Ajuste | Opciones | Qué hace | | --- | --- | --- | | **Renderizador** | Auto, GPU, CPU | *Auto* (por defecto) usa la GPU si hay una de verdad y el renderizador por CPU si no (máquinas virtuales, escritorios remotos). *GPU* la usa siempre: la necesitan los [shaders](https://albakore.github.io/pyzed/es/shading/shaders/). *CPU* (tiny-skia) arranca antes, usa menos memoria y funciona en todas partes. | | **API gráfica** | Auto, Vulkan, Metal, DirectX 12, OpenGL | La API de la GPU. *Auto* prueba Vulkan, Metal o DirectX 12 y después OpenGL. | | **GPU** | Auto, Rápida, Ahorra batería | En portátiles con dos GPU: la dedicada o la integrada. | | **Vsync** | Auto, Activado, Desactivado | *Activado* espera a la pantalla (sin cortes); *Desactivado* dibuja lo más rápido posible (para medir rendimiento). | (Image: El renderizador del proyecto) ```toml [tool.pyzed.rendering] renderer = "gpu" # "auto", "gpu" o "cpu" gpu_api = "vulkan" # "auto", "vulkan", "metal", "dx12" o "gl" power = "low" # "auto", "high" o "low" vsync = "off" # "auto", "on" u "off" ``` En código, `App.rendering` hace lo mismo para una app y manda sobre `pyproject.toml`: ```python class Game(pz.App): rendering = pz.Rendering(renderer="gpu", vsync="off") ``` Los ajustes se convierten en las variables de entorno que leen Iced y wgpu (`ICED_BACKEND`, `WGPU_BACKEND`, `WGPU_POWER_PREF`, `ICED_PRESENT_MODE`); una que definas tú sigue mandando. Mientras un proyecto elige su propio renderizador, Ejecutar arranca siempre el formulario en su propio proceso, porque la ventana del diseñador ya tiene el suyo. #### Apariencia | Ajuste | Qué hace | | --- | --- | | **Tema** | El tema del editor (One Dark, One Light, Ayu, Gruvbox...). Los formularios conservan su propio tema. | | **Idioma** | *Idioma del sistema*, English o Español: el idioma de los menús y paneles. | | **Barra de herramientas** | Muestra u oculta la barra de herramientas. | | **Barra de estado** | Muestra u oculta la barra de estado. | | **Ventana de bienvenida** | Abre la [ventana de bienvenida](https://albakore.github.io/pyzed/es/editor/examples/) al iniciar (activada por defecto). | | **Marco de ventana propio** | Activado por defecto: Pyzed Design dibuja su propia barra de título y botones de ventana, con el estilo de tu sistema. Desactívalo para usar el marco del sistema (se aplica la próxima vez que se abre). | | **Marco de la vista previa** | El marco de ventana que el lienzo muestra alrededor del formulario: el de este sistema, macOS, Windows 11, GNOME, Ubuntu o KDE Plasma. | #### Fuentes y tamaño | Ajuste | Qué hace | | --- | --- | | **Fuente de la interfaz** | La fuente de los menús, paneles, diálogos y propiedades. *Por defecto* es IBM Plex Sans; la lista tiene las fuentes incluidas, las del sistema y las cargadas con `pz.load_font`. | | **Tamaño de la interfaz** | Agranda o achica todo el editor, del 75% al 150%, ventanas de paneles incluidas. **Ver → Ampliar la interfaz**, **Reducir la interfaz** y **Restablecer el zoom de la interfaz** hacen lo mismo en pasos del 5%; dales teclas en el [editor de atajos](https://albakore.github.io/pyzed/es/editor/keymap/). | | **Fuente del código** | La fuente del editor de código y del resto del código. *Por defecto* es IBM Plex Mono. | | **Tamaño de la fuente del código** | De 8 a 32 puntos, el mismo ajuste que en los [ajustes del editor de código](https://albakore.github.io/pyzed/es/editor/code-editor/#ajustes-del-editor). | | **Interlineado del código** | Compacto, Normal o Amplio. | Cada cambio se ve al momento, sin reiniciar, y se guarda para la próxima vez. :::tip Una fuente monoespaciada mantiene alineadas las columnas del código: elige una para **Fuente del código** (su nombre suele decir *Mono* o *Code*). Para la interfaz se ve mejor una fuente con pesos normal, medio y seminegrita. ::: #### Lienzo del formulario | Ajuste | Qué hace | | --- | --- | | **Mostrar la cuadrícula** | La cuadrícula de puntos del lienzo (también G). | | **Ajustar a la cuadrícula** | Redondea posiciones y tamaños a la cuadrícula. | | **Guías inteligentes** | Alinea con otros controles al arrastrar. | | **Ojo de la estructura** | **Oculto al ejecutar** (lo normal): el ojo apaga Visible; el control tampoco se ve en el lienzo, salvo mientras está seleccionado. **Oculto solo en el editor**: el ojo lo oculta solo en el lienzo y al ejecutar el formulario se sigue viendo. | | **Mostrar todos los controles ocultos** | Dibuja atenuados todos los controles con Visible apagado. Apagado (lo normal), un control oculto solo aparece mientras él o algo dentro de él está seleccionado. | | **Cuadrícula de los formularios nuevos** | El paso de cuadrícula (2 a 64) que reciben los formularios y componentes que creas. Luego cada formulario conserva el suyo en Propiedades. | #### Ejecución | Ajuste | Qué hace | | --- | --- | | **Guardar antes de ejecutar** | F5 guarda antes el documento. | | **Abrir la consola al ejecutar** | Trae la consola al frente cuando arranca un formulario. | | **Registrar los eventos en la consola** | Escribe cada evento que atiende el formulario en ejecución, con su valor y su manejador. | #### Logs y errores | Ajuste | Qué hace | | --- | --- | | **Mostrar un diálogo con los errores** | Abre el [diálogo de error](https://albakore.github.io/pyzed/es/editor/console-and-running/#cuando-falla-el-propio-editor) cuando algo falla. Apagado, los errores solo van al panel Logs. | | **Escribir un archivo de log** | Escribe además los [logs](https://albakore.github.io/pyzed/es/editor/console-and-running/#el-panel-logs) en `logs/pyzed-design.log`, en la carpeta de configuración de Pyzed (se aplica al reiniciar). | #### Más El final del índice enlaza con el resto: | Elemento | Qué hace | | --- | --- | | **Atajos de teclado** | El [editor de keymap](https://albakore.github.io/pyzed/es/editor/keymap/): cambia las teclas de cualquier acción. | | **Agentes de IA** | Los [ajustes del agente](https://albakore.github.io/pyzed/es/editor/ai-agents/#ajustes): proveedores de modelos, claves e inicio de sesión con el navegador, modelo predeterminado, esfuerzo y permisos. | | **Restablecer los paneles** | Devuelve cada panel a su dock y tamaño iniciales. | #### Telemetría El diseñador puede guardar datos de uso y rendimiento para ayudar a mejorarlo, pero solo si la activas: viene **desactivada**. En **Ajustes → Telemetría**, *Recopilar datos de uso y rendimiento* registra: - cuántas veces usas cada comando y cada acción del editor (`command.save`, `editor.select_next`…); - cuánto tarda el diseñador en arrancar, en dibujar un fotograma y en ejecutar cada comando (cantidad, total, máximo y un histograma); - el número de sesiones y su duración. Nunca registra tu código, textos, nombres de formularios o de archivos, ni rutas. Los datos se guardan en `telemetry.json` en la carpeta de configuración de Pyzed; **Ver** lo abre y **Borrar** lo vacía (y cambia el id aleatorio de la instalación). Nada sale de tu equipo salvo que además indiques un **Destino**: entonces el diseñador envía allí los datos como JSON al cerrarse y vuelve a contar desde cero. Si el envío falla, se reintenta la próxima vez. `PYZED_TELEMETRY=0` desactiva la telemetría digan lo que digan los ajustes. ```json {"schema": 1, "installation": "4f0c…", "app": "pyzed-designer", "version": "0.4.0", "os": "linux", "arch": "x86_64", "python": "3.13", "since": "…", "until": "…", "sessions": 3, "counters": {"command.save": 12, "editor.select_next": 7}, "timings": {"render": {"count": 840, "total_ms": 2210.4, "max_ms": 41.2, "buckets": [690, 120, 25, 5, 0, 0, 0, 0]}}} ``` Los `buckets` cuentan duraciones de hasta 4, 8, 16, 33, 50, 100 y 250 ms, y las más largas. #### Dónde se guardan Todo lo que recuerda el editor va a un único archivo, `designer.json`, en la carpeta de ajustes de Pyzed: | Plataforma | Carpeta | | --- | --- | | Windows | `%APPDATA%\pyzed` | | macOS | `~/Library/Application Support/pyzed` | | Linux | `$XDG_CONFIG_HOME/pyzed` o `~/.config/pyzed` | Define `PYZED_CONFIG_DIR` para usar otra carpeta (útil para una instalación portátil o para tests). El archivo tiene dos secciones: `"designer"` (tema, idioma, valores del lienzo, barra de herramientas y de estado, opciones de ejecución, archivos recientes, y en qué dock está cada panel, qué docks están ocultos y sus tamaños) y `"editor"` (el editor de código). Ambas guardan el perfil activo; `"profile"` lo nombra y `"profiles"` guarda los demás (solo lo que difiere de los valores por defecto). Los ajustes propios de un proyecto están en su `.pyzed/settings.json`. Los valores no válidos se ignoran y se usan los de por defecto, así que un archivo editado a mano no puede romper el editor. ```json { "designer": { "theme": "One Light", "language": "es", "ui_font": "Inter", "ui_scale": 1.1, "grid": 8, "snap": true, "console_on_run": true, "docks": { "events": "left" }, "sizes": { "right": 360 }, "recent": ["/home/ada/shop/ui/main.pzform"] }, "editor": { "font_family": "JetBrains Mono", "font_size": 15, "tab_size": 4, "minimap": false, "cursor_shape": "block", "format_on_apply": true } } ``` #### Traducir el editor La interfaz del editor se traduce con [`pz.t`](https://albakore.github.io/pyzed/es/guides/i18n/). El inglés y el español vienen incluidos; añade un catálogo bajo la clave `designer` para traducirlo a otro idioma. El ajuste **Idioma** ofrece los incluidos; para otro idioma, deja que el editor siga al sistema o pon `"language"` en la sección `designer` de `designer.json`. Las cadenas que faltan se muestran en inglés, así que una traducción parcial ya funciona. ### Trucos y consejos > Pequeños hábitos que te hacen más rápido en Pyzed Design: atajos, selección múltiple, guías inteligentes, plantillas y trucos para crear formularios. Source: https://albakore.github.io/pyzed/es/editor/tips-and-tricks/ #### Colocar y ordenar - **Dibuja en lugar de soltar**: haz clic en un componente de la caja de herramientas y arrastra un rectángulo en el formulario. El control toma exactamente ese tamaño, ajustado a la cuadrícula, y te ahorras redimensionarlo. - **Añade sin la caja de herramientas**: Ctrl+Shift+A, escribe dos o tres letras, Enter. Con el dock izquierdo oculto (Ctrl+B) el lienzo tiene todo el ancho. - **Elige la referencia de alineación**: tras seleccionar varios controles, haz clic en el que ya está bien. Pasa a ser el control principal (borde más grueso), y **Mismo ancho** / **Mismo alto** copian *su* tamaño. - **Alinea con el formulario**: con un solo control seleccionado, los botones de alinear lo ajustan a los bordes y al centro del formulario; **Centrar en el formulario** centra toda la selección como grupo. - **Empuja en pasos de cuadrícula**: Shift+flechas mueve un paso de cuadrícula, las flechas solas un píxel. Varios empujones seguidos son un solo paso de deshacer. - **Cancela un arrastre fallido**: pulsa Esc antes de soltar el ratón y todo vuelve a su sitio. - **Arregla una disposición desordenada de golpe**: sin nada seleccionado, **Ajustar la selección a la cuadrícula** (barra de herramientas) ajusta todos los controles del formulario a la cuadrícula. - **¿Las guías te estorban?** Apaga las **Guías inteligentes** (regla) o el **Snap** (imán, o clic en *Snap* en la barra de estado) para colocar al píxel, y vuelve a encenderlos después. #### Contenedores - **Envuelve, no reconstruyas**: selecciona controles que ya están bien y usa **Group in a row / column / grid / panel** en el menú contextual. El contenedor se crea a su alrededor. - **Agarra el contenedor, no su hijo**: usa la pestaña sobre su esquina superior izquierda, o su fila en la estructura. - **Empuja al final**: un **Espaciador** en una fila flex empuja todo lo que va detrás hasta el otro extremo; dos espaciadores centran lo que hay entre ellos. - **Columnas iguales**: en una fila flex, da a cada hijo **Crecer** 1; en una grilla, usa **Alto de fila** 0 para que las filas se repartan el alto. - **Hazla redimensionable**: ancla el contenedor principal por los cuatro lados y deja que los contenedores flex de dentro se repartan el espacio con **Crecer**. Ejecuta con F5 y cambia el tamaño de la ventana para comprobarlo. #### Estructura - **Oculta mientras trabajas**: el ojo de la estructura oculta un control (un diálogo, una capa encima) para trabajar en lo que hay debajo. Acuérdate de volver a mostrarlo, o hazlo visible desde código (`form.panel.show()`). - **Bloquea el fondo**: bloquea los paneles grandes y las imágenes para que un arrastre despistado no los mueva mientras colocas los controles de encima. Las herramientas de alinear e igualar tamaños también saltan los controles bloqueados. - **Reordena los hijos de un flex** arrastrando sus filas en la estructura: la línea muestra exactamente dónde caen. #### Código - **Enter es "editar código"**: selecciona un control y pulsa Enter para saltar al manejador de su evento principal. - **Primero el nombre, luego el código**: renombra los controles (**Nombre** y Enter) antes de escribir manejadores. Renombrar después sigue actualizando los manejadores que siguen la convención `control_evento`, pero el código que menciona `form.button1` en otros sitios no se reescribe. - **Deja que el autocompletado te enseñe**: `form.` lista los controles, `form.status.` las propiedades de ese control, `event.` los campos del evento, y el panel de documentación muestra la descripción de cada propiedad. - **Inserta llamadas a miembros**: en un componente diseñado colocado, abre su flecha en la estructura y haz clic en un método para escribir `form.card.rate()` en el cursor. - **Comparte lógica entre controles**: los manejadores son funciones normales, así que pon la parte común en una función auxiliar al principio del archivo y llámala desde cada manejador; `event.sender` dice qué control lanzó el evento. - **Formatea al aplicar**: activa *Format with ruff on Apply* en los ajustes del editor y pulsa **Apply** para ordenar el manejador que acabas de escribir. - **Letra más grande para demos**: Ctrl+= en el editor de código, Ctrl+0 para volver. - **Restart servers** después de `uv add` / `uv sync` si el autocompletado aún no conoce un paquete nuevo. #### Ejecutar - **F5 ejecuta lo que ves**: dentro del entorno del proyecto, Ejecutar usa el documento actual, con los cambios sin guardar. Deja la ventana abierta y pulsa Ctrl+S para actualizarla en su sitio, conservando lo que escribiste en ella. - **Lee la consola, no la terminal**: los eventos muestran sus valores (`email.change ('ada@') → email_change()`), y los errores, la traza completa empezando por tu manejador. Fíltrala por el nombre de un control para seguir solo ese. - **Apaga el registro de eventos** (Ajustes → Ejecución) cuando un temporizador o un deslizador inundan la consola. - **Prueba los componentes por separado**: abre un `.pzcomp` y pulsa F5; la vista previa lista cada evento personalizado que lanza el componente, con su valor. #### La disposición del editor - **Dos paneles a la vez**: mueve **Eventos** al dock izquierdo (o al superior) con el botón del dock, para ver Propiedades y Eventos a la vez. - **Pliega las secciones que no usas** en Propiedades: siguen plegadas mientras pasas de un control a otro. - **¿Perdiste un panel?** **Ver → Restablecer los paneles** lo devuelve todo a su sitio. - **Ajustes portátiles**: define `PYZED_CONFIG_DIR` para llevar los ajustes del editor con un proyecto o en un USB. #### Archivos - **Guarda los dos archivos en git**: el `.pzform` es JSON legible que da buenos diffs, y el `.py` es Python normal. Quien revise puede leer los dos. - **Edita el código donde quieras**: el editor solo reescribe las funciones manejadoras que gestiona, así que puedes tener el archivo de código abierto también en otro editor; ejecuta con `pyzed run app.pzform --reload` para ver cada guardado. - **Las plantillas te adelantan trabajo**: la plantilla *Diálogo* ya conecta Aceptar y Cancelar; *Ajustes* muestra una grilla de etiquetas y campos. Registra las tuyas con `designer.register_template`. ## Shading ### Shading > Shaders de GPU en Pyzed: créalos conectando nodos en Pyzed Design, carga grafos .pzshad desde Python o escribe WGSL a mano, y pinta cualquier widget con ellos. Source: https://albakore.github.io/pyzed/es/shading/ Un **shader** es un programa pequeño que decide el color de cada píxel de un widget, muchas veces por segundo, en la tarjeta gráfica. Así dibuja Pyzed fondos animados, efectos de brillo, radares y gráficos de juegos sin ralentizar tu app. Si usaste los shaders visuales de Godot o el editor de materiales de Unreal Engine, los grafos de shaders de Pyzed te van a resultar familiares. examples/showcase/shaders." size="wide" /> ```mermaid flowchart LR nodes["Nodos en la vista de shaders"] --> file["Archivo .pzshad"] code["pyzed.shading en Python"] --> file file --> wgsl["WGSL"] hand["WGSL escrito a mano"] --> widget wgsl --> widget["Widget pz.shader"] --> gpu["GPU"] ``` #### Empieza aquí #### Todos los nodos El [catálogo de nodos](https://albakore.github.io/pyzed/es/shading/nodes/) describe todos los nodos de un grafo, por categoría, con sus puertos, ajustes y un ejemplo para cada uno. #### Tres formas de crear uno | Forma | Para | Página | | --- | --- | --- | | **Nodos en la vista Shader** | Todo el mundo: conecta cajas con el ratón y ve el resultado en cada nodo. | [La vista Shader](https://albakore.github.io/pyzed/es/shading/shader-designer/) | | **`pyzed.shading` en Python** | Generar o editar grafos desde código, pruebas, herramientas. | [Grafos de shaders](https://albakore.github.io/pyzed/es/shading/shader-graphs/) | | **WGSL a mano** | Control total, portar shaders de otros sitios. | [Shaders en WGSL](https://albakore.github.io/pyzed/es/shading/shaders/) | Las tres acaban siendo el mismo código WGSL en la GPU, así que son igual de rápidas, y un `.pzshad` hecho con el ratón se puede cambiar desde Python (y al revés). **Los shaders necesitan GPU:** En un equipo sin una tarjeta gráfica utilizable, un widget de shader dibuja su color o gradiente `fallback`. Dale a cada shader un fallback razonable para que la interfaz se siga viendo bien. #### Míralos en acción pz.shader en la biblioteca de componentes.', href: '/pyzed/es/library/shader/', icon: 'puzzle' }, { title: 'API de pyzed.shading', text: 'Todas las clases y funciones para cargar, crear y editar grafos en código.', href: '/pyzed/es/reference/shading/', icon: 'list-format' }, ]} /> **Estado de esta sección:** Los grafos de shaders son nuevos en Pyzed. Cada página describe lo que funciona hoy, y el catálogo de nodos se genera a partir del código, así que siempre coincide con tu versión, con una captura de cada nodo. La mayoría de las recetas muestran su grafo en la vista Shader; el resto se arma en un minuto siguiendo sus pasos. ### Catálogo de nodos > Todos los nodos de los grafos de shader de Pyzed, por categoría: qué hacen, sus entradas, salidas y ajustes, y un pequeño ejemplo para copiar. Source: https://albakore.github.io/pyzed/es/shading/nodes/ Un grafo de shader se compone de **92 tipos de nodo** (y 137 entradas en la barra lateral de la vista Shader, porque muchos nodos vienen en varias variantes: *Sumar*, *Multiplicar* y *Step* son todos el nodo **Math**). Este catálogo describe cada uno: para qué sirve en palabras sencillas, sus puertos y ajustes, y un ejemplo en Python. #### Cómo leer un nodo ```mermaid flowchart LR in1["entradas (izquierda)"] --> node["Nodo
ajustes dentro"] --> out1["salidas (derecha)"] ``` - Las **entradas** están a la izquierda de un nodo y las **salidas** a la derecha. Un cable va de una salida a una entrada. - Una entrada sin cable usa su **valor por defecto**: un número, un vector, un color o un valor de *contexto* como *UV* (la posición del píxel) o *Tiempo*. Puedes escribir otro valor directamente en el nodo. - Los **ajustes** no son puertos: una operación, un tipo de ruido, un modo de fusión, las paradas de un degradado. Se cambian en el panel Propiedades (o como argumentos con nombre en Python). ##### Tipos | Tipo | Qué guarda | Ejemplo | | --- | --- | --- | | `float` | Un número | `0.5`, un brillo, un tiempo | | `int` | Un número entero | `5` octavas | | `bool` | Verdadero o falso | ¿está el ratón sobre el shader? | | `vec2` | Dos números | una posición `(x, y)`, un tamaño | | `vec3` | Tres números | un color `(r, g, b)` de 0 a 1 | | `vec4` | Cuatro números | un color con alfa `(r, g, b, a)` | | `texture` | Una imagen | una variable `texture` | | `T` | Cualquiera de los tipos numéricos | nodos de matemáticas: el tipo más ancho que se les conecta | Los valores se convierten por el camino: un `float` rellena todos los componentes de un vector (`0.5` pasa a ser `(0.5, 0.5, 0.5)`) y un `vec3` se vuelve un color con alfa 1. Un cable que nunca puede funcionar, como una textura hacia un número, se rechaza. **Números de 0 a 1:** En los shaders, los colores y la mayoría de las máscaras van de **0 a 1**, no de 0 a 255: `(1, 0.5, 0)` es naranja, `0` es negro y `1` es blanco. Muchos nodos devuelven 0..1 para que puedas multiplicarlos entre sí, como apilar plantillas. #### Todos los nodos | | | | --- | --- | | **Entrada** | [UV](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#uv), [Píxel](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#píxel), [Tamaño](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#tamaño), [Tiempo](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#tiempo), [Ratón](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#ratón), [UV centrado](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#uv-centrado) | | **Parámetros** | [Parámetro](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#parámetro) | | **Constantes** | [Decimal](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#decimal), [Entero](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#entero), [Booleano](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#booleano), [Vector2](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#vector2), [Vector3](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#vector3), [Vector4](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#vector4), [Color](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#color), [Constantes matemáticas](https://albakore.github.io/pyzed/es/shading/nodes/inputs/#constantes-matemáticas) | | **Matemáticas** | [Matemáticas](https://albakore.github.io/pyzed/es/shading/nodes/math/#matemáticas-1), [Función](https://albakore.github.io/pyzed/es/shading/nodes/math/#función), [Mezclar](https://albakore.github.io/pyzed/es/shading/nodes/math/#mezclar), [Limitar](https://albakore.github.io/pyzed/es/shading/nodes/math/#limitar), [Paso suave](https://albakore.github.io/pyzed/es/shading/nodes/math/#paso-suave), [Reasignar rango](https://albakore.github.io/pyzed/es/shading/nodes/math/#reasignar-rango), [Multiplicar y sumar](https://albakore.github.io/pyzed/es/shading/nodes/math/#multiplicar-y-sumar) | | **Vector** | [Componer](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#componer), [Descomponer](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#descomponer), [Reordenar](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#reordenar), [Producto escalar](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#producto-escalar), [Producto vectorial](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#producto-vectorial), [Longitud](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#longitud), [Distancia](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#distancia), [Normalizar](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#normalizar), [Reflejar](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#reflejar), [Polar](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#polar) | | **UV** | [Repetición y desplazamiento](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#repetición-y-desplazamiento), [Desplazar](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#desplazar), [Rotar](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#rotar), [Remolino](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#remolino), [Pixelar](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#pixelar), [Barril](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#barril), [Caleidoscopio](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#caleidoscopio), [Voltear](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#voltear), [Repetir](https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/#repetir) | | **Ruido** | [Ruido](https://albakore.github.io/pyzed/es/shading/nodes/noise-patterns/#ruido-1), [Ruido fractal](https://albakore.github.io/pyzed/es/shading/nodes/noise-patterns/#ruido-fractal), [Voronoi](https://albakore.github.io/pyzed/es/shading/nodes/noise-patterns/#voronoi), [Ruido blanco](https://albakore.github.io/pyzed/es/shading/nodes/noise-patterns/#ruido-blanco), [Aleatorio](https://albakore.github.io/pyzed/es/shading/nodes/noise-patterns/#aleatorio) | | **Patrones** | [Tablero](https://albakore.github.io/pyzed/es/shading/nodes/noise-patterns/#tablero), [Franjas](https://albakore.github.io/pyzed/es/shading/nodes/noise-patterns/#franjas), [Cuadrícula](https://albakore.github.io/pyzed/es/shading/nodes/noise-patterns/#cuadrícula), [Puntos](https://albakore.github.io/pyzed/es/shading/nodes/noise-patterns/#puntos), [Degradado lineal](https://albakore.github.io/pyzed/es/shading/nodes/noise-patterns/#degradado-lineal), [Degradado radial](https://albakore.github.io/pyzed/es/shading/nodes/noise-patterns/#degradado-radial), [Onda](https://albakore.github.io/pyzed/es/shading/nodes/noise-patterns/#onda) | | **Formas** | [Círculo](https://albakore.github.io/pyzed/es/shading/nodes/shapes/#círculo), [Rectángulo](https://albakore.github.io/pyzed/es/shading/nodes/shapes/#rectángulo), [Anillo](https://albakore.github.io/pyzed/es/shading/nodes/shapes/#anillo), [Segmento](https://albakore.github.io/pyzed/es/shading/nodes/shapes/#segmento), [Polígono](https://albakore.github.io/pyzed/es/shading/nodes/shapes/#polígono), [Estrella](https://albakore.github.io/pyzed/es/shading/nodes/shapes/#estrella), [Combinar formas](https://albakore.github.io/pyzed/es/shading/nodes/shapes/#combinar-formas), [Relleno](https://albakore.github.io/pyzed/es/shading/nodes/shapes/#relleno), [Contorno](https://albakore.github.io/pyzed/es/shading/nodes/shapes/#contorno), [Resplandor](https://albakore.github.io/pyzed/es/shading/nodes/shapes/#resplandor) | | **Color** | [HSV a RGB](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#hsv-a-rgb), [RGB a HSV](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#rgb-a-hsv), [Desplazar tono](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#desplazar-tono), [Saturación](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#saturación), [Brillo y contraste](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#brillo-y-contraste), [Luminancia](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#luminancia), [Escala de grises](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#escala-de-grises), [Invertir](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#invertir), [Gamma](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#gamma), [Posterizar](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#posterizar), [Fusionar](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#fusionar), [Paleta](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#paleta), [Degradado](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#degradado), [RGBA](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#rgba), [Capa](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#capa) | | **Efectos** | [Viñeta](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#viñeta), [Líneas de barrido](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#líneas-de-barrido), [Grano de película](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#grano-de-película) | | **Textura** | [Muestrear textura](https://albakore.github.io/pyzed/es/shading/nodes/color-effects/#muestrear-textura) | | **Lógica** | [Comparar](https://albakore.github.io/pyzed/es/shading/nodes/logic-utility/#comparar), [Elegir](https://albakore.github.io/pyzed/es/shading/nodes/logic-utility/#elegir), [Lógica](https://albakore.github.io/pyzed/es/shading/nodes/logic-utility/#lógica-1) | | **Variables** | [Guardar valor](https://albakore.github.io/pyzed/es/shading/nodes/logic-utility/#guardar-valor), [Leer valor](https://albakore.github.io/pyzed/es/shading/nodes/logic-utility/#leer-valor) | | **Utilidades** | [Desvío](https://albakore.github.io/pyzed/es/shading/nodes/logic-utility/#desvío), [Expresión](https://albakore.github.io/pyzed/es/shading/nodes/logic-utility/#expresión), [Función shader](https://albakore.github.io/pyzed/es/shading/nodes/logic-utility/#función-shader), [Comentario](https://albakore.github.io/pyzed/es/shading/nodes/logic-utility/#comentario) | | **Salida** | [Salida](https://albakore.github.io/pyzed/es/shading/nodes/logic-utility/#salida-1) | **Prueba un nodo:** Cada nodo se muestra tal como se ve en la vista Shader, con la vista previa de sus ajustes por defecto. Para jugar con uno, añádelo desde la barra lateral Shading y conéctalo a la Salida, o pega su ejemplo en un archivo de Python y ejecútalo. ### Entradas, parámetros y constantes > Los nodos de los que parte todo shader: dónde está el píxel, el tiempo, el ratón, tus variables y valores fijos. Source: https://albakore.github.io/pyzed/es/shading/nodes/inputs/ Cada ejemplo de esta página se ejecuta después de estas dos líneas y termina enviando un color al nodo Salida: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Entrada Dónde está el píxel que se pinta, el tamaño del área, el tiempo y el ratón. Casi todo grafo empieza con uno de estos. ##### UV `uv` · Entrada · `g.uv(...)` Dónde está el píxel, de (0, 0) arriba a la izquierda a (1, 1) abajo a la derecha. El nodo más usado. `x` va de 0 a la izquierda a 1 a la derecha e `y` de 0 arriba a 1 abajo, sea cual sea el tamaño del widget. Conecta `x` a un Degradado para obtener una rampa horizontal. | Salida | Tipo | | --- | --- | | `uv` | `vec2` | | `x` | `float` | | `y` | `float` | ```python title="Ejemplo" uv = g.uv() g.output(g.gradient(uv["x"])) ``` ##### Píxel *Pixel* · `pixel` · Entrada · `g.pixel(...)` Dónde está el píxel, en píxeles desde arriba a la izquierda. Como UV pero en píxeles: úsalo para lo que debe mantener su tamaño en píxeles cuando el widget crece, como líneas cada 3 píxeles. | Salida | Tipo | | --- | --- | | `px` · píxel | `vec2` | | `x` | `float` | | `y` | `float` | ```python title="Ejemplo" px = g.pixel() lines = g.fract(px["y"] / 4.0) g.output(g.rgba(g.step(0.5, lines), 1.0)) ``` ##### Tamaño *Size* · `size` · Entrada · `g.size(...)` El tamaño del área sombreada en píxeles y su proporción (ancho / alto). El ancho y el alto del área en píxeles. `aspect` (ancho ÷ alto) mantiene redondos los círculos en widgets anchos; Centered UV ya lo usa por ti. | Salida | Tipo | | --- | --- | | `size` · tamaño | `vec2` | | `width` · ancho | `float` | | `height` · alto | `float` | | `aspect` · proporción | `float` | ```python title="Ejemplo" s = g.size() g.output(g.rgba(g.fract(s["aspect"]), 1.0)) ``` ##### Tiempo *Time* · `time` · Entrada · `g.time(...)` Segundos desde que arrancó la app (por scale), y su seno y coseno. Segundos desde que arrancó la app: todo lo que se conecta a él se mueve. `sin` y `cos` oscilan suavemente entre −1 y 1, perfectos para pulsos. `scale` lo acelera o lo frena. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `scale` · escala | `float` | `1` | | Salida | Tipo | | --- | --- | | `time` · tiempo | `float` | | `sin` | `float` | | `cos` | `float` | ```python title="Ejemplo" 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)) ``` ##### Ratón *Mouse* · `mouse` · Entrada · `g.mouse(...)` Dónde está el puntero de 0 a 1 (el centro mientras está fuera), y si está sobre el shader. Dónde está el puntero, en UV (0 a 1). Mientras está fuera del shader, `position` es el centro e `inside` es falso. Ideal para focos y efectos al pasar el ratón. | Salida | Tipo | | --- | --- | | `position` · posición | `vec2` | | `inside` · dentro | `bool` | ```python title="Ejemplo" m = g.mouse() light = g.radial_gradient(center=m["position"], radius=0.3) g.output(g.rgba(light, 1.0)) ``` ##### UV centrado *Centered UV* · `centered_uv` · Entrada · `g.centered_uv(...)` UV con (0, 0) en el centro y círculos redondos: donde dibujar formas. UV con proporción va de 0 a la proporción a lo ancho, para que el ruido y las celdas queden cuadrados. Las coordenadas para dibujar formas: (0, 0) es el centro y una unidad es la altura, así un círculo sigue redondo en cualquier widget. `aspect_uv` mantiene cuadrados el ruido y las celdas. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | Salida | Tipo | | --- | --- | | `centered` · centrado | `vec2` | | `aspect_uv` · uv con proporción | `vec2` | ```python title="Ejemplo" c = g.centered_uv() d = g.length(c["centered"]) g.output(g.rgba(g.smoothstep(0.4, 0.0, d), 1.0)) ``` #### Parámetros Un nodo **Parámetro** lee una de las variables del shader: los valores que cambia tu código Python (o un deslizador en la pestaña Vista previa). ##### Parámetro *Parameter* · `parameter` · Parámetros · `g.parameter(...)` Lee una variable del shader (ver la lista Variables): una mutable cambia sin recompilar, una constante queda dentro del código. Los mandos de tu shader. `g.uniform(...)` añade una variable mutable y un nodo Parámetro a la vez; en el diseñador, arrastra una variable de la Estructura (Outline) al grafo. Una variable `color` da las salidas `color`, `rgb` y `alpha`. | Salida | Tipo | | --- | --- | | `value` · valor | `float` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `name` | `variable` | — | La salida toma el tipo de la variable; una variable `color` da `color`, `rgb` y `alpha`. ```python title="Ejemplo" tint = g.uniform("tint", "color", "#22d3ee") glow = g.radial_gradient(radius=0.6) g.output(g.rgba(tint["rgb"] * glow, 1.0)) ``` #### Constantes Valores fijos escritos en el shader. Si un valor nunca cambia, una constante es lo más simple; si quieres cambiarlo desde código, usa una variable. ##### Decimal *Float* · `float` · Constantes · `g.float(...)` Un número fijo. Un número fijo. La mayoría de las entradas ya aceptan un valor escrito directamente, así que solo necesitas este nodo para llevar el mismo número a varios sitios. | Salida | Tipo | | --- | --- | | `value` · valor | `float` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `value` | `float` | `0.5` | ```python title="Ejemplo" half = g.float(0.5) g.output(g.rgba(g.compose(half, half, 1.0), 1.0)) ``` ##### Entero *Int* · `int` · Constantes · `g.int(...)` Un número entero fijo. Un número entero fijo, para entradas como las octavas de Ruido fractal. | Salida | Tipo | | --- | --- | | `value` · valor | `int` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `value` | `int` | `1` | ```python title="Ejemplo" octaves = g.int(3) n = g.fbm(octaves=octaves) g.output(g.rgba(n, 1.0)) ``` ##### Booleano *Bool* · `bool` · Constantes · `g.bool(...)` Un verdadero o falso fijo. Un verdadero o falso fijo, para los nodos Elegir y Lógica. | Salida | Tipo | | --- | --- | | `value` · valor | `bool` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `value` | `bool` | `true` | ```python title="Ejemplo" on = g.bool(True) g.output(g.rgba(g.select(on, 1.0, 0.2), 1.0)) ``` ##### Vector2 `vec2` · Constantes · `g.vec2(...)` Dos números fijos. Dos números fijos: un punto o un tamaño. | Salida | Tipo | | --- | --- | | `value` · valor | `vec2` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `value` | `vec2` | `(0.5, 0.5)` | ```python title="Ejemplo" center = g.vec2([0.3, 0.5]) g.output(g.rgba(g.radial_gradient(center=center, radius=0.3), 1.0)) ``` ##### Vector3 `vec3` · Constantes · `g.vec3(...)` Tres números fijos. Tres números fijos: a menudo un color RGB escrito con números de 0 a 1. | Salida | Tipo | | --- | --- | | `value` · valor | `vec3` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `value` | `vec3` | `(1, 1, 1)` | ```python title="Ejemplo" orange = g.vec3([1.0, 0.5, 0.0]) g.output(g.rgba(orange, 1.0)) ``` ##### Vector4 `vec4` · Constantes · `g.vec4(...)` Cuatro números fijos. Cuatro números fijos: un color con alfa, en números. | Salida | Tipo | | --- | --- | | `value` · valor | `vec4` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `value` | `vec4` | `(1, 1, 1, 1)` | ```python title="Ejemplo" g.output(g.vec4([0.1, 0.2, 0.4, 1.0])) ``` ##### Color `color` · Constantes · `g.color(...)` Un color fijo. Un color fijo elegido con el selector de color (o escrito como `#rrggbb`). | Salida | Tipo | | --- | --- | | `color` | `vec4` | | `rgb` | `vec3` | | `alpha` | `float` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `value` | `color` | `#7c3aed` | ```python title="Ejemplo" g.output(g.color("#7c3aed")) ``` ##### Constantes matemáticas *Math constants* · `math_constants` · Constantes · `g.math_constants(...)` π, τ (2π), e y la proporción áurea. π, τ (2π), e y φ, para no escribir 3.14159. Multiplica un valor 0..1 por τ para obtener una vuelta completa en radianes. | Salida | Tipo | | --- | --- | | `pi` · π | `float` | | `tau` · τ | `float` | | `e` | `float` | | `phi` · φ | `float` | ```python title="Ejemplo" 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)) ``` **Siempre al día:** Esta página se genera a partir del catálogo de nodos de `pyzed.shading`, así que los puertos, tipos y valores por defecto son siempre los de esta versión. Cada ejemplo se compila al construir la documentación. **Ver también:** [Primeros pasos](https://albakore.github.io/pyzed/es/shading/getting-started/) · [La vista Shader](https://albakore.github.io/pyzed/es/shading/shader-designer/) · [Recetas](https://albakore.github.io/pyzed/es/shading/recipes/) · [Grafos de shader en Python](https://albakore.github.io/pyzed/es/shading/shader-graphs/) ### Grafos de shaders > Shaders hechos de nodos (.pzshad): dibújalos con pz.shader, cambia sus variables desde Python y crea o edita grafos en código con pyzed.shading. Source: https://albakore.github.io/pyzed/es/shading/shader-graphs/ Un **grafo de shader** es un [shader](https://albakore.github.io/pyzed/es/shading/shaders/) hecho de nodos en lugar de WGSL: entradas como UV y Time, matemáticas, ruido, formas y colores, conectados a un nodo Output. Lo creas en la [vista de shaders](https://albakore.github.io/pyzed/es/shading/shader-designer/) del diseñador y lo guardas como archivo `.pzshad`; `pyzed.shading` lo compila al WGSL que `pz.shader` ejecuta en la GPU, así que es exactamente igual de rápido que un shader escrito a mano. ```python import pyzed as pz pz.shader("shaders/fuego.pzshad", uniforms={"speed": 2.0}, width=320, height=200) ``` #### Dibujar un `.pzshad` `pz.shader` acepta la ruta de un `.pzshad` (un `str` o un `Path`) donde acepta código WGSL. `uniforms` cambia las **variables mutables** del shader por nombre, y los ajustes del propio shader (su color fallback, si se anima) se aplican salvo que los pases tú: ```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)), ]) ``` Cambiar una variable mutable no cuesta nada: el código se compila una vez y solo cambia el valor, así que un deslizador o una animación pueden moverla a la máxima velocidad de fotogramas. #### Cargar y cambiar variables `shading.load` devuelve un `Shader`: el grafo más sus propios valores de las variables. Se guarda en caché por ruta y se recarga solo cuando el archivo cambia, así que guardar en el diseñador actualiza una app en marcha. ```python from pyzed import shading # un shader con las variables speed y tint (mutables) y octaves (una constante) fuego = shading.load("shaders/fuego.pzshad") fuego["speed"] = 2.0 # una variable mutable: no recompila fuego.set(speed=2.0, tint="#ff8800") # varias a la vez fuego.set(octaves=6) # una constante: se vuelve a generar el código fuego.variables # {"speed": 2.0, "tint": "#ff8800", "octaves": 6} pz.shader(fuego, width=320, height=200) # o fuego.widget(width=320, height=200) print(fuego.wgsl) # el código generado ``` | Variable | En el grafo | Cambiarla | | --- | --- | --- | | **Mutable** | Un uniform que la GPU lee en cada frame. | Gratis: `fuego["speed"] = 2`. | | **Constante** | Un literal escrito en el código. | Vuelve a generar el código (bien de vez en cuando, no en cada frame). | Las variables pueden ser `float`, `int`, `bool`, `vec2`, `vec3`, `vec4`, `color` (un color hex o un token del tema como `"accent"`) o `texture` (la ruta de una imagen). Un shader admite hasta 32 variables mutables y 8 texturas. #### Crear un grafo en código `shading.Graph` es el mismo grafo que edita el diseñador, con una API que se lee como los nodos: llama a un tipo de nodo sobre el grafo para añadirlo, pasa las entradas como argumentos y elige una salida con `nodo["nombre"]`. Las operaciones aritméticas entre salidas añaden nodos matemáticos por ti. ```python from pyzed import shading g = shading.Graph("anillos") speed = g.uniform("speed", "float", 1.0, min=0, max=4) # una variable mutable y un nodo que la lee d = g.ring(g.centered_uv()["centered"], radius=0.3, thickness=0.02) brillo = g.glow(d["distance"], radius=0.1) tono = g.time()["time"] * speed["value"] * 0.1 # nodos Multiply color = g.hsv_to_rgb(tono, 0.8, 1.0)["rgb"] * brillo["value"] g.output(g.rgba(color, 1.0)["color"]) g.arrange() # columnas, de izquierda a derecha, como Arrange del diseñador g.save("shaders/anillos.pzshad") # ábrelo en Pyzed Design para seguir editándolo ``` - `g.add("noise", kind="simplex", scale=4)` es la forma larga; `g.noise(...)`, `g.sin(x)`, `g.multiply(a, b)` y todas las entradas de la barra funcionan como métodos. Claves como `"func:sin"` o `"math:max"` funcionan en `add`. - Los argumentos posicionales van a las entradas en orden; los de palabra clave van a entradas o ajustes por nombre. Un valor puede ser un número, una lista (un vector), un color, la salida de otro nodo o un contexto como `"@uv"`. - `nodo["x"]` es una salida; los vectores tienen swizzles (`uv["uv"].x`, `color.xyz`). - `g.uniform(nombre, tipo, valor)` y `g.constant(...)` añaden una variable y un nodo que la lee; `g.get(nombre)` añade otro lector; `g.set_variable(nombre, value=..., mutable=False)` la cambia. - `g.connect(origen, salida, destino, entrada)` y `g.disconnect(destino, entrada)` conectan por id de nodo; se comprueban los tipos y se rechazan los ciclos (`GraphError`). - `g.compile()` devuelve el WGSL (`.code`), los uniforms y texturas para `pz.shader` y los problemas por nodo (`.errors`); `.check()` también ejecuta el compilador de la GPU y pone lo que encuentra en el nodo que escribió la línea. #### Nodos que hacen grafos más pequeños | Nodo | Qué hace | | --- | --- | | **Set value / Get value** | Nombra un valor una vez y lo lee en cualquier parte, sin una conexión larga. | | **Expression** | Tu propio WGSL: una expresión con `a`, `b`, `c` y `d` (y `uv`, `px`, `size`, `time`), o el cuerpo de una función con `return`. Los tipos de `a`–`d` siguen lo que tengan conectado. | | **Shader function** | Otro `.pzshad` usado como función: sus variables mutables son entradas, su entrada `uv` lo mueve o lo deforma, sus salidas son el color. | | **Reroute** | Un punto para llevar una conexión alrededor de otros nodos. | #### En formularios y desde la línea de comandos El control **Shader** del diseñador (caja de herramientas → Display) dibuja un `.pzshad` en un formulario; su propiedad `variables` (`{"speed": 2}`) cambia las variables, desde el panel de propiedades o desde un manejador. `pyzed run fuego.pzshad` muestra un shader ocupando una ventana, con deslizadores para sus variables mutables (pulsa **V**). #### El formato de archivo Un `.pzshad` es JSON: el nombre y la descripción del shader, sus variables, sus nodos (tipo, posición, entradas y ajustes) y sus conexiones. Se compara bien en git, y un agente de IA puede escribir uno: el [servidor MCP](https://albakore.github.io/pyzed/es/guides/mcp/) tiene `list_shader_nodes`, `new_shader` y `check_shader`. ```json { "format": "pyzed.shader/1", "name": "brillo", "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"}, "..."] } ``` ### Tu primer shader > Un comienzo tranquilo con los shaders de GPU en Pyzed: crea un círculo brillante que late en la vista Shader de Pyzed Design sin escribir código y úsalo en una app. Source: https://albakore.github.io/pyzed/es/shading/getting-started/ En esta página creas un **círculo brillante que late**, en unos diez minutos, conectando cajas con el ratón. No necesitas saber matemáticas ni WGSL. Al final tendrás un archivo `.pzshad` que puedes usar en cualquier app de Pyzed y conocerás las pocas ideas sobre las que se construye todo shader. #### Tres ideas, en palabras sencillas 1. **Un shader es una receta que se ejecuta para cada píxel.** La tarjeta gráfica pregunta "¿de qué color es *este* píxel?" para cada píxel del widget, sesenta veces por segundo, todos a la vez. Tu grafo es la respuesta. 2. **Cada píxel sabe dónde está.** El nodo **UV** lo dice: `x` va de 0 (izquierda) a 1 (derecha) e `y` de 0 (arriba) a 1 (abajo). Formas, degradados y patrones son solo cálculos sobre esa posición. 3. **Los colores son números de 0 a 1.** El rojo es `(1, 0, 0)`, el blanco `(1, 1, 1)`, un gris medio `(0.5, 0.5, 0.5)`. Multiplicar un color por un número de 0 a 1 lo oscurece; así funcionan las máscaras. **Terreno conocido:** Si usaste los visual shaders de Godot, el editor de materiales de Unreal Engine o los nodos de shader de Blender, la vista Shader funciona igual: nodos en un lienzo, cables de salidas a entradas y una vista previa en vivo en cada nodo. #### Créalo en la vista Shader 1. **Abre Pyzed Design.** Inicia la app que [descargaste](https://albakore.github.io/pyzed/es/download/) o ábrela desde Python: **uv:** ```bash uv run pyzed design # dentro de un proyecto uv con pyzed-ui uvx --from "pyzed-ui[designer]" pyzed design # sin proyecto ``` **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. **Crea un shader.** Pulsa Ctrl+N para abrir el diálogo **Nuevo**, elige la tarjeta **Shader**, llámalo `pulse` y elige la plantilla **En blanco**. **Crear shader** guarda `pulse.pzshad` y lo abre. Shader, un nombre, una carpeta y una plantilla." size="narrow" /> 3. **Echa un vistazo.** La plantilla en blanco son tres nodos: **UV** → **Degradado** → **Salida**. Cada nodo muestra una pequeña vista previa de lo que produce, y la vista previa de **Salida** en la esquina es el shader terminado. El panel izquierdo muestra ahora la barra **Shading** (todos los nodos, por categoría) y la **Estructura** (variables y nodos). 4. **Dibuja un círculo.** Haz doble clic en un hueco del lienzo (o pulsa Shift+A) y escribe `círculo`; pulsa Enter para añadir el nodo **Círculo**. Haz lo mismo con **Relleno**. Arrastra desde la salida `distance` del Círculo hasta la entrada `distance` del Relleno: su vista previa muestra un disco blanco sobre negro. Círculo. Su vista previa muestra la distancia: el disco en el centro y anillos que crecen hacia fuera." size="small" plain /> **¿Por qué dos nodos?:** Un nodo de forma da una *distancia* (lo lejos que está cada píxel del borde), no una imagen. **Relleno** convierte esa distancia en una máscara limpia: 1 dentro, 0 fuera, con bordes suaves. **Contorno** dibujaría solo el borde y **Resplandor** una luz suave. 5. **Dale color.** Añade un nodo **RGBA**. Conecta la salida `mask` del Relleno a la entrada `rgb` de RGBA (un número rellena rojo, verde y azul: blanco) y la salida `color` de RGBA a la entrada `color` de la Salida. El Degradado ya no se usa: selecciónalo y pulsa Supr. 6. **Hazlo latir.** Añade un nodo **Tiempo** y un nodo **Multiplicar y sumar**. Conecta la salida `sin` del Tiempo (un valor que oscila entre −1 y 1) a la entrada `a`, y escribe `0.05` en `b` y `0.3` en `c`: el resultado oscila entre 0.25 y 0.35. Conéctalo al `radius` del Círculo. Todas las vistas previas empiezan a respirar. 7. **Añade un resplandor.** Añade un nodo **Resplandor**, conéctale la `distance` del Círculo, y un nodo **Sumar** que sume la `mask` del Relleno y el `value` del Resplandor. Conecta la suma a `rgb` de RGBA en lugar de la máscara. 8. **Dale un color que se pueda cambiar.** En la Estructura, pulsa la flecha de **Añadir** y elige **variable color**; llámala `tint` y elige un rosa. Arrastra `tint` de la Estructura al grafo: se convierte en un nodo **Parámetro**. Añade un nodo **Multiplicar**: el `rgb` de `tint` por la suma, hacia `rgb` de RGBA. 9. **Pruébalo.** Abre la pestaña **Vista previa**: el shader en grande, con un deslizador o una muestra de color para cada variable. La pestaña **WGSL** muestra el código en que se compila el grafo. Pulsa F5 para abrirlo en una ventana propia que sigue tus cambios. Guarda con Ctrl+S. **Cuando se rechaza un cable:** Los cables tienen tipo: un color no puede entrar en una entrada que espera verdadero/falso, por ejemplo. Cuando la vista Shader rechaza un cable, explica el motivo en una notificación. La insignia de la barra de herramientas dice **Compila**, o cuántos problemas hay; pasa el ratón por encima para leerlos, y el nodo con el problema se pone rojo. #### Úsalo en una app Un archivo `.pzshad` se usa como una imagen: pasa su ruta a `pz.shader`. `uniforms` fija sus variables mutables por nombre, así tu código puede cambiar el color mientras la app corre, sin coste. ```python title="app.py" from pathlib import Path import pyzed as pz HERE = Path(__file__).parent class Pulse(pz.App): title = "Mi primer 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) ``` Ejecuta la app, o mira el shader solo (pulsa V en esa ventana para un deslizador por 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 ``` **¿Sin GPU?:** En un equipo sin una tarjeta gráfica utilizable (algunas máquinas virtuales y escritorios remotos), un widget de shader dibuja su color o degradado `fallback`. Dale siempre uno, para que tu interfaz se siga viendo bien. #### El mismo shader en Python Todo lo que hace la vista Shader también es una API de Python. Esto crea el mismo grafo y lo guarda; abre el archivo en Pyzed Design para seguir editándolo con el ratón. ```python title="make_pulse.py" from pyzed import shading g = shading.Graph("pulse") tint = g.uniform("tint", "color", "#f472b6") # una variable y un nodo Parámetro radius = g.time()["sin"] * 0.05 + 0.3 # nodos de matemáticas, añadidos por ti 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() # columnas ordenadas, como Ordenar en la barra g.save("pulse.pzshad") ``` #### Qué sigue .pzshad desde código.', href: '/pyzed/es/shading/shader-graphs/', icon: 'seti:python' }, ]} /> ### La vista Shader > Crea shaders de GPU en Pyzed Design conectando nodos, como los visual shaders de Godot: vista previa en vivo en cada nodo, una barra de nodos de shading y variables que tu código cambia. Source: https://albakore.github.io/pyzed/es/shading/shader-designer/ :::tip[¿Nuevo en los shaders?] Empieza por [Tu primer shader](https://albakore.github.io/pyzed/es/shading/getting-started/): un recorrido de diez minutos por esta vista que crea un círculo brillante paso a paso. Esta página es la referencia completa de la vista. ::: El diseñador tiene un tercer tipo de documento: los **shaders**. En vez de escribir WGSL, conectas nodos en un grafo, como en los visual shaders de Godot o el editor de shaders de Blender. Cada nodo muestra una vista previa en vivo de lo que produce, el nodo **Output** es el shader terminado, y el grafo se guarda como un archivo `.pzshad` que `pz.shader` dibuja en la GPU. (Image: La vista de shaders: la barra Shading, el grafo con una vista previa en cada nodo y la vista previa de Output) No hay WGSL que escribir ni Rust: el diseñador compila el grafo a WGSL mientras lo editas, lo comprueba con el compilador de la GPU y señala el nodo cuando algo está mal. #### Crear uno Pulsa **New** (Ctrl+N) y elige la tarjeta **Shader**. Ponle un nombre y una carpeta y elige una plantilla: **Blank** (un degradado), **Plasma**, **Clouds**, **Glow ring**, **Cells** o **Retro screen**. **Create shader** lo guarda y lo abre. También puedes abrir un archivo `.pzshad` directamente: (Image: El diálogo New shader con sus plantillas) ```bash pyzed design efectos/fuego.pzshad # se crea al guardar si no existe pyzed run efectos/fuego.pzshad # el shader ocupando una ventana ``` #### La vista de shaders Un shader se abre en la **vista de shaders** (Shader view), con tres pestañas en su barra de herramientas: | Pestaña | Qué muestra | | --- | --- | | **Graph** | Los nodos en un lienzo que desplazas y amplías. Cada nodo tiene una vista previa de su primera salida; la ventanita **Output** de la esquina muestra el shader terminado. | | **Preview** | El shader terminado, en grande, sobre un tablero (para que se vea la transparencia), con un deslizador por cada variable. | | **WGSL** | El código al que se compila el grafo, de solo lectura, con los mensajes del compilador en sus líneas. **Copy** lo copia al portapapeles. | La barra también tiene **Add a node** (Shift+A), **Delete**, **Arrange nodes in columns** (Ctrl+L), **Fit everything** (F), **Stop time** (congela todas las vistas previas en un instante), **Small preview** y **Run** (F5), que abre el shader en una ventana propia que sigue tus cambios. Una insignia dice **Compiles** o cuántos problemas hay; pasa el ratón por encima para leerlos. (Image: La pestaña WGSL: el código en el que se compila el grafo del anillo brillante, con números de línea) ##### Trabajar en el lienzo | Gesto | Qué hace | | --- | --- | | Arrastrar de una salida a una entrada | Las conecta. La conexión toma el color del tipo del valor. | | Arrastrar una conexión sobre un nodo | La conecta a la primera entrada (o salida) que la acepte. | | Soltar una conexión en un hueco | Abre el buscador de nodos; el nodo que elijas se añade ahí, ya conectado. | | Arrastrar desde una entrada conectada | Vuelve a tomar la conexión, para moverla o soltarla (desconectar). | | Doble clic o clic derecho en el lienzo | El buscador de nodos, donde está el puntero. | | Arrastrar un nodo | Lo mueve (con el resto de la selección). | | Arrastrar en un hueco | Selecciona los nodos del recuadro. | | Rueda | Amplía alrededor del puntero. | | Botón central, o Espacio + arrastrar | Desplaza la vista. | | Clic derecho en un nodo | Duplicar, copiar, mostrar u ocultar su vista previa, desconectar todo, eliminar. | | Doble clic en un nodo | Muestra sus ajustes; en un nodo **Shader function**, abre ese `.pzshad`. | El teclado funciona como en los formularios: Ctrl+Z / Ctrl+Shift+Z, Supr, Ctrl+C / Ctrl+X / Ctrl+V (los nodos viajan entre shaders y ventanas), Ctrl+D, Ctrl+A, flechas para desplazar, **P** para mostrar u ocultar las vistas previas de los nodos seleccionados, Ctrl+0 para el 100 % y Ctrl+= / Ctrl+- para ampliar. (Image: La búsqueda de nodos abierta sobre el grafo, filtrada por "noi": Noise, Random, Voronoi, Film grain y los demás nodos de ruido) (Image: El menú de clic derecho de un nodo: Duplicar, Copiar, Ocultar vista previa, Desconectar todo y Eliminar) ##### Valores en el nodo Una entrada sin conectar muestra su valor en el propio nodo: un campo numérico, un interruptor o una muestra de color. Entradas como `uv`, `p` o `px` usan por defecto un valor de **contexto** (el UV del píxel, su posición centrada, sus coordenadas en píxeles); el panel de propiedades las cambia entre eso y un valor fijo. #### La barra Shading El panel **Herramientas** del dock izquierdo muestra la barra **Shading** en lugar de la caja de herramientas de formularios. Sus entradas se agrupan por categoría; haz clic en una para añadirla en el centro del grafo, o arrástrala al grafo. El buscador encuentra títulos, categorías y palabras clave en inglés y en el idioma del diseñador. | Categoría | Nodos | | --- | --- | | **Input** | UV, Pixel, Size, Time, Mouse, Centered UV | | **Parameters** | Parameter (lee una variable) | | **Constants** | Float, Int, Bool, Vector2/3/4, Color, constantes matemáticas | | **Math** | Sumar, restar, multiplicar, dividir, módulo, potencia, mínimo, máximo, Step, Atan2, Snap; Abs, Floor, Fract, Sqrt, Sin, Cos, Exp, Log y las demás funciones de un valor; 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** | Ruido de valor, Perlin y simplex, ruido fractal (fBm), Voronoi, ruido blanco, Random | | **Patterns** | Checker, Stripes, Grid, Dots, degradados lineal y radial, ondas (seno, triangular, cuadrada, diente de sierra) | | **Shapes** | Circle, Box, Ring, Segment, Polygon, Star (distancias con signo), unión, resta e intersección (también suaves), Fill, Stroke, Glow | | **Color** | HSV ↔ RGB, Hue shift, Saturation, brillo y contraste, Luminance, Grayscale, Invert, Gamma, Posterize, Blend (12 modos de capa), Palette, Gradient (rampa de color), RGBA, Layer | | **Effects** | Vignette, Scanlines, Film grain | | **Texture** | Sample texture | | **Logic** | Compare, Select (si/si no), And/Or/Xor/Not | | **Variables** | Set value, Get value (valores con nombre dentro del grafo) | | **Utility** | Reroute, Expression (tu propio WGSL), Shader function (otro `.pzshad`), Comment | | **Output** | Output: el color de cada píxel (y su alfa) | Cada nodo, con sus puertos, ajustes y un ejemplo, se describe en el [catálogo de nodos](https://albakore.github.io/pyzed/es/shading/nodes/). Los puertos tienen tipo, como en Godot: `float`, `int`, `bool`, `vec2`, `vec3`, `vec4` y `texture`. La mayoría de los nodos matemáticos son genéricos: toman el tipo más amplio que les conectes (un `float` por un `vec3` es un `vec3`), y los valores se convierten por el camino (un `float` llena todos los componentes, un `vec3` se convierte en un color con alfa 1). Una conexión que nunca puede funcionar (una textura en un número) se rechaza con un mensaje. #### Variables en el esquema El **Outline** debajo de la barra lista las **variables** del shader, sus **valores con nombre** (de los nodos Set value) y todos los nodos. **Add** crea una variable; su flecha elige el tipo (`float`, `int`, `bool`, `vec2`, `vec3`, `vec4`, `color` o `texture`). Arrastra una variable al grafo para añadir un nodo que la lea. (Image: La pestaña Preview con un deslizador por variable, y una variable en el panel de propiedades) | Tipo | Qué es | Desde código | | --- | --- | --- | | **Mutable** | Un uniform: la GPU lo lee en cada frame, así que cambiarlo no cuesta nada ni recompila. | `shader["speed"] = 2` o `pz.shader("x.pzshad", uniforms={"speed": 2})` | | **Constante** | Se escribe en el código generado como un literal. Cambiarla vuelve a generar el código. | `shader.set(steps=8)` | Una variable tiene un valor, un rango (para los deslizadores) y una descripción. Una variable `texture` es un archivo de imagen; un nodo **Sample texture** lee su color en un UV. En un shader caben hasta 32 variables mutables y 8 texturas. #### Propiedades El panel de propiedades muestra lo seleccionado: - **Un nodo**: su descripción, su título, sus ajustes (operación, tipo de ruido, modo de fusión, paradas del degradado, el código de una Expression...), cada entrada con su tipo (un enlace al nodo conectado, o su valor) y sus salidas. Los problemas que encuentra el compilador se muestran aquí y en el nodo, en rojo. - **Una variable**: nombre, tipo, mutable o constante, valor, rango y descripción, y la línea de Python que la cambia. - **Nada**: el propio shader: su nombre y descripción, el tamaño de las ventanas Preview y Run, el color **fallback** que se dibuja donde no hay GPU y si se anima. (Image: El panel Propiedades de un nodo Glow: su título, sus entradas con el cable desde Ring y dos valores, y su salida) (Image: El panel Propiedades de la variable color: tipo color, mutable, su valor, una descripción y la línea de Python que la cambia) #### Reutilizar shaders Un `.pzshad` es un archivo como una imagen: úsalo donde quieras. - **En código**: `pz.shader("shaders/fuego.pzshad", uniforms={"speed": 2})`, o cárgalo con `pyzed.shading.load` y cambia sus variables. Mira [Grafos de shaders](https://albakore.github.io/pyzed/es/shading/shader-graphs/). - **En formularios**: el control **Shader** de la caja de herramientas (en Display) dibuja un `.pzshad`. Su propiedad **Shader file** es la ruta, y **variables** (`{"speed": 2}`) cambia sus variables, desde el panel de propiedades o desde un manejador. - **En otros shaders**: el nodo **Shader function** ejecuta otro `.pzshad` como una función. Sus variables mutables se convierten en entradas del nodo, y su entrada `uv` lo mueve, lo repite o lo deforma. - **En juegos**: los fondos del [showcase](https://albakore.github.io/pyzed/es/showcase/) (nebulosa, synthwave, brasas, manchas de color, el radar) son archivos `.pzshad` en `examples/showcase/shaders`; abre uno para ver cómo está hecho. Una app en marcha recarga un `.pzshad` cuando el archivo cambia, así que guardar en el diseñador la actualiza. ### Matemáticas > Aritmética, funciones de un valor (sin, fract, abs...), mix, clamp, smoothstep y remap: el pegamento de todo grafo. Source: https://albakore.github.io/pyzed/es/shading/nodes/math/ Cada ejemplo de esta página se ejecuta después de estas dos líneas y termina enviando un color al nodo Salida: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Matemáticas Todos los nodos de matemáticas funcionan con números **y** con vectores: con un `vec3` trabajan en cada componente (rojo, verde y azul a la vez). Sus entradas y salidas tienen el tipo genérico `T`. ##### Matemáticas *Math* · `math` · Matemáticas · `g.math(...)` Una operación aritmética entre dos valores (de cualquier tipo: los vectores se procesan por componente). Dos valores entran, uno sale. En Python casi nunca lo añades a mano: `a + b`, `a * 2` o `a ** 2` sobre puertos añaden nodos Matemáticas por ti. Cada operación es su propia entrada en la barra lateral (Sumar, Multiplicar, Escalón...). | Entrada | Tipo | Por defecto | | --- | --- | --- | | `a` | `T` | `0` | | `b` | `T` | `1` | | Salida | Tipo | | --- | --- | | `result` · resultado | `T` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `op` | `add`, `subtract`, `multiply`, `divide`, `modulo`, `power`, `min`, `max`, `step`, `atan2`, `snap` | `add` | **Entradas de la barra lateral** | Entrada | Qué hace | En Python | | --- | --- | --- | | Sumar | a + b | `g.add(...)` | | Restar | a − b | `g.subtract(...)` | | Multiplicar | a × b | `g.multiply(...)` | | Dividir | a ÷ b | `g.divide(...)` | | Módulo | El resto de a ÷ b (siempre con el signo de b). | `g.modulo(...)` | | Potencia | a elevado a b. | `g.power(...)` | | Mínimo | El menor de a y b. | `g.min(...)` | | Máximo | El mayor de a y b. | `g.max(...)` | | Escalón | 0 donde b está por debajo del borde a, 1 a partir de ahí. | `g.step(...)` | | Atan2 | El ángulo del punto (b, a) en radianes: a es y, b es x. | `g.atan2(...)` | | Ajustar | a redondeado al múltiplo de b más cercano. | `g.snap(...)` | ```python title="Ejemplo" 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)) ``` ##### Función *Function* · `func` · Matemáticas · `g.func(...)` Una función de un valor (los vectores se procesan por componente). Un valor entra, uno sale: redondeos, raíces, potencias, trigonometría y más. Cada función es su propia entrada en la barra lateral (Sin, Fract, Abs...) y un método en Python (`g.sin(x)`, `g.fract(x)`). | Entrada | Tipo | Por defecto | | --- | --- | --- | | `x` | `T` | `0` | | Salida | Tipo | | --- | --- | | `result` · resultado | `T` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `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` | **Entradas de la barra lateral** | Entrada | Qué hace | En Python | | --- | --- | --- | | Abs | x sin signo. | `g.abs(...)` | | Negar | −x | `g.negate(...)` | | Signo | −1, 0 o 1: el signo de x. | `g.sign(...)` | | Floor | x redondeado hacia abajo. | `g.floor(...)` | | Ceil | x redondeado hacia arriba. | `g.ceil(...)` | | Redondear | x redondeado al entero más cercano. | `g.round(...)` | | Fract | La parte decimal: x − floor(x). Se repite cada 1. | `g.fract(...)` | | Truncar | x sin su parte decimal. | `g.trunc(...)` | | Raíz | La raíz cuadrada de x. | `g.sqrt(...)` | | Raíz inversa | 1 / √x | `g.inverse_sqrt(...)` | | Cuadrado | x × x | `g.square(...)` | | Exp | e elevado a x. | `g.exp(...)` | | Exp2 | 2 elevado a x. | `g.exp2(...)` | | Log | El logaritmo natural de x. | `g.log(...)` | | Log2 | El logaritmo en base 2 de x. | `g.log2(...)` | | Sin | El seno de x (radianes). | `g.sin(...)` | | Cos | El coseno de x (radianes). | `g.cos(...)` | | Tan | La tangente de x (radianes). | `g.tan(...)` | | Asin | El arcoseno de x, en radianes. | `g.asin(...)` | | Acos | El arcocoseno de x, en radianes. | `g.acos(...)` | | Atan | La arcotangente de x, en radianes. | `g.atan(...)` | | Sinh | El seno hiperbólico de x. | `g.sinh(...)` | | Cosh | El coseno hiperbólico de x. | `g.cosh(...)` | | Tanh | La tangente hiperbólica de x: una curva en S de −1 a 1. | `g.tanh(...)` | | Uno menos | 1 − x: invierte 0..1. | `g.one_minus(...)` | | Recíproco | 1 / x | `g.reciprocal(...)` | | Saturar | x limitado entre 0 y 1. | `g.saturate(...)` | | Grados | Radianes a grados. | `g.degrees(...)` | | Radianes | Grados a radianes. | `g.radians(...)` | ```python title="Ejemplo" t = g.time()["time"] wave = g.abs(g.sin(g.uv()["x"] * 10.0 + t)) g.output(g.rgba(wave, 1.0)) ``` ##### Mezclar *Mix* · `mix` · Matemáticas · `g.mix(...)` Mezcla de a (t = 0) a b (t = 1): una interpolación lineal. Mezcla dos valores: `t` = 0 da `a`, 1 da `b`, 0.5 mitad de cada uno. El nodo que más usarás para combinar colores. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `a` | `T` | `0` | | `b` | `T` | `1` | | `t` | `T` | `0.5` | | Salida | Tipo | | --- | --- | | `result` · resultado | `T` | ```python title="Ejemplo" 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)) ``` ##### Limitar *Clamp* · `clamp` · Matemáticas · `g.clamp(...)` x limitado entre min y max. Mantiene un valor dentro de un rango, para que un punto brillante nunca pase de 1 ni uno oscuro baje de 0. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `x` | `T` | `0` | | `min` | `T` | `0` | | `max` | `T` | `1` | | Salida | Tipo | | --- | --- | | `result` · resultado | `T` | ```python title="Ejemplo" v = g.uv()["x"] * 2.0 - 0.5 g.output(g.rgba(g.clamp(v, 0.0, 1.0), 1.0)) ``` ##### Paso suave *Smoothstep* · `smoothstep` · Matemáticas · `g.smoothstep(...)` 0 por debajo de edge0, 1 por encima de edge1 y una curva suave entre ambos. Un umbral suave: 0 por debajo de `edge0`, 1 por encima de `edge1` y una curva suave entre ambos. Úsalo para convertir una distancia o un ruido en una máscara de borde suave. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `edge0` | `T` | `0` | | `edge1` | `T` | `1` | | `x` | `T` | `0.5` | | Salida | Tipo | | --- | --- | | `result` · resultado | `T` | ```python title="Ejemplo" n = g.noise(scale=6.0) mask = g.smoothstep(0.45, 0.55, n) g.output(g.rgba(mask, 1.0)) ``` ##### Reasignar rango *Remap* · `remap` · Matemáticas · `g.remap(...)` x llevado del rango in_min..in_max a out_min..out_max. Pasa un valor de un rango a otro: de −1..1 (un seno) a 0..1 (un color), o de 0..1 a 0.2..0.8. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `x` | `T` | `0.5` | | `in_min` · entrada mín. | `T` | `0` | | `in_max` · entrada máx. | `T` | `1` | | `out_min` · salida mín. | `T` | `-1` | | `out_max` · salida máx. | `T` | `1` | | Salida | Tipo | | --- | --- | | `result` · resultado | `T` | ```python title="Ejemplo" s = g.time()["sin"] v = g.remap(s, -1.0, 1.0, 0.2, 0.8) g.output(g.rgba(v, 1.0)) ``` ##### Multiplicar y sumar *Multiply add* · `multiply_add` · Matemáticas · `g.multiply_add(...)` a × b + c `a × b + c` en un solo nodo: escala y desplaza un valor, como `sin × 0.5 + 0.5` para llevar −1..1 a 0..1. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `a` | `T` | `1` | | `b` | `T` | `1` | | `c` | `T` | `0` | | Salida | Tipo | | --- | --- | | `result` · resultado | `T` | ```python title="Ejemplo" s = g.time()["sin"] g.output(g.rgba(g.multiply_add(s, 0.5, 0.5), 1.0)) ``` **Siempre al día:** Esta página se genera a partir del catálogo de nodos de `pyzed.shading`, así que los puertos, tipos y valores por defecto son siempre los de esta versión. Cada ejemplo se compila al construir la documentación. **Ver también:** [Primeros pasos](https://albakore.github.io/pyzed/es/shading/getting-started/) · [La vista Shader](https://albakore.github.io/pyzed/es/shading/shader-designer/) · [Recetas](https://albakore.github.io/pyzed/es/shading/recipes/) · [Grafos de shader en Python](https://albakore.github.io/pyzed/es/shading/shader-graphs/) ### Recetas de shaders > Grafos de shader listos para usar en Pyzed: degradados, un brillo neón, plasma, nubes, celdas, una pantalla CRT retro, un foco que sigue al ratón, un spinner de carga, fuego y rayas en movimiento, cada uno con sus nodos y su código Python. Source: https://albakore.github.io/pyzed/es/shading/recipes/ Cada receta es un pequeño shader que puedes crear en la [vista Shader](https://albakore.github.io/pyzed/es/shading/shader-designer/) siguiendo su cadena de nodos, o generar en Python ejecutando su código. Las seis primeras también son **plantillas** del diálogo Nuevo (**Nuevo → Shader**), así que puedes abrirlas ya hechas y desmontarlas. **Cómo leer las cadenas:** `UV.y → Degradado → Salida` significa: conecta la salida `y` de un nodo **UV** a un **Degradado**, y el Degradado a la **Salida**. Los números entre paréntesis son valores para escribir en el nodo. Cada nodo se describe en el [catálogo de nodos](https://albakore.github.io/pyzed/es/shading/nodes/). Cada bloque de código es un script completo: guárdalo como `recipe.py`, ejecútalo y escribirá un `.pzshad` a su lado. **uv:** ```bash uv run recipe.py uv run pyzed run sky.pzshad # mira el resultado ``` **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 ``` #### Fondo degradado *Plantilla: **En blanco*** · Nivel: primeros pasos Una rampa de color suave de arriba abajo: el shader útil más simple, y un fondo más bonito que un color plano. **Cadena:** `UV.y → Degradado → Salida`. Haz clic en el nodo Degradado y edita sus paradas en Propiedades: arrastra una parada para moverla, haz clic en la barra para añadir otra. ```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") ``` Prueba `UV.x` para una rampa horizontal, o un nodo **Degradado lineal** antes del Degradado para cualquier ángulo. #### Anillo de neón *Plantilla: **Anillo luminoso*** · Nivel: primeros pasos Un anillo que respira, con una luz suave alrededor, en un color que tu código puede cambiar: pantallas de carga, indicadores de estado, power-ups de juegos. Anillo de neón abierta en la vista Shader." size="wide" /> **Cadena:** `Tiempo × pulse → Sin × 0.02 + 0.28 → Anillo.radius`, `Anillo → Resplandor (radius 0.12) × color.rgb → RGBA → Salida`. ```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") ``` Cambia el **Anillo** por una **Estrella** o un **Rectángulo** y tendrás un letrero de neón. #### Plasma *Plantilla: **Plasma*** · Nivel: matemáticas básicas Tres ondas seno (a lo ancho, a lo alto y alrededor del centro) sumadas y coloreadas con una **Paleta**: el efecto clásico de la demoscene. Plasma abierta en la vista Shader." size="wide" /> **Cadena:** `sin(UV.x × frequency + time)` + `sin(UV.y × frequency + time × 1.3)` + `sin(longitud(UV centrado) × frequency × 1.5 − time)`, luego `× 0.166 + 0.5 → Paleta → RGBA → Salida`. ```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 # tres ondas de −1..1, llevadas a 0..1 g.output(g.rgba(g.palette(v), 1.0)) g.save("plasma.pzshad") ``` #### Nubes a la deriva *Plantilla: **Nubes*** · Nivel: ruido Ruido fractal que se desplaza con el viento, recortado en nubes por un **Paso suave** sobre un degradado de cielo. `cover` decide cuán nublado está. Nubes a la deriva abierta en la vista Shader." size="wide" /> **Cadena:** `Desplazar (speed: wind) → Ruido fractal → Paso suave` (bordes a partir de `cover`), luego `Mezclar (cielo, blanco, nubes) → RGBA → Salida`. ```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:** Cambia el blanco por un naranja y las paradas del cielo por un morado oscuro y tendrás un atardecer; desplázalo más rápido y baja `cover` para una tormenta. #### Celdas vivas *Plantilla: **Celdas*** · Nivel: ruido Celdas de Voronoi que se mueven, cada una con su color, separadas por bordes brillantes: fondos orgánicos, vistas de microscopio, vitrales. Celdas vivas abierta en la vista Shader." size="wide" /> **Cadena:** `Voronoi (scale) → cell → Paleta × 0.6 → RGBA`; `Voronoi → edge → Paso suave (0, 0.06)`; luego `Mezclar (color del borde, celdas, borde) → Salida`. ```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) # su fase sigue al tiempo: las celdas se mueven 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") ``` #### Pantalla CRT retro *Plantilla: **Pantalla retro*** · Nivel: efectos Un degradado synthwave visto a través de un televisor antiguo: líneas de barrido que se desplazan, esquinas oscuras y grano de película. Los nodos de efecto devuelven números cercanos a 1, así que multiplicas tu color por ellos. Pantalla CRT retro abierta en la vista Shader." size="wide" /> **Cadena:** `UV.y → Degradado → rgb × Líneas de barrido × Viñeta × Grano de película → RGBA → Salida`. ```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") ``` Añade un nodo **Barril** delante del UV del degradado (y multiplica por su `inside`) para curvar el cristal, como en el `crt.pzshad` del [showcase](https://albakore.github.io/pyzed/es/showcase/shaders/). #### Foco que sigue al ratón Nivel: primeros pasos Una habitación oscura iluminada donde está el puntero. Cuando el puntero sale del shader, la luz descansa en el centro. **Cadena:** `Ratón.position → Degradado radial.center`, `Mezclar (oscuro, cálido, luz) → RGBA → Salida`. ```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") ``` #### Spinner de carga Nivel: matemáticas básicas Un anillo con una cabeza brillante y una estela que se desvanece y gira sin fin. El fondo transparente le permite estar sobre cualquier tarjeta. **Cadena:** `Polar.angle01 − Tiempo × 0.8 → Fract` (la estela), `Anillo → Relleno` (la forma), y el `alpha` de la Salida a partir de `Relleno × estela`. ```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)) # alfa: solo se ve el anillo g.save("spinner.pzshad") ``` #### Fuego Nivel: ruido Ruido fractal que sube, se desvanece hacia arriba y se colorea con una rampa negro → rojo → naranja → amarillo. examples/showcase/shaders, con la misma idea: ruido que sube por una rampa de color." size="wide" /> **Cadena:** `Desplazar (speed: 0, speed × 0.6) → Ruido fractal × UV.y × 1.6 → Degradado → Salida`. ```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:** La `y` de UV crece hacia abajo, así que `× UV.y` hace que la parte de abajo sea la más caliente. Desplazar suma `speed × time` a las coordenadas, de modo que el ruido se mueve en sentido *contrario*: una velocidad `y` positiva hace subir las llamas. Cambia el signo para que bajen. #### Rayas en movimiento Nivel: primeros pasos Rayas diagonales que se deslizan de lado, como un poste de barbería o una barra de "en curso". **Cadena:** `Desplazar (0.2, 0) → Franjas (count 10, angle 0.785) → Mezclar (blanco, rojo) → RGBA → Salida`. ```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") ``` #### Pon una receta a trabajar ```python import pyzed as pz class Splash(pz.App): title = "Cargando" 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) ``` **Rendimiento:** Los shaders son rápidos, pero cada píxel ejecuta el grafo entero. El ruido fractal con muchas octavas en un widget a pantalla completa es lo más costoso de esta página; baja las octavas o el tamaño del widget si un portátil antiguo sufre. #### Más para explorar ### Shaders > Pinta widgets en la GPU con tus propios shaders WGSL (fondos, efectos y visuales de videojuego) con uniforms desde Python y la posición del ratón. Source: https://albakore.github.io/pyzed/es/shading/shaders/ `pz.shader` es un widget que pinta la GPU con una función que escribes en [WGSL](https://www.w3.org/TR/WGSL/), el lenguaje de shaders de WebGPU. La GPU la ejecuta para cada píxel en cada frame, así que fondos animados, plasma, fuego o el barrido de un radar no le cuestan nada a Python. :::tip[¿Prefieres nodos?] Puedes crear shaders sin escribir WGSL: conecta nodos en la [vista de shaders](https://albakore.github.io/pyzed/es/shading/shader-designer/) del diseñador y dibuja el archivo `.pzshad` con `pz.shader("fuego.pzshad")`. Mira [Grafos de shaders](https://albakore.github.io/pyzed/es/shading/shader-graphs/). ::: (Image: Seis shaders: plasma, ondas, túnel, CRT, Voronoi y fuego) ```python ONDAS = """ fn shade(uv: vec2, px: vec2, size: vec2, time: f32) -> vec4 { let onda = 0.5 + 0.5 * sin(uv.x * 12.0 + time * speed()); return vec4(mix(vec3(0.1, 0.1, 0.3), tint().rgb, onda), 1.0); } """ pz.shader(ONDAS, uniforms={"speed": 2.0, "tint": "accent"}, height=200) ``` #### La función `shade` Tu código define `shade` y las funciones auxiliares que necesite. Recibe: | Argumento | Significado | | --- | --- | | `uv` | Dónde está el píxel, de `(0, 0)` arriba a la izquierda a `(1, 1)` abajo a la derecha. | | `px` | La misma posición en píxeles. | | `size` | El tamaño del widget en píxeles. | | `time` | Segundos desde que arrancó la app. | Devuelve el color del píxel como `vec4(rojo, verde, azul, alfa)`, cada uno de 0 a 1, en el mismo espacio de color que los colores CSS. `mouse()` devuelve la posición del puntero como un `uv` (negativo cuando está fuera del widget); un shader que la usa se redibuja al mover el ratón. #### Uniforms `uniforms` pasa hasta 32 valores desde Python. Cada uno se convierte en una función con su nombre: | Valor | WGSL | | --- | --- | | Un número | `speed()` devuelve `f32` | | 2, 3 o 4 números | `vec2`, `vec3` o `vec4` | | Un color (hex o token del tema) | `vec4` con el rojo, verde, azul y alfa del color | Cambia los valores en `view()` como cualquier otra propiedad: el shader se compila una vez y solo cambian los valores, así que un slider puede moverlo a la frecuencia de la pantalla. #### Opciones - `width`, `height`: el tamaño (`"fill"` por defecto). - `animate=False` dibuja una imagen quieta que solo cambia cuando cambia la vista. - `time=1.5` congela `time` en ese valor. - `fallback` es un color o degradado que se dibuja donde no hay GPU (y detrás del shader donde es transparente). - `textures={"foto": "assets/foto.png"}` pasa hasta 8 imágenes: cada una se convierte en una función, `foto(uv)`, que devuelve el color de la imagen en `uv` (se repite fuera de 0..1). #### Errores El código se comprueba al crear el widget, así que un error lanza un `ValueError` en Python con el mensaje del compilador y la línea: ```text ValueError: invalid shader: error: no definition in scope for identifier: `colr` ┌─ wgsl:24:12 ``` #### Combinarlo con otros efectos Un shader es un widget, así que funciona con el resto de Pyzed: ponlo en un `pz.stack` detrás de paneles de cristal (que lo desenfocan), recórtalo con un contenedor, gíralo o desvanécelo con `style()` y `animate()`. Las [demos](https://albakore.github.io/pyzed/es/showcase/) usan shaders para una nebulosa, un horizonte synthwave, brasas y manchas de color en movimiento. #### Límites - Los shaders necesitan el renderer de GPU. En el renderer por software (sin GPU, y en algunas máquinas de CI) el widget muestra `fallback`, y las capturas hechas ahí también. Sin una GPU de verdad, Pyzed elige el renderer por software; arranca la app con `ICED_BACKEND=wgpu` para ejecutar los shaders en una GPU emulada (en Linux, el driver Vulkan por software de Mesa, `mesa-vulkan-drivers`). - Las esquinas de un shader son rectas: `radius` en un contenedor alrededor no las redondea. ### Vectores y UV > Crea y separa vectores, mide distancias y ángulos, y mueve, repite, gira, retuerce o curva las coordenadas. Source: https://albakore.github.io/pyzed/es/shading/nodes/vectors-uv/ Cada ejemplo de esta página se ejecuta después de estas dos líneas y termina enviando un color al nodo Salida: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Vector Los vectores son grupos de 2, 3 o 4 números: una posición (x, y), un color (r, g, b) o un color con alfa (r, g, b, a). Estos nodos los crean, separan y miden. ##### Componer *Compose* · `compose` · Vector · `g.compose(...)` Construye un vector a partir de sus componentes. Construye un vector a partir de números sueltos: tres valores en un color RGB, dos en un punto. El ajuste `type` elige vec2, vec3 o vec4 (la barra lateral también tiene *Componer vec2* y *Componer vec4*). | Entrada | Tipo | Por defecto | | --- | --- | --- | | `x` | `float` | `0` | | `y` | `float` | `0` | | `z` | `float` | `0` | | Salida | Tipo | | --- | --- | | `vector` | `vec3` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `type` | `vec2`, `vec3`, `vec4` | `vec3` | Las entradas siguen el ajuste `type`: `x` e `y` para vec2, hasta `w` para vec4. **Entradas de la barra lateral** | Entrada | Qué hace | En Python | | --- | --- | --- | | Componer | Construye un vector a partir de sus componentes. | `g.compose(...)` | | Componer vec2 | Construye un vec2 a partir de x e y. | `g.compose_vec2(...)` | | Componer vec4 | Construye un vec4 a partir de x, y, z y w. | `g.compose_vec4(...)` | ```python title="Ejemplo" uv = g.uv() rgb = g.compose(uv["x"], uv["y"], 0.5) g.output(g.rgba(rgb, 1.0)) ``` ##### Descomponer *Decompose* · `decompose` · Vector · `g.decompose(...)` Los componentes de un vector (x, y, z y w; r, g, b y a para colores). Separa un vector en sus componentes: `x`, `y`, `z`, `w` (el rojo, verde, azul y alfa de un color). | Entrada | Tipo | Por defecto | | --- | --- | --- | | `vector` | `T` | `(0, 0, 0, 1)` | | Salida | Tipo | | --- | --- | | `x` | `float` | | `y` | `float` | | `z` | `float` | | `w` | `float` | ```python title="Ejemplo" c = g.color("#f97316") parts = g.decompose(c) g.output(g.rgba(parts["x"], 1.0)) ``` ##### Reordenar *Swizzle* · `swizzle` · Vector · `g.swizzle(...)` Elige y reordena componentes: “yx”, “zzz”, “rgb”, “x”… Elige y reordena componentes con una máscara: `yx` intercambia x e y, `rgb` quita el alfa, `xxx` repite uno. En Python, `puerto.yx` o `puerto.rgb` lo añade por ti. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `vector` | `T` | `(0, 0, 0, 1)` | | Salida | Tipo | | --- | --- | | `result` · resultado | `vec2` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `mask` | `text` | `yx` | ```python title="Ejemplo" uv = g.uv()["uv"] g.output(g.rgba(g.compose(uv.y, uv.x, 0.0), 1.0)) ``` ##### Producto escalar *Dot* · `dot` · Vector · `g.dot(...)` El producto escalar de a y b: cuánto apuntan en la misma dirección. El producto escalar: cuánto apuntan dos direcciones hacia el mismo lado (1 igual, 0 en ángulo recto, −1 opuestas). `p · p` es la longitud al cuadrado. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `a` | `T` | `(1, 0)` | | `b` | `T` | `(1, 0)` | | Salida | Tipo | | --- | --- | | `result` · resultado | `float` | ```python title="Ejemplo" p = g.uv()["uv"] - 0.5 d = g.dot(p, p) g.output(g.rgba(1.0 - d * 4.0, 1.0)) ``` ##### Producto vectorial *Cross* · `cross` · Vector · `g.cross(...)` El producto vectorial de dos vec3. El producto vectorial de dos vectores 3D: una dirección perpendicular a ambos. Sobre todo para trucos de iluminación. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `a` | `vec3` | `(1, 0, 0)` | | `b` | `vec3` | `(0, 1, 0)` | | Salida | Tipo | | --- | --- | | `result` · resultado | `vec3` | ```python title="Ejemplo" n = g.cross([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]) g.output(g.rgba(n, 1.0)) ``` ##### Longitud *Length* · `length` · Vector · `g.length(...)` La longitud de un vector. Lo largo que es un vector. La longitud de UV centrado es la distancia al centro: un círculo en un nodo. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `vector` | `T` | `(1, 1)` | | Salida | Tipo | | --- | --- | | `length` · longitud | `float` | ```python title="Ejemplo" d = g.length(g.centered_uv()["centered"]) g.output(g.rgba(1.0 - d, 1.0)) ``` ##### Distancia *Distance* · `distance` · Vector · `g.distance(...)` Lo lejos que están a y b. A qué distancia están dos puntos. Por defecto `a` es el UV del píxel, así que mide del píxel a `b`. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `a` | `T` | *UV* (contexto) | | `b` | `T` | `(0.5, 0.5)` | | Salida | Tipo | | --- | --- | | `distance` · distancia | `float` | ```python title="Ejemplo" d = g.distance(b=[0.5, 0.5]) g.output(g.rgba(g.smoothstep(0.3, 0.0, d), 1.0)) ``` ##### Normalizar *Normalize* · `normalize` · Vector · `g.normalize(...)` El vector con longitud 1. La misma dirección con longitud 1: para direcciones, vectores de luz y reflejos. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `vector` | `T` | `(1, 1)` | | Salida | Tipo | | --- | --- | | `result` · resultado | `T` | ```python title="Ejemplo" 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)) ``` ##### Reflejar *Reflect* · `reflect` · Vector · `g.reflect(...)` La dirección i reflejada en una superficie orientada a n. Hace rebotar la dirección `i` en una superficie orientada hacia `n`, como una pelota en una pared. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `i` | `T` | `(1, -1)` | | `n` | `T` | `(0, 1)` | | Salida | Tipo | | --- | --- | | `result` · resultado | `T` | ```python title="Ejemplo" 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(...)` La distancia y el ángulo del píxel alrededor de center (respeta la proporción): ángulo en radianes (−π..π) y de 0 a 1. Distancia y ángulo alrededor de un centro: la clave de lo radial, como barridos de radar, gráficos de tarta, rayos de sol y espirales. `angle01` va de 0 a 1 alrededor del círculo. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | `center` · centro | `vec2` | `(0.5, 0.5)` | | Salida | Tipo | | --- | --- | | `radius` · radio | `float` | | `angle` · ángulo | `float` | | `angle01` · angle 0..1 | `float` | | `polar` | `vec2` | ```python title="Ejemplo" p = g.polar() rays = g.step(0.5, g.fract(p["angle01"] * 12.0)) g.output(g.rgba(rays, 1.0)) ``` #### UV Los nodos UV cambian **dónde** miran los nodos siguientes, no el color: conecta su salida `uv` a la entrada `uv` (o `p`) de un patrón, un ruido o una forma para moverlo, repetirlo, girarlo o deformarlo. ##### Repetición y desplazamiento *Tiling and offset* · `tiling_offset` · UV · `g.tiling_offset(...)` uv × tiling + offset: repite y mueve lo que lo use. Repite y mueve lo que viene después: `tiling` (2, 2) lo muestra dos veces en cada dirección, `offset` lo desplaza. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | `tiling` · repetición | `vec2` | `(2, 2)` | | `offset` · desplazamiento | `vec2` | `(0, 0)` | | Salida | Tipo | | --- | --- | | `uv` | `vec2` | ```python title="Ejemplo" uv = g.tiling_offset(tiling=[4.0, 4.0]) g.output(g.rgba(g.fract(uv["uv"]).x, 1.0)) ``` ##### Desplazar *Scroll* · `scroll` · UV · `g.scroll(...)` Mueve uv con el tiempo: uv + speed × time. Mueve las coordenadas con el tiempo: ponlo antes de un ruido o un patrón para que se desplace como nubes o una cinta transportadora. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | `speed` · velocidad | `vec2` | `(0.1, 0)` | | `time` · tiempo | `float` | *Tiempo* (contexto) | | Salida | Tipo | | --- | --- | | `uv` | `vec2` | ```python title="Ejemplo" uv = g.scroll(speed=[0.1, 0.0]) n = g.fbm(uv["uv"], scale=3.0) g.output(g.rgba(n, 1.0)) ``` ##### Rotar *Rotate* · `rotate` · UV · `g.rotate(...)` Gira uv alrededor de center un ángulo angle (radianes). Gira las coordenadas alrededor de `center` `angle` radianes (conecta el Tiempo para que gire solo). | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | `center` · centro | `vec2` | `(0.5, 0.5)` | | `angle` · ángulo | `float` | `0.5` | | Salida | Tipo | | --- | --- | | `uv` | `vec2` | ```python title="Ejemplo" uv = g.rotate(angle=g.time()["time"]) s = g.stripes(uv["uv"], count=8.0) g.output(g.rgba(s, 1.0)) ``` ##### Remolino *Twirl* · `twirl` · UV · `g.twirl(...)` Retuerce uv alrededor de center, más cuanto más lejos. Retuerce las coordenadas alrededor de un centro, más cuanto más lejos: remolinos y portales. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | `center` · centro | `vec2` | `(0.5, 0.5)` | | `strength` · intensidad | `float` | `6` | | Salida | Tipo | | --- | --- | | `uv` | `vec2` | ```python title="Ejemplo" uv = g.twirl(strength=8.0) c = g.checker(uv["uv"], cells=[8.0, 8.0]) g.output(g.rgba(c, 1.0)) ``` ##### Pixelar *Pixelate* · `pixelate` · UV · `g.pixelate(...)` Ajusta uv a una cuadrícula de celdas, para un aspecto pixel art. Ajusta las coordenadas a una cuadrícula de celdas, así lo que viene después parece pixel art. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | `cells` · celdas | `vec2` | `(48, 32)` | | Salida | Tipo | | --- | --- | | `uv` | `vec2` | ```python title="Ejemplo" uv = g.pixelate(cells=[24.0, 16.0]) g.output(g.gradient(g.noise(uv["uv"], scale=4.0))) ``` ##### Barril *Barrel* · `barrel` · UV · `g.barrel(...)` Curva uv como una pantalla CRT; inside es 0 donde el uv curvado sale de la pantalla. Curva las coordenadas como el cristal de un CRT antiguo. Multiplica el color por `inside` para oscurecer las esquinas que se salen de la pantalla. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | `amount` · cantidad | `float` | `0.25` | | Salida | Tipo | | --- | --- | | `uv` | `vec2` | | `inside` · dentro | `float` | ```python title="Ejemplo" 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)) ``` ##### Caleidoscopio *Kaleidoscope* · `kaleidoscope` · UV · `g.kaleidoscope(...)` Refleja uv alrededor de center en segmentos. Refleja las coordenadas en `segments` porciones alrededor de un centro, como un caleidoscopio. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | `center` · centro | `vec2` | `(0.5, 0.5)` | | `segments` · segmentos | `float` | `6` | | Salida | Tipo | | --- | --- | | `uv` | `vec2` | ```python title="Ejemplo" uv = g.kaleidoscope(segments=8.0) n = g.noise(uv["uv"], scale=6.0) g.output(g.rgba(g.palette(n), 1.0)) ``` ##### Voltear *Flip* · `flip` · UV · `g.flip(...)` Refleja uv en horizontal, en vertical o en ambos. Refleja las coordenadas en horizontal (`x`), en vertical (`y`) o en ambas. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | Salida | Tipo | | --- | --- | | `uv` | `vec2` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `axis` | `x`, `y`, `both` | `x` | ```python title="Ejemplo" uv = g.flip(axis="y") g.output(g.gradient(uv["uv"].y)) ``` ##### Repetir *Repeat* · `repeat` · UV · `g.repeat(...)` Repite el espacio en celdas alrededor de (0, 0): dibuja una forma y obtén una cuadrícula de ellas. Cell indica en qué celda está el píxel. Repite el espacio en celdas: dibuja una forma y obtén una cuadrícula entera. `cell` dice en qué celda está el píxel, para dar a cada copia su color. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `p` | `vec2` | *Centrado* (contexto) | | `cell` · celda | `vec2` | `(0.25, 0.25)` | | Salida | Tipo | | --- | --- | | `p` | `vec2` | | `cell` · celda | `vec2` | ```python title="Ejemplo" 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)) ``` **Siempre al día:** Esta página se genera a partir del catálogo de nodos de `pyzed.shading`, así que los puertos, tipos y valores por defecto son siempre los de esta versión. Cada ejemplo se compila al construir la documentación. **Ver también:** [Primeros pasos](https://albakore.github.io/pyzed/es/shading/getting-started/) · [La vista Shader](https://albakore.github.io/pyzed/es/shading/shader-designer/) · [Recetas](https://albakore.github.io/pyzed/es/shading/recipes/) · [Grafos de shader en Python](https://albakore.github.io/pyzed/es/shading/shader-graphs/) ### Ruido y patrones > Aleatoriedad orgánica (Perlin, simplex, fractal, Voronoi) y patrones regulares (damero, rayas, cuadrícula, puntos, degradados, ondas). Source: https://albakore.github.io/pyzed/es/shading/nodes/noise-patterns/ Cada ejemplo de esta página se ejecuta después de estas dos líneas y termina enviando un color al nodo Salida: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Ruido El ruido da valores entre 0 y 1 que parecen aleatorios pero cambian suavemente: la base de nubes, fuego, agua, mármol y terrenos. ##### Ruido *Noise* · `noise` · Ruido · `g.noise(...)` Valores aleatorios suaves de 0 a 1 (ruido de valor, Perlin o simplex). Aleatoriedad suave de 0 a 1. **Perlin** es la clásica, **simplex** se ve más natural, **value** es más cuadrada y más barata. `scale` es cuántas ondulaciones caben. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `p` | `vec2` | *UV con proporción* (contexto) | | `scale` · escala | `float` | `8` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `kind` | `value`, `perlin`, `simplex` | `perlin` | **Entradas de la barra lateral** | Entrada | Qué hace | En Python | | --- | --- | --- | | Ruido | Valores aleatorios suaves de 0 a 1 (ruido de valor, Perlin o simplex). | `g.noise(...)` | | Ruido de valor | Ruido suave en bloques de 0 a 1. | `g.value_noise(...)` | | Ruido simplex | Ruido simplex de 0 a 1. | `g.simplex_noise(...)` | ```python title="Ejemplo" n = g.noise(scale=5.0, kind="simplex") g.output(g.rgba(n, 1.0)) ``` ##### Ruido fractal *Fractal noise* · `fbm` · Ruido · `g.fbm(...)` Capas de ruido, cada una más fina y tenue (fBm): nubes, humo, fuego, terreno. Varias capas de ruido, cada una más fina y más tenue: el aspecto de nubes, humo y montañas. Más `octaves` añaden detalle (y coste). | Entrada | Tipo | Por defecto | | --- | --- | --- | | `p` | `vec2` | *UV con proporción* (contexto) | | `scale` · escala | `float` | `4` | | `octaves` · octavas | `int` | `5` | | `lacunarity` · lacunaridad | `float` | `2` | | `gain` · ganancia | `float` | `0.5` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `kind` | `value`, `perlin`, `simplex` | `value` | ```python title="Ejemplo" n = g.fbm(scale=3.0, octaves=6) g.output(g.gradient(n)) ``` ##### Voronoi `voronoi` · Ruido · `g.voronoi(...)` Celdas alrededor de puntos aleatorios: la distancia al punto más cercano y al segundo, al borde más cercano, y un número aleatorio por celda. Phase mueve los puntos. Celdas alrededor de puntos aleatorios, como barro agrietado, cristales o piel de jirafa. `edge` es pequeño cerca de los bordes y `cell` es un número aleatorio por celda (ideal para un color por celda). `phase` mueve los puntos. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `p` | `vec2` | *UV con proporción* (contexto) | | `scale` · escala | `float` | `6` | | `jitter` · dispersión | `float` | `1` | | `phase` · fase | `float` | *Tiempo* (contexto) | | Salida | Tipo | | --- | --- | | `distance` · distancia | `float` | | `second` · segundo | `float` | | `edge` · borde | `float` | | `cell` · celda | `float` | ```python title="Ejemplo" 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)) ``` ##### Ruido blanco *White noise* · `white_noise` · Ruido · `g.white_noise(...)` Un número aleatorio distinto, de 0 a 1, para cada valor de p. Un número aleatorio distinto para cada valor de entrada, sin suavidad: estática de TV, destellos, tramado. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `p` | `vec2` | *Píxel* (contexto) | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | | `random2` | `vec2` | ```python title="Ejemplo" n = g.white_noise() g.output(g.rgba(n, 1.0)) ``` ##### Aleatorio *Random* · `random` · Ruido · `g.random(...)` Un número aleatorio de 0 a 1 para una semilla (misma semilla, mismo número). Un número aleatorio para una semilla: la misma semilla da siempre el mismo número. Pásale el número de una celda para dar a cada celda su propio valor. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `seed` · semilla | `float` | `1` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | ```python title="Ejemplo" 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)) ``` #### Patrones Patrones regulares, casi siempre 0 o 1 (negro o blanco). Multiplica un color por ellos, o úsalos como `t` de un Mezclar o un Degradado. ##### Tablero *Checker* · `checker` · Patrones · `g.checker(...)` Un tablero de ajedrez de 0 y 1. Un damero: 0 y 1 en cuadros alternos. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV con proporción* (contexto) | | `cells` · celdas | `vec2` | `(8, 8)` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | ```python title="Ejemplo" c = g.checker(cells=[8.0, 8.0]) g.output(g.rgba(g.mix(0.2, 0.9, c), 1.0)) ``` ##### Franjas *Stripes* · `stripes` · Patrones · `g.stripes(...)` Franjas paralelas de 0 y 1 (ángulo en radianes, ancho de 0 a 1). Rayas paralelas en cualquier ángulo; `width` es cuánto de cada raya es blanco. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | `count` · cantidad | `float` | `10` | | `angle` · ángulo | `float` | `0` | | `width` · ancho | `float` | `0.5` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | ```python title="Ejemplo" s = g.stripes(count=12.0, angle=0.785) g.output(g.rgba(s, 1.0)) ``` ##### Cuadrícula *Grid* · `grid` · Patrones · `g.grid(...)` Líneas de cuadrícula: 1 sobre las líneas, 0 entre ellas (grosor de 0 a 1 de una celda). Líneas finas de cuadrícula: papel milimetrado, planos y suelos synthwave. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV con proporción* (contexto) | | `cells` · celdas | `vec2` | `(8, 8)` | | `thickness` · grosor | `float` | `0.06` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | ```python title="Ejemplo" 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)) ``` ##### Puntos *Dots* · `dots` · Patrones · `g.dots(...)` Una cuadrícula de puntos redondos: 1 dentro, 0 fuera (radio de 0 a 0,5 de una celda). Una cuadrícula de puntos redondos: lunares y aspecto de trama de imprenta. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV con proporción* (contexto) | | `cells` · celdas | `vec2` | `(10, 10)` | | `radius` · radio | `float` | `0.3` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | ```python title="Ejemplo" d = g.dots(cells=[12.0, 12.0], radius=0.25) g.output(g.rgba(d, 1.0)) ``` ##### Degradado lineal *Linear gradient* · `linear_gradient` · Patrones · `g.linear_gradient(...)` De 0 a 1 a lo ancho del área en una dirección (ángulo en radianes). De 0 a 1 a lo ancho del área en una dirección (`angle` en radianes, 0 es de izquierda a derecha). | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | `angle` · ángulo | `float` | `0` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | ```python title="Ejemplo" t = g.linear_gradient(angle=1.57) g.output(g.gradient(t)) ``` ##### Degradado radial *Radial gradient* · `radial_gradient` · Patrones · `g.radial_gradient(...)` 1 en el centro, se desvanece hasta 0 en el radio (respeta la proporción). 1 en el centro que se desvanece a 0 en `radius`: focos, brillos suaves y viñetas. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | `center` · centro | `vec2` | `(0.5, 0.5)` | | `radius` · radio | `float` | `0.5` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | ```python title="Ejemplo" r = g.radial_gradient(radius=0.5) g.output(g.rgba(g.compose(r, r * 0.6, 0.2), 1.0)) ``` ##### Onda *Wave* · `wave` · Patrones · `g.wave(...)` Una onda repetida de 0 a 1 (seno, triangular, cuadrada o diente de sierra): frequency ondas por unidad de x. Una onda repetida de 0 a 1 en cuatro formas: **sine** (suave), **triangle**, **square** (encendido/apagado, parpadeo) y **sawtooth** (rampa). Con la `x` por defecto (el tiempo) se anima; conecta una coordenada UV para obtener bandas. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `x` | `float` | *Tiempo* (contexto) | | `frequency` · frecuencia | `float` | `1` | | `phase` · fase | `float` | `0` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `shape` | `sine`, `triangle`, `square`, `sawtooth` | `sine` | **Entradas de la barra lateral** | Entrada | Qué hace | En Python | | --- | --- | --- | | Onda | Una onda repetida de 0 a 1 (seno, triangular, cuadrada o diente de sierra): frequency ondas por unidad de x. | `g.wave(...)` | | Onda cuadrada | Encendido y apagado: 0 o 1, repetido. | `g.square_wave(...)` | | Onda diente de sierra | Sube de 0 a 1 y vuelve a empezar. | `g.sawtooth_wave(...)` | ```python title="Ejemplo" w = g.wave(g.uv()["x"], frequency=6.0, shape="triangle") g.output(g.rgba(w, 1.0)) ``` **Siempre al día:** Esta página se genera a partir del catálogo de nodos de `pyzed.shading`, así que los puertos, tipos y valores por defecto son siempre los de esta versión. Cada ejemplo se compila al construir la documentación. **Ver también:** [Primeros pasos](https://albakore.github.io/pyzed/es/shading/getting-started/) · [La vista Shader](https://albakore.github.io/pyzed/es/shading/shader-designer/) · [Recetas](https://albakore.github.io/pyzed/es/shading/recipes/) · [Grafos de shader en Python](https://albakore.github.io/pyzed/es/shading/shader-graphs/) ### Formas > Círculos, cajas, anillos, líneas, polígonos y estrellas como distancias con signo, combinadas y dibujadas con relleno, trazo y brillo. Source: https://albakore.github.io/pyzed/es/shading/nodes/shapes/ Cada ejemplo de esta página se ejecuta después de estas dos líneas y termina enviando un color al nodo Salida: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Formas Las formas son **distancias con signo** (SDF): cada píxel recibe su distancia al borde de la forma, negativa dentro y positiva fuera. Una distancia todavía no es una imagen: **Relleno**, **Contorno** y **Resplandor** la convierten en una máscara de 0 a 1. Las distancias se pueden combinar (unión, resta, intersección) antes de dibujar, incluso con una unión suave. ##### Círculo *Circle* · `circle` · Formas · `g.circle(...)` La distancia con signo a un círculo (negativa dentro). Usa Relleno o Contorno para dibujarlo. Un círculo alrededor de `center` con un `radius` (en unidades de la altura). Dibújalo con Relleno, Contorno o Resplandor. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `p` | `vec2` | *Centrado* (contexto) | | `center` · centro | `vec2` | `(0, 0)` | | `radius` · radio | `float` | `0.3` | | Salida | Tipo | | --- | --- | | `distance` · distancia | `float` | ```python title="Ejemplo" d = g.circle(radius=0.3) g.output(g.rgba(g.fill(d), 1.0)) ``` ##### Rectángulo *Box* · `box` · Formas · `g.box(...)` La distancia con signo a un rectángulo de tamaño size (redondeado por radius). Un rectángulo de tamaño `size`, con esquinas redondeadas por `radius`: tarjetas, botones, paneles. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `p` | `vec2` | *Centrado* (contexto) | | `center` · centro | `vec2` | `(0, 0)` | | `size` · tamaño | `vec2` | `(0.6, 0.4)` | | `radius` · radio | `float` | `0.05` | | Salida | Tipo | | --- | --- | | `distance` · distancia | `float` | ```python title="Ejemplo" d = g.box(size=[0.8, 0.5], radius=0.08) g.output(g.rgba(g.fill(d), 1.0)) ``` ##### Anillo *Ring* · `ring` · Formas · `g.ring(...)` La distancia con signo a un anillo de radio radius y grosor thickness. Un anillo de `radius` y `thickness`: indicadores de carga, dianas, halos. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `p` | `vec2` | *Centrado* (contexto) | | `center` · centro | `vec2` | `(0, 0)` | | `radius` · radio | `float` | `0.3` | | `thickness` · grosor | `float` | `0.05` | | Salida | Tipo | | --- | --- | | `distance` · distancia | `float` | ```python title="Ejemplo" d = g.ring(radius=0.3, thickness=0.04) g.output(g.rgba(g.fill(d), 1.0)) ``` ##### Segmento *Segment* · `segment` · Formas · `g.segment(...)` La distancia con signo a una línea de a a b, de grosor thickness. Una línea de `a` a `b` con extremos redondeados y un grosor `thickness`. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `p` | `vec2` | *Centrado* (contexto) | | `a` | `vec2` | `(-0.3, 0)` | | `b` | `vec2` | `(0.3, 0)` | | `thickness` · grosor | `float` | `0.04` | | Salida | Tipo | | --- | --- | | `distance` · distancia | `float` | ```python title="Ejemplo" 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)) ``` ##### Polígono *Polygon* · `polygon` · Formas · `g.polygon(...)` La distancia con signo a un polígono regular (sides esquinas, radius hasta ellas). Un polígono regular de `sides` esquinas: triángulos, hexágonos, octógonos. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `p` | `vec2` | *Centrado* (contexto) | | `center` · centro | `vec2` | `(0, 0)` | | `radius` · radio | `float` | `0.3` | | `sides` · lados | `float` | `6` | | `rotation` · rotación | `float` | `0` | | Salida | Tipo | | --- | --- | | `distance` · distancia | `float` | ```python title="Ejemplo" d = g.polygon(radius=0.35, sides=6.0) g.output(g.rgba(g.fill(d), 1.0)) ``` ##### Estrella *Star* · `star` · Formas · `g.star(...)` La distancia con signo a una estrella (inner es la profundidad de sus valles, de 0 a 1). Una estrella de `points` puntas; `inner` es la profundidad de los valles entre puntas. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `p` | `vec2` | *Centrado* (contexto) | | `center` · centro | `vec2` | `(0, 0)` | | `radius` · radio | `float` | `0.35` | | `points` · puntas | `float` | `5` | | `inner` · interior | `float` | `0.45` | | `rotation` · rotación | `float` | `0` | | Salida | Tipo | | --- | --- | | `distance` · distancia | `float` | ```python title="Ejemplo" d = g.star(radius=0.4, points=5.0, inner=0.45) g.output(g.rgba(g.fill(d), 1.0)) ``` ##### Combinar formas *Combine shapes* · `sdf_op` · Formas · `g.sdf_op(...)` Une dos formas (unión), recorta b de a (resta) o conserva donde se solapan (intersección); smooth funde la unión. Combina dos formas antes de dibujarlas: **Unión** las une, **Restar forma** recorta `b` de `a` e **Intersecar** deja la parte común. `smooth` mayor que 0 funde la unión, como dos gotas de agua. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `a` | `float` | `1` | | `b` | `float` | `1` | | `smooth` · suavizado | `float` | `0` | | Salida | Tipo | | --- | --- | | `distance` · distancia | `float` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `op` | `union`, `subtract`, `intersect` | `union` | **Entradas de la barra lateral** | Entrada | Qué hace | En Python | | --- | --- | --- | | Unión | Une dos formas; smooth las funde. | `g.union(...)` | | Restar forma | Recorta la forma b de la forma a. | `g.subtract_shape(...)` | | Intersecar | Conserva donde se solapan dos formas. | `g.intersect(...)` | ```python title="Ejemplo" 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)) ``` ##### Relleno *Fill* · `fill` · Formas · `g.fill(...)` Dibuja una forma: 1 dentro, 0 fuera, con un borde suavizado de softness píxeles. Dibuja una forma rellena: 1 dentro, 0 fuera, con un borde suavizado de `softness` píxeles. Sube `softness` para un borde difuso. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `distance` · distancia | `float` | `0` | | `softness` · suavidad | `float` | `1` | | Salida | Tipo | | --- | --- | | `mask` · máscara | `float` | ```python title="Ejemplo" d = g.box(size=[0.6, 0.6], radius=0.1) g.output(g.rgba(g.fill(d, softness=8.0), 1.0)) ``` ##### Contorno *Stroke* · `stroke` · Formas · `g.stroke(...)` Dibuja el contorno de una forma, de grosor width (en las unidades de la forma). Dibuja solo el contorno de una forma, de ancho `width`. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `distance` · distancia | `float` | `0` | | `width` · ancho | `float` | `0.02` | | `softness` · suavidad | `float` | `1` | | Salida | Tipo | | --- | --- | | `mask` · máscara | `float` | ```python title="Ejemplo" d = g.star(radius=0.4) g.output(g.rgba(g.stroke(d, width=0.02), 1.0)) ``` ##### Resplandor *Glow* · `glow` · Formas · `g.glow(...)` Una luz suave alrededor de una forma que se desvanece a lo largo de radius. Una luz suave alrededor de una forma que se desvanece en `radius`: letreros de neón y halos. Súmalo a un Relleno para una forma sólida con brillo. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `distance` · distancia | `float` | `0` | | `radius` · radio | `float` | `0.08` | | `intensity` · intensidad | `float` | `1` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | ```python title="Ejemplo" 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)) ``` **Siempre al día:** Esta página se genera a partir del catálogo de nodos de `pyzed.shading`, así que los puertos, tipos y valores por defecto son siempre los de esta versión. Cada ejemplo se compila al construir la documentación. **Ver también:** [Primeros pasos](https://albakore.github.io/pyzed/es/shading/getting-started/) · [La vista Shader](https://albakore.github.io/pyzed/es/shading/shader-designer/) · [Recetas](https://albakore.github.io/pyzed/es/shading/recipes/) · [Grafos de shader en Python](https://albakore.github.io/pyzed/es/shading/shader-graphs/) ### Color, efectos y texturas > Degradados, paletas, tono, saturación, modos de fusión y capas, efectos de pantalla (viñeta, líneas de barrido, grano) e imágenes. Source: https://albakore.github.io/pyzed/es/shading/nodes/color-effects/ Cada ejemplo de esta página se ejecuta después de estas dos líneas y termina enviando un color al nodo Salida: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Color Los colores son `vec3` (r, g, b) o `vec4` (r, g, b, a) con valores de 0 a 1, no de 0 a 255: `(1, 0.5, 0)` es naranja. ##### HSV a RGB *HSV to RGB* · `hsv_to_rgb` · Color · `g.hsv_to_rgb(...)` Un color a partir de tono (de 0 a 1 alrededor de la rueda), saturación y valor. Un color a partir del tono (0..1 alrededor de la rueda: rojo, amarillo, verde, cian, azul, magenta, rojo), la saturación y el valor. Conecta el Tiempo al tono para un arcoíris que gira. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `h` · tono | `float` | `0` | | `s` · saturación | `float` | `1` | | `v` · valor | `float` | `1` | | Salida | Tipo | | --- | --- | | `rgb` | `vec3` | ```python title="Ejemplo" uv = g.uv() g.output(g.rgba(g.hsv_to_rgb(uv["x"], 0.8, 1.0), 1.0)) ``` ##### RGB a HSV *RGB to HSV* · `rgb_to_hsv` · Color · `g.rgb_to_hsv(...)` El tono, la saturación y el valor de un color. Separa un color en tono, saturación y valor, para cambiar uno de ellos. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `rgb` | `vec3` | `(1, 0, 0)` | | Salida | Tipo | | --- | --- | | `hsv` | `vec3` | | `h` · tono | `float` | | `s` · saturación | `float` | | `v` · valor | `float` | ```python title="Ejemplo" 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)) ``` ##### Desplazar tono *Hue shift* · `hue_shift` · Color · `g.hue_shift(...)` Gira el tono de un color (shift de 0 a 1 es una vuelta completa). Gira el tono de un color por la rueda (`shift` 1 es una vuelta completa). | Entrada | Tipo | Por defecto | | --- | --- | --- | | `rgb` | `vec3` | `(1, 0, 0)` | | `shift` · desplazamiento | `float` | `0.25` | | Salida | Tipo | | --- | --- | | `rgb` | `vec3` | ```python title="Ejemplo" g.output(g.rgba(g.hue_shift([1.0, 0.2, 0.2], g.time(scale=0.2)["time"]), 1.0)) ``` ##### Saturación *Saturation* · `saturation` · Color · `g.saturation(...)` Más (por encima de 1) o menos (por debajo de 1) colorido; 0 es gris. Más (por encima de 1) o menos (por debajo de 1) colorido; 0 es gris. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `rgb` | `vec3` | `(1, 0.5, 0)` | | `amount` · cantidad | `float` | `1.5` | | Salida | Tipo | | --- | --- | | `rgb` | `vec3` | ```python title="Ejemplo" c = g.palette(g.uv()["x"]) g.output(g.rgba(g.saturation(c, 0.4), 1.0)) ``` ##### Brillo y contraste *Brightness and contrast* · `brightness_contrast` · Color · `g.brightness_contrast(...)` Más claro u oscuro, y más o menos contraste. Más claro u oscuro, y más o menos contraste, como en un editor de fotos. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `rgb` | `vec3` | `(0.5, 0.5, 0.5)` | | `brightness` · brillo | `float` | `0` | | `contrast` · contraste | `float` | `1` | | Salida | Tipo | | --- | --- | | `rgb` | `vec3` | ```python title="Ejemplo" c = g.palette(g.uv()["x"]) g.output(g.rgba(g.brightness_contrast(c, 0.1, 1.4), 1.0)) ``` ##### Luminancia *Luminance* · `luminance` · Color · `g.luminance(...)` Lo brillante que se ve un color, de 0 a 1. Lo brillante que parece un color al ojo, como un número de 0 a 1. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `rgb` | `vec3` | `(1, 1, 1)` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | ```python title="Ejemplo" l = g.luminance([0.2, 0.7, 0.3]) g.output(g.rgba(l, 1.0)) ``` ##### Escala de grises *Grayscale* · `grayscale` · Color · `g.grayscale(...)` El color en tonos de gris. El color en tonos de gris (blanco y negro). | Entrada | Tipo | Por defecto | | --- | --- | --- | | `rgb` | `vec3` | `(1, 0.5, 0)` | | Salida | Tipo | | --- | --- | | `rgb` | `vec3` | ```python title="Ejemplo" c = g.palette(g.uv()["x"]) g.output(g.rgba(g.grayscale(c), 1.0)) ``` ##### Invertir *Invert* · `invert` · Color · `g.invert(...)` El color opuesto: 1 − color (conserva el alfa). El negativo de un color (1 − color); el alfa se mantiene. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `color` | `T` | `(1, 0.5, 0)` | | Salida | Tipo | | --- | --- | | `color` | `T` | ```python title="Ejemplo" c = g.gradient(g.uv()["x"]) g.output(g.invert(c["color"])) ``` ##### Gamma `gamma` · Color · `g.gamma(...)` Aclara (por encima de 1) u oscurece (por debajo de 1) los tonos medios. Aclara (por encima de 1) u oscurece (por debajo de 1) los medios tonos sin tocar el blanco y el negro. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `rgb` | `vec3` | `(0.5, 0.5, 0.5)` | | `gamma` | `float` | `2.2` | | Salida | Tipo | | --- | --- | | `rgb` | `vec3` | ```python title="Ejemplo" v = g.uv()["x"] g.output(g.rgba(g.gamma(g.compose(v, v, v), 2.2), 1.0)) ``` ##### Posterizar *Posterize* · `posterize` · Color · `g.posterize(...)` Conserva solo unos pocos niveles de cada componente. Deja solo unos pocos escalones de cada color: sombreado cel y aspecto retro. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `color` | `T` | `(0.3, 0.6, 0.9)` | | `steps` · niveles | `float` | `4` | | Salida | Tipo | | --- | --- | | `color` | `T` | ```python title="Ejemplo" c = g.palette(g.uv()["x"]) g.output(g.rgba(g.posterize(c, 4.0), 1.0)) ``` ##### Fusionar *Blend* · `blend` · Color · `g.blend(...)` Fusiona layer sobre base como los modos de capa de un editor de imágenes. Mezcla `layer` sobre `base` como los modos de capa de un editor de imágenes: normal, multiply, screen, overlay, add, subtract, difference, darken, lighten, soft light, color dodge y color burn. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `base` | `vec3` | `(0.2, 0.4, 0.8)` | | `layer` · capa | `vec3` | `(1, 0.6, 0.2)` | | `opacity` · opacidad | `float` | `1` | | Salida | Tipo | | --- | --- | | `rgb` | `vec3` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `mode` | `normal`, `multiply`, `screen`, `overlay`, `add`, `subtract`, `difference`, `darken`, `lighten`, `soft_light`, `color_dodge`, `color_burn` | `multiply` | ```python title="Ejemplo" 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)) ``` ##### Paleta *Palette* · `palette` · Color · `g.palette(...)` Un color suave tipo arcoíris para t, con las paletas de coseno de Inigo Quilez: a + b × cos(2π(c × t + d)). Convierte un número de 0 a 1 en un color suave tipo arcoíris con una fórmula de cosenos. Cambia `d` para desplazar los colores; es la forma más rápida de colorear un ruido. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `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)` | | Salida | Tipo | | --- | --- | | `rgb` | `vec3` | ```python title="Ejemplo" n = g.noise(scale=4.0) g.output(g.rgba(g.palette(n), 1.0)) ``` ##### Degradado *Gradient* · `gradient` · Color · `g.gradient(...)` Una rampa de color: el color en t (0 a 1) entre las paradas que elijas. Una rampa de color que editas con paradas: el color en `t` entre las paradas. En el diseñador arrastras las paradas; en Python pasa `stops=[[posición, color], ...]`. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `t` | `float` | `0.5` | | Salida | Tipo | | --- | --- | | `color` | `vec4` | | `rgb` | `vec3` | | `alpha` | `float` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `stops` | `stops` | `0` #1e1b4b, `0.5` #db2777, `1` #fde047 | ```python title="Ejemplo" ramp = g.gradient(g.uv()["x"], stops=[[0.0, "#0f172a"], [0.6, "#7c3aed"], [1.0, "#f472b6"]]) g.output(ramp) ``` ##### RGBA `rgba` · Color · `g.rgba(...)` Un color con alfa a partir de un color vec3 y una opacidad. Une un color RGB y una opacidad en el `vec4` que quiere la Salida. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `rgb` | `vec3` | `(1, 1, 1)` | | `alpha` | `float` | `1` | | Salida | Tipo | | --- | --- | | `color` | `vec4` | ```python title="Ejemplo" g.output(g.rgba([0.2, 0.6, 1.0], 0.5)) ``` ##### Capa *Layer* · `layer` · Color · `g.layer(...)` Pone front sobre back usando el alfa de front. Pone `front` sobre `back` usando el alfa de front, como apilar dos capas. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `back` · fondo | `vec4` | `(0, 0, 0, 1)` | | `front` · frente | `vec4` | `(1, 1, 1, 0.5)` | | Salida | Tipo | | --- | --- | | `color` | `vec4` | ```python title="Ejemplo" 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)) ``` #### Efectos Efectos de pantalla que devuelven un número cercano a 1: multiplica tu color final por ellos. ##### Viñeta *Vignette* · `vignette` · Efectos · `g.vignette(...)` Oscurece hacia las esquinas: multiplica un color por él. Oscurece hacia las esquinas y lleva la mirada al centro. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | `strength` · intensidad | `float` | `0.8` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | ```python title="Ejemplo" c = g.gradient(g.uv()["x"]) g.output(g.rgba(c["rgb"] * g.vignette(strength=0.9), 1.0)) ``` ##### Líneas de barrido *Scanlines* · `scanlines` · Efectos · `g.scanlines(...)` Las líneas horizontales de una pantalla CRT: multiplica un color por ellas. Las líneas horizontales de un televisor antiguo; `speed` las hace desplazarse. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `px` · píxel | `vec2` | *Píxel* (contexto) | | `spacing` · espaciado | `float` | `3` | | `strength` · intensidad | `float` | `0.25` | | `speed` · velocidad | `float` | `0` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | ```python title="Ejemplo" c = g.gradient(g.uv()["y"]) g.output(g.rgba(c["rgb"] * g.scanlines(spacing=3.0, strength=0.3), 1.0)) ``` ##### Grano de película *Film grain* · `grain` · Efectos · `g.grain(...)` Motas aleatorias que cambian cada fotograma: multiplica un color por ellas. Motas aleatorias que cambian en cada fotograma, como en una película. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `px` · píxel | `vec2` | *Píxel* (contexto) | | `strength` · intensidad | `float` | `0.12` | | Salida | Tipo | | --- | --- | | `value` · valor | `float` | ```python title="Ejemplo" c = g.gradient(g.uv()["x"]) g.output(g.rgba(c["rgb"] * g.grain(strength=0.15), 1.0)) ``` #### Textura Imágenes en un shader: añade una variable `texture` (un archivo de imagen) y lee sus colores. ##### Muestrear textura *Sample texture* · `sample_texture` · Textura · `g.sample_texture(...)` El color de una textura (una variable de imagen) en uv. Lee el color de una imagen en un UV. Añade una variable `texture` cuyo valor sea la ruta de una imagen (relativa al `.pzshad`), conéctala a `texture` y deforma antes el UV para ondas o pixelado. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `texture` · textura | `texture` | — | | `uv` | `vec2` | *UV* (contexto) | | Salida | Tipo | | --- | --- | | `color` | `vec4` | | `rgb` | `vec3` | | `alpha` | `float` | ```python title="Ejemplo" photo = g.uniform("photo", "texture", "photo.png") uv = g.pixelate(cells=[40.0, 30.0]) g.output(g.sample_texture(photo, uv["uv"])) ``` **Siempre al día:** Esta página se genera a partir del catálogo de nodos de `pyzed.shading`, así que los puertos, tipos y valores por defecto son siempre los de esta versión. Cada ejemplo se compila al construir la documentación. **Ver también:** [Primeros pasos](https://albakore.github.io/pyzed/es/shading/getting-started/) · [La vista Shader](https://albakore.github.io/pyzed/es/shading/shader-designer/) · [Recetas](https://albakore.github.io/pyzed/es/shading/recipes/) · [Grafos de shader en Python](https://albakore.github.io/pyzed/es/shading/shader-graphs/) ### Lógica, variables y utilidades > Comparaciones y si/si no, valores con nombre, desvíos, tu propio WGSL, otros shaders como funciones, comentarios y el nodo Salida. Source: https://albakore.github.io/pyzed/es/shading/nodes/logic-utility/ Cada ejemplo de esta página se ejecuta después de estas dos líneas y termina enviando un color al nodo Salida: ```python from pyzed import shading g = shading.Graph("demo") ``` #### Lógica Valores verdadero/falso y elegir entre dos valores. Los shaders corren en todos los píxeles a la vez: prefiere Mezclar y Paso suave para transiciones suaves y deja Elegir para elecciones tajantes. ##### Comparar *Compare* · `compare` · Lógica · `g.compare(...)` Si a es menor, mayor o igual que b (en vectores: todos los componentes). Compara `a` y `b` (`<`, `<=`, `>`, `>=`, `==`, `!=`) y da verdadero o falso. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `a` | `T` | `0` | | `b` | `T` | `0.5` | | Salida | Tipo | | --- | --- | | `result` · resultado | `bool` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `op` | `<`, `<=`, `>`, `>=`, `==`, `!=` | `<` | ```python title="Ejemplo" left = g.compare(g.uv()["x"], 0.5, op="<") g.output(g.rgba(g.select(left, 1.0, 0.2), 1.0)) ``` ##### Elegir *Select* · `select` · Lógica · `g.select(...)` si condition entonces a, si no b. Si `condition` es verdadero, `a` (entonces); si no, `b` (si no). | Entrada | Tipo | Por defecto | | --- | --- | --- | | `condition` · condición | `bool` | `true` | | `a` · entonces | `T` | `1` | | `b` · si no | `T` | `0` | | Salida | Tipo | | --- | --- | | `result` · resultado | `T` | ```python title="Ejemplo" 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)) ``` ##### Lógica *Logic* · `logic` · Lógica · `g.logic(...)` and, or, xor y not de valores verdadero/falso. `and`, `or`, `xor` y `not` de valores verdadero/falso. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `a` | `bool` | `true` | | `b` | `bool` | `false` | | Salida | Tipo | | --- | --- | | `result` · resultado | `bool` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `op` | `and`, `or`, `xor`, `not` | `and` | ```python title="Ejemplo" 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 Valores con nombre dentro del grafo: calcula algo una vez, ponle nombre y léelo en cualquier lugar sin un cable largo por el lienzo. ##### Guardar valor *Set value* · `set_var` · Variables · `g.set_var(...)` Da nombre a un valor para que los nodos Leer valor lo lean en cualquier parte sin una conexión larga. Da nombre a un valor. Cualquier **Leer valor** con el mismo nombre lo lee, así puedes reutilizarlo lejos sin un cable que cruce todo el lienzo. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `value` · valor | `T` | `0` | | Salida | Tipo | | --- | --- | | `value` · valor | `T` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `name` | `text` | `value1` | ```python title="Ejemplo" 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)) ``` ##### Leer valor *Get value* · `get_var` · Variables · `g.get_var(...)` Lee un valor nombrado por un nodo Guardar valor. Lee un valor al que un nodo **Guardar valor** puso nombre. | Salida | Tipo | | --- | --- | | `value` · valor | `T` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `name` | `local` | `value1` | ```python title="Ejemplo" g.set_var(g.uv()["x"], name="x") g.output(g.gradient(g.get_var(name="x"))) ``` #### Utilidades Nodos que mantienen ordenados los grafos grandes y te dejan ir más allá del catálogo. ##### Desvío *Reroute* · `reroute` · Utilidades · `g.reroute(...)` Un punto para desviar una conexión alrededor de otros nodos. Un punto que solo desvía un cable alrededor de otros nodos: no cambia nada. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `in` · entrada | `T` | `0` | | Salida | Tipo | | --- | --- | | `out` · salida | `T` | ```python title="Ejemplo" x = g.reroute(g.uv()["x"]) g.output(g.gradient(x)) ``` ##### Expresión *Expression* · `expression` · Utilidades · `g.expression(...)` Tu propio WGSL: una expresión con a, b, c y d (y uv, px, size, time), o el cuerpo de una función con return. Los tipos de a a d siguen lo que esté conectado. Tu propia línea de WGSL cuando ningún nodo hace lo que necesitas: una expresión de `a`, `b`, `c` y `d` (más `uv`, `px`, `size` y `time`), o el cuerpo de una función con `return`. Elige el `type` del resultado. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `a` | `any` | `0` | | `b` | `any` | `0` | | `c` | `any` | `0` | | `d` | `any` | `0` | | Salida | Tipo | | --- | --- | | `result` · resultado | `float` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `code` | `code` | `a + b` | | `type` | `float`, `vec2`, `vec3`, `vec4`, `int`, `bool` | `float` | Los tipos de `a` a `d` siguen lo que se les conecta (`any`); `type` fija el del resultado. ```python title="Ejemplo" e = g.expression(g.uv()["x"], 8.0, code="abs(sin(a * b + time))", type="float") g.output(g.rgba(e, 1.0)) ``` ##### Función shader *Shader function* · `subgraph` · Utilidades · `g.subgraph(...)` Otro .pzshad usado como función: sus variables mutables son entradas y su salida es el color. Conecta uv para moverlo, repetirlo o deformarlo. Usa otro `.pzshad` como función: sus variables mutables se vuelven entradas y su color la salida. Pasa un `uv` para mover, repetir o deformar el shader entero. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `uv` | `vec2` | *UV* (contexto) | | Salida | Tipo | | --- | --- | | `color` | `vec4` | | `rgb` | `vec3` | | `alpha` | `float` | | Ajuste | Valores | Por defecto | | --- | --- | --- | | `path` | `file` | — | Además de `uv`, cada variable mutable del otro shader se vuelve una entrada. Un Shader function necesita otro archivo `.pzshad`: `g.subgraph(path="plasma.pzshad", uv=...)`. En el diseñador, haz doble clic en el nodo para abrir ese archivo. ##### Comentario *Comment* · `comment` · Utilidades · `g.comment(...)` Una nota en el grafo. Una nota en el lienzo para explicar una parte del grafo. No afecta al shader. | Ajuste | Valores | Por defecto | | --- | --- | --- | | `text` | `text` | `Note` | ```python title="Ejemplo" g.comment(text="Sky: gradient from top to bottom") g.output(g.gradient(g.uv()["y"])) ``` #### Salida Todo shader tiene un nodo Salida: lo que llega a él es el color del píxel. ##### Salida *Output* · `output` · Salida · `g.output(...)` El color de cada píxel. Alpha, si se conecta, reemplaza el alfa de color. El final del grafo: el color que recibe es el color del píxel. Si `alpha` está conectado, reemplaza el alfa del color. En Python, `g.output(color)` lo añade o lo encuentra. | Entrada | Tipo | Por defecto | | --- | --- | --- | | `color` | `vec4` | `(0, 0, 0, 1)` | | `alpha` | `float` | — (sin usar hasta que se conecta) | ```python title="Ejemplo" g.output([1.0, 0.4, 0.1, 1.0], alpha=g.radial_gradient(radius=0.5)) ``` **Siempre al día:** Esta página se genera a partir del catálogo de nodos de `pyzed.shading`, así que los puertos, tipos y valores por defecto son siempre los de esta versión. Cada ejemplo se compila al construir la documentación. **Ver también:** [Primeros pasos](https://albakore.github.io/pyzed/es/shading/getting-started/) · [La vista Shader](https://albakore.github.io/pyzed/es/shading/shader-designer/) · [Recetas](https://albakore.github.io/pyzed/es/shading/recipes/) · [Grafos de shader en Python](https://albakore.github.io/pyzed/es/shading/shader-graphs/) ## Temas ### Temas > Personaliza el aspecto de una app Pyzed: temas incluidos y propios, tokens de color, estilos tipo CSS para cualquier widget, variantes de componentes, temas de formularios, iconos y fuentes, en código y en Pyzed Design. Source: https://albakore.github.io/pyzed/es/theming/ Pyzed separa **qué** muestra tu interfaz de **cómo se ve**. Todos los componentes leen sus colores del **tema** actual, un conjunto de colores con nombre llamados tokens (`background`, `text`, `border`, `accent`...). Cambia el tema y toda la app lo sigue; cambia un widget con `style()` y solo cambia ese widget. ```python import pyzed as pz from pyzed import ui class Notas(pz.App): theme = "Gruvbox Dark" # cualquier nombre de pz.themes.names() def view(self): tarjeta = pz.Style(background="elevated", border=(1, "border"), radius=10, padding=16) return pz.column([ ui.heading("Notas"), pz.container(ui.label("Con estilo como CSS, con los colores del tema.")).style(tarjeta), ]) ``` #### Cómo encaja todo ```mermaid flowchart LR T["Tema
colores con nombre (tokens)"] --> C["Componentes
variantes · tintes · tamaños"] C --> S["style()
un widget, como CSS"] G["Estilo global
App.style"] --> S ``` - **Tema**: la paleta de toda la app. Elige uno incluido, sigue el modo claro u oscuro del sistema o crea el tuyo a partir de tres colores. - Los **componentes** toman sus colores del tema. Sus **variantes** y **tintes** (`variant="outline"`, `tint="error"`) eligen cuáles de esos colores usan. - **`style()`** cambia un widget, como CSS; los estilos con nombre reutilizan un aspecto; `App.style` fija la fuente y los colores de toda la ventana. #### Pyzed Design también lo sigue El diseñador visual es una app de Pyzed, así que también tiene temas: elige uno en **Ajustes → Apariencia → Tema** y todo el editor cambia a la vez. Cada formulario conserva su propio tema, que se fija en sus propiedades de **Aspecto** (ver [Estilo de formularios](https://albakore.github.io/pyzed/es/theming/forms/)), así que el editor y la app que diseñas pueden verse distintos. **One Light:** #### En esta sección style() en componentes, estilos con nombre y componentes que siguen el tema.', href: '/pyzed/es/theming/components/', icon: 'puzzle' }, { title: 'Estilo de formularios', text: 'Tema, acento, radio y fuentes de un formulario en Pyzed Design, y qué guarda el archivo .pzform.', href: '/pyzed/es/theming/forms/', icon: 'laptop', tag: 'Diseñador' }, { title: 'Iconos y fuentes', text: 'Unos 2000 iconos incluidos, iconos por tipo de archivo y cargar tus propias fuentes.', href: '/pyzed/es/theming/icons-fonts/', icon: 'star' }, { title: 'Recetario de temas', text: 'Seis temas completos para copiar: marca, alto contraste, terminal retro, vidrio, pastel y mixto.', href: '/pyzed/es/theming/cookbook/', icon: 'rocket', tag: 'Ejemplos' }, { title: 'API de temas', text: 'Theme, themes y las funciones de color, método por método.', href: '/pyzed/es/reference/theme/', icon: 'document' }, ]} /> **Usa tokens, no colores hex:** Escribe `"accent"` o `"border"` en vez de `"#3a7bc8"` siempre que puedas. Los colores de los tokens cambian con el tema, así que tu app se sigue viendo bien en modo claro y oscuro. **Estado de esta sección:** Todo lo que describen estas páginas funciona hoy, en código y en Pyzed Design. Se están añadiendo capturas de cada tema incluido en claro y oscuro. Los temas para móvil y web llegarán con esas plataformas, que están planificadas pero todavía no disponibles. ### Elegir un tema > Elige uno de los temas claros y oscuros incluidos en Pyzed, sigue el modo claro u oscuro del sistema, cambia de tema mientras la app corre y lee los colores del tema desde Python. Source: https://albakore.github.io/pyzed/es/theming/themes/ Un **tema** es un conjunto de colores y tamaños con nombre. Todos los componentes de `pyzed.ui` leen sus colores del **tema actual**, así que al cambiarlo se reestiliza toda la interfaz de una vez, sin tocar tus vistas. examples/styling.py: la misma pantalla con cualquier tema incluido, según el sistema o generada a partir de tres colores." /> #### Fija el tema de tu app Dale a tu app un `theme` con el nombre de un tema: ```python import pyzed as pz from pyzed import ui class MiApp(pz.App): theme = "Ayu Dark" def view(self): return pz.column([ui.heading("Hola"), ui.button("Guardar", self.guardar, variant="solid")], padding=24) def guardar(self): print("guardado") pz.run(MiApp) ``` `theme` acepta: | Valor | Ejemplo | Significado | | --- | --- | --- | | Un nombre de tema | `"Gruvbox Light"` | Cualquier nombre de `pz.themes.names()`; no distingue mayúsculas. | | Un objeto `Theme` | `pz.themes.ONE_LIGHT`, o uno tuyo | Usa exactamente ese tema. | | `pz.themes.auto(claro, oscuro)` | `pz.themes.auto("One Light", "One Dark")` | Sigue el modo claro u oscuro del sistema operativo. | | `"auto"` | `"auto"` | Atajo de `auto("One Light", "One Dark")`. | #### Los temas incluidos Pyzed trae cuatro familias de temas, cada una con variantes claras y oscuras: | Familia | Oscuros | Claros | | --- | --- | --- | | **One** (la predeterminada) | `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` | Las tres primeras son los temas del editor [Zed](https://zed.dev) (licencia MIT), cuya interfaz inspiró buena parte del aspecto de Pyzed. La familia Pyzed es propia: cada tema sale de una pequeña paleta con `pyzed.theme_palettes.from_palette`, que puedes usar para hacer el tuyo (mira [Temas propios](https://albakore.github.io/pyzed/es/theming/custom-themes/)). Lístalos desde Python: **uv:** ```bash uv run python -c "import pyzed as pz; print(pz.themes.names())" ``` **pip:** ```bash title="macOS y 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())" ``` **Pruébalos todos:** Ejecuta `examples/styling.py` y elige un tema de la lista: toda la ventana se reestiliza al elegir. Mira [Ejecutar los ejemplos](https://albakore.github.io/pyzed/es/examples/) para el comando en tu sistema. #### Oscuro, claro o según el sistema La mayoría de la gente espera que una app siga el modo claro u oscuro de su computadora. `pz.themes.auto` lo hace: recibe un tema claro y uno oscuro (nombres u objetos `Theme`) y elige uno cuando el sistema cambia. ```python class MiApp(pz.App): theme = pz.themes.auto("Gruvbox Light", "Gruvbox Dark") ``` `self.system_dark` dice en qué modo está el sistema (`True` en modo oscuro), y Pyzed lo actualiza, junto con el tema, cuando el usuario lo cambia. Para que el usuario elija, guarda su elección en una variable y fija `self.theme` a partir de ella: ```python class MiApp(pz.App): def __init__(self): self.modo = "Sistema" self.theme = pz.themes.auto() def fijar_modo(self, modo): self.modo = modo self.theme = {"Claro": "One Light", "Oscuro": "One Dark"}.get(modo, pz.themes.auto()) def view(self): return ui.segmented(["Sistema", "Claro", "Oscuro"], self.modo, self.fijar_modo) ``` #### Cambiar de tema mientras la app corre `self.theme` es solo un atributo: asígnalo y el siguiente fotograma usa el tema nuevo. Un selector de temas es una línea: ```python ui.select(pz.themes.names(), self.resolved_theme().name, lambda nombre: setattr(self, "theme", nombre)) ``` Un botón claro/oscuro: ```python def alternar_tema(self): self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark" ``` self.theme con la app en marcha vuelve a pintar todos los componentes." /> #### Leer los colores del tema `self.theme` puede ser un nombre o un `auto(...)`, así que usa **`self.resolved_theme()`** para obtener el `Theme` concreto que se usa ahora. Sus [tokens](https://albakore.github.io/pyzed/es/theming/tokens/) son atributos con cadenas `#rrggbb`: ```python def view(self): t = self.resolved_theme() return pz.container(contenido, bg=t.surface, border_color=t.border, border_width=1, radius=t.radius) ``` Dentro de `view()` (y de los componentes) `pz.theme.current()` devuelve el mismo tema. Casi siempre no necesitas ninguno de los dos: escribe el **nombre del token** en lugar de un color y Pyzed lo busca por ti: ```python pz.text("Guardado").style(color="success") pz.container(contenido).style(background="surface", border=(1, "border")) ``` **Los colores hex no siguen el tema:** `"#1e1e1e"` sigue siendo oscuro cuando el usuario pasa a un tema claro. Prefiere nombres de tokens (`"surface"`, `"text_muted"`, `"accent"`...) y deja los colores hex para lo que de verdad no debe cambiar, como el logo de una marca. #### Un tema para una ventana o un bloque Cada [ventana](https://albakore.github.io/pyzed/es/guides/windows/) puede tener su propio tema (`Window.theme`); una ventana sin tema usa el de la app. Un `Theme` también es un gestor de contexto: los componentes que se construyen dentro de un bloque `with` lo usan. Esto dibuja una vista previa clara dentro de una app oscura: ```python with pz.themes.get("One Light"): vista_previa = ui.card(ui.label("Así se ve en modo claro")) ``` Y todos los componentes de `pyzed.ui` aceptan `theme=` para dibujar solo ese componente con otro tema: ```python ui.button("Botón claro", al_pulsar, theme=pz.themes.ONE_LIGHT) ``` Mira [`examples/theming/mixed.py`](https://github.com/albakore/pyzed/blob/main/examples/theming/mixed.py) para una barra lateral clara en una ventana oscura, con un selector que recuerda el tema elegido. #### Siguiente - [Tokens del tema](https://albakore.github.io/pyzed/es/theming/tokens/): qué pinta cada color de un tema. - [Crear temas](https://albakore.github.io/pyzed/es/theming/custom-themes/): tus propios colores, en código o en un archivo JSON. ### Tokens del tema > Cada token de color y tamaño de un tema de Pyzed, qué pinta en la interfaz y su valor en One Dark y One Light. Source: https://albakore.github.io/pyzed/es/theming/tokens/ Un tema es una lista de **tokens**: nombres como `accent` o `text_muted` con un color (o un tamaño) cada uno. Los componentes nunca fijan colores a mano; le piden un token al tema. Por eso un solo cambio en el tema lo reestiliza todo. Los tokens se usan de dos formas: ```python pz.text("Total").style(color="text_muted") # por nombre, en cualquier sitio donde va un color pz.container(cuerpo, bg=self.resolved_theme().surface) # como atributo del tema ``` Los colores son `#rrggbb` o `#rrggbbaa` (los dos últimos dígitos son la opacidad). Los nombres de tokens funcionan en `style()`, en los parámetros `color=`/`tint=` de los componentes, en [`pz.Paint`](https://albakore.github.io/pyzed/es/theming/styles/#gradientes-opacidad-y-máscaras) y en los campos de color del diseñador. **Piensa en capas:** De atrás hacia adelante: la **ventana** (`background`), las **superficies** sobre ella (`surface`, `panel`, `elevated`), los **elementos** sobre ellas (`element`, `ghost_*`) y el **texto y los iconos** encima. Haz cada capa un poco más clara (temas oscuros) o más oscura (temas claros) que la de atrás. #### Superficies Las áreas grandes de la ventana, de atrás hacia adelante. ui.card), estadísticas y otras áreas elevadas.'], ['elevated', 'Lo que flota: menús, popovers, desplegables, tooltips, diálogos, toasts y la paleta de comandos.'], ['editor', 'Donde se escribe: campos de texto, el editor de código, el fondo de la terminal en temas generados.'], ['panel', 'Paneles laterales y docks de un workspace, el panel de ayuda.'], ['title_bar', 'La barra de título propia (ui.title_bar, ui.window_frame).'], ['status_bar', 'La barra de estado al pie de un workspace.'], ['tab_bar', 'La franja detrás de las pestañas.'], ['tab_active', 'La pestaña seleccionada.'], ['tab_inactive', 'Las demás pestañas.'], ['terminal', 'El fondo del widget de terminal.'], ]} /> #### Bordes #### Elementos y elementos ghost Los **elementos** son controles rellenos (el `ui.button` por defecto, chips, controles segmentados). Los elementos **ghost** no tienen fondo hasta que pasas el puntero: filas de listas, botones ghost y de icono, ítems de menú, filas de árboles. #### Texto, iconos y acento solid), interruptores, sliders, barras de progreso, casillas.'], ['on_accent', 'Texto e iconos dibujados sobre el acento (dentro de un botón solid o una insignia solid).'], ]} /> **Mantén legible on_accent:** Si cambias `accent` a mano, cambia también `on_accent`, o usa `theme.with_accent(color)`, que elige un `on_accent` legible (y actualiza anillos de foco, selección y enlaces) por ti. #### Estado Los usa `tint=` (`ui.badge`, `ui.callout`, `ui.button`, `ui.progress`...), los toasts, los mensajes de validación y los gráficos. ui.stat.'], ['info', 'Avisos informativos (el acento, por defecto).'], ['hint', 'Pistas, el aviso más discreto.'], ]} /> #### Control de versiones Para árboles de archivos, pestañas y diffs (`ui.tree`, `ui.diff_stat`, el margen del editor de código). #### Editor de código Para [`pz.code_editor`](https://albakore.github.io/pyzed/es/library/editor/), `ui.terminal` y los bloques de código. #### Sintaxis, terminal y personas Estos tres son colecciones en lugar de colores sueltos. - **`syntax`**: un diccionario de un nombre de resaltado a un color, o a `(color, "italic")` / `(color, "bold")`. Los nombres principales son `primary`, `comment`, `comment.doc`, `keyword`, `function`, `type`, `string`, `string.escape`, `number`, `boolean`, `constant`, `property`, `attribute`, `operator`, `variable`, `variable.special`, `punctuation`, `punctuation.bracket`, `tag`, `title` y `link_uri`. Un nombre con punto que falta usa su primera parte (`string.escape` → `string`). - **`ansi`**: los colores de la terminal: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `bright_black` y `dim`. - **`players`**: una tupla de colores para avatares y colaboradores; `ui.avatar(index=n)` y `ui.facepile` toman el n-ésimo. def, if, return...'], ['syntax.function', 'Nombres de funciones.'], ['syntax.string', 'Cadenas.'], ['syntax.comment', 'Comentarios.'], ['syntax.type', 'Clases y tipos.'], ['syntax.number', 'Números.'], ['ansi.red', 'Texto rojo en la terminal (y así con cada color ANSI).'], ['players.0', 'El primer color de avatar (el acento, por defecto).'], ]} /> #### Tamaños y metadatos | Token | Por defecto | Qué fija | | --- | --- | --- | | `radius` | `6.0` | Radio de las esquinas de tarjetas, diálogos y superficies grandes (las tarjetas usan `radius + 2`). | | `radius_sm` | `4.0` | Radio de botones, campos, chips y filas de listas. | | `ui_font_size` | `14.0` | El tamaño base del texto de la interfaz; lo fijan el **Tamaño de fuente** de un formulario y el `size` del estilo global. | | `buffer_font_size` | `14.0` | Tamaño del texto en el editor de código. | | `name` | | El nombre que usan `pz.themes.get` y `theme = "..."`. | | `appearance` | | `"dark"` o `"light"`; `theme.is_dark` lo lee. | #### Desde Python ```python t = pz.themes.get("One Dark") t.accent # '#74ade8' t.color("text_muted") # lo mismo que t.text_muted; los colores hex pasan sin cambios t.tokens() # todos los tokens como diccionario t.to_dict() # apto para JSON, el formato que escribe Theme.save ``` Los nombres siguen el formato de temas del editor Zed, así que los temas hechos para él se pueden importar (mira [Crear temas](https://albakore.github.io/pyzed/es/theming/custom-themes/#importar-un-tema-de-zed)). La [API de temas](https://albakore.github.io/pyzed/es/reference/theme/) lista cada método. ### Crear temas > Crea tu propio tema de Pyzed: cambia el acento, deriva un tema, genera uno a partir de tres colores, escríbelo en JSON, importa temas de Zed y regístralo por nombre. Source: https://albakore.github.io/pyzed/es/theming/custom-themes/ Hay cuatro formas de hacer un tema, de la más rápida a la más detallada. Todas devuelven un `Theme`, que puedes usar directamente (`theme = mi_tema`) o [registrar](#darle-un-nombre) para usarlo por nombre. | Quieres... | Usa | Esfuerzo | | --- | --- | --- | | Cambiar solo el color de marca | `tema.with_accent("#e11d48")` | Una línea | | Cambiar algunos tokens de un tema existente | `tema.derive(radius=12, success="#16a34a")` | Unas líneas | | Un tema nuevo a partir de tus colores | `pz.Theme.generate(nombre, background=, text=, accent=)` | Tres colores | | Control total, o un tema en un archivo | `pz.Theme.from_dict({...})`, `Theme.load("tema.json")` | Cualquier token | Los temas son **inmutables**: cada método devuelve un tema nuevo y deja el original intacto, así que `pz.themes.ONE_DARK` siempre es el One Dark de verdad. #### 1. Cambiar el acento El acento es el color de los botones principales, interruptores, sliders, anillos de foco, enlaces y selecciones. `with_accent` los cambia todos a la vez y elige un `on_accent` legible (el texto sobre el acento): ```python class MiApp(pz.App): theme = pz.themes.ONE_DARK.with_accent("#e11d48") # One Dark, en rojo ``` #### 2. Derivar: cambiar algunos tokens `derive` copia un tema con los [tokens](https://albakore.github.io/pyzed/es/theming/tokens/) que pases. Los diccionarios `syntax` y `ansi` se combinan con los del original, así que solo listas lo que cambia: ```python oceano = pz.themes.ONE_DARK.derive( name="Océano", background="#0b1622", surface="#102131", radius=10, radius_sm=8, syntax={"keyword": "#ff79c6", "comment": ("#5c6f80", "italic")}, ).with_accent("#4fb3ff") ``` **Note:** `derive` cambia exactamente los tokens que nombras. Si cambias `background`, los colores de hover y los bordes pensados para el fondo anterior se quedan como estaban: para un conjunto nuevo de superficies, `generate` (abajo) suele ser más fácil. #### 3. Generar un tema a partir de tres colores `Theme.generate` construye todos los tokens a partir de un **fondo**, un color de **texto** y un **acento**: superficies, bordes, estados hover, texto atenuado y los colores del editor se mezclan entre el fondo y el texto. ```python propio = pz.Theme.generate( "Bosque", background="#14201a", # donde va el contenido (el token editor); la ventana se mezcla un poco más clara text="#e3efe6", # el color del texto principal accent="#4ade80", # botones, foco, enlaces ) ``` Argumentos opcionales: | Argumento | Por defecto | Significado | | --- | --- | --- | | `appearance` | se deduce | `"dark"` o `"light"`; se deduce de lo claro que es `background`. | | `error`, `warning`, `success` | de One Dark / One Light | Colores de estado (también colorean los tokens de control de versiones). | | `syntax` | los de One | Colores de código que cambian (se combinan). | | `radius` | `6.0` | Radio de las esquinas; `radius_sm` pasa a ser `radius - 2`. | **Buenos colores para empezar:** Para un tema oscuro, empieza con un fondo entre `#101010` y `#2a2a2a` y un texto casi blanco. Para uno claro, un fondo casi blanco y un texto entre `#1a1a1a` y `#3a3a3a`. Luego comprueba el contraste (abajo). Cuando tienes una paleta completa (el marco alrededor del editor y seis tonos para estados y sintaxis), `theme_palettes.from_palette` usa cada color como lo hacen las familias de temas de Zed. Los temas propios de Pyzed (Midnight, Graphite, Forest, Rosé, Dawn, Glacier) están hechos con ella: ```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. Cualquier token, desde un diccionario o un archivo `Theme.from_dict` toma los tokens que listes y completa el resto con **One Dark** (o **One Light** con `"appearance": "light"`). Los nombres desconocidos lanzan un `ValueError`, así que las erratas no pasan inadvertidas: ```python papel = pz.Theme.from_dict({ "name": "Papel", "appearance": "light", "background": "#f5f1e8", "surface": "#fbf8f1", "editor": "#fffdf8", "text": "#2b2620", "text_muted": "#6b6257", "border": "#d9d0c1", "accent": "#b45309", "on_accent": "#ffffff", }) ``` ##### Guardar y cargar un archivo de tema `save` escribe un tema en JSON y `Theme.load` lo vuelve a leer. El archivo tiene las mismas claves que `from_dict`, así que también puedes escribirlo a mano (solo hacen falta los tokens que cambias): ```json title="temas/papel.json" { "name": "Papel", "appearance": "light", "background": "#f5f1e8", "text": "#2b2620", "accent": "#b45309", "on_accent": "#ffffff", "radius": 2 } ``` ```python papel = pz.Theme.load("temas/papel.json") papel.save("temas/papel-completo.json") # todos los tokens, listos para editar ``` **Incluye los archivos de tema en tu app:** Guarda los archivos de tema dentro de tu proyecto y lista la carpeta en `include` de `[tool.pyzed.build]` para que [`pyzed build`](https://albakore.github.io/pyzed/es/packaging/build/#opciones) la meta en la app empaquetada. #### Importar un tema de Zed Como los nombres de los tokens siguen el formato de temas del editor Zed, cualquier tema de la [galería de temas de Zed](https://zed.dev/extensions?filter=themes) funciona en Pyzed. Descarga su `.json` y: ```python mocha = pz.Theme.from_zed("catppuccin.json", name="Catppuccin Mocha") # un tema del archivo pz.themes.load_zed("catppuccin.json") # todos, registrados ``` #### Darle un nombre `pz.themes.register` añade un tema a la lista de temas, así que se puede usar por nombre, aparece en `pz.themes.names()` (y en cualquier selector de temas hecho con ella) y funciona con `pz.themes.auto`: 1. Crea los temas, normalmente en su propio módulo: ```python title="mitema.py" import pyzed as pz CLARO = pz.themes.register(pz.Theme.generate("Acme Light", background="#ffffff", text="#1f1b2e", accent="#6d28d9")) OSCURO = pz.themes.register(pz.Theme.generate("Acme Dark", background="#16131f", text="#ece9f5", accent="#a78bfa")) ``` 2. Importa ese módulo antes de que arranque la app y usa los nombres: ```python title="main.py" import pyzed as pz import mitema # noqa: F401 - registra los temas Acme class MiApp(pz.App): theme = pz.themes.auto("Acme Light", "Acme Dark") ``` 3. Ejecútala: **uv:** ```bash uv run python main.py ``` **pip:** ```bash title="macOS y Linux" python3 main.py ``` ```powershell title="Windows (PowerShell)" py main.py ``` Registrar un tema con el nombre de uno existente lo reemplaza. #### Comprobar el contraste `pyzed.theme` tiene pequeñas funciones de color, útiles al construir temas en código: | Función | Devuelve | | --- | --- | | `contrast(a, b)` | La relación de contraste WCAG de dos colores, de 1 a 21. El texto debería tener al menos **4.5** (AA), idealmente **7** (AAA). | | `readable_on(fondo)` | Un color de texto claro u oscuro, el que mejor se lea sobre `fondo`. | | `luminance(color)` | Lo claro que es un color, de 0 a 1. | | `mix(a, b, cantidad)` | Un color entre `a` y `b` (`cantidad` 0 es `a`, 1 es `b`). | | `lighten(color, cantidad)`, `darken(color, cantidad)` | El color mezclado con blanco o con negro. | | `alpha(color, valor)` | El color con una opacidad (`alpha("#ff0000", 0.5)` es `#ff000080`). | ```python from pyzed.theme import contrast t = pz.Theme.generate("Bosque", background="#14201a", text="#e3efe6", accent="#4ade80") print(round(contrast(t.text, t.background), 1)) # 11.2 -> excelente print(round(contrast(t.text_muted, t.background), 1)) # 6.0 -> sigue por encima de 4.5 ``` #### Ejemplos completos El [Recetario de temas](https://albakore.github.io/pyzed/es/theming/cookbook/) tiene temas completos para copiar: un tema de marca en claro y oscuro, alto contraste, terminal retro, vidrio oscuro y pastel. ### Estilos > Da estilo a cualquier widget de Pyzed como en CSS: colores, bordes, radios, sombras y estados, estilos reutilizables y el estilo global de la app. Source: https://albakore.github.io/pyzed/es/theming/styles/ Todo widget tiene un método `style()` que le da estilo como en CSS: fondo, bordes, radio, sombra, padding, margen, tamaño y tipografía. (Image: Botones con estilo, una tarjeta y un gradiente con máscara) ```python ui.button("Guardar", self.guardar).style(radius=0, border=(2, "#ef4444")) pz.text("Título").style(size=20, weight="bold", color="accent") pz.column([...]).style(background="panel", padding=16, radius=10, shadow=("#00000055", (0, 6), 18)) ``` En un botón o un contenedor, `style()` cambia el propio widget (un botón cuadrado sigue siendo un botón). Los demás widgets reciben una caja alrededor que lleva el estilo. #### Propiedades | Propiedad | Valores | | --- | --- | | `background` | Un color, un `pz.Gradient` o un `pz.Paint`. | | `color` | Color del texto. | | `font` | `"sans"`, `"mono"`, `"serif"` o una familia cargada con `pz.load_font`. | | `weight` | `"light"`, `"normal"`, `"medium"`, `"semibold"`, `"bold"`… o de 100 a 900. | | `italic`, `size`, `line_height` | Tipografía. | | `border_color`, `border_width` | El borde; `border=(ancho, color)` fija ambos. | | `radius` | Un número, o `(arriba_izq, arriba_der, abajo_der, abajo_izq)`. | | `shadow` | `(color, (x, y), desenfoque)`. | | `padding`, `margin` | Un número, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `width`, `height` | Píxeles, `"fill"`, `"shrink"` o `"fill:N"`; `max_width`, `max_height`, `min_width` y `min_height` en píxeles. | | `align_x`, `align_y` | Dónde se coloca el contenido dentro de la caja. | | `clip` | Recorta el contenido a la caja. | | `position`, `top`, `right`, `bottom`, `left`, `z_index` | `position="absolute"` coloca un hijo de una [`pz.box`](https://albakore.github.io/pyzed/es/guides/layout/#cajas) por sus desplazamientos, fuera del flujo. | | `opacity`, `translate`, `scale`, `rotate` | Desvanecen, desplazan, escalan o giran cómo se dibuja el widget, sin cambiar el layout. | | `blur`, `backdrop_blur` | Desenfocan el widget o lo que tiene detrás. | | `glow`, `bloom`, `glass` | Efectos visuales: un halo, luz que desborda de las partes brillantes y un panel esmerilado. Ver [Animación y efectos](https://albakore.github.io/pyzed/es/animation/effects/). | Los colores pueden ser textos hex (con alfa, `"#3b82f680"`), tuplas `(r, g, b[, a])` o tokens del tema como `"accent"`, `"panel"` o `"text_muted"`, así que los estilos siguen al tema. Las propiedades de texto se aplican a **todos los textos de dentro** del widget, también a las etiquetas de los componentes de `pyzed.ui`. Gana el estilo más cercano. #### Estados y transiciones Como `:hover`, `:active` y `:focus` en CSS, un estilo puede decir cómo se ve el widget mientras el puntero está encima (`hover`), mientras se pulsa (`pressed`, o `active`) y mientras tiene el foco del teclado (`focus`). `transition` anima el cambio, como `transition` en CSS: ```python ui.button("Guardar", self.save).style( background="accent", radius=6, hover={"background": "#4f8cff", "translate": (0, -1)}, pressed={"scale": 0.97}, transition=120, # milisegundos, "0.2s", True o {"duration", "easing", "delay"} ) ``` Un estado puede cambiar `background` (un color), `color`, `border_color`, `border_width`, `radius`, `opacity`, `translate`, `scale`, `rotate` y `blur`. Ninguno mueve los widgets de alrededor, así que el hover nunca descoloca el layout. Sin `transition` el estado se ve al instante. Con ella, los cambios de `opacity`, `translate`, `scale`, `rotate` y `blur` de un render al siguiente también se animan. Para algo más, ver [Animación](https://albakore.github.io/pyzed/es/animation/). #### Hojas de estilo `pz.stylesheet` añade reglas por selector, como un archivo CSS. `widget.classes(...)` le da clases (y un `id`) a un widget, y las reglas que coinciden le dan estilo: ```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}, # todos los componentes Rating }) pz.column([...]).classes("card", "selected" if self.chosen else None) Rating(value=3, class_="card", id="featured") # los componentes aceptan class_= e id= ``` - Selectores: `.clase`, varias clases (`.card.selected`), `#id`, el nombre de la clase de un componente y un `:hover`, `:pressed` (o `:active`) o `:focus` opcional. Las comas juntan varios selectores en una regla. - Como en CSS, ganan los selectores más específicos (un id gana a las clases, más clases ganan a menos y las clases ganan al nombre de un componente) y después la regla posterior. Un `style()` después de `classes()` va encima. - Definir otra vez un selector reemplaza su regla, así que recargar el código no acumula reglas. `pz.clear_stylesheet()` las quita todas. - Los estilos con nombre de `pz.define_style` también sirven como clases. - Las reglas para el nombre de un componente son valores por defecto: lo que fija su `render()` les gana, y las reglas de la `class_` o el `id` que recibe ganan a ambos. #### Estilos responsive Los estilos pueden seguir el tamaño de la ventana, como las media queries de CSS: ```python pz.stylesheet({ ".sidebar": {"width": 260}, "@media (max_width: 700)": {".sidebar": {"width": 64}}, "@md": {".content": {"padding": 32}}, # un breakpoint: desde 768 px de ancho }) panel.style(padding=24, media={"max_width: 600": {"padding": 8}}) ``` - Una consulta es `min_width`, `max_width`, `min_height` o `max_height` en píxeles (`"max_width: 600"` o `"(max_width: 600px)"`), `orientation` (`portrait` o `landscape`) o `theme` (`dark` o `light`). Se juntan varias con `and`. - Los breakpoints siguen a Tailwind y se aplican desde ese ancho en adelante: `sm` (640), `md` (768), `lg` (1024), `xl` (1280) y `xxl` (1536). - Los estilos se actualizan al cambiar el tamaño de la ventana. - En una vista, `pz.viewport()` devuelve el tamaño de la ventana y `pz.media("max_width: 600")` dice si una consulta se cumple. `pz.current_breakpoint()` devuelve `"xs"`, `"sm"`, `"md"`, etc. `pz.responsive(lambda w, h: ...)` construye contenido con el espacio que recibe de verdad un widget. #### Reutilizar estilos ```python tarjeta = pz.Style(background="elevated", border=(1, "border"), radius=10, padding=16) pz.column([...]).style(tarjeta) pz.column([...]).style(tarjeta | pz.Style(shadow=("#0006", (0, 4), 12))) # gana el de la derecha pz.define_style("pill", radius=999, padding=(4, 12), size=12, weight="semibold") pz.text("nuevo").style("pill", background="accent") ``` `widget.style(a, b, color=...)` combina en orden, y volver a llamar a `style()` combina con lo que el widget ya tiene. Pasar `None` en una propiedad la quita. `Style.to_dict()` y `Style.from_dict()` convierten un estilo en datos aptos para JSON y viceversa. Los componentes también aceptan `style=`: `Rating(value=3, style="card")`. #### Gradientes, opacidad y máscaras ```python atardecer = pz.Gradient(["#f97316", "#db2777", "#7c3aed"], angle=120) caja.style(background=atardecer) caja.style(background=pz.Paint("success", opacity=0.2)) # un token translúcido caja.style(background=pz.Paint(atardecer, mask=pz.Mask([1, 0.15]))) # se desvanece ``` - `pz.Gradient(paradas, angle=180)`: colores repartidos por igual o pares `(posición, color)`. Los ángulos siguen CSS (0 arriba, 90 derecha). Hasta 8 paradas. - `pz.Mask(paradas)`: opacidades en la dirección del relleno (sobre un color liso, en su propio `angle`). - `pz.Paint(relleno, opacity=, mask=)`: un color o gradiente con opacidad general y máscara. #### Selector de color `ui.ColorPicker` edita cualquiera de ellos: un color con alfa, un gradiente (un clic en la barra añade una parada, las paradas se arrastran y la papelera quita la seleccionada) o una máscara. Devuelve un texto hex, un `pz.Gradient` o un `pz.Paint`, listos para `background`: ```python ui.ColorPicker(value=self.fill, on_change=lambda e: setattr(self, "fill", e.value)) ``` (Image: El selector de color editando un gradiente) `alpha=False`, `gradients=False` y `masks=False` ocultan esas partes; `swatches=[...]` fija los colores rápidos. `ui.color_swatch(valor)` muestra un valor como un cuadrito. Ver [`examples/color_picker.py`](https://github.com/albakore/pyzed/blob/main/examples/color_picker.py). #### El estilo global `App.style` da estilo a toda la ventana, y `pz.set_style(...)` fija un estilo para todas las ventanas (el `style` de cada ventana va encima): ```python class MiApp(pz.App): style = pz.Style(font="serif", size=15, color="#e6e1cf", radius=2) ``` Sus propiedades de texto completan lo que los textos no fijan, y su `color`, `size`, `background`, `border_color` y `radius` también se vuelven los tokens `text`, `ui_font_size`, `background`, `border` y `radius` del tema, así que los componentes de `pyzed.ui` los siguen. Cambia `self.style` cuando quieras para cambiar el estilo de la app. Para otros colores del tema, deriva un tema: `self.theme = themes.get("One Dark").with_accent("#ff6b6b")` (ver [Temas](https://albakore.github.io/pyzed/es/theming/themes/)). Ver [`examples/styles.py`](https://github.com/albakore/pyzed/blob/main/examples/styles.py). ### Estilo de componentes > Cambia el aspecto de los componentes de Pyzed: variantes, tintes y tamaños, style() en cualquier componente, estilos con nombre, otro tema para un componente y componentes propios que siguen el tema. Source: https://albakore.github.io/pyzed/es/theming/components/ Antes de escribir un estilo, mira si el componente ya tiene el aspecto que buscas. La mayoría de los componentes de [`pyzed.ui`](https://albakore.github.io/pyzed/es/library/) vienen en **variantes**, **tintes** y **tamaños** que siguen el tema. Cuando ninguno encaja, `style()` cambia cualquier cosa, como en CSS. ```mermaid flowchart LR T["Tema
colores para todo"] --> V["Variante · tinte · tamaño
ui.button(variant='outline')"] V --> S["style()
.style(radius=0)"] ``` Cada paso solo sobrescribe lo que fija: el tema da los colores, la variante elige cuáles usa un componente y `style()` tiene la última palabra. #### Variantes Una **variante** es un aspecto listo del mismo componente. ##### Botones ```python ui.button("Guardar", guardar, variant="solid") # relleno con el acento: la acción principal ui.button("Cancelar", cancelar) # "filled" (por defecto): un botón relleno neutro ui.button("Detalles", detalles, variant="outline") # transparente con borde ui.button("Más", mas, variant="ghost") # sin fondo hasta pasar el puntero ui.button("Omitir", omitir, variant="subtle") # como ghost, con texto atenuado ui.button("Archivar", archivar, variant="tinted") # un acento translúcido ``` | Variante | Fondo | Úsala para | | --- | --- | --- | | `filled` (por defecto) | `element` | La mayoría de los botones. | | `solid` | el acento (o el tinte) | La acción principal de una pantalla, una por zona. | | `tinted` | un acento (o tinte) translúcido | Acciones secundarias que deben destacar un poco. | | `outline` | ninguno, con `border` | Barras de herramientas, acciones secundarias sobre superficies cargadas. | | `ghost` | ninguno hasta pasar el puntero (`ghost_hover`) | Barras de herramientas, menús, botones de icono. | | `subtle` | como `ghost`, con `text_muted` | Acciones de poca prioridad. | ##### Insignias ```python ui.badge("Nuevo") # "soft" (por defecto): fondo translúcido ui.badge("Pagado", tint="success", variant="solid") ui.badge("Falló", tint="error", variant="outline") ``` `ui.icon_button` también acepta `variant` (`"ghost"` por defecto), y muchos componentes tienen sus propias opciones, como `borderless=True` y `mono=True` en `ui.input`. La página de cada componente en la [Biblioteca](https://albakore.github.io/pyzed/es/library/) muestra sus variantes. #### Tintes Un **tinte** colorea un componente con uno de los [tokens de estado](https://albakore.github.io/pyzed/es/theming/tokens/#estado): `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` (y `"hint"` en los avisos): ```python ui.button("Eliminar", eliminar, variant="solid", tint="error") ui.button("Eliminar", eliminar, variant="error") # atajo: un botón tintado de error ui.callout("Guardado", "Todo está al día.", tint="success") ui.progress(0.4, tint="warning") ui.spinner(tint="info") ui.count_badge(3, tint="error") ``` Como los tintes son tokens, siguen el tema: el mismo botón rojo es un rojo más suave en un tema claro. #### Tamaños ```python ui.button("Mínimo", al_pulsar, size="xs") # 18 px de alto ui.button("Pequeño", al_pulsar, size="sm") # 22 px ui.button("Normal", al_pulsar) # "md": 28 px ui.button("Grande", al_pulsar, size="lg") ui.input(self.nombre, self.fijar_nombre, size="lg") ui.select(opciones, self.eleccion, self.elegir, size="sm") ``` #### style() en cualquier componente Todos los widgets, incluidos todos los componentes de `pyzed.ui`, tienen un método `style()` con propiedades tipo CSS ([la lista completa](https://albakore.github.io/pyzed/es/theming/styles/#propiedades)). Se aplica encima del aspecto propio del componente: ```python ui.button("Cuadrado", al_pulsar).style(radius=0, border=(2, "accent")) ui.card(cuerpo, title="Uso").style(shadow=("#0006", (0, 8), 24), radius=16) ui.label("Total").style(size=20, weight="bold", color="success") ``` `ui.button` también acepta directamente los colores de bajo nivel del botón (`bg`, `hover_bg`, `pressed_bg`, `color`), para un botón totalmente propio que sigue comportándose como un botón: ```python ui.button("Comprar", comprar, bg="#16a34a", hover_bg="#15803d", pressed_bg="#166534", color="#ffffff") ``` #### Reutilizar un aspecto: estilos con nombre Dale un nombre a un estilo una vez y úsalo en cualquier parte. Los estilos con nombre son las "clases CSS" de Pyzed: ```python pz.define_style("tarjeta", background="surface", border=(1, "border_variant"), radius=12, padding=16) pz.define_style("peligro", color="error", weight="semibold") pz.column([...]).style("tarjeta") ui.label("El pago falló").style("peligro") pz.column([...]).style("tarjeta", shadow=("#0005", (0, 4), 12)) # un nombre más propiedades extra ``` Un objeto `pz.Style` hace lo mismo sin un nombre global, y los estilos se combinan con `|` (gana el de la derecha): ```python TARJETA = pz.Style(background="surface", radius=12, padding=16) ELEVADA = TARJETA | pz.Style(shadow=("#0006", (0, 8), 24)) ``` #### Otro tema para un componente Todos los componentes de `pyzed.ui` aceptan `theme=` para dibujar solo ese componente con otro tema, y un bloque `with tema:` cambia el tema de todo lo que se construye dentro: ```python ui.button("Claro", al_pulsar, theme=pz.themes.ONE_LIGHT) with pz.themes.ONE_DARK.with_accent("#f97316"): zona_aviso = pz.column([ui.button("Ir", ir, variant="solid"), ui.progress(0.3)]) ``` #### Tus propios componentes Un [componente](https://albakore.github.io/pyzed/es/guides/components/) propio también debería tomar sus colores del tema, para encajar en cualquier tema sin cambios. Usa nombres de tokens, o lee el tema con `pz.theme.current()` en `render()`: ```python class Etiqueta(pz.Component): texto: str = "" tint: str = "accent" def render(self): t = pz.theme.current() return pz.text(self.texto, size=12).style( color=self.tint, background=pz.Paint(self.tint, opacity=0.15), radius=t.radius_sm, padding=(2, 8), ) Etiqueta(texto="beta", tint="warning") Etiqueta(texto="nuevo", style={"weight": "bold"}) # todo componente acepta también style= ``` Los componentes hechos en la vista de componentes del diseñador siguen las mismas reglas; mira [Estilo de formularios en el diseñador](https://albakore.github.io/pyzed/es/theming/forms/). **¿Cuál uso?:** Primero variante o tinte; un estilo con nombre cuando el mismo aspecto se repite; `style()` para cambios puntuales; un tema propio cuando el cambio está en todas partes (por ejemplo, esquinas cuadradas en toda la app: `radius=0` en el tema, no en cada botón). ### Estilo de formularios en el diseñador > Dale su aspecto a un formulario de Pyzed Design: tema, acento, fondo, radio de esquina y tamaño de letra, el estilo global del formulario, estilos por control, temas propios en formularios y qué guarda el archivo .pzform. Source: https://albakore.github.io/pyzed/es/theming/forms/ Los formularios hechos en [Pyzed Design](https://albakore.github.io/pyzed/es/editor/) usan los mismos temas y estilos que el código. Los fijas en el panel **Propiedades** en lugar de escribir Python, y se guardan en el archivo `.pzform`. El aspecto de un formulario se construye en capas, de lo general a lo concreto; cada capa solo cambia lo que fija: ```mermaid flowchart LR T["Tema
One Dark, Gruvbox..."] --> L["Aspecto
acento · fondo · radio · tamaño de letra"] L --> G["Estilo global
la sección Estilo del formulario"] G --> C["Estilo del control
la sección Estilo de cada control"] ``` #### 1. El tema y el aspecto del formulario Haz clic en un hueco del lienzo para seleccionar el formulario y abre **Propiedades → Aspecto**: | Campo | Qué hace | Se guarda como | | --- | --- | --- | | **Tema** | Cualquier tema registrado: los incluidos y los que registre tu código. | `"theme": "Gruvbox Dark"` | | **Acento** | Un color predefinido, **×** para el del tema o **Acento propio** con cualquier `#rrggbb`. Colorea botones principales, foco, enlaces y selección (`with_accent`). | `"accent": "#e11d48"` | | **Fondo** | El fondo de la ventana; vacío mantiene el del tema. | `"background": "#101418"` | | **Radio de esquina** | El radio que usan todos los controles; `0` para esquinas rectas. | `"radius": 0` | | **Tamaño de letra** | El tamaño base de todo el texto. | `"font_size": 15` | El tema del formulario es independiente del tema del propio editor (Ajustes → Apariencia): puedes diseñar un formulario claro en un editor oscuro. #### 2. El estilo global del formulario Con el formulario seleccionado, su sección **Estilo** es el estilo global de la ventana (lo mismo que `App.style` en código): - **Color del texto, fuente, peso, cursiva, tamaño de letra e interlineado** se aplican a todo texto que no fije los suyos. Una fuente serif aquí cambia de golpe cada etiqueta, botón y campo del formulario. - **Fondo, color del borde, radio de esquina y relleno** pasan a ser los de la ventana. #### 3. El estilo de un control Selecciona un control y usa su propia sección **Estilo**: fondo (un color, un degradado o un relleno con máscara, con el selector de color), color del texto, fuente, peso, bordes, radio de esquina, sombra, relleno, margen y alineación. Los campos vacíos mantienen el aspecto propio del componente y **Restablecer estilo** los borra todos. La página [Estilos y temas](https://albakore.github.io/pyzed/es/editor/styles-and-themes/) del diseñador describe cada campo y el selector de color. Algunos controles también tienen propiedades de **variante**, **tinte** y **tamaño**, las mismas que en código (mira [Estilo de componentes](https://albakore.github.io/pyzed/es/theming/components/)): prueba la variante de un botón antes de darle estilo a mano. **Conserva colores que siguen el tema:** El selector de color trabaja con colores concretos. Para conservar un color que cambia con el tema, escribe un token (`accent`, `text_muted`, `surface`...) en el campo junto a la muestra. Así el formulario se ve bien si más adelante lo pasas a un tema claro. #### Qué guarda el archivo .pzform Todo esto es JSON simple dentro del archivo del formulario, así que es fácil de revisar en Git o de cambiar a mano: ```json title="ui/main.pzform (extracto)" { "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": "guardar", "type": "button", "props": { "text": "Guardar" }, "style": { "radius": 0, "border_color": "accent", "border_width": 2 } } ] } ``` `style` guarda las mismas propiedades que [`style()`](https://albakore.github.io/pyzed/es/theming/styles/#propiedades), escritas como lo hace `Style.to_dict()` (un degradado se vuelve `{"type": "linear", "angle": ..., "stops": [...]}`). #### Usar tu propio tema en un formulario La lista **Tema** muestra todos los temas registrados. Para usar uno tuyo: 1. Regístralo en el archivo de código del formulario (el `.py` junto al formulario, que Pyzed carga antes de abrirlo): ```python title="ui/main.py" import pyzed as pz pz.themes.register(pz.Theme.generate("Acme", background="#16131f", text="#ece9f5", accent="#a78bfa")) def guardar_click(form, event): ... ``` 2. Escribe su nombre en el archivo del formulario (`"theme": "Acme"`), o elígelo en **Propiedades → Aspecto → Tema** cuando el diseñador lo liste. 3. Ejecuta el formulario (**Ejecutar**, o `pyzed run`). Usa el tema Acme. **Vista previa en el lienzo:** Un formulario cuyo nombre de tema no está registrado usa **One Dark**. Si el lienzo muestra One Dark en vez de tu tema, el módulo que lo registra todavía no se cargó en el diseñador; la app en ejecución usa el tema correcto en cuanto importa ese módulo. #### Cambiar el aspecto mientras el formulario corre **Aún no está soportado:** Los manejadores de un formulario pueden cambiar sus controles (`form.guardar.text = "Guardado"`, `form.estado.color = "error"`), pero no su tema: el tema se elige al abrir el formulario. Para una app cuyo tema cambia el usuario mientras corre, escribe esa ventana en código con `pz.App` (mira [Elegir un tema](https://albakore.github.io/pyzed/es/theming/themes/#cambiar-de-tema-mientras-la-app-corre)), o colorea controles concretos desde los manejadores con tokens del tema. ### Iconos y fuentes > Usa unos 2000 iconos de Lucide e iconos por tipo de archivo en GUIs de escritorio en Python con Pyzed, y carga tus propias fuentes como IBM Plex. Source: https://albakore.github.io/pyzed/es/theming/icons-fonts/ #### Iconos Pyzed incluye la fuente de iconos de [Lucide](https://lucide.dev) (~2000 iconos, licencia ISC). Los iconos se dibujan como texto, así que se ven nítidos a cualquier tamaño y aceptan cualquier color: ```python pz.icon("folder", size=16, color="#74ade8") ui.button("Abrir", self.abrir, icon="folder-open") ui.list_item("Ajustes", self.ajustes, icon="settings") ``` Sin `color`, `pz.icon` toma el color del texto que lo rodea. Los iconos dentro de los componentes de `pyzed.ui` usan los [tokens](https://albakore.github.io/pyzed/es/theming/tokens/#texto-iconos-y-acento) `icon` e `icon_muted` del tema, así que siguen el tema. Busca nombres en [lucide.dev/icons](https://lucide.dev/icons) o desde la 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 y Linux" pyzed icons git # o: python3 -m pyzed icons git ``` ```powershell title="Windows (PowerShell)" pyzed icons git # o: py -m pyzed icons git ``` `pyzed.icons.names()` devuelve la lista completa y `pyzed.icons.glyph("folder")` el carácter, para mezclarlo con texto usando `font="icons"`. ##### Buscar y elegir iconos `icons.search` busca iconos por nombre y por las etiquetas de Lucide (en inglés), los mejores primero, e `icons.category` los agrupa para explorarlos: ```python from pyzed import icons icons.search("person") # ["person-standing", "user", "contact", ...] icons.category("arrows") # todas las flechas, chevrons, deshacer... ``` Para que el usuario elija uno, usa [`ui.IconPicker`](https://albakore.github.io/pyzed/es/library/icon-picker/), o `ui.icon_picker_dialog` en una `ui.dialog_layer`: un buscador, categorías y una cuadrícula con todos los iconos. ##### Iconos por tipo de archivo ```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` y `ui.tree` ya los usan. #### Fuentes Fuentes incluidas: | Nombre | Fuente | | --- | --- | | `"sans"` (por defecto) | IBM Plex Sans | | `"mono"` | IBM Plex Mono | | `"icons"` | Lucide | `"serif"` usa la fuente serif del sistema. Elige una fuente para un texto, para un bloque (todo texto dentro la hereda) o para toda la ventana: ```python pz.text("fn main() {}").style(font="mono") # un texto pz.column([...]).style(font="serif", size=15) # todo texto dentro class MiApp(pz.App): style = pz.Style(font="mono") # toda la ventana ``` ##### Tus propias fuentes Carga un archivo `.ttf` u `.otf` antes de ejecutar la app y refiérete a él por su **nombre de familia** (el nombre que declara la fuente, no el del archivo): ```python pz.load_font("fonts/JetBrainsMono-Regular.ttf") pz.text("fn main() {}", font="JetBrains Mono") pz.code_editor("buf", texto, font="JetBrains Mono") ``` `load_font` también acepta los `bytes` del archivo. Carga cada peso que uses (Regular, Bold...) como su propio archivo. **Fuentes en una app empaquetada:** Guarda los archivos de fuentes en tu proyecto (por ejemplo `assets/fonts/`), añade la carpeta a `include` en `[tool.pyzed.build]` y resuelve la ruta desde el paquete como se muestra en [Compilar y distribuir](https://albakore.github.io/pyzed/es/packaging/build/#opciones), para que la app empaquetada las encuentre. **Licencias:** Comprueba que la licencia de una fuente permita incluirla en una app que distribuyes. IBM Plex y Lucide, que Pyzed incluye, lo permiten (SIL Open Font License e ISC). ### Recetario de temas > Temas completos y ejecutables de Pyzed para copiar: un tema de marca en claro y oscuro, alto contraste, terminal retro, vidrio oscuro, pastel y dos temas en una ventana. Source: https://albakore.github.io/pyzed/es/theming/cookbook/ Seis temas completos, listos para copiar en tu app. Cada uno es un archivo de [`examples/theming/`](https://github.com/albakore/pyzed/tree/main/examples/theming) y dibuja la misma pantalla (`preview.py`: botones en cada variante, un pequeño formulario, insignias, un aviso y dos estadísticas), así puedes comparar cómo la cambia cada tema. **Código en inglés:** Los ejemplos del repositorio están escritos en inglés, como el resto del código de Pyzed; los comentarios explican cada parte. #### Ejecutarlos Desde un clon del repositorio (mira [Ejecutar los ejemplos](https://albakore.github.io/pyzed/es/examples/)): **uv:** ```bash uv run python examples/theming/brand.py ``` **pip:** ```bash title="macOS y Linux" python3 examples/theming/brand.py ``` ```powershell title="Windows (PowerShell)" py examples\theming\brand.py ``` Cambia `brand.py` por `high_contrast.py`, `retro_terminal.py`, `glassy_dark.py`, `pastel.py` o `mixed.py`. **Copiar uno a tu proyecto:** Copia la parte del tema (todo lo que está encima de `class ...App`) a un módulo de tu proyecto, como `mitema.py`, e impórtalo antes de que arranque la app. El import de `preview` es solo para estos ejemplos. #### Tema de marca, claro y oscuro La paleta de una empresa en dos versiones que siguen el modo claro u oscuro del sistema. `Theme.generate` construye cada versión a partir de tres colores; `derive` fija el verde propio de la marca para el éxito. `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) ``` **Qué cambiar:** los tres colores de cada `generate`, y `radius` para esquinas más redondas o más rectas. #### Alto contraste Negro y blanco puros con foco amarillo, para quien necesita un contraste fuerte (y una buena prueba de cómo se ve tu app con bordes grandes y claros). `Theme.from_dict` lista cada token que cambia; el resto viene de One Dark. El script muestra la relación de contraste de los colores principales al arrancar. `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) ``` **Accesibilidad:** Un tema es una parte de la accesibilidad. Mira [Accesibilidad](https://albakore.github.io/pyzed/es/guides/accessibility/) para la navegación con teclado, los lectores de pantalla y el movimiento reducido. #### Terminal retro Fósforo verde sobre negro. Un único verde se vuelve el texto, el acento y el color de éxito; el estilo global hace que todo el texto sea monoespaciado. `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) ``` #### Vidrio oscuro Un degradado detrás de la ventana y un panel de vidrio esmerilado encima. El tema es One Dark con acento cian; el aspecto viene del estilo global (`background=` un degradado) y del efecto `glass`. `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) ``` **Efectos y GPU:** `glass`, `backdrop_blur` y `glow` están pensados para el renderizador por GPU. En equipos sin GPU (algunas máquinas virtuales y escritorios remotos) o con el renderizador por CPU (`ICED_BACKEND=tiny-skia`), pueden verse más simples que en esta captura. Asegúrate de que el texto se siga leyendo bien sin ellos. #### Pastel divertido Un fondo crema, colores de caramelo y esquinas muy redondas. `derive` fija colores pastel para los avatares (`players`), y un estilo con nombre (`pz.define_style("bubble", ...)`) dibuja la tarjeta alrededor de la pantalla. `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) ``` #### Dos temas en una ventana Una barra lateral clara en una app oscura, con un selector de temas que guarda el tema elegido en un archivo JSON y lo carga al siguiente arranque (`Theme.save` y `Theme.load`). with tema: dibuja la barra lateral con 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) ``` #### La vista de prueba compartida `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) ``` Cada color que usa es un token o un tinte; por eso un mismo archivo se ve bien en seis temas muy distintos. Escribe tus propias vistas de la misma forma. ## Distribución ### Distribuye tu app > Empaqueta una app Pyzed para Windows, macOS y Linux: un ejecutable independiente con Python dentro, creado con pyzed build, más iconos, firma, instaladores, compilaciones en CI y autoactualización. Source: https://albakore.github.io/pyzed/es/packaging/ Cuando tu app funciona en tu equipo, `pyzed build` (o **Archivo → Exportar aplicación…** en [Pyzed Design](https://albakore.github.io/pyzed/es/editor/exporting/)) la empaqueta con PyInstaller o Nuitka: Python, Pyzed, tus dependencias, tu código y tus formularios van a una carpeta (o un archivo) que funciona por sí sola. Quien la use no necesita Python, pip ni nada más. Ejecútalo desde la raíz de tu [proyecto](https://albakore.github.io/pyzed/es/getting-started/new-project/): **uv:** ```bash title="Windows, macOS y Linux" uv run pyzed build ``` **pip:** ```bash title="macOS y Linux" source .venv/bin/activate pyzed build # o: python -m pyzed build ``` ```powershell title="Windows (PowerShell)" .venv\Scripts\Activate.ps1 pyzed build # o: py -m pyzed build ``` | Compilas en | Obtienes | La gente la ejecuta | | --- | --- | --- | | Windows | `dist\Shop\Shop.exe` | Con doble clic, desde un zip o un instalador | | macOS | `dist/Shop.app` | Abriéndola (fírmala y notarízala para compartirla) | | Linux | `dist/Shop/Shop` | Con `./Shop`, o desde el menú de aplicaciones | #### De tu proyecto a tus usuarios ```mermaid flowchart LR P["Tu proyecto
pyproject.toml · ui/ · src/"] --> B["pyzed build
(PyInstaller o Nuitka)"] B --> W["Windows
Shop.exe"] B --> M["macOS
Shop.app"] B --> L["Linux
Shop/Shop"] W --> S["Firmar · comprimir · instalador"] M --> S L --> S S --> R["GitHub Release"] R -. "pyzed.updater" .-> U["Apps instaladas
se actualizan solas"] ``` **Una compilación por plataforma:** Una compilación solo funciona en el sistema donde se hizo: compila la app de Windows en Windows, la de macOS en macOS y la de Linux en Linux. Un [flujo de CI](https://albakore.github.io/pyzed/es/packaging/ci/) puede hacer las tres por ti. #### En esta sección pyproject.toml, PyInstaller o Nuitka, archivos de datos y la pantalla de inicio.', href: '/pyzed/es/packaging/build/', icon: 'rocket', tag: 'Empieza aquí' }, { title: 'Exportar desde el diseñador', text: 'Archivo → Exportar aplicación… en Pyzed Design: plataformas, empaquetador, icono y pantalla de inicio en un solo diálogo.', href: '/pyzed/es/editor/exporting/', icon: 'pencil' }, { title: 'Windows', text: 'El .exe, carpeta o archivo único, un zip o un instalador de Inno Setup, y firma para SmartScreen.', href: '/pyzed/es/packaging/windows/', icon: 'seti:windows' }, { title: 'macOS', text: 'El paquete .app, identificador de paquete, firma, notarización y un .dmg.', href: '/pyzed/es/packaging/macos/', icon: 'apple' }, { title: 'Linux', text: 'En qué distribución compilar, archivos comprimidos, entradas de menú, y AppImage o .deb.', href: '/pyzed/es/packaging/linux/', icon: 'linux' }, { title: 'Iconos y metadatos', text: 'Nombre, versión, editor, los iconos creados a partir de un PNG y el identificador de paquete de macOS.', href: '/pyzed/es/packaging/icons-metadata/', icon: 'star' }, { title: 'CI con GitHub Actions', text: 'Compila los tres sistemas en cada etiqueta, publica una versión y firma con secretos.', href: '/pyzed/es/packaging/ci/', icon: 'github' }, { title: 'Apps que se actualizan solas', text: 'Busca en GitHub Releases, descarga con suma de verificación y cambia la app al reiniciar.', href: '/pyzed/es/packaging/updates/', icon: 'random' }, { title: 'Solución de problemas', text: 'Módulos que faltan, ventanas en blanco, apps bloqueadas, errores de glibc y mensajes de actualización.', href: '/pyzed/es/packaging/troubleshooting/', icon: 'warning' }, { title: 'Referencia de la CLI', text: 'Todas las opciones de pyzed build y de los demás comandos.', href: '/pyzed/es/reference/cli/', icon: 'list-format' }, ]} /> #### Qué está soportado hoy | | Windows | macOS | Linux | | --- | --- | --- | --- | | Carpeta de la app (`pyzed build`) | ✅ `.exe` | ✅ `.app` | ✅ ejecutable | | Archivo único (`--onefile`) | ✅ | ✅ | ✅ | | PyInstaller o Nuitka (`--backend`) | ✅ | ✅ | ✅ | | x64 y ARM | ✅ | ✅ | ✅ | | Icono a partir de un PNG (`icon =`) | ✅ `.ico` + ventana | ✅ `.icns` | ✅ ventana; menú con un archivo `.desktop` | | Datos del archivo (versión, editor...) | ✅ | ✅ `Info.plist` | - | | Pantalla de inicio | ✅ | ✅ | ✅ | | Flujo de CI (`--workflow`) y archivos comprimidos para releases | ✅ | ✅ | ✅ | | Autoactualización (`pyzed.updater`) | ✅ | ✅ | ✅ | | Firma | Con `signtool` | Con `codesign` + notarización | No hace falta | | Instalador | El tuyo (Inno Setup, WiX) | Tu propio `.dmg` (`hdiutil`) | El tuyo (archivo, script, AppImage...) | | Compilación cruzada | ❌ | ❌ | ❌ | **Estado de esta sección:** El empaquetado para escritorio está listo para usarse hoy. Los instaladores, la firma y los canales de actualización se documentan con las herramientas que usa el propio Pyzed Design, pero `pyzed build` todavía no las ejecuta por ti. Las plataformas móvil y web están planificadas para el futuro y aún no están disponibles. ### Cómo funciona la compilación > Cómo empaqueta pyzed build un proyecto Pyzed como app independiente con PyInstaller o Nuitka: qué se incluye, cada opción de pyproject.toml, iconos, pantalla de inicio, archivos de datos y el resultado en cada plataforma. Source: https://albakore.github.io/pyzed/es/packaging/build/ `pyzed build` convierte un [proyecto Pyzed](https://albakore.github.io/pyzed/es/guides/projects/) en una app que funciona en equipos sin Python: una carpeta (o un único archivo) con el intérprete de Python, `pyzed-ui`, tus dependencias, el código de tu backend y tus formularios. Usa [PyInstaller](https://pyinstaller.org) o [Nuitka](https://nuitka.net) por debajo, dentro del entorno del propio proyecto, así que no hay que instalar nada de forma global. **Desde el diseñador:** Pyzed Design hace todo esto desde un diálogo: **Archivo → Exportar aplicación…** ajusta cada opción de abajo, muestra el icono y la pantalla de inicio, compila con la salida en la consola y escribe el workflow de CI. Consulta [Exportar tu aplicación](https://albakore.github.io/pyzed/es/editor/exporting/). #### Compilar en tres pasos 1. Abre una terminal en la carpeta del proyecto (la que tiene `pyproject.toml`). Si todavía no tienes un proyecto, créalo con `pyzed new` (mira [Crear un proyecto](https://albakore.github.io/pyzed/es/getting-started/new-project/)). 2. Compila: **uv:** ```bash title="Windows, macOS y Linux" uv run pyzed build ``` **pip:** ```bash title="macOS y Linux" source .venv/bin/activate pyzed build # o: python -m pyzed build ``` ```powershell title="Windows (PowerShell)" .venv\Scripts\Activate.ps1 pyzed build # o: py -m pyzed build ``` 3. Abre el resultado en `dist/`: | Plataforma | Resultado | Cómo se ejecuta | | --- | --- | --- | | Windows | `dist\Shop\Shop.exe` (o `dist\Shop.exe` con `--onefile`) | Doble clic. | | macOS | `dist/Shop.app` | Ábrela desde Finder. | | Linux | `dist/Shop/Shop` (o `dist/Shop` con `--onefile`) | `./dist/Shop/Shop` | **Hace falta uv, incluso con pip:** `pyzed build` ejecuta PyInstaller con `uv run --with pyinstaller` (y Nuitka con `uv run --with nuitka`), así que [uv](https://docs.astral.sh/uv/) tiene que estar instalado aunque uses pip para todo lo demás. No necesitas instalar PyInstaller ni Nuitka. Instala uv con `pip install uv` o desde su web. **Una compilación por plataforma:** Ni PyInstaller ni Nuitka hacen compilación cruzada: compila la app de Windows en Windows, la de macOS en macOS y la de Linux en Linux, cada una en la arquitectura a la que va dirigida (x64 o ARM). Un [flujo de CI](https://albakore.github.io/pyzed/es/packaging/ci/) las compila todas por ti. #### Qué pasa al compilar ```mermaid flowchart LR P["pyproject.toml
[tool.pyzed.build]"] --> B["pyzed build"] U["ui/ formularios + manejadores"] --> B S["src/ backend"] --> B B --> L["script lanzador
build/pyzed/"] B --> I["PyInstaller o Nuitka
(uv run --with ...)"] L --> I I --> D["dist/
carpeta de la app, .exe o .app"] ``` 1. `pyzed build` lee `[tool.pyzed]` y `[tool.pyzed.build]` de `pyproject.toml`. 2. Escribe un pequeño **lanzador** (`build/pyzed/_launcher.py`) que abre el formulario principal (`[tool.pyzed] main`) desde dentro del paquete, con el icono de la ventana y la pantalla de inicio. 3. Crea los archivos de icono (`.ico`, `.icns`, `.png`) a partir de tu imagen, y el archivo de versión de Windows. 4. Analiza los `.py` de la carpeta de la interfaz con el módulo `ast` de Python para saber qué importan. 5. Ejecuta el empaquetador en el entorno del proyecto con las opciones adecuadas, y la app queda en `dist/`. Por último completa el `Info.plist` de macOS y comprime el archivo, si lo pediste. ##### Qué se incluye Los formularios cargan sus manejadores desde archivos `.py` al abrirse, así que PyInstaller no puede seguir sus imports por sí solo. `pyzed build` completa lo que falta: - **La carpeta de la interfaz** se incluye como datos, con su estructura (`ui/main.pzform`, `ui/main.py`, módulos auxiliares...). - **Cada módulo que importa la interfaz** se pasa como import oculto, así que el backend en `src/`, los módulos junto a `pyproject.toml` y los paquetes de terceros se incluyen con todas sus dependencias. - **El paquete propio del proyecto** (`src/`) se incluye entero, también los módulos importados dinámicamente. - **Pyzed** con sus archivos de datos (temas, iconos, plantillas). - `ui/`, `src/` y la raíz del proyecto se añaden a la ruta de búsqueda de PyInstaller. El código de fuera del proyecto, como una biblioteca compartida en una carpeta vecina, se incluye como cualquier otra dependencia una vez instalado en el entorno: ```bash pyzed add --editable ../facturacion # luego `from facturacion import factura` en un manejador ``` **Apps escritas en código:** `pyzed build` arranca el **formulario** principal del proyecto. Para una app escrita totalmente en código (una clase `pz.App` y `pz.run(...)`), mira [Apps escritas en código](#apps-escritas-en-código) más abajo. #### Opciones Las opciones de la línea de comandos ganan a `[tool.pyzed.build]` de `pyproject.toml`: | Opción | Efecto | | --- | --- | | `--backend nuitka` | Compila con Nuitka en lugar de PyInstaller (mira [Elegir un empaquetador](#elegir-un-empaquetador)). | | `--onefile` | Un único ejecutable en lugar de una carpeta. | | `--console` | Mantiene la ventana de consola (útil para ver los tracebacks). | | `--name Shop` | Nombre del ejecutable o del paquete. | | `--archive zip` | Además comprime la app en un `zip` (o `tar.gz`) con el nombre que busca el [actualizador](https://albakore.github.io/pyzed/es/packaging/updates/). | | `--target linux-x64` | Comprueba que este equipo compila ese destino (lo usa la CI); no hace compilación cruzada. | | `--dry-run` | Muestra el comando del empaquetador y se detiene. | | `--workflow` | Escribe un flujo de GitHub Actions para los `targets` del proyecto en lugar de compilar (mira [CI](https://albakore.github.io/pyzed/es/packaging/ci/)). | Toda la configuración del proyecto está en `pyproject.toml` (el [diálogo Exportar](https://albakore.github.io/pyzed/es/editor/exporting/) de Pyzed Design escribe la misma tabla): ```toml title="pyproject.toml" [tool.pyzed.build] name = "Shop" # nombre del ejecutable / paquete (por defecto: el del proyecto) backend = "pyinstaller" # o "nuitka" onefile = false # un único archivo autoextraíble en lugar de una carpeta windowed = true # sin ventana de consola en Windows y macOS icon = "assets/icon.png" # una imagen cuadrada; de ella salen el .ico y el .icns version = "1.2.0" # por defecto: la versión de [project] publisher = "Acme" # la empresa en las propiedades del archivo en Windows identifier = "com.acme.shop" # id del paquete en macOS e id de la app en Linux copyright = "© 2026 Acme" optimize = 0 # 1 quita los assert, 2 también los docstrings slim = true # deja fuera tkinter, test, idlelib... (las apps de Pyzed no los usan) exclude = ["numpy.tests"] # más módulos que dejar fuera include = ["assets", "data/precios.csv"] # archivos y carpetas extra, con la misma ruta relativa hidden-imports = ["shop.plugins.csv"] # módulos importados con importlib o por nombre collect = ["babel"] # paquetes a incluir enteros: código, datos y binarios archive = "zip" # además comprime la app: shop-windows-x86_64.zip... targets = ["windows-x64", "macos-arm64", "linux-x64"] # lo que compila el flujo de CI args = [] # cualquier otra cosa, pasada al empaquetador tal cual [tool.pyzed.build.splash] image = "assets/splash.png" duration = 1.5 # segundos, como mínimo text = "Cargando…" native = true # también la pantalla propia del empaquetador mientras se descomprime [tool.pyzed.build.nuitka] # opciones solo para un empaquetador ([tool.pyzed.build.pyinstaller] también) args = ["--lto=yes"] ``` | Ajuste | Por defecto | Notas | | --- | --- | --- | | `name` | el nombre del proyecto | También el nombre de la carpeta en `dist/`. | | `backend` | `"pyinstaller"` | `"nuitka"` compila a C: mira más abajo. | | `onefile` | `false` | Un único archivo arranca más lento (se descomprime cada vez) y no puede [actualizarse solo](https://albakore.github.io/pyzed/es/packaging/updates/). | | `windowed` | `true` | `false` (o `--console`) muestra una terminal junto a la app. | | `icon` | ninguno | Un PNG se convierte en `.ico`, `.icns` y el icono de la ventana; mira [Iconos y metadatos](https://albakore.github.io/pyzed/es/packaging/icons-metadata/). | | `version`, `publisher`, `identifier`, `description`, `copyright` | de `[project]` | Se escriben en las propiedades del archivo en Windows y en el `Info.plist` de macOS. | | `splash` | ninguna | Una [pantalla de inicio](#pantalla-de-inicio). | | `optimize` | `0` | El `-O` (`1`) o `-OO` (`2`) de Python. | | `slim` | `true` | Deja fuera `tkinter`, `test`, `idlelib`, `lib2to3`, `ensurepip`, `pydoc_data`, `turtle` y `turtledemo`. | | `strip`, `upx` | `true`, `false` | Quita los símbolos de depuración (Linux y macOS); comprime con [UPX](https://upx.github.io/) si está instalado. | | `exclude` | `[]` | Módulos que dejar fuera. | | `include` | `[]` | Rutas relativas a la raíz del proyecto. | | `hidden-imports` | `[]` | Módulos que el empaquetador no puede ver. | | `collect` | `[]` | Paquetes con archivos de datos o plugins. | | `archive` | ninguno | `"zip"` o `"tar.gz"`. | | `targets` | este equipo | Para `--workflow`: `windows-x64`, `windows-arm64`, `macos-arm64`, `macos-x64`, `linux-x64`, `linux-arm64`. | | `args` | `[]` | Cualquier otra opción del empaquetador ([PyInstaller](https://pyinstaller.org/en/stable/usage.html), [Nuitka](https://nuitka.net/user-documentation/)). | #### Elegir un empaquetador | | PyInstaller | Nuitka | | --- | --- | --- | | Cómo | Empaqueta tus `.py` tal cual, junto a Python | Compila tu código y sus dependencias a C y luego a código máquina | | Tiempo de compilación | Segundos | Minutos | | Velocidad | Python normal | Compilado: código Python puro más rápido | | Tamaño de un archivo único | Mayor | Alrededor de un 15 % menor (mejor compresión) | | Tamaño de una carpeta | Menor | Mayor (el código compilado ocupa más que los `.pyc`) | | Necesita | Nada (uv lo instala) | Un compilador de C | Medido con una app de un formulario en Linux x64: PyInstaller creó una carpeta de 57 MB o un archivo único de 25 MB en unos 25 segundos; Nuitka una carpeta de 72 MB o un archivo único de 21 MB en unos 3 minutos. Casi todo el tamaño es el motor de Pyzed (unos 33 MB) y el propio Python, así que `slim`, `exclude` y `strip` importan más que el empaquetador. Empieza con PyInstaller mientras desarrollas y prueba Nuitka para las versiones publicadas. Nuitka necesita un compilador de C: en Linux `gcc` (`sudo apt install gcc`), en macOS las herramientas de línea de comandos de Xcode (`xcode-select --install`), en Windows las Build Tools de Visual Studio, o Nuitka descarga un compilador la primera vez. `pyzed build` añade por su cuenta `patchelf` (Linux) y `zstandard` (archivos únicos). #### Pantalla de inicio Con `[tool.pyzed.build.splash]`, la app abre antes del formulario principal una ventana sin marco con tu imagen, centrada en la pantalla. Se queda al menos `duration` segundos y hasta que el formulario principal ha cargado, y luego se cierra. `text` se escribe sobre la parte inferior de la imagen. Con `native = true` también se muestra la pantalla propia del empaquetador antes de que arranque Python (PyInstaller en Windows y Linux, que necesita `tkinter` en el Python del proyecto; Nuitka para archivos únicos en Windows), útil en apps de un solo archivo que tardan un momento en descomprimirse. La misma pantalla funciona en una app escrita en código, con `App.splash`: ```python import pyzed as pz class Shop(pz.App): splash = pz.Splash("assets/splash.png", duration=1.5, text="Cargando…") ``` ##### Encontrar tus archivos de datos Los archivos de `include` terminan bajo la raíz del paquete, con la misma ruta relativa. Encuéntralos desde tu código con: ```python import sys from pathlib import Path RAIZ = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parents[1])) precios = RAIZ / "data" / "precios.csv" ``` `sys._MEIPASS` es la carpeta del paquete en una app de PyInstaller. En una app de Nuitka y mientras desarrollas se usa la alternativa: desde un manejador de `ui/` apunta a la raíz del paquete (o del proyecto); ajusta `parents[1]` según dónde esté el archivo que ejecuta esto. **No escribas junto a la app:** La carpeta de la app puede ser de solo lectura una vez instalada (por ejemplo en `/Applications` o `Program Files`). Guarda los datos del usuario en sus carpetas: el paquete `platformdirs` (`pyzed add platformdirs`) da el sitio correcto en cada sistema con `platformdirs.user_data_dir("Shop")`. ##### Dónde van los archivos Los archivos intermedios van a `build/pyzed/` y el resultado a `dist/`; ambos están en el `.gitignore` de los proyectos nuevos. Cada compilación empieza limpia, así que no hay que borrar nada a mano. #### Apps escritas en código `pyzed build` arranca el **formulario** principal del proyecto. Para una app escrita totalmente en código (una clase `pz.App` y `pz.run(...)`), ejecuta tú mismo PyInstaller sobre tu script, incluyendo los datos de Pyzed. `pyzed build --dry-run` muestra el comando exacto que ejecutaría `pyzed build`, que puedes adaptar: **uv:** ```bash uv run --with pyinstaller pyinstaller main.py --name Shop --windowed --collect-data pyzed --collect-submodules pyzed ``` **pip:** ```bash title="macOS y 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 ``` Con Nuitka: ```bash uv run --with nuitka --with patchelf python -m nuitka --mode=standalone --include-package-data=pyzed main.py ``` #### Siguiente - Por plataforma: [Windows](https://albakore.github.io/pyzed/es/packaging/windows/), [macOS](https://albakore.github.io/pyzed/es/packaging/macos/), [Linux](https://albakore.github.io/pyzed/es/packaging/linux/). - [Compilar todas las plataformas en CI](https://albakore.github.io/pyzed/es/packaging/ci/) y [publicar actualizaciones](https://albakore.github.io/pyzed/es/packaging/updates/). - ¿Algo falla? [Solución de problemas](https://albakore.github.io/pyzed/es/packaging/troubleshooting/). ### Compilar en Windows > Empaqueta una app Pyzed para Windows: crea un .exe con pyzed build, añade un icono, distribúyela en un zip o con un instalador hecho con Inno Setup, y fírmala con signtool para evitar los avisos de SmartScreen. Source: https://albakore.github.io/pyzed/es/packaging/windows/ En Windows, `pyzed build` crea una carpeta con `Shop.exe` y todo lo que necesita. Puedes compartir esa carpeta en un zip o envolverla en un instalador. #### Crear el .exe 1. Instala [uv](https://docs.astral.sh/uv/) si no lo tienes (PowerShell): ```powershell powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` 2. En la carpeta del proyecto, compila: **uv:** ```powershell uv run pyzed build ``` **pip:** ```powershell .venv\Scripts\Activate.ps1 pyzed build # o: py -m pyzed build ``` 3. Pruébala: doble clic en `dist\Shop\Shop.exe`, o desde PowerShell: ```powershell .\dist\Shop\Shop.exe ``` El resultado no tiene ventana de consola (`windowed = true`). Mientras pruebas, compila con `--console` para ver los errores y la salida de `print` en una terminal junto a la app. #### Carpeta o archivo único | | Carpeta (por defecto) | `--onefile` | | --- | --- | --- | | Resultado | `dist\Shop\Shop.exe` más una carpeta `_internal` | `dist\Shop.exe` | | Arranque | Rápido | Más lento: se descomprime en una carpeta temporal cada vez | | Antivirus | Rara vez lo marca | Lo marca más a menudo (falsos positivos) | | Autoactualización | Soportada ([Actualizaciones](https://albakore.github.io/pyzed/es/packaging/updates/)) | No soportada | Prefiere la compilación en carpeta y entrega un zip o un instalador. #### El icono Windows usa un archivo `.ico`. Ponlo en el proyecto y apunta a él: ```toml title="pyproject.toml" [tool.pyzed.build] icon = "assets/icon.ico" ``` Mira [Iconos y metadatos](https://albakore.github.io/pyzed/es/packaging/icons-metadata/) para crear un `.ico` a partir de un PNG y para fijar la versión que se ve en las propiedades del archivo. #### Compartirla ##### En un zip La forma más sencilla: comprime la carpeta de la app y comparte el zip. La gente lo descomprime donde quiera y ejecuta `Shop.exe`. ```powershell Compress-Archive -Path dist\Shop -DestinationPath Shop-windows-x86_64.zip ``` Llámalo `-windows-.zip` si piensas [actualizar la app sola](https://albakore.github.io/pyzed/es/packaging/updates/). ##### Con un instalador Un instalador añade la app al menú Inicio, puede crear un acceso directo en el escritorio y registra un desinstalador. **pyzed build todavía no lo crea:** `pyzed build` crea la carpeta de la app, no un programa de instalación. Crea el instalador con otra herramienta como [Inno Setup](https://jrsoftware.org/isinfo.php) (gratuita) o [WiX](https://wixtoolset.org/), a partir de la carpeta de `dist\`. Pyzed Design se instala así: su script de Inno Setup es [`installer/windows/pyzed-design.iss`](https://github.com/albakore/pyzed/blob/main/installer/windows/pyzed-design.iss) y es una buena plantilla. Instala **solo para el usuario actual, sin permisos de administrador**, en `%LOCALAPPDATA%\Programs\`, lo que mantiene la carpeta con permiso de escritura para que la app pueda actualizarse sola. Un script mínimo: ```ini title="installer/shop.iss" #define AppName "Shop" #define AppExe "Shop.exe" [Setup] ; Genera tu propio AppId una vez (Tools -> Generate GUID en Inno Setup) y no lo cambies nunca. 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 ``` Compílalo (Inno Setup 6 instala `ISCC.exe`; también sirve `choco install innosetup`): ```powershell & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer\shop.iss ``` Con `PrivilegesRequired=lowest`, `{autopf}` es la carpeta de programas del propio usuario. #### Firmarla Windows SmartScreen avisa de los ejecutables sin firmar ("Windows protegió tu PC"). Las apps sin firmar se pueden ejecutar igual (**Más información → Ejecutar de todas formas**), pero firmar con un certificado de firma de código quita el aviso con el tiempo. ```powershell signtool sign /f certificado.pfx /p $env:CERT_PASSWORD /fd sha256 ` /tr http://timestamp.digicert.com /td sha256 dist\Shop\Shop.exe ``` `signtool.exe` viene con el Windows SDK. Firma el `.exe` antes de comprimir o crear el instalador, y luego firma también el `.exe` del instalador. La [página de CI](https://albakore.github.io/pyzed/es/packaging/ci/#firmar-en-ci) muestra cómo guardar el certificado en un secreto de GitHub. **Reputación en SmartScreen:** Incluso las apps firmadas pueden mostrar el aviso al principio: SmartScreen construye la reputación de cada certificado a medida que la gente descarga y ejecuta tu app. Los certificados de validación extendida (EV) empiezan con más reputación. #### A tener en cuenta - **Arquitectura:** la app se compila para la arquitectura del Python que ejecuta la compilación (normalmente x86-64). Una compilación x86-64 también funciona en Windows sobre ARM mediante emulación. - **Rutas:** las rutas de `pyproject.toml` usan barras normales (`assets/icon.ico`); funciona en todos los sistemas. - **Antivirus:** si un antivirus pone la app en cuarentena, prefiere la compilación en carpeta, fírmala e informa del falso positivo al fabricante. ### Compilar en macOS > Empaqueta una app Pyzed para macOS: crea un paquete .app con pyzed build, fija su icono e identificador de paquete, fírmala con tu Developer ID, notarízala y distribúyela en un .dmg. Source: https://albakore.github.io/pyzed/es/packaging/macos/ En macOS, `pyzed build` crea un **paquete de app**, `dist/Shop.app`: una carpeta que Finder muestra como una sola app, con Python, Pyzed y tu código dentro. #### Crear la .app 1. Instala [uv](https://docs.astral.sh/uv/) si no lo tienes: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` 2. En la carpeta del proyecto, compila: **uv:** ```bash uv run pyzed build ``` **pip:** ```bash source .venv/bin/activate pyzed build # o: python -m pyzed build ``` 3. Pruébala: ```bash open dist/Shop.app ``` **Apple silicon e Intel:** La app se compila para la arquitectura del Python que ejecuta la compilación: `arm64` en Apple silicon, `x86_64` en Macs Intel. Compila en cada tipo de Mac (GitHub ofrece ambos, mira [CI](https://albakore.github.io/pyzed/es/packaging/ci/)) para soportar los dos. `pyzed build` no prepara compilaciones universales (las dos en una). #### Icono e identificador de paquete macOS usa un icono `.icns`. Cada app de Mac tiene además un **identificador de paquete**, un dominio invertido como `com.acme.shop` que macOS usa para guardar sus ajustes y permisos. Fija ambos en `pyproject.toml`; el identificador llega a PyInstaller con `args`: ```toml title="pyproject.toml" [tool.pyzed.build] icon = "assets/icon.icns" args = ["--osx-bundle-identifier", "com.acme.shop"] ``` Mira [Iconos y metadatos](https://albakore.github.io/pyzed/es/packaging/icons-metadata/) para crear un `.icns` a partir de un PNG. #### Firmar y notarizar **Gatekeeper** revisa las apps descargadas de internet. Una app sin firmar ni notarizar queda bloqueada ("No se puede abrir Shop porque Apple no puede comprobar si contiene software malicioso"). La gente puede abrirla igual con clic derecho → **Abrir**, o en **Ajustes del Sistema → Privacidad y seguridad → Abrir igualmente**, pero para una versión real, fírmala y notarízala. Necesitas una membresía del [Apple Developer Program](https://developer.apple.com/programs/), un certificado **Developer ID Application** en tu llavero y una [contraseña específica de app](https://support.apple.com/es-es/102654) para tu Apple ID. 1. Firma el paquete con el runtime reforzado y una marca de tiempo: ```bash codesign --force --deep --options runtime --timestamp \ --sign "Developer ID Application: Tu Nombre (TEAMID)" dist/Shop.app ``` 2. Comprímelo con `ditto` (conserva los enlaces simbólicos y la firma del paquete) y envíalo al servicio de notarización de Apple: ```bash ditto -c -k --keepParent dist/Shop.app Shop.zip xcrun notarytool submit Shop.zip --wait \ --apple-id tu@ejemplo.com --team-id TEAMID --password "$APPLE_APP_PASSWORD" ``` 3. Grapa el ticket a la app, para que abra incluso sin conexión: ```bash xcrun stapler staple dist/Shop.app ``` Comprueba el resultado con `spctl --assess --verbose dist/Shop.app` ("accepted, source=Notarized Developer ID"). **Firma después de cada cambio:** Cambiar cualquier cosa dentro de la `.app` después de firmarla (incluso añadir un archivo) rompe la firma. Firma al final, y luego comprime o crea la imagen de disco. #### Una imagen de disco (.dmg) Los usuarios de Mac esperan un `.dmg` con la app y un acceso directo a Aplicaciones para arrastrarla. **pyzed build todavía no lo crea:** `pyzed build` crea la `.app`, no la imagen de disco. Créala con `hdiutil`, que viene con macOS, como se muestra abajo. ```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: Tu Nombre (TEAMID)" Shop-macos-arm64.dmg ``` Notariza y grapa el `.dmg` igual que el zip de arriba. Así se crea el `.dmg` de Pyzed Design (mira `macos_dmg` en [`scripts/build_designer.py`](https://github.com/albakore/pyzed/blob/main/scripts/build_designer.py)). #### A tener en cuenta - **Dónde vive:** la gente arrastra la app a `/Applications`, donde pueden escribir los usuarios administradores, así que la app puede [actualizarse sola](https://albakore.github.io/pyzed/es/packaging/updates/) ahí. - **Cuarentena:** una app descargada lleva una marca de cuarentena hasta que el primer arranque pasa Gatekeeper. Para tus propias pruebas puedes quitarla con `xattr -dr com.apple.quarantine dist/Shop.app`; no se lo pidas a tus usuarios. - **Permisos:** si la app necesita la cámara, el micrófono u otros recursos protegidos, macOS pregunta al usuario. Añade las descripciones de uso al `Info.plist` del paquete con las opciones del archivo spec de PyInstaller; `pyzed build` no las fija por ti. ### Compilar en Linux > Empaqueta una app Pyzed para Linux: compila con pyzed build, elige en qué distribución compilar por la compatibilidad de glibc, distribúyela en un tarball o zip con una entrada .desktop, y qué pasa con AppImage, .deb y Flatpak. Source: https://albakore.github.io/pyzed/es/packaging/linux/ En Linux, `pyzed build` crea una carpeta, `dist/Shop/`, con un ejecutable llamado `Shop` y todo lo que necesita. No hace falta firmarla. #### Compilar 1. Instala [uv](https://docs.astral.sh/uv/) si no lo tienes: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` 2. En la carpeta del proyecto, compila: **uv:** ```bash uv run pyzed build ``` **pip:** ```bash source .venv/bin/activate pyzed build # o: python -m pyzed build ``` 3. Pruébala: ```bash ./dist/Shop/Shop ``` #### En qué distribución compilar La app enlaza con la biblioteca de C (glibc) del sistema donde se compiló, y funciona en esa versión **o en una más nueva**. Compila en la distribución **más antigua** que quieras soportar. Por ejemplo, una app compilada en Ubuntu 22.04 funciona en Ubuntu 22.04, 24.04 y posteriores, y en otras distribuciones con glibc 2.35 o más nueva, pero no en Ubuntu 20.04. **Compilar en un contenedor:** Para compilar para sistemas antiguos desde uno nuevo, compila dentro de un contenedor de la distribución antigua (por ejemplo `docker run -it -v "$PWD":/app -w /app ubuntu:22.04`), instala uv allí y ejecuta `uv run pyzed build`. Los wheels de Pyzed necesitan glibc 2.28 o más nueva (`manylinux_2_28`), así que esa es la base más antigua posible. Alpine y otros sistemas basados en musl necesitan una compilación hecha en ellos. #### Qué necesita el equipo de destino La app lleva Python y Pyzed consigo, pero usa las bibliotecas gráficas y de ventanas del sistema: una sesión de escritorio con X11 o Wayland, `libxkbcommon` y un driver de Vulkan u OpenGL (Mesa en la mayoría de sistemas). Todas las distribuciones de escritorio los tienen; las imágenes mínimas de servidor puede que no. #### Compartirla ##### En un archivo comprimido La forma más sencilla: un archivo comprimido de la carpeta. La gente lo descomprime donde quiera y ejecuta `./Shop/Shop`. ```bash tar -czf Shop-linux-x86_64.tar.gz -C dist Shop # o un zip, que es lo que usa la autoactualización: (cd dist && zip -qr ../Shop-linux-x86_64.zip Shop) ``` Llama al zip `-linux-.zip` si piensas [actualizar la app sola](https://albakore.github.io/pyzed/es/packaging/updates/). ##### Añadirla al menú de aplicaciones Un archivo `.desktop` hace que la app aparezca en el menú de aplicaciones del escritorio, con su icono. Instálalo por usuario, sin root: ```ini title="~/.local/share/applications/com.acme.shop.desktop" [Desktop Entry] Type=Application Name=Shop Exec=/home/tu/.local/share/shop/Shop Icon=/home/tu/.local/share/shop/icon.png Categories=Office; Terminal=false ``` El instalador de Pyzed Design para Linux hace exactamente esto: descarga el zip de la versión, lo comprueba con el `SHA256SUMS` de la versión, lo descomprime en `~/.local/share/pyzed-design`, enlaza un comando en `~/.local/bin` y añade la entrada al menú. Su script, [`docs/public/install.sh`](https://github.com/albakore/pyzed/blob/main/docs/public/install.sh), es una buena plantilla para tu propio instalador `curl ... | sh`. ##### AppImage, .deb, .rpm, Flatpak, Snap **pyzed build todavía no los crea:** `pyzed build` solo crea la carpeta de la app. Empaquetarla como AppImage, `.deb`/`.rpm` o Flatpak/Snap se hace con las herramientas propias de esos formatos, a partir de `dist/Shop/`: - **AppImage**: pon la carpeta en un `AppDir` con un `AppRun` que arranque `Shop`, un archivo `.desktop` y un icono, y ejecuta `appimagetool`. - **.deb / .rpm**: herramientas como [fpm](https://fpm.readthedocs.io) convierten una carpeta en cualquiera de los dos: `fpm -s dir -t deb -n shop -v 1.0.0 dist/Shop/=/opt/shop/`. - **Flatpak / Snap**: escribe un manifiesto que copie la carpeta y declare los permisos de gráficos y pantalla. Pyzed no prueba estas recetas. #### A tener en cuenta - **Arquitectura:** la app se compila para la máquina donde se compila: `x86_64`, o `aarch64` en ARM (GitHub también ofrece máquinas ARM, mira [CI](https://albakore.github.io/pyzed/es/packaging/ci/)). - **Sin GPU:** en máquinas virtuales sin GPU, arranca la app con el renderizador por CPU si la ventana queda en blanco; mira [Solución de problemas](https://albakore.github.io/pyzed/es/packaging/troubleshooting/). - **Bit de ejecución:** los zip creados en Windows pierden el bit de ejecución. Compila y comprime las apps de Linux en Linux. ### Iconos y metadatos > Dale a una app Pyzed empaquetada su nombre, icono (.ico, .icns y el icono de la ventana a partir de un PNG), versión, editor, identificador de paquete y propiedades del archivo en Windows, macOS y Linux. Source: https://albakore.github.io/pyzed/es/packaging/icons-metadata/ La identidad de una app son unas pocas cosas: su **nombre**, su **icono**, su **versión**, quién la **publica** y, en macOS, su **identificador de paquete**. Todas se fijan en `[tool.pyzed.build]` de `pyproject.toml`, a mano o desde el [diálogo Exportar](https://albakore.github.io/pyzed/es/editor/exporting/#general) de Pyzed Design, y `pyzed build` las escribe donde las busca cada sistema. #### Nombre, versión y datos ```toml title="pyproject.toml" [tool.pyzed.build] name = "Shop" # Shop.exe, Shop.app, dist/Shop/ version = "1.2.0" # por defecto: la versión de [project] publisher = "Acme" identifier = "com.acme.shop" # por defecto: com.. description = "Punto de venta" # por defecto: la descripción de [project] copyright = "© 2026 Acme" ``` | Qué | Windows | macOS | Linux | | --- | --- | --- | --- | | `name` | `Shop.exe`, el nombre del producto | `Shop.app`, `CFBundleDisplayName` | El ejecutable | | `version` | Versión del archivo y del producto (pestaña Detalles) | `CFBundleShortVersionString` y `CFBundleVersion` | - | | `publisher` | Nombre de la empresa | - | - | | `identifier` | - | `CFBundleIdentifier` | El id de la app de sus ventanas (mira [El icono de la ventana](#el-icono-de-la-ventana)) | | `description` | Descripción del archivo | - | - | | `copyright` | Copyright legal | `NSHumanReadableCopyright` | - | Con PyInstaller, `pyzed build` escribe un archivo de versión de Windows (`build/pyzed/version.txt`) y completa el `Info.plist` del paquete tras compilar; Nuitka recibe los mismos valores como opciones. Usa como `identifier` un dominio invertido que te pertenezca. macOS guarda las preferencias y los permisos de privacidad de la app con él, así que no lo cambies entre versiones. El **título de la ventana** no va aquí: es el **Título** del formulario en el diseñador, o `title` de tu `pz.App`. Si tu app muestra su versión o [busca actualizaciones](https://albakore.github.io/pyzed/es/packaging/updates/), guarda también la versión en tu código, por ejemplo en el `__init__.py` de tu paquete, y actualízala junto con `pyproject.toml`: ```python title="src/shop/__init__.py" __version__ = "1.2.0" ``` **¿Por qué no importlib.metadata?:** `importlib.metadata.version("shop")` lee los metadatos del paquete instalado, que PyInstaller no copia al paquete salvo que se lo pidas (`args = ["--copy-metadata", "shop"]`). Una constante en tu código siempre funciona. #### El icono Dale a `icon` una imagen cuadrada, idealmente un PNG de 1024 × 1024 píxeles con fondo transparente: ```toml title="pyproject.toml" [tool.pyzed.build] icon = "assets/icon.png" ``` `pyzed build` crea a partir de ella todos los archivos que piden los sistemas, con el código de imagen del propio Pyzed (sin Pillow, `sips` ni `iconutil`), en `build/pyzed/`: | Archivo | Tamaños dentro | Para | | --- | --- | --- | | `app-icon.ico` | 16, 20, 24, 32, 40, 48, 64, 128, 256 px | El `.exe` de Windows: el Explorador, la barra de tareas, Alt+Tab | | `app-icon.icns` | de 16 a 1024 px, con tamaños Retina | El paquete de macOS: el Finder y el Dock | | `app-icon.png` | 256 px | El icono de las ventanas de la app (va dentro de la app) | Los tamaños pequeños salen de reducir la imagen grande. Si las versiones de 16 y 32 píxeles se ven borrosas, dibuja tú una versión más simple para ellas (abajo) o usa un diseño con trazos gruesos. El [diálogo Exportar](https://albakore.github.io/pyzed/es/editor/exporting/#icono) muestra una vista previa de cada tamaño. Un archivo `.ico` o `.icns` dado como `icon` se usa tal cual, para su sistema. ##### Crear tú los archivos de icono Para controlar cada tamaño, crea los archivos con tu herramienta favorita a partir de un PNG de 1024 × 1024: **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 # escribe icon.icns (solo macOS) ``` #### El icono de la ventana Los iconos del `.exe` y de la `.app` son los del **archivo**. Una app empaquetada también da el icono a sus **ventanas** (la barra de título y la barra de tareas en Windows y Linux): el lanzador fija `pz.Window.icon` a `app-icon.png`. macOS siempre muestra en el Dock el icono del paquete. En tu propio código, cualquier ventana puede tener su icono, una ruta a una imagen o sus bytes: ```python import pyzed as pz class Shop(pz.App): icon = "assets/icon.png" app_id = "com.acme.shop" # Linux: coincide con el archivo .desktop ``` En Linux, `app_id` da nombre a la app ante el escritorio. Wayland toma el icono de la barra de tareas y del dock del archivo `.desktop` cuyo nombre coincide (`com.acme.shop.desktop`, mira [Linux](https://albakore.github.io/pyzed/es/packaging/linux/#añadirla-al-menú-de-aplicaciones)); X11 usa `icon` directamente. #### Propiedades del archivo en Windows a mano Para usar tu propio recurso de versión en lugar del generado (más campos, otros idiomas), captura uno de un `.exe` existente con `pyi-grab_version` y pásaselo a PyInstaller: ```toml title="pyproject.toml" [tool.pyzed.build.pyinstaller] args = ["--version-file", "assets/version.txt"] ``` Las rutas de `args` llegan a PyInstaller sin cambios, y PyInstaller escribe su archivo spec en `build/pyzed/`: si no encuentra el archivo, usa su ruta absoluta. ### Compilar en CI con GitHub Actions > Compila una app Pyzed para Windows, macOS y Linux en x64 y ARM en cada versión con GitHub Actions: el flujo que escribe pyzed build --workflow, publicar una GitHub Release y firmar con secretos. Source: https://albakore.github.io/pyzed/es/packaging/ci/ Como cada sistema compila su propia app, la forma más fácil de publicarlas todas es dejar que GitHub las compile en sus propias máquinas Windows, macOS y Linux ("runners"), en x64 y ARM, cada vez que publicas una versión. #### Escribir el flujo **uv:** ```bash uv run pyzed build --workflow ``` **pip:** ```bash title="macOS y Linux" pyzed build --workflow # o: python -m pyzed build --workflow ``` ```powershell title="Windows (PowerShell)" pyzed build --workflow # o: py -m pyzed build --workflow ``` Escribe `.github/workflows/build.yml`, con un trabajo por cada destino de `targets` en `[tool.pyzed.build]` (por defecto Windows x64, macOS en Apple silicon y Linux x64). En Pyzed Design, marca las plataformas en **Archivo → Exportar aplicación… → Plataformas** y pulsa **Escribir workflow de CI** ([Exportar tu app](https://albakore.github.io/pyzed/es/editor/exporting/#plataformas-y-arquitecturas)). ```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` hace que cada trabajo compruebe que su máquina compila lo que dice la matriz. El empaquetador, el icono, la pantalla de inicio y el resto de ajustes salen de `pyproject.toml`, igual que en tu equipo. #### Ejecutarlo 1. Haz commit del flujo y de tu `uv.lock` (la compilación usa `uv sync --locked`, así que el archivo de bloqueo tiene que estar al día). 2. Sube una etiqueta de versión: ```bash git tag v1.0.0 git push origin v1.0.0 ``` O lánzalo a mano: **Actions → Build → Run workflow**. 3. Cuando termine, descarga las compilaciones desde la página de la ejecución, en **Artifacts**. #### Qué máquinas | Destino | Runner | Compila para | | --- | --- | --- | | `windows-x64` | `windows-latest` | Windows 10 y 11 en Intel y AMD | | `windows-arm64` | `windows-11-arm` | Windows 11 en ARM | | `macos-arm64` | `macos-latest` | Macs con Apple silicon | | `macos-x64` | `macos-15-intel` | Macs con Intel | | `linux-x64` | `ubuntu-22.04` | Linux en Intel y AMD | | `linux-arm64` | `ubuntu-22.04-arm` | Linux en ARM | Los runners de Linux son a propósito el Ubuntu más antiguo que ofrece GitHub: la app funciona con la glibc del runner o una más nueva ([Linux](https://albakore.github.io/pyzed/es/packaging/linux/#en-qué-distribución-compilar)). Con `backend = "nuitka"` todos los runners ya tienen el compilador de C que necesita Nuitka. #### Publicar una GitHub Release Los artifacts caducan pasado un tiempo y piden iniciar sesión en GitHub para descargarlos. Para dar a la gente una página de descarga permanente, fija `archive` antes de escribir el flujo: ```toml title="pyproject.toml" [tool.pyzed.build] archive = "zip" ``` Cada compilación empaqueta entonces la app como `--.zip` (`shop-windows-x86_64.zip`, `shop-macos-arm64.zip`, `shop-linux-x86_64.zip`), los nombres que busca la [autoactualización](https://albakore.github.io/pyzed/es/packaging/updates/), y el flujo gana un último paso que lo adjunta a la Release de la etiqueta (y el permiso para crearla): ```yaml title=".github/workflows/build.yml (lo que añade archive)" jobs: build: permissions: contents: write steps: # ... la compilación, y luego: - 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 ``` En macOS el zip se crea con `ditto`, que conserva los enlaces simbólicos y la firma del paquete. Si [firmas](#firmar-en-ci) la app, fírmala antes de comprimirla: fija `archive` solo después de añadir el paso de firma, o comprime tú tras firmar. #### Firmar en CI Guarda los certificados en **secretos de GitHub** (Settings → Secrets and variables → Actions), nunca en el repositorio. Luego decodifícalos en un paso antes de firmar. Es lo que hace el flujo de versiones de Pyzed para Pyzed Design: **Windows:** Guarda el archivo `.pfx` en base64 en `WINDOWS_CERTIFICATE` y su contraseña en `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:** Guarda el certificado Developer ID (`.p12`) en base64 en `MACOS_CERTIFICATE` con su contraseña, y los datos de tu Apple ID para la notarización: ```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 ``` Pon el paso de firma **después** de `uv run pyzed build` y **antes** de comprimir. Mira [Windows](https://albakore.github.io/pyzed/es/packaging/windows/#firmarla) y [macOS](https://albakore.github.io/pyzed/es/packaging/macos/#firmar-y-notarizar) para saber qué hace cada comando. **Cómo se publica Pyzed Design:** El flujo que compila el propio Pyzed Design, [`.github/workflows/release.yml`](https://github.com/albakore/pyzed/blob/main/.github/workflows/release.yml), es un ejemplo real completo: cinco plataformas, firma y notarización opcionales, un instalador de Inno Setup en Windows, un `.dmg` en macOS, `SHA256SUMS`, atestaciones de procedencia y una GitHub Release con notas. ### Apps que se actualizan solas > Haz que una app Pyzed empaquetada se actualice sola desde GitHub Releases con pyzed.updater: buscar una versión nueva, descargarla con verificación SHA-256, cambiar la carpeta de la app al reiniciar y canales preview. Source: https://albakore.github.io/pyzed/es/packaging/updates/ `pyzed.updater` permite que una app hecha con `pyzed build` se actualice sola desde las **GitHub Releases** de su repositorio: busca la versión más nueva, descarga la compilación para este sistema, la verifica y cambia la carpeta de la app por la nueva cuando la app se reinicia. Pyzed Design se actualiza con este mismo módulo. ```mermaid sequenceDiagram participant App as Tu app (v1.0.0) participant GH as GitHub Releases App->>GH: latest("acme/shop") GH-->>App: v1.1.0 + shop-windows-x86_64.zip (y su SHA-256) App->>GH: download(asset) Note over App: verifica el SHA-256, descomprime el zip App->>App: replace_on_exit(carpeta nueva) y se cierra Note over App: un pequeño script espera, cambia las carpetas y arranca la v1.1.0 ``` #### Qué debe contener una versión 1. **Una etiqueta de versión** como `v1.1.0` (versiones preliminares: `v1.2.0-rc.1`, `v1.2.0-beta.2`...). 2. **Un zip por sistema**, llamado `--.zip`: | Sistema | Ejemplo | | --- | --- | | 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")` devuelve el nombre para la máquina donde se ejecuta. 3. **Dentro de cada zip, exactamente una entrada**: la carpeta de la app tal como la creó `pyzed build` (`Shop/`), o `Shop.app` en macOS (comprímela con `ditto -c -k --keepParent` para conservar la firma del paquete). La [página de CI](https://albakore.github.io/pyzed/es/packaging/ci/#publicar-una-github-release) tiene un flujo que compila y sube estos zip. **Cada descarga se verifica:** `updater.download` solo instala un archivo cuyo **SHA-256** coincide con el que publica GitHub para ese archivo de la versión. Si falta o es distinto, lanza `UpdateError` y borra el archivo, así que nunca se instala una descarga dañada o manipulada. #### Buscar, descargar, reiniciar Las funciones bloquean mientras hablan con la red, así que ejecútalas fuera del hilo de la interfaz, por ejemplo con `asyncio.to_thread` en una corrutina (mira [Async](https://albakore.github.io/pyzed/es/guides/async/)). Una app completa: ```python title="src/shop/app.py" import asyncio import os import pyzed as pz from pyzed import ui, updater __version__ = "1.0.0" # mantenla igual que en pyproject.toml REPO = "acme/shop" # el repositorio de GitHub con las versiones APP = "shop" # la parte de los nombres de los zip class Shop(pz.App): title = "Shop" def __init__(self): self.release = None # una versión más nueva, cuando la hay self.ready = None # la versión nueva descargada, lista para cambiarla self.status = "" def on_start(self): if updater.is_frozen(): # solo una app empaquetada puede reemplazarse return self.check() async def check(self): try: release = await asyncio.to_thread(updater.latest, REPO) except updater.UpdateError as err: return ("status", f"No se pudo buscar actualizaciones: {err}") if updater.is_newer(release.version, __version__) and release.asset_for(updater.asset_name(APP)): return ("found", release) return ("status", "Tienes la última versión.") async def download(self): asset = self.release.asset_for(updater.asset_name(APP)) self.status = f"Descargando {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"La actualización falló: {err}") return ("ready", bundle) def restart(self): updater.replace_on_exit(self.ready) # cambia las carpetas cuando este proceso termina... os._exit(0) # ...así que termínalo ya; la versión nueva arranca sola def update(self, message): kind, value = message if kind == "found": self.release, self.status = value, f"La versión {value.version} está disponible." elif kind == "ready": self.ready, self.status = value, "Reinicia para terminar la actualización." else: self.status = value def view(self): acciones = [] if self.ready: acciones.append(ui.button("Reiniciar para actualizar", self.restart, variant="solid")) elif self.release: acciones.append(ui.button("Descargar e instalar", self.download, variant="solid")) return pz.column([ui.heading("Shop"), ui.muted(f"Versión {__version__}"), ui.label(self.status), *acciones], spacing=10, padding=24) ``` #### Las funciones | Función | Qué hace | | --- | --- | | `latest(repo, prerelease=False)` | La `Release` más nueva de `"dueño/nombre"`. Con `prerelease=True` también cuentan las preliminares (un "canal preview"). | | `release(repo, version)` | La versión indicada (`"1.1.0"` o `"v1.1.0"`). | | `is_newer(candidata, actual)` | Compara versiones; una versión final es más nueva que sus preliminares (`1.2.0` > `1.2.0-rc.1`). | | `asset_name(app)` | `"--.zip"` para esta máquina. | | `Release.asset_for(nombre)` | El archivo de la versión con ese nombre, o `None` si la versión no tiene compilación para este sistema. | | `download(asset, folder=None, progress=None)` | Descarga en `folder` (una carpeta temporal si no se indica), verifica el SHA-256 y devuelve la ruta. `progress` recibe la fracción completada, de 0 a 1. Si `folder` ya tiene el archivo con el SHA-256 correcto, lo reutiliza. | | `extract(archivo)` | Descomprime el zip y devuelve la app que contiene. | | `replace_on_exit(bundle, relaunch=True, args=(), keep=())` | Arranca un pequeño script que espera a que la app se cierre, cambia la carpeta vieja por `bundle` (restaurando la vieja si falla) y arranca la versión nueva. `keep` lista archivos a conservar, como el desinstalador de un instalador (`"unins*"`). Para antes los procesos hijos de la app: en Windows una carpeta en uso no se puede mover (el script reintenta unos 30 segundos y luego vuelve a abrir la versión vieja). | | `is_frozen()` | Si es una app empaquetada. | | `install_dir()` | La carpeta (o `.app`) de la app en ejecución, `None` si corre desde Python. | | `can_update_in_place()` | Si la app está empaquetada y su carpeta admite escritura, para que `replace_on_exit` funcione. | Una `Release` tiene `version`, `tag`, `notes` (las notas de la versión en Markdown: muéstralas con [`ui.markdown`](https://albakore.github.io/pyzed/es/library/markdown/)), `url`, `published`, `prerelease` y `assets`. Los errores son `UpdateError` con un mensaje legible ("could not reach GitHub", "rate limit"...): los problemas de red nunca cierran la app. #### A tener en cuenta - **Importa dónde vive la app.** La app solo puede reemplazar una carpeta en la que puede escribir. Comprueba `updater.can_update_in_place()` antes de ofrecer la actualización; si es `False` (por ejemplo una app en `C:\Program Files`), manda a la gente a la página de descarga. Las instalaciones por usuario (`%LOCALAPPDATA%\Programs` en Windows, `~/.local/share` en Linux, `/Applications` para administradores en macOS) funcionan. - **Solo compilaciones en carpeta.** Una compilación `--onefile` no puede actualizarse así. - **Las apps firmadas siguen firmadas** en macOS si el zip se creó con `ditto` a partir de una `.app` firmada (y notarizada). - **La API de GitHub tiene límite** de 60 peticiones por hora por IP sin token: busca una vez al día, no en cada arranque. **Recordar las búsquedas:** `updater.load_state()`, `save_state()` y `due()` guardan sus datos en un archivo compartido por todas las apps Pyzed del equipo (la carpeta de ajustes de Pyzed), incluido Pyzed Design. Para tu app, guarda la hora de la última búsqueda en tus propios ajustes. **Otras fuentes de actualizaciones:** La autoactualización solo lee GitHub Releases. Actualizar desde tu propio servidor, una tienda de apps o un gestor de paquetes todavía no está soportado. ### Solución de problemas al compilar > Resuelve problemas comunes al empaquetar una app Pyzed: módulos o archivos que faltan, ventanas en blanco, apps bloqueadas en macOS y Windows, errores de glibc en Linux, compilaciones grandes y errores de actualización. Source: https://albakore.github.io/pyzed/es/packaging/troubleshooting/ #### Primero, mira el error Una app empaquetada no tiene terminal, así que los errores no se ven. Compílala con consola y arráncala desde una terminal para leer el 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 # el ejecutable dentro del paquete ``` **Linux:** ```bash uv run pyzed build --console ./dist/Shop/Shop ``` `pyzed build --dry-run` muestra el comando de PyInstaller que ejecutaría, útil para ver qué se incluye. #### Al compilar | Síntoma | Causa y solución | | --- | --- | | `uv: command not found`, o `uv` no se reconoce | `pyzed build` ejecuta PyInstaller y Nuitka con uv. Instálalo: `pip install uv`, o mira [docs.astral.sh/uv](https://docs.astral.sh/uv/). | | `ui/main.pzform does not exist; set [tool.pyzed] main in pyproject.toml` | El formulario principal se movió o cambió de nombre. Fija `main = "ui/.pzform"` en `[tool.pyzed]`. | | `uv sync --locked` falla en CI | `uv.lock` está desactualizado: ejecuta `uv lock` (o `pyzed sync`) y haz commit. | | La primera compilación es muy lenta | uv descarga PyInstaller y PyInstaller analiza cada dependencia; las siguientes reutilizan la caché de uv. Nuitka compila todo a C, lo que lleva minutos. | | Nuitka: no se encuentra un compilador de C | Instala uno: `sudo apt install gcc` (Linux), `xcode-select --install` (macOS), las Build Tools de Visual Studio (Windows), o deja que Nuitka descargue MinGW cuando lo pregunte. | | *La pantalla de inicio temprana de PyInstaller dibuja con Tk, que no tiene el Python del proyecto* | El Python del proyecto no tiene `tkinter`, así que solo se ve la pantalla de Pyzed. Usa un Python de `uv python install` o instala `python3-tk`. | #### Al arrancar la app | Síntoma | Causa y solución | | --- | --- | | `ModuleNotFoundError` | Un módulo importado por nombre o con `importlib`: añádelo a `hidden-imports`, o su paquete a `collect`. | | Falta un archivo de datos de una dependencia (`FileNotFoundError` dentro de un paquete) | Añade el paquete a `collect` (incluye también sus datos y binarios). | | Faltan tus propios archivos | Añádelos a `include` y búscalos desde `sys._MEIPASS` ([Cómo funciona la compilación](https://albakore.github.io/pyzed/es/packaging/build/#encontrar-tus-archivos-de-datos)). | | La ventana queda en blanco o la app se cierra enseguida en una máquina virtual o escritorio remoto | No hay GPU utilizable. Arráncala con el renderizador por CPU: `ICED_BACKEND=tiny-skia ./Shop` (PowerShell: `$env:ICED_BACKEND="tiny-skia"; .\Shop.exe`). | | Una app `--onefile` tarda en arrancar | Se descomprime en una carpeta temporal en cada arranque. Usa la compilación en carpeta. | | Los ajustes o archivos que guardó la app desaparecen tras actualizar | Se escribieron dentro de la carpeta de la app, que una actualización reemplaza. Guarda los datos del usuario en sus carpetas. | #### En cada sistema | Síntoma | Causa y solución | | --- | --- | | **macOS:** "Shop está dañada y no se puede abrir" o "no se puede abrir porque no se puede verificar el desarrollador" | La app no está firmada ni notarizada. [Fírmala y notarízala](https://albakore.github.io/pyzed/es/packaging/macos/#firmar-y-notarizar). Solo para tus pruebas: `xattr -dr com.apple.quarantine Shop.app`. | | **macOS:** la app se abre y se cierra enseguida | Ejecuta el ejecutable de dentro del paquete desde una terminal (arriba) para ver el error. | | **Windows:** "Windows protegió tu PC" | SmartScreen avisa de las apps sin firmar. [Firma el .exe](https://albakore.github.io/pyzed/es/packaging/windows/#firmarla); la gente puede usar **Más información → Ejecutar de todas formas**. | | **Windows:** el antivirus borra o bloquea la app | Un falso positivo, más común con `--onefile`. Usa la compilación en carpeta, fírmala e informa al fabricante. | | **Linux:** `version 'GLIBC_2.xx' not found` | La app se compiló en una distribución más nueva que la que la ejecuta. [Compila en una más antigua](https://albakore.github.io/pyzed/es/packaging/linux/#en-qué-distribución-compilar). | | **Linux:** errores sobre `libxkbcommon`, X11 o Wayland | El equipo no tiene bibliotecas de escritorio (una imagen mínima de servidor). Instala la sesión de escritorio o `libxkbcommon`. | | **Linux:** `Permission denied` al arrancarla | Se perdió el bit de ejecución (por ejemplo, comprimida en Windows): `chmod +x Shop/Shop`. | #### Tamaño | Síntoma | Solución | | --- | --- | | La app es grande | Deja las herramientas de desarrollo en el grupo dev (`pyzed add --dev ...`), deja `slim = true`, excluye módulos que no usas con `exclude = [...]` y evita `collect` en paquetes grandes cuando basta con `hidden-imports`. Un archivo único (`onefile`) va comprimido. | | El zip es más grande de lo esperado | No incluyas `build/`, `.venv/` ni datos de prueba en `include`. | #### Autoactualización | Mensaje | Significado | | --- | --- | | `could not reach GitHub` | Sin red, o un proxy bloquea GitHub. Prueba más tarde. | | `GitHub refused the request (rate limit)` | Más de 60 peticiones en una hora desde esta IP. Busca con menos frecuencia. | | `no release found` | El repositorio no tiene versiones publicadas (los borradores no cuentan), o el nombre está mal. | | `the release does not publish a SHA-256 for this file` | La descarga no se puede verificar, así que no se instala. Vuelve a subir el archivo a la versión. | | `the download is corrupt` | El archivo no coincide con su SHA-256. Prueba de nuevo. | | `the update does not contain exactly one app` | El zip debe tener una sola carpeta (o una `.app`) en su raíz. | | `only a packaged app can replace itself` | La app corre desde Python: actualiza con `pip` o `uv`. | Mira [Apps que se actualizan solas](https://albakore.github.io/pyzed/es/packaging/updates/) para saber cómo organizar las versiones. **¿Sigues atascado?:** Busca en los [issues](https://github.com/albakore/pyzed/issues) o abre uno con tu sistema, el comando de compilación, la salida de `pyzed build --dry-run` y el traceback de la compilación con consola. ## Showcase ### Showcase > Una galería de apps de escritorio en Python hechas con Pyzed: utilidades, formularios, videojuegos, shaders y efectos de GPU, y dashboards, cada una con una explicación, el código completo y un comando para ejecutarla. Source: https://albakore.github.io/pyzed/es/showcase/ Todo lo que hay en estas páginas es un programa real de la carpeta [`examples`](https://github.com/albakore/pyzed/tree/main/examples) del repositorio, dibujado por Pyzed con la GPU. Haz clic en una tarjeta para ver cómo está hecho, leer el código completo y ejecutarlo. ¿Quieres construir uno tú, paso a paso? Los [Tutoriales](https://albakore.github.io/pyzed/es/tutorials/) te llevan ahí. **Ejecuta cualquiera:** Descarga o clona el [repositorio](https://github.com/albakore/pyzed), abre una terminal en su carpeta y ejecuta el archivo. Cada página tiene un bloque como este, con los comandos para uv, pip y Python a secas en Windows, macOS y Linux: #### Apps y utilidades Programas pequeños y completos: visores, temporizadores, datos en vivo, un chat con IA y un editor de proyectos. [Verlas todas](https://albakore.github.io/pyzed/es/showcase/apps/). #### Formularios Acceso, registro, ajustes y asistentes, dibujados en Pyzed Design o escritos en código. [Verlos todos](https://albakore.github.io/pyzed/es/showcase/forms/). #### Videojuegos Juegos 2D en tiempo real con un bucle por fotograma, teclas mantenidas, partículas y un HUD hecho con widgets normales. [Cómo funcionan los juegos](https://albakore.github.io/pyzed/es/games/). #### Shaders y efectos Shaders de GPU hechos con nodos, y cristal, bloom, desenfoque y animaciones en cualquier widget. [Verlos todos](https://albakore.github.io/pyzed/es/showcase/shaders/). #### Dashboards y datos Paneles de administración, tablas, tableros y apps multimedia. [Verlos todos](https://albakore.github.io/pyzed/es/showcase/dashboards/). **Sobre esta sección:** El Showcase crece con cada versión. Todos los programas de aquí se dibujan fuera de pantalla en la suite de tests, así que funcionan con la versión actual de Pyzed; se están añadiendo grabaciones cortas de los que se mueven. ¿Hiciste algo con Pyzed que te gustaría mostrar? Cuéntanos en [GitHub](https://github.com/albakore/pyzed). ### Apps y utilidades > Apps de escritorio en Python pequeñas y completas hechas con Pyzed: una lista de tareas, visores de Markdown e imágenes, un reloj, un cronómetro, un generador de QR, datos en vivo, un chat con IA, paneles, varias ventanas y un editor de proyectos. Source: https://albakore.github.io/pyzed/es/showcase/apps/ Programas de todos los días, cada uno un único archivo de Python en [`examples/`](https://github.com/albakore/pyzed/tree/main/examples) que funciona tal cual. Son cortos a propósito: lee uno de arriba abajo y habrás visto el estado, una vista, eventos y una suscripción trabajando juntos. Varios son versiones en Pyzed de las demos clásicas de [Iced](https://iced.rs), la biblioteca de interfaces de Rust con la que dibuja Pyzed. **Cómo leer estas apps:** Cada app es una clase con **estado** (atributos que se crean en `__init__`), un **`view()`** que construye la pantalla a partir del estado y **métodos** que cambian el estado cuando pasa algo. Pyzed redibuja después de cada cambio. ¿Es nueva para ti esta idea? Empieza por el tutorial de la [Lista de tareas](https://albakore.github.io/pyzed/es/tutorials/basic/todo-app/). #### Todos Añade, edita, completa, filtra y borra tareas; se guardan como JSON en tu carpeta de usuario, así que siguen ahí la próxima vez que abras la app. - Un dataclass por tarea y una lista en el estado; el filtro es un control `ui.segmented`. - Haz doble clic en una tarea para editarla en el sitio; las tareas se escriben en disco en cada cambio. - Una versión más simple se construye paso a paso en el tutorial de la [Lista de tareas](https://albakore.github.io/pyzed/es/tutorials/basic/todo-app/).
Código completo: examples/todos.py `examples/todos.py`: ```py """Todos, after Iced's ``todos`` demo: add, edit, complete, filter and persist tasks. python examples/todos.py Tasks are saved as JSON next to your user config, so they survive restarts. """ import json from dataclasses import asdict, dataclass, field from itertools import count from pathlib import Path import pyzed as pz from pyzed import ui STORE = Path.home() / ".config" / "pyzed-todos.json" _ids = count() @dataclass class Task: text: str done: bool = False id: int = field(default_factory=lambda: next(_ids)) class Todos(pz.App): title = "Todos" size = (560, 680) def __init__(self, store=STORE): self.store = store self.draft = "" self.filter = "All" self.editing = None # (task id, edited text) self.tasks = self.load() # -- persistence -------------------------------------------------------- def load(self): try: return [Task(t["text"], t["done"]) for t in json.loads(self.store.read_text())] except (OSError, ValueError, KeyError, TypeError): return [Task("Read the Pyzed examples"), Task("Build something nice", done=True)] def save(self): try: self.store.parent.mkdir(parents=True, exist_ok=True) self.store.write_text(json.dumps([{"text": t.text, "done": t.done} for t in self.tasks], indent=2)) except OSError: pass # -- actions -------------------------------------------------------------- def add(self): if self.draft.strip(): self.tasks.append(Task(self.draft.strip())) self.draft = "" self.save() def toggle(self, task): task.done = not task.done self.save() def remove(self, task): self.tasks.remove(task) self.save() def start_edit(self, task): self.editing = (task.id, task.text) return pz.Command.focus(f"edit-{task.id}") def finish_edit(self): task_id, text = self.editing for task in self.tasks: if task.id == task_id and text.strip(): task.text = text.strip() self.editing = None self.save() def clear_done(self): self.tasks = [t for t in self.tasks if not t.done] self.save() def on_key(self, combo): if combo == "escape": self.editing = None # -- view ------------------------------------------------------------------- def visible(self): if self.filter == "Active": return [t for t in self.tasks if not t.done] if self.filter == "Completed": return [t for t in self.tasks if t.done] return self.tasks def task_row(self, task): t = self.resolved_theme() if self.editing and self.editing[0] == task.id: return pz.row([ ui.input(self.editing[1], lambda text: setattr(self, "editing", (task.id, text)), on_submit=self.finish_edit, id=f"edit-{task.id}"), ui.icon_button("check", self.finish_edit, tooltip="Save"), ui.icon_button("trash-2", lambda: self.remove(task), tooltip="Delete", color=t.error), ], spacing=6, align="center") return pz.row([ ui.checkbox(task.done, lambda _: self.toggle(task)), pz.mouse_area( ui.label(task.text, color="text_muted" if task.done else "text", width="fill"), on_double_click=lambda: self.start_edit(task), ), ui.icon_button("pencil", lambda: self.start_edit(task), tooltip="Edit"), ], spacing=10, align="center") def view(self): t = self.resolved_theme() left = sum(not task.done for task in self.tasks) tasks = self.visible() body = (pz.column([self.task_row(task) for task in tasks], spacing=6) if tasks else ui.empty_state("Nothing here", f"No {self.filter.lower()} tasks.")) return pz.container( pz.column([ ui.heading("todos"), ui.input(self.draft, lambda text: setattr(self, "draft", text), on_submit=self.add, placeholder="What needs to be done?", icon="plus"), pz.row([ ui.muted(f"{left} {'task' if left == 1 else 'tasks'} left"), pz.hspace(), ui.segmented(["All", "Active", "Completed"], self.filter, lambda f: setattr(self, "filter", f)), ], align="center"), pz.scrollable(body, height="fill"), pz.row([pz.hspace(), ui.button("Clear completed", self.clear_done, variant="ghost", size="sm")]), ], spacing=14, max_width=520), padding=24, width="fill", height="fill", align_x="center", bg=t.editor, ) if __name__ == "__main__": pz.run(Todos) ```
#### Visor de Markdown Un editor de Markdown en vivo: escribe a la izquierda, mira el resultado con formato a la derecha y arrastra el divisor entre los dos. - `ui.editor` con `on_change` guarda el texto en el estado; `ui.markdown` lo muestra con formato. - `ui.split` hace que los dos paneles se puedan redimensionar; los enlaces de la vista previa se abren en tu navegador.
Código completo: examples/markdown_viewer.py `examples/markdown_viewer.py`: ```py """A live Markdown editor: code editor on the left, rendered preview on the right. python examples/markdown_viewer.py Shows ``ui.editor`` with ``on_change``, ``ui.markdown`` and a draggable ``ui.split``. Links in the preview open in your browser. """ import webbrowser import pyzed as pz from pyzed import ui SAMPLE = """# Pyzed Build **fast** desktop apps in *Python*, rendered by [Iced](https://iced.rs). ## Features - Zed-style components in `pyzed.ui` - Themes, including the ones from [Zed](https://zed.dev) - `async def` everywhere ```python import pyzed as pz class Hello(pz.App): def view(self): return pz.text("Hello!") ``` > Tip: drag the divider to resize the panes. """ class MarkdownViewer(pz.App): title = "Markdown" size = (1100, 720) def __init__(self): self.source = SAMPLE self.ratio = 0.5 self.wrap = True def view(self): t = self.resolved_theme() editor = pz.column([ pz.container( pz.row([ui.label("Markdown", size="sm", color="text_muted"), pz.hspace(), ui.checkbox(self.wrap, lambda v: setattr(self, "wrap", v), label="Wrap")], align="center"), padding=(6, 12), bg=t.tab_bar, width="fill", ), ui.editor("source", self.source, language="md", wrap=self.wrap, on_change=lambda text: setattr(self, "source", text)), ]) preview = pz.container( pz.scrollable(pz.container(ui.markdown(self.source, on_link=webbrowser.open), padding=24, max_width=760), height="fill"), width="fill", height="fill", bg=t.background, ) return ui.split(editor, preview, ratio=self.ratio, on_resize=lambda r: setattr(self, "ratio", r)) if __name__ == "__main__": pz.run(MarkdownViewer) ```
#### Visor de imágenes Muestra una imagen con `pz.image` y en un `pz.image_viewer` con zoom: la rueda hace zoom y arrastrar la mueve. Pasa una ruta para abrir tu propia imagen; sin ella dibuja una imagen a partir de píxeles generados en Python puro.
Código completo: examples/image_viewer.py `examples/image_viewer.py`: ```py """Bitmap images: ``pz.image`` and the zoomable ``pz.image_viewer``. python examples/image_viewer.py [picture.png] Without an argument it shows a picture generated in pure Python (raw RGBA pixels passed with ``size=(w, h)``). Scroll to zoom the viewer and drag to pan. """ import math import sys import pyzed as pz from pyzed import ui W, H = 480, 320 def generated_pixels(): """A colorful plasma pattern as raw RGBA bytes.""" out = bytearray() for y in range(H): for x in range(W): v = math.sin(x / 23) + math.sin(y / 17) + math.sin((x + y) / 31) + math.sin(math.hypot(x - W / 2, y - H / 2) / 19) out += bytes((int(128 + 127 * math.sin(v * math.pi)), int(128 + 127 * math.sin(v * math.pi + 2)), int(128 + 127 * math.sin(v * math.pi + 4)), 255)) return bytes(out) class Viewer(pz.App): title = "Image viewer" size = (1000, 640) def __init__(self, path=None): self.path = path self.pixels = None if path else generated_pixels() self.fit = "contain" self.radius = 12.0 self.opacity = 1.0 def source(self, **kwargs): if self.path: return dict(source=self.path, **kwargs) return dict(source=self.pixels, size=(W, H), **kwargs) def view(self): t = self.resolved_theme() thumbs = pz.column([ ui.label("pz.image", weight="semibold"), ui.field("Fit", ui.select(["contain", "cover", "fill", "none", "scale_down"], self.fit, lambda v: setattr(self, "fit", v), width=130)), ui.field("Radius", pz.container(ui.slider(self.radius, lambda v: setattr(self, "radius", v), min=0, max=60), width=130)), ui.field("Opacity", pz.container(ui.slider(self.opacity, lambda v: setattr(self, "opacity", v)), width=130)), pz.container(pz.image(**self.source(width="fill", height=200, content_fit=self.fit, radius=self.radius, opacity=self.opacity)), width="fill", height=200, bg=t.surface, radius=self.radius), pz.row([pz.image(**self.source(width=64, height=64, content_fit="cover", radius=32)), pz.image(**self.source(width=64, height=64, content_fit="cover", radius=8, rotation=15)), pz.image(**self.source(width=64, height=64, content_fit="cover", filter="nearest", scale=2))], spacing=12), ], spacing=12, width=320) viewer = pz.column([ ui.label("pz.image_viewer (scroll to zoom, drag to pan)", weight="semibold"), pz.container(pz.image_viewer(**self.source(width="fill", height="fill")), width="fill", height="fill", bg=t.surface, radius=t.radius, clip=True), ], spacing=12, width="fill") return pz.container(pz.row([thumbs, viewer], spacing=20), padding=20, bg=t.background, width="fill", height="fill") if __name__ == "__main__": pz.run(Viewer(sys.argv[1] if len(sys.argv) > 1 else None)) ```
#### Reloj Un reloj analógico dibujado en un `pz.canvas`. Una suscripción, `pz.every(1, ...)`, lo redibuja cada segundo, y las agujas son trazos girados alrededor del centro.
Código completo: examples/clock.py `examples/clock.py`: ```py """An analog clock drawn on a canvas, after Iced's ``clock`` demo. python examples/clock.py ``pz.every(1, ...)`` redraws it every second; the hands are rotated paths. """ import math import time import pyzed as pz from pyzed import draw SIZE = 320 def hand(cx, cy, angle, length, width, color): a = math.radians(angle - 90) return draw.line(cx, cy, cx + length * math.cos(a), cy + length * math.sin(a), stroke=color, width=width, cap="round") class Clock(pz.App): title = "Clock" size = (SIZE + 40, SIZE + 80) def __init__(self): self.now = time.localtime() def subscriptions(self): return [pz.every(1.0, lambda: setattr(self, "now", time.localtime()))] def view(self): t = self.resolved_theme() c = SIZE / 2 r = c - 8 shapes = [draw.circle(c, c, r, fill=t.surface, stroke=t.border, width=2)] for i in range(60): a = math.radians(i * 6) outer, inner = r - 6, r - (18 if i % 5 == 0 else 11) shapes.append(draw.line(c + inner * math.sin(a), c - inner * math.cos(a), c + outer * math.sin(a), c - outer * math.cos(a), stroke=t.text if i % 5 == 0 else t.text_muted, width=2.5 if i % 5 == 0 else 1)) for h in range(1, 13): a = math.radians(h * 30) shapes.append(draw.text(c + (r - 38) * math.sin(a), c - (r - 38) * math.cos(a), h, color=t.text, size=16, align_x="center", align_y="center")) hours, minutes, seconds = self.now.tm_hour % 12, self.now.tm_min, self.now.tm_sec shapes += [ hand(c, c, hours * 30 + minutes / 2, r * 0.5, 6, t.text), hand(c, c, minutes * 6 + seconds / 10, r * 0.75, 4, t.text), hand(c, c, seconds * 6, r * 0.82, 2, t.error), draw.circle(c, c, 5, fill=t.error), ] return pz.container( pz.column([pz.canvas(shapes, width=SIZE, height=SIZE), pz.text(time.strftime("%H:%M:%S", self.now), size=20, font="mono", color=t.text)], spacing=12, align="center"), center=True, bg=t.background, ) if __name__ == "__main__": pz.run(Clock) ```
#### Cronómetro Un cronómetro con vueltas. Espacio lo arranca y lo para, L guarda una vuelta y R lo reinicia. - La suscripción del temporizador solo existe **mientras el cronómetro corre**: si no, `subscriptions()` devuelve una lista vacía, así que una app en reposo no usa CPU.
Código completo: examples/stopwatch.py `examples/stopwatch.py`: ```py """A stopwatch with laps, after Iced's ``stopwatch`` demo. python examples/stopwatch.py The timer subscription only exists while the watch runs, so an idle app uses no CPU. Space starts/stops, ``l`` records a lap, ``r`` resets. """ import time import pyzed as pz from pyzed import ui def fmt(seconds): minutes, rest = divmod(seconds, 60) return f"{int(minutes):02d}:{rest:05.2f}" class Stopwatch(pz.App): title = "Stopwatch" size = (420, 520) def __init__(self): self.elapsed = 0.0 self.started = None self.laps = [] @property def running(self): return self.started is not None def now(self): return self.elapsed + (time.monotonic() - self.started if self.running else 0.0) def toggle(self): if self.running: self.elapsed = self.now() self.started = None else: self.started = time.monotonic() def reset(self): self.elapsed, self.started, self.laps = 0.0, None, [] def lap(self): if self.running: self.laps.insert(0, self.now()) def subscriptions(self): # An empty message still triggers a redraw; ``now()`` does the rest. return [pz.every(0.03, lambda: None)] if self.running else [] def on_key(self, combo): {"space": self.toggle, "l": self.lap, "r": self.reset}.get(combo, lambda: None)() def view(self): t = self.resolved_theme() laps = [ pz.row([ui.muted(f"Lap {len(self.laps) - i}"), pz.hspace(), pz.text(fmt(v), font="mono", color=t.text)]) for i, v in enumerate(self.laps) ] return pz.container( pz.column([ pz.text(fmt(self.now()), size=56, font="mono", color=t.text), pz.row([ ui.button("Stop" if self.running else "Start", self.toggle, variant="solid", tint="error" if self.running else "success", keys="space"), ui.button("Lap", self.lap, disabled=not self.running, keys="l"), ui.button("Reset", self.reset, variant="ghost", keys="r"), ], spacing=8), pz.scrollable(pz.column(laps, spacing=6, width="fill"), height="fill"), ], spacing=20, align="center", max_width=340), padding=28, width="fill", height="fill", align_x="center", bg=t.editor, ) if __name__ == "__main__": pz.run(Stopwatch) ```
#### Generador de QR Escribe un texto o un enlace y obtén su código QR, con los colores del tema o en blanco y negro. Toda la app son unas 30 líneas: `pz.qr_code(data, cell_size=7)` hace el trabajo.
Código completo: examples/qr_code.py `examples/qr_code.py`: ```py """Type something and get its QR code, after Iced's ``qr_code`` demo.""" import pyzed as pz from pyzed import ui class QR(pz.App): title = "QR code" size = (420, 520) def __init__(self): self.data = "https://github.com/albakore/pyzed" self.dark = True def view(self): t = self.resolved_theme() fg, bg = (t.text, t.surface) if self.dark else ("#000000", "#ffffff") return pz.container( pz.column([ ui.heading("QR code", level=2), ui.input(self.data, lambda v: setattr(self, "data", v), placeholder="Text or URL"), ui.checkbox(self.dark, lambda v: setattr(self, "dark", v), label="Use theme colors"), pz.container(pz.qr_code(self.data, cell_size=7, color=fg, bg=bg) if self.data else ui.muted("Empty"), padding=16, bg=bg, radius=t.radius), ], spacing=14, align="center"), padding=24, width="fill", height="fill", align_x="center", bg=t.background, ) if __name__ == "__main__": pz.run(QR) ```
#### Pomodoro Un temporizador de concentración con periodos de trabajo y descanso: la app que la guía [Cómo pedir](https://albakore.github.io/pyzed/es/ai/prompting/) construye con un asistente de IA, un pedido cada vez.
Código completo: examples/pomodoro.py `examples/pomodoro.py`: ```py """A Pomodoro timer: the app built step by step in the AI prompting guide.""" import pyzed as pz from pyzed import ui WORK, BREAK = 25 * 60, 5 * 60 class Pomodoro(pz.App): title = "Pomodoro" size = (420, 380) def __init__(self): self.mode = "Focus" self.left = WORK self.running = False self.done = 0 def subscriptions(self): return [pz.every(1, self.tick, key="timer")] if self.running else [] def tick(self): self.left -= 1 if self.left <= 0: if self.mode == "Focus": self.done += 1 self.mode = "Break" if self.mode == "Focus" else "Focus" self.left = BREAK if self.mode == "Break" else WORK def switch(self, mode): self.mode, self.running = mode, False self.left = WORK if mode == "Focus" else BREAK def toggle(self): self.running = not self.running def reset(self): self.running, self.left = False, WORK if self.mode == "Focus" else BREAK def view(self): total = WORK if self.mode == "Focus" else BREAK return pz.center(pz.column([ ui.segmented(["Focus", "Break"], self.mode, self.switch), pz.text(f"{self.left // 60:02d}:{self.left % 60:02d}", size=64, weight="bold"), ui.progress(1 - self.left / total, height=8, width=280), pz.row([ ui.button("Pause" if self.running else "Start", self.toggle, icon="pause" if self.running else "play", variant="solid", size="lg"), ui.icon_button("rotate-ccw", self.reset, tooltip="Reset"), ], spacing=8, align="center"), ui.muted(f"{self.done} sessions today", size="sm"), ], spacing=18, align="center")).style(background="editor") if __name__ == "__main__": pz.run(Pomodoro) ```
#### Selector de color `ui.ColorPicker` en sus tres modos: un color con transparencia, un degradado lineal y un relleno con máscara de opacidad. Devuelve un texto `"#rrggbbaa"`, un `pz.Gradient` o un `pz.Paint`, y los tres sirven como `background`.
Código completo: examples/color_picker.py `examples/color_picker.py`: ```py """Color picker: a color with alpha, a linear gradient, or a fill with an opacity mask. python examples/color_picker.py ``ui.ColorPicker`` reports a ``"#rrggbbaa"`` string, a ``pz.Gradient`` or a ``pz.Paint``; all of them work as a ``background`` in ``widget.style``. """ import pyzed as pz from pyzed import ui class ColorPickerDemo(pz.App): title = "Color picker" size = (640, 560) def __init__(self): self.fill = pz.Gradient(["#6366f1", "#ec4899"], angle=135) def pick(self, event): self.fill = event.value def view(self): preview = pz.container(None, width="fill", height="fill").style(background=self.fill) return pz.row([ ui.ColorPicker(value=self.fill, on_change=self.pick), pz.column([ pz.stack([ui.checkerboard(320, 360, 12), preview], width=320, height=360).style(border=(1, "border")), pz.text(repr(self.fill)).style(font="mono", size=12, color="text_muted"), ], spacing=10, width="fill"), ], spacing=20).style(padding=24, width="fill", height="fill") if __name__ == "__main__": pz.run(ColorPickerDemo) ```
#### Juego de la vida El Juego de la vida de Conway: haz clic o arrastra para pintar celdas, Espacio para reproducir o pausar, N para avanzar una generación. Miles de celdas se dibujan como una sola figura en un `pz.canvas`, y `pz.every` mueve la simulación.
Código completo: examples/game_of_life.py `examples/game_of_life.py`: ```py """Conway's Game of Life, after Iced's ``game_of_life`` demo. python examples/game_of_life.py Click or drag on the grid to paint cells, Space to play/pause, ``n`` to step once. Shows ``pz.canvas`` drawing thousands of cells in one shape and ``pz.every`` driving the simulation. """ import random import pyzed as pz from pyzed import draw, ui CELL = 10 COLS, ROWS = 80, 56 def step(alive): counts = {} for x, y in alive: for dx in (-1, 0, 1): for dy in (-1, 0, 1): if dx or dy: key = ((x + dx) % COLS, (y + dy) % ROWS) counts[key] = counts.get(key, 0) + 1 return {cell for cell, n in counts.items() if n == 3 or (n == 2 and cell in alive)} class Life(pz.App): title = "Game of Life" size = (COLS * CELL + 48, ROWS * CELL + 120) def __init__(self): self.alive = set() self.playing = False self.speed = 10 self.generation = 0 self.painting = None # True paints cells, False erases them self.randomize() def randomize(self): self.alive = {(x, y) for x in range(COLS) for y in range(ROWS) if random.random() < 0.18} self.generation = 0 def clear(self): self.alive.clear() self.generation = 0 def tick(self): self.alive = step(self.alive) self.generation += 1 def subscriptions(self): return [pz.every(1 / self.speed, self.tick, key="life")] if self.playing else [] def on_key(self, combo): if combo == "space": self.playing = not self.playing elif combo == "n": self.tick() def cell_at(self, x, y): return int(x // CELL) % COLS, int(y // CELL) % ROWS def press(self, x, y): cell = self.cell_at(x, y) self.painting = cell not in self.alive self.paint(x, y) def paint(self, x, y): if self.painting is None: return cell = self.cell_at(x, y) (self.alive.add if self.painting else self.alive.discard)(cell) def view(self): t = self.resolved_theme() grid = [draw.background(t.editor)] grid.append(draw.rects(((x * CELL, y * CELL, CELL - 1, CELL - 1) for x, y in self.alive), fill=t.accent)) board = pz.canvas( grid, width=COLS * CELL, height=ROWS * CELL, on_press=self.press, on_move=self.paint, on_release=lambda: setattr(self, "painting", None), cursor="crosshair", ) controls = pz.row([ ui.button("Pause" if self.playing else "Play", lambda: setattr(self, "playing", not self.playing), icon="pause" if self.playing else "play", variant="solid", keys="space"), ui.button("Step", self.tick, icon="step-forward", disabled=self.playing), ui.button("Random", self.randomize, icon="shuffle"), ui.button("Clear", self.clear, icon="eraser"), pz.hspace(), ui.muted(f"Speed {self.speed}/s"), pz.container(ui.slider(self.speed, lambda v: setattr(self, "speed", int(v)), min=1, max=60, step=1), width=160), ui.muted(f"Generation {self.generation} · {len(self.alive)} alive"), ], spacing=8, align="center") return pz.container(pz.column([controls, board], spacing=12), padding=16, bg=t.background, width="fill", height="fill") if __name__ == "__main__": pz.run(Life) ```
#### Descargas con progreso Varias descargas a la vez, cada una con su barra de progreso, y una ventana que nunca se congela. Cada descarga es un generador `async def`: cada valor que hace `yield` va a `update` mientras sigue corriendo en el loop de eventos de Pyzed. Cambia la transferencia simulada por streaming con `httpx` para descargar archivos reales.
Código completo: examples/download_progress.py `examples/download_progress.py`: ```py """Concurrent downloads with live progress, after Iced's ``download_progress`` demo. python examples/download_progress.py Each download is an ``async def`` generator: every value it yields goes to ``update`` while it keeps running on Pyzed's asyncio loop, so the UI never blocks. Swap the simulated transfer for ``aiohttp``/``httpx`` streaming to download real files. """ import asyncio import random import pyzed as pz from pyzed import ui async def download(index, size): """Simulates a transfer that sometimes fails; yields ``(index, event, value)``.""" received = 0 while received < size: await asyncio.sleep(random.uniform(0.02, 0.08)) received = min(size, received + random.randint(20_000, 120_000)) if random.random() < 0.004: yield index, "failed", "connection reset" return yield index, "progress", received / size yield index, "finished", size class Download: def __init__(self, index): self.index = index self.size = random.randint(2, 12) * 1_000_000 self.state = "idle" # idle | running | finished | failed self.progress = 0.0 self.error = "" class Downloads(pz.App): title = "Downloads" size = (560, 520) def __init__(self): self.downloads = [Download(0)] def start(self, item): item.state, item.progress = "running", 0.0 # Returning an async generator runs it in the background. return download(item.index, item.size) def add(self): self.downloads.append(Download(len(self.downloads))) def start_all(self): return [pz.Command.perform(self.start(d)) for d in self.downloads if d.state != "running"] def update(self, message): index, event, value = message item = self.downloads[index] if event == "progress": item.progress = value elif event == "finished": item.state, item.progress = "finished", 1.0 elif event == "failed": item.state, item.error = "failed", value def row(self, item): t = self.resolved_theme() mb = item.size / 1_000_000 if item.state == "idle": status, action = ui.muted(f"{mb:.0f} MB"), ui.button("Start", lambda: self.start(item), size="sm") elif item.state == "running": status, action = ui.muted(f"{item.progress * mb:.1f} / {mb:.0f} MB"), ui.spinner(size=14) elif item.state == "finished": status = ui.label("Done", color="success", size="sm") action = ui.icon_button("rotate-ccw", lambda: self.start(item), tooltip="Again") else: status = ui.label(f"Failed: {item.error}", color="error", size="sm") action = ui.button("Retry", lambda: self.start(item), size="sm", tint="error") return ui.card(pz.column([ pz.row([ui.label(f"file-{item.index + 1}.bin", weight="medium"), pz.hspace(), status, action], spacing=10, align="center"), ui.progress(item.progress, tint="error" if item.state == "failed" else "accent", theme=t), ], spacing=8), padding=12) def subscriptions(self): # Keep the spinners turning while something is downloading. busy = any(d.state == "running" for d in self.downloads) return [pz.every(1 / 30, lambda: None)] if busy else [] def view(self): t = self.resolved_theme() return pz.container( pz.column([ pz.row([ui.heading("Downloads", level=2), pz.hspace(), ui.button("Add", self.add, icon="plus"), ui.button("Start all", self.start_all, icon="download", variant="solid")], spacing=8, align="center"), pz.scrollable(pz.column([self.row(d) for d in self.downloads], spacing=8), height="fill"), ], spacing=16), padding=24, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(Downloads) ```
La misma idea con hilos (para código bloqueante) se explica en el tutorial [Tareas en segundo plano](https://albakore.github.io/pyzed/es/tutorials/intermediate/background-tasks/). #### Datos en vivo Un panel de cotizaciones alimentado por `pz.listen`: una suscripción respaldada por un generador async. Corre mientras `subscriptions()` la devuelva y envía cada valor que produce a `update`. ```python async def feed(): # una app real leería aquí un websocket prices = {s: random.uniform(50, 150) for s in SYMBOLS} while True: await asyncio.sleep(0.25) symbol = random.choice(SYMBOLS) prices[symbol] *= 1 + random.gauss(0, 0.01) yield symbol, prices[symbol] # cada valor va a update() class Ticker(pz.App): def subscriptions(self): return [pz.listen(feed)] if self.connected else [] # el interruptor pausa el flujo def update(self, message): symbol, price = message self.history[symbol] = (self.history[symbol] + [price])[-60:] ```
Código completo: examples/websocket_stream.py `examples/websocket_stream.py`: ```py """Live data with ``pz.listen``: a subscription backed by an async generator. python examples/websocket_stream.py ``listen`` runs the generator for as long as it is returned from ``subscriptions()`` and sends every value it yields to ``update``. Here it simulates a price feed; replace the body with ``async for message in websocket:`` to consume a real socket. """ import asyncio import random import pyzed as pz from pyzed import ui SYMBOLS = ["PYZ", "ICE", "ZED", "RST"] async def feed(): prices = {s: random.uniform(50, 150) for s in SYMBOLS} while True: await asyncio.sleep(0.25) symbol = random.choice(SYMBOLS) prices[symbol] *= 1 + random.gauss(0, 0.01) yield symbol, prices[symbol] class Ticker(pz.App): title = "Live feed" size = (720, 460) def __init__(self): self.connected = True self.history = {s: [] for s in SYMBOLS} def subscriptions(self): return [pz.listen(feed)] if self.connected else [] def update(self, message): symbol, price = message self.history[symbol] = (self.history[symbol] + [price])[-60:] def view(self): t = self.resolved_theme() cards = [] for symbol, values in self.history.items(): if len(values) < 2: cards.append(ui.stat(symbol, "…", width="fill")) continue delta = (values[-1] / values[0] - 1) * 100 cards.append(ui.stat(symbol, f"{values[-1]:.2f}", delta=delta, trend=values, width="fill")) return pz.container( pz.column([ pz.row([ui.heading("Live feed", level=2), pz.hspace(), ui.switch(self.connected, lambda v: setattr(self, "connected", v)), ui.muted("connected" if self.connected else "paused")], spacing=8, align="center"), pz.row(cards[:2], spacing=12), pz.row(cards[2:], spacing=12), ], spacing=16), padding=24, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(Ticker) ```
#### Chat con IA Un chat cuyas respuestas aparecen palabra a palabra. Habla con cualquier servidor compatible con OpenAI (Ollama, LM Studio, OpenAI...) a través de `httpx`, y tiene un modo demo con respuestas predefinidas, así que funciona sin conexión. Para usar un modelo real, define tres variables de entorno antes de ejecutarlo, por ejemplo con [Ollama](https://ollama.com) en tu máquina: `CHAT_BASE_URL=http://localhost:11434/v1` y `CHAT_MODEL=llama3.2` (más `CHAT_API_KEY` para un servicio en la nube). La [sección de IA](https://albakore.github.io/pyzed/es/ai/) explica el resto.
Código completo: examples/ai_chat.py `examples/ai_chat.py`: ```py """A streaming AI chat: any OpenAI-compatible server through httpx, with a built-in demo stream. python examples/ai_chat.py # demo: canned answers, no network CHAT_BASE_URL=http://localhost:11434/v1 CHAT_MODEL=llama3.2 python examples/ai_chat.py # Ollama CHAT_BASE_URL=https://api.openai.com/v1 CHAT_API_KEY=sk-... CHAT_MODEL= python examples/ai_chat.py The answer streams token by token: ``send`` returns an async generator, Pyzed runs it on its asyncio loop and every value it yields goes to ``update``, so the window keeps responding while the model writes. """ import asyncio import json import os import time import pyzed as pz from pyzed import ui BASE_URL = os.environ.get("CHAT_BASE_URL", "") # empty: demo mode API_KEY = os.environ.get("CHAT_API_KEY", "") MODEL = os.environ.get("CHAT_MODEL", "demo" if not BASE_URL else "llama3.2") SYSTEM = "You are a concise assistant inside a desktop app. Answer in Markdown." SUGGESTIONS = [ ("code", "Write a Python function that removes duplicates but keeps the order"), ("lightbulb", "Explain async/await like I'm new to Python"), ("table", "Compare SQLite, PostgreSQL and DuckDB in a table"), ] DEMO_ANSWER = """Sure! A `dict` keeps insertion order, so it does the work for you: ```python def unique(items): \"\"\"Removes duplicates and keeps the first occurrence of each item.\"\"\" return list(dict.fromkeys(items)) unique([3, 1, 3, 2, 1]) # [3, 1, 2] ``` - **O(n)**: one pass, no nested loops. - Works with any *hashable* item (numbers, strings, tuples). - For unhashable items such as lists, keep a `seen` list instead. """ # -- the model --------------------------------------------------------------------------------- async def openai_stream(messages, *, model=MODEL, base_url=BASE_URL, api_key=API_KEY): """Yields the answer's text pieces from an OpenAI-compatible ``/chat/completions`` endpoint.""" import httpx # only needed when talking to a real server headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} body = {"model": model, "messages": messages, "stream": True} async with httpx.AsyncClient(timeout=httpx.Timeout(60, connect=10)) as client: async with client.stream("POST", f"{base_url.rstrip('/')}/chat/completions", json=body, headers=headers) as response: if response.status_code >= 400: detail = (await response.aread()).decode(errors="replace")[:300] raise RuntimeError(f"HTTP {response.status_code}: {detail}") async for line in response.aiter_lines(): # server-sent events: "data: {...}" if not line.startswith("data:"): continue data = line[5:].strip() if data == "[DONE]": break delta = json.loads(data)["choices"][0].get("delta", {}).get("content") if delta: yield delta async def demo_stream(messages, *, delay=0.015): """A stand-in for the model: streams a canned answer a few characters at a time.""" text = DEMO_ANSWER for i in range(0, len(text), 4): await asyncio.sleep(delay) yield text[i:i + 4] def model_stream(messages): return demo_stream(messages) if not BASE_URL else openai_stream(messages) # -- the app ----------------------------------------------------------------------------------- class AIChat(pz.App): title = "AI chat" size = (980, 720) def __init__(self, stream=model_stream): self.stream = stream # a function: messages -> async iterator of text self.messages = [] # [{"role": "user" | "assistant", "content": str}] self.draft = "" self.busy = False self.error = "" self.turn = 0 # tags each reply, so pieces of a stopped one are ignored self.started = 0.0 self.tokens = 0 # -- actions -------------------------------------------------------------------------------- def send(self, text=None): text = (text if text is not None else self.draft).strip() if not text or self.busy: return None self.messages += [{"role": "user", "content": text}, {"role": "assistant", "content": ""}] self.draft, self.error, self.busy = "", "", True self.turn += 1 self.started, self.tokens = time.monotonic(), 0 history = [{"role": "system", "content": SYSTEM}] + self.messages[:-1] return [self.reply(self.turn, history), pz.Command.scroll_to_end("thread")] async def reply(self, turn, history): """Runs on Pyzed's asyncio loop; each yield goes to update() on the UI thread.""" try: async for piece in self.stream(history): if turn != self.turn: # stopped or superseded return yield ("delta", turn, piece) except Exception as error: # network errors, bad keys... shown in the thread yield ("error", turn, str(error) or type(error).__name__) yield ("done", turn, None) def stop(self): self.turn += 1 # the running reply sees a new turn and ends self.busy = False def clear(self): self.stop() self.messages, self.error = [], "" def update(self, message): kind, turn, value = message if turn != self.turn: return None if kind == "delta": self.messages[-1]["content"] += value self.tokens += 1 return pz.Command.scroll_to_end("thread") if kind == "error": self.error = value elif kind == "done": self.busy = False return None # -- view ----------------------------------------------------------------------------------- def bubble(self, index, message): last = index == len(self.messages) - 1 if message["role"] == "user": return pz.row([pz.hspace(), pz.container(ui.label(message["content"]), padding=(10, 14), max_width=560).style(background="element", radius=14)]) content = message["content"] if last and self.busy: content += " ▍" body = [ui.markdown(content) if content.strip() else pz.row([ui.spinner(size=14), ui.muted("Thinking…")], spacing=8, align="center")] if not (last and self.busy) and content: body.append(pz.row([ ui.icon_button("copy", pz.Command.copy(message["content"]), size="sm", tooltip="Copy"), ui.icon_button("refresh-cw", self.retry, size="sm", tooltip="Regenerate") if last else None, ui.icon_button("thumbs-up", None, size="sm"), ui.icon_button("thumbs-down", None, size="sm"), ], spacing=2)) avatar = pz.container(pz.icon("sparkles", size=15, color="#ffffff"), width=30, height=30, center=True).style( background=pz.Gradient(["#8b5cf6", "#ec4899"], angle=135), radius=15) return pz.row([avatar, pz.column(body, spacing=6, width="fill")], spacing=12) def retry(self): if self.messages and not self.busy: question = self.messages[-2]["content"] self.messages = self.messages[:-2] return self.send(question) return None def empty(self): cards = [pz.button(pz.row([pz.icon(icon, size=16), ui.label(text, size="sm")], spacing=10, align="center"), lambda text=text: self.send(text), width="fill", padding=(12, 14), radius=10, bg=self.resolved_theme().surface, hover_bg=self.resolved_theme().element_hover, border_color=self.resolved_theme().border_variant, border_width=1) for icon, text in SUGGESTIONS] return pz.container(pz.column([ pz.container(pz.icon("sparkles", size=26, color="#ffffff"), width=56, height=56, center=True).style( background=pz.Gradient(["#8b5cf6", "#ec4899"], angle=135), radius=16, glow=("#8b5cf6", 18)), ui.heading("How can I help?"), ui.muted(f"Model: {MODEL}" + (" · demo stream, no network" if not BASE_URL else "")), pz.space(height=8), pz.column(cards, spacing=8, width=480), ], spacing=10, align="center"), width="fill", height="fill", center=True) def composer(self): action = (ui.icon_button("square", self.stop, variant="solid", tooltip="Stop") if self.busy else ui.icon_button("arrow-up", self.send, variant="solid" if self.draft.strip() else "filled", tooltip="Send", disabled=not self.draft.strip())) return pz.container(pz.row([ ui.input(self.draft, lambda v: setattr(self, "draft", v), placeholder="Ask anything…", on_submit=self.send, borderless=True, id="prompt", size="lg"), action, ], spacing=8, align="center"), padding=(6, 8, 6, 12), max_width=760).style( background="surface", border=(1, "border"), radius=16, shadow=("#00000026", (0, 4), 16)) def view(self): speed = "" if self.tokens and self.busy: speed = f"{self.tokens / max(time.monotonic() - self.started, 0.001):.0f} chunks/s" header = ui.toolbar( [pz.icon("sparkles", size=14), ui.label("Assistant", weight="semibold"), ui.badge(MODEL, tint="info", variant="soft")], [ui.muted(speed, size="sm") if speed else None, ui.icon_button("trash-2", self.clear, size="sm", tooltip="New chat")], height=44) if self.messages: thread = pz.column([self.bubble(i, m) for i, m in enumerate(self.messages)], spacing=22, max_width=760, padding=(24, 24, 8, 24)) if self.error: thread = pz.column([thread, pz.container(ui.callout("The model failed", self.error, tint="error", icon="circle-alert"), max_width=760, padding=(0, 24))]) middle = pz.scrollable(pz.container(thread, width="fill", align_x="center"), height="fill", id="thread", anchor_bottom=True) else: middle = self.empty() return pz.column([ header, ui.divider(), middle, pz.container(self.composer(), width="fill", align_x="center", padding=(8, 24, 20, 24)), ], height="fill").style(background="editor") if __name__ == "__main__": pz.run(AIChat) ```
#### Varias ventanas Abre tantas ventanas como quieras: cada `pz.Window` tiene su propia vista, título, tamaño y tema, pero todas comparten el estado de la app, así que el contador de clics es el mismo en todas. Cerrar la ventana principal cierra la app.
Código completo: examples/multi_window.py `examples/multi_window.py`: ```py """Several windows in one app, after Iced's ``multi_window`` demo. python examples/multi_window.py Every ``pz.Window`` has its own view, title, size and theme, but they all share the app's state: the counter below is the same in every window. Closing the main window quits the app. """ import pyzed as pz from pyzed import ui class Note(pz.Window): size = (360, 260) def __init__(self, number, theme): self.number = number self.theme = theme self.title = f"Window {number}" self.text = "" def on_close(self): # The user clicked the window's close button. self.app.windows.remove(self) def dismiss(self): self.app.windows.remove(self) return self.close() def view(self): t = self.resolved_theme() return pz.container( pz.column([ ui.heading(self.title, level=3), ui.muted(f"Theme: {t.name}"), ui.input(self.text, lambda v: setattr(self, "text", v), placeholder="Type something…"), pz.row([ ui.button(f"Shared clicks: {self.app.clicks}", self.app.click, icon="mouse-pointer-click"), pz.hspace(), ui.button("Close", self.dismiss, variant="ghost"), ], align="center"), ], spacing=12), padding=20, width="fill", height="fill", bg=t.background, ) class Main(pz.App): title = "Multi window" size = (480, 360) def __init__(self): self.clicks = 0 self.windows = [] self.opened = 0 def click(self): self.clicks += 1 def open_window(self): self.opened += 1 themes = pz.themes.all() window = Note(self.opened, themes[self.opened % len(themes)]) self.windows.append(window) return window.open() def close_all(self): commands = [w.close() for w in self.windows] self.windows = [] return commands def view(self): t = self.resolved_theme() items = [ui.list_item(w.title, None, icon="app-window", end=ui.muted(w.text or "empty")) for w in self.windows] return pz.container( pz.column([ ui.heading("Windows", level=2), pz.row([ui.button("Open window", self.open_window, icon="plus", variant="solid"), ui.button("Close all", self.close_all, disabled=not self.windows), ui.button(f"Clicks: {self.clicks}", self.click)], spacing=8), pz.column(items) if items else ui.muted("No extra windows yet."), ], spacing=14), padding=24, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(Main) ```
#### Paneles Paneles redimensionables como los de un editor: divide cualquier panel a la derecha o hacia abajo, arrastra los divisores y cierra paneles. La disposición es un pequeño árbol de listas guardado en el estado y dibujado con `ui.split` anidados.
Código completo: examples/panes.py `examples/panes.py`: ```py """Resizable panes, after Iced's ``pane_grid`` demo, using nested ``ui.split``. python examples/panes.py Drag the dividers; each pane can be split again or closed. The layout is a small tree of ``("h" | "v", ratio, first, second)`` tuples kept in your state. """ from itertools import count import pyzed as pz from pyzed import ui _ids = count(1) class Panes(pz.App): title = "Panes" size = (1000, 680) def __init__(self): self.layout = next(_ids) # a leaf is a pane number self.focus = self.layout def replace(self, node, target, new): if node == target: return new if isinstance(node, list): node[2] = self.replace(node[2], target, new) node[3] = self.replace(node[3], target, new) return node def remove(self, node, target): if isinstance(node, list): if node[2] == target: return node[3] if node[3] == target: return node[2] node[2] = self.remove(node[2], target) node[3] = self.remove(node[3], target) return node def split(self, pane, vertical): new = next(_ids) self.layout = self.replace(self.layout, pane, ["v" if vertical else "h", 0.5, pane, new]) self.focus = new def close(self, pane): if self.layout != pane: self.layout = self.remove(self.layout, pane) self.focus = None def pane(self, number): t = self.resolved_theme() focused = number == self.focus header = pz.container( pz.row([ ui.label(f"Pane {number}", weight="medium", color="text" if focused else "text_muted"), pz.hspace(), ui.icon_button("columns-2", lambda: self.split(number, False), size="sm", tooltip="Split right"), ui.icon_button("rows-2", lambda: self.split(number, True), size="sm", tooltip="Split down"), ui.icon_button("x", lambda: self.close(number), size="sm", tooltip="Close", disabled=self.layout == number), ], spacing=2, align="center"), padding=(4, 8), width="fill", bg=t.tab_active if focused else t.tab_inactive, ) body = pz.mouse_area(pz.center(ui.muted("Click to focus")), on_press=lambda: setattr(self, "focus", number)) return pz.container(pz.column([header, body]), width="fill", height="fill", bg=t.editor, border_color=t.border_focused if focused else t.border_variant, border_width=1) def render(self, node): if not isinstance(node, list): return self.pane(node) direction, ratio, first, second = node def resize(r, node=node): node[1] = r return ui.split(self.render(first), self.render(second), ratio=ratio, on_resize=resize, vertical=direction == "v", min_size=120) def view(self): return pz.container(self.render(self.layout), padding=8, width="fill", height="fill", bg=self.resolved_theme().background) if __name__ == "__main__": pz.run(Panes) ```
#### Menú contextual Un menú de clic derecho hecho con `ui.popover` y `ui.menu`. `mouse_area(on_move=...)` recuerda dónde está el puntero, así que el menú se abre justo ahí; hacer clic en otro sitio o pulsar Esc lo cierra.
Código completo: examples/context_menu.py `examples/context_menu.py`: ```py """A right-click context menu built from ``ui.popover`` and ``ui.menu``. python examples/context_menu.py The pointer position is remembered with ``mouse_area(on_move=...)`` so the menu opens where you clicked. Clicking anywhere else (or pressing Escape) closes it. """ import pyzed as pz from pyzed import ui class ContextMenu(pz.App): title = "Context menu" size = (640, 420) def __init__(self): self.pointer = (0, 0) self.menu_at = None self.log = ["Right-click anywhere in the box."] self.soft_wrap = True def act(self, what): def run(): self.log.append(what) self.menu_at = None return run def toggle_wrap(self): self.soft_wrap = not self.soft_wrap self.menu_at = None def on_key(self, combo): if combo == "escape": self.menu_at = None def view(self): t = self.resolved_theme() area = pz.mouse_area( pz.container(pz.column([ui.muted(line) for line in self.log[-12:]], spacing=4), padding=16, width="fill", height="fill", bg=t.editor, border_color=t.border_variant, border_width=1, radius=t.radius), on_move=lambda x, y: setattr(self, "pointer", (x, y)), on_right_press=lambda: setattr(self, "menu_at", self.pointer), ) menu = None if self.menu_at: menu = ui.menu([ ui.MenuItem("Cut", self.act("cut"), icon="scissors", keys="ctrl-x"), ui.MenuItem("Copy", self.act("copy"), icon="copy", keys="ctrl-c"), ui.MenuItem("Paste", self.act("paste"), icon="clipboard", keys="ctrl-v"), ui.SEPARATOR, ui.MenuItem("Soft wrap", self.toggle_wrap, checked=self.soft_wrap), ui.SEPARATOR, ui.MenuItem("Delete", self.act("delete"), icon="trash-2", danger=True), ], width=220) x, y = self.menu_at or (0, 0) return pz.container( ui.popover(area, menu, x=x + 16, y=y + 16, on_dismiss=lambda: setattr(self, "menu_at", None)), padding=16, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(ContextMenu) ```
#### Componentes propios Tres piezas reutilizables escritas en código: una valoración con estrellas y un editor de etiquetas (clases `pz.Component` con propiedades, su propio estado y sus propios eventos), y una nota que puedes arrastrar, a la que puedes hacer doble clic y escribirle, que registra los eventos comunes de todos los widgets.
Código completo: examples/components.py `examples/components.py`: ```py """Custom components and events: a star rating, a tag editor and a draggable note. python examples/components.py - ``Rating`` and ``TagInput`` are ``pz.Component`` classes: props, their own state, their own events (``change``, ``add``, ``remove``), reusable anywhere. - The note uses the common events every widget has: drag it around, double-click to pin it, hover it, click it and press keys (they show up in the log). """ import pyzed as pz from pyzed import ui class Rating(pz.Component): """Stars the user can click; shows a preview while hovering.""" value: int = 0 max: int = 5 events = {"change": "A star was picked (``event.value`` is the new rating)."} icon = "star" size = (140, 28) def setup(self): self.hover = None # state: which star is under the pointer def render(self): t = pz.theme.current() shown = self.hover or self.value stars = [] for i in range(1, self.max + 1): color = t.warning if i <= shown else t.text_disabled stars.append( pz.icon("star", size=20, color=color).on( click=lambda i=i: self.emit("change", i), mouse_enter=lambda i=i: setattr(self, "hover", i), mouse_leave=lambda: setattr(self, "hover", None), ) ) return pz.row(stars, spacing=2) class TagInput(pz.Component): """A list of tags with a box to add more.""" tags: list = () placeholder: str = "Add a tag…" events = {"add": "Enter was pressed (``event.value`` is the text).", "remove": "A tag's x was clicked (``event.value`` is the tag)."} icon = "tags" size = (280, 64) def setup(self): self.draft = "" def submit(self): text, self.draft = self.draft.strip(), "" return self.emit("add", text) if text else None def render(self): chips = [ui.chip(tag, on_remove=lambda tag=tag: self.emit("remove", tag)) for tag in self.tags] return pz.column([ pz.row(chips, spacing=6, wrap=True, width="fill") if chips else ui.label("No tags yet", color="text_muted", size="sm"), ui.input(self.draft, lambda text: setattr(self, "draft", text), placeholder=self.placeholder, on_submit=self.submit, icon="tag"), ], spacing=8) class Demo(pz.App): title = "Components and events" size = (720, 460) def __init__(self): self.rating = 3 self.tags = ["python", "rust"] self.note = (20, 40) self.pinned = False self.log = ["Drag the note, double-click it, click it and type."] def record(self, event): detail = event.key or (f"{event.dx:+.0f}, {event.dy:+.0f}" if event.type == "drag" else "") self.log = [f"{event.type} {detail}".strip(), *self.log][:9] def move(self, event): if not self.pinned: x, y = self.note self.note = (x + event.dx, y + event.dy) def view(self): t = self.resolved_theme() form = ui.card(pz.column([ ui.field("Rating", Rating(value=self.rating, on_change=lambda e: setattr(self, "rating", e.value))), ui.label(f"{self.rating} of 5", color="text_muted", size="sm"), ui.label("Tags"), TagInput( tags=self.tags, on_add=lambda e: setattr(self, "tags", [*self.tags, e.value]), on_remove=lambda e: setattr(self, "tags", [x for x in self.tags if x != e.value]), ), ], spacing=14), title="Components", width=320) note = pz.container( pz.column([ pz.row([pz.icon("pin" if self.pinned else "grip", size=14, color=t.text_muted), ui.label("Pinned" if self.pinned else "Drag me", weight="semibold")], spacing=6, align="center"), ui.label("Double-click to pin", size="sm", color="text_muted"), ], spacing=4), width=180, padding=12, bg=t.elevated, border_color=t.accent if self.pinned else t.border, border_width=1, radius=t.radius, ).on( drag=self.move, drag_end=self.record, double_click=lambda: setattr(self, "pinned", not self.pinned), click=self.record, key_down=self.record, focus=self.record, blur=self.record, cursor="pointer" if self.pinned else "grab", ) log = ui.card(pz.column([ui.label(line, mono=True, size="sm") for line in self.log], spacing=2), title="Event log", width=320) x, y = self.note canvas = pz.stack([ pz.container(width="fill", height="fill"), pz.pin(note, x=x, y=y), ], width="fill", height="fill") return pz.row([pz.column([form, log], spacing=16), canvas], spacing=24, padding=24) if __name__ == "__main__": pz.run(Demo) ```
Para dibujar componentes con el mouse, sigue [Un componente propio](https://albakore.github.io/pyzed/es/tutorials/intermediate/custom-component/). #### Todos los componentes Un recorrido por los componentes listos de `pyzed.ui`: botones, campos, pestañas, un editor de código, menús, árboles, notificaciones, formularios, avisos y datos, con un selector de tema para verlos en cualquier tema.
Código completo: examples/gallery.py `examples/gallery.py`: ```py """A tour of the ready-made components in ``pyzed.ui``. python examples/gallery.py """ import pyzed as pz from pyzed import ui SNIPPET = '''from pyzed import ui def view(self): return ui.button("Deploy", self.deploy) ''' class Gallery(pz.App): title = "Pyzed components" size = (1440, 900) def __init__(self, theme=pz.themes.ONE_DARK): self.theme = theme self.name = "Pyzed" self.query = "" self.format_on_save = True self.vim_mode = False self.telemetry = True self.font_size = 0.55 self.view_mode = "Split" self.active_tab = "main.py" self.selected_file = "src/app.py" self.expanded = {"src"} self.language = "Python" self.size_choice = "Medium" self.tab_size = 4 self.tags = {"rust", "python", "ui"} self.advanced_open = True self.sort_by = "Size" self.descending = True self.selected_row = None def set(self, field): return lambda value: setattr(self, field, value) def section(self, title, *children, spacing=10): return ui.card(pz.column(list(children), spacing=spacing), title=title, width="fill") def view(self): t = self.theme buttons = self.section( "Buttons", pz.row([ ui.button("Filled", lambda: None), ui.button("Ghost", lambda: None, variant="ghost"), ui.button("Outline", lambda: None, variant="outline"), ui.button("Solid", lambda: None, variant="solid"), ui.button("Disabled", disabled=True), ], spacing=8), pz.row([ ui.button("Error", lambda: None, variant="error", icon="circle-x"), ui.button("Warning", lambda: None, variant="warning", icon="triangle-alert"), ui.button("Success", lambda: None, variant="success", icon="circle-check"), ui.button("Save", lambda: None, icon="save", keys="ctrl-s"), ], spacing=8), pz.row([ ui.button("XS", lambda: None, size="xs"), ui.button("Small", lambda: None, size="sm"), ui.button("Medium", lambda: None, size="md"), ui.button("Large", lambda: None, size="lg", icon="rocket", variant="solid"), ], spacing=8, align="center"), pz.row([ ui.icon_button("files", lambda: None, tooltip="Project"), ui.icon_button("search", lambda: None, selected=True), ui.icon_button("git-branch", lambda: None), ui.icon_button("bug", lambda: None), ui.icon_button("settings", lambda: None, variant="filled"), pz.space(width=12), ui.avatar("KV", index=0), ui.avatar("AL", index=3), ui.avatar("MZ", index=7), pz.space(width=12), ui.indicator("success"), ui.indicator("warning"), ui.indicator("error"), ], spacing=6, align="center"), ) badges = self.section( "Badges & key bindings", pz.row([ ui.badge("New"), ui.badge("Beta", tint="warning"), ui.badge("Stable", tint="success", icon="check"), ui.badge("Deprecated", tint="error", variant="outline"), ui.badge("v0.1.0", tint="accent", variant="solid"), ], spacing=6), pz.row([ui.kbd("ctrl-shift-p"), ui.kbd("ctrl-k ctrl-s"), ui.kbd("alt-enter"), ui.kbd("escape")], spacing=12, align="center"), ) inputs = self.section( "Inputs", ui.input(self.name, self.set("name"), placeholder="Project name"), ui.search_input(self.query, self.set("query"), placeholder="Search symbols…"), ui.field("Format on save", ui.switch(self.format_on_save, self.set("format_on_save")), description="Run the formatter whenever a buffer is saved"), ui.field("Vim mode", ui.switch(self.vim_mode, self.set("vim_mode"))), ui.checkbox(self.telemetry, self.set("telemetry"), label="Send anonymous usage data"), pz.row([ui.label("Font size", size="sm", color="text_muted"), ui.slider(self.font_size, self.set("font_size"))], spacing=12, align="center"), pz.row([ui.label("Indexing", size="sm", color="text_muted"), ui.progress(0.68)], spacing=12, align="center"), ui.segmented(["Unified", "Split", "Inline"], self.view_mode, self.set("view_mode")), spacing=12, ) menu = self.section( "Context menu", ui.menu([ ui.MenuItem("Cut", lambda: None, icon="scissors", keys="ctrl-x"), ui.MenuItem("Copy", lambda: None, icon="copy", keys="ctrl-c"), ui.MenuItem("Paste", lambda: None, icon="clipboard", keys="ctrl-v"), ui.SEPARATOR, ui.MenuItem("Soft wrap", lambda: None, checked=True), ui.MenuItem("Show minimap", lambda: None, checked=False), ui.SEPARATOR, ui.MenuItem("Delete file", lambda: None, icon="trash-2", danger=True), ], width=280), ) files = ui.TreeNode.from_paths( ["src/app.py", "src/views/editor.py", "src/views/panel.py", "tests/test_app.py", "pyproject.toml", "README.md"], {"src/app.py": "modified", "tests/test_app.py": "created"}, ) tree = self.section( "Tree & list", ui.tree(files, expanded=self.expanded, selected=self.selected_file, on_toggle=lambda f: self.expanded.symmetric_difference_update({f}), on_select=self.set("selected_file")), ui.divider(), ui.list_item("Recent projects", icon="history", end=ui.badge("3"), on_press=lambda: None), ui.list_item("Extensions", icon="blocks", end=ui.kbd("ctrl-shift-x"), on_press=lambda: None), spacing=6, ) tabs = self.section( "Tabs, breadcrumbs & editor", pz.container( pz.column([ ui.tab_bar( [ui.Tab.for_file("main.py"), ui.Tab.for_file("theme.rs", dirty=True), ui.Tab.for_file("README.md", preview=True)], self.active_tab, on_select=self.set("active_tab"), on_close=lambda _: None, end=[ui.icon_button("plus", lambda: None, size="sm")], ), ui.toolbar([ui.breadcrumbs(["examples", self.active_tab, "def view"])]), pz.container(ui.editor("gallery-snippet", SNIPPET, language="py"), height=150), ]), border_color=t.border, border_width=1, radius=t.radius, clip=True, ), ) toasts = self.section( "Notifications", ui.toast("Update available", "Pyzed 0.2 is ready to install.", kind="info", actions=[ui.button("Restart", lambda: None, size="sm", variant="solid"), ui.button("Later", lambda: None, size="sm", variant="ghost")], width=None), ui.toast("Saved 3 files", kind="success", on_dismiss=lambda: None, width=None), ui.toast("Language server crashed", "rust-analyzer exited with code 101.", kind="error", on_dismiss=lambda: None, width=None), ) forms = self.section( "Forms", ui.field("Language", ui.select(["Python", "Rust", "TypeScript", "Go"], self.language, self.set("language"), width=160)), ui.field("Tab size", ui.number_field(self.tab_size, self.set("tab_size"), min=1, max=8)), ui.radio_group(["Small", "Medium", "Large"], self.size_choice, self.set("size_choice"), horizontal=True), pz.row([ui.chip(tag, on_remove=lambda tag=tag: self.tags.discard(tag)) for tag in sorted(self.tags)], spacing=6), ui.disclosure("Advanced", ui.muted("Settings that most people never need."), open=self.advanced_open, on_toggle=lambda: setattr(self, "advanced_open", not self.advanced_open)), pz.row([ui.split_button("Run", lambda: None, lambda: None, icon="play"), ui.toggle_group([("list", "list"), ("grid", "layout-grid")], "list", lambda v: None)], spacing=8), ) feedback = self.section( "Feedback", ui.callout("Update available", "Pyzed 0.2 is ready.", tint="info", actions=[ui.button("Update", lambda: None, size="sm", variant="solid")]), ui.callout("Build failed", "3 errors in src/main.rs", tint="error"), pz.row([ui.count_badge(3), ui.count_badge(128, tint="error"), ui.facepile(["KV", "AL", "MZ", "JD", "RS", "TT"]), ui.diff_stat(42, 7)], spacing=12, align="center"), ui.keybinding_hint("Open file", "ctrl-p"), ) files = [dict(name="main.rs", size=1200, lang="Rust"), dict(name="app.py", size=840, lang="Python"), dict(name="README.md", size=310, lang="Markdown"), dict(name="theme.json", size=96, lang="JSON")] def sort(column): self.descending = not self.descending if self.sort_by == column else False self.sort_by = column data = self.section( "Data", ui.table([ui.Column("Name", "name"), ui.Column("Size", "size", width=70, align="end"), ui.Column("Language", "lang")], files, sort_by=self.sort_by, descending=self.descending, on_sort=sort, on_row_press=self.set("selected_row"), selected=self.selected_row, height=156), pz.row([ui.stat("Requests", "12.4k", delta=4.2, trend=[3, 5, 4, 6, 8, 7, 9]), ui.donut([5, 3, 2], label="62%", size=88)], spacing=12, align="center"), ui.bar_chart([4, 8, 6, 9, 3], labels=["Mon", "Tue", "Wed", "Thu", "Fri"], width=300, height=120), ) theme_picker = ui.select(pz.themes.names(), t.name, lambda name: setattr(self, "theme", pz.themes.get(name)), width=200) header = pz.row([ pz.icon("layers", size=22, color=t.accent), ui.heading("Pyzed components", level=2), pz.hspace(), theme_picker, ], spacing=10, align="center") grid = pz.row([ pz.column([buttons, badges, menu], spacing=16, width="fill"), pz.column([inputs, tree], spacing=16, width="fill"), pz.column([tabs, toasts], spacing=16, width="fill"), ], spacing=16) more = pz.row([ pz.column([forms], width="fill"), pz.column([feedback], width="fill"), pz.column([data], width="fill"), ], spacing=16) return pz.container( pz.scrollable(pz.column([header, grid, more], spacing=20, padding=24), height="fill"), width="fill", height="fill", bg=t.background if not t.is_dark else t.editor, color=t.text, ) if __name__ == "__main__": pz.run(Gallery) ```
#### Contador La app de Pyzed más pequeña: un número y dos botones. Es el mejor archivo para empezar cuando haces algo nuevo.
Código completo: examples/counter.py `examples/counter.py`: ```py """The smallest Pyzed app: state + update + view.""" import pyzed as pz from pyzed import ui class Counter(pz.App): title = "Counter" size = (360, 220) def __init__(self): self.count = 0 def update(self, message): if message == "increment": self.count += 1 elif message == "decrement": self.count -= 1 def view(self): return pz.center( pz.column( [ ui.heading(str(self.count)), pz.row( [ ui.button("1", "decrement", icon="minus"), ui.button("1", "increment", icon="plus", variant="solid"), ui.icon_button("rotate-ccw", lambda: setattr(self, "count", 0), tooltip="Reset"), ], spacing=8, ), ], spacing=16, align="center", ), bg=self.theme.editor, ) if __name__ == "__main__": pz.run(Counter) ```
#### Editor de proyectos Un workspace de editor de código completo en unas 450 líneas: un panel de proyecto con los archivos de una carpeta, pestañas, un editor con resaltado de sintaxis (editar, deshacer, guardar con Ctrl+S), colores de estado de git, un panel de estructura, cambio de tema, una paleta de comandos (Ctrl+Shift+P) y un buscador de archivos (Ctrl+P). Su aspecto recrea el del editor [Zed](https://zed.dev), cuya interfaz inspiró el tema por defecto de Pyzed. El panel de terminal muestra una salida de ejemplo; no ejecuta una shell. Pasa una carpeta para abrirla: `python examples/zed_editor.py ruta/al/proyecto`. Una versión más pequeña se construye paso a paso en el tutorial del [mini editor de código](https://albakore.github.io/pyzed/es/tutorials/advanced/code-editor/).
Código completo: examples/zed_editor.py `examples/zed_editor.py`: ```py """A recreation of the Zed editor written with Pyzed. python examples/zed_editor.py [folder] Real features: project panel over the folder's files, tabs, a syntax-highlighted editor (edit, undo, save with ctrl-s), git status colors, an outline panel, theme switching, the command palette (ctrl-shift-p) and the file finder (ctrl-p). The terminal panel shows static sample output; it does not run a shell. """ from __future__ import annotations import re import subprocess import sys from pathlib import Path from typing import Dict, List, Optional, Tuple import pyzed as pz from pyzed import Command, ui IGNORED_SUFFIXES = (".so", ".pyd", ".dll", ".dylib", ".pyc") IGNORED = {".git", "target", ".venv", "venv", "__pycache__", "node_modules", ".pytest_cache", "dist", ".mypy_cache"} MAX_FILES = 3000 TERMINAL_SAMPLE = ( "\x1b[32m~/pyzed\x1b[0m \x1b[34mmain\x1b[0m $ maturin develop --release\n" "🔗 Found pyo3 bindings with abi3 support\n" "\x1b[32m Compiling\x1b[0m pyzed v0.1.0 (~/pyzed)\n" "\x1b[32m Finished\x1b[0m `release` profile [optimized] target(s) in 41.20s\n" "📦 Built wheel for abi3 Python ≥ 3.9\n" "🛠 Installed pyzed-0.1.0\n" "\x1b[32m~/pyzed\x1b[0m \x1b[34mmain\x1b[0m $ python examples/zed_editor.py\n" ) # --------------------------------------------------------------------------- # Project helpers # --------------------------------------------------------------------------- def scan(root: Path) -> List[str]: files: List[str] = [] for path in sorted(root.rglob("*")): rel = path.relative_to(root) if any(part in IGNORED for part in rel.parts) or path.suffix in IGNORED_SUFFIXES or not path.is_file(): continue files.append(rel.as_posix()) if len(files) >= MAX_FILES: break return files def git(root: Path, *args: str) -> str: try: return subprocess.run(["git", *args], cwd=root, capture_output=True, text=True, timeout=5).stdout except (OSError, subprocess.SubprocessError): return "" def git_status(root: Path) -> Dict[str, str]: status = {} for line in git(root, "status", "--porcelain").splitlines(): code, path = line[:2], line[3:].strip() if "D" in code: status[path] = "deleted" elif "?" in code or "A" in code: status[path] = "created" elif code.strip(): status[path] = "modified" return status def git_markers(root: Path, path: str) -> Dict[int, str]: markers: Dict[int, str] = {} for hunk in re.finditer(r"^@@ -\d+(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", git(root, "diff", "-U0", "--", path), re.M): removed, start, added = int(hunk.group(1) or 1), int(hunk.group(2)), int(hunk.group(3) or 1) if added == 0: markers[max(start, 1)] = "deleted" for line in range(start, start + added): markers[line] = "created" if removed == 0 else "modified" return markers SYMBOLS = re.compile( r"^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?Pfn|struct|enum|trait|impl|mod|def|class)\s+(?P[A-Za-z_]\w*)", ) def outline(text: str) -> List[Tuple[int, str, str]]: out = [] for number, line in enumerate(text.splitlines(), start=1): match = SYMBOLS.match(line) if match: out.append((number, match["kind"], match["name"].strip())) return out # --------------------------------------------------------------------------- # The app # --------------------------------------------------------------------------- class Zed(pz.App): title = "Pyzed — a Zed-like editor in Python" size = (1440, 900) min_size = (800, 500) def __init__(self, root: str = ".", *, status: Optional[Dict[str, str]] = None): self.root = Path(root).resolve() self.files = scan(self.root) self.status = git_status(self.root) if status is None else status self.nodes = [ui.TreeNode(self.root.name, children=ui.TreeNode.from_paths(self.files, self.status), id="")] self.expanded = {""} self.selected: Optional[str] = None self.tabs: List[str] = [] self.active: Optional[str] = None self.texts: Dict[str, str] = {} self.markers: Dict[str, Dict[int, str]] = {} self.dirty: set = set() self.cursor = (1, 1) self.show_left, self.show_bottom, self.show_right = True, False, False self.palette: Optional[str] = None # "commands" | "files" self.query, self.choice = "", 0 self.notices: List[Tuple[str, str]] = [] self.theme = pz.themes.ONE_DARK def on_start(self): commands = [] if self.active and self.cursor != (1, 1): commands.append(Command.set_editor_cursor(self.active, *self.cursor)) if self.palette: commands.append(Command.focus("palette")) return commands # -- files ------------------------------------------------------------- def open_file(self, path: str): if path not in self.texts: try: self.texts[path] = (self.root / path).read_text(encoding="utf-8") except (UnicodeDecodeError, OSError): self.notify("Can't open binary file", "warning") return self.markers.setdefault(path, git_markers(self.root, path)) if path not in self.tabs: self.tabs.append(path) self.active = self.selected = path parts = path.split("/") self.expanded.update("/".join(parts[:i]) for i in range(1, len(parts))) self.cursor = (1, 1) def close_tab(self, path: str): if path in self.tabs: index = self.tabs.index(path) self.tabs.remove(path) if self.active == path: self.active = self.tabs[min(index, len(self.tabs) - 1)] if self.tabs else None def edited(self, path: str, text: str): self.texts[path] = text self.dirty.add(path) def save(self): if self.active: (self.root / self.active).write_text(self.texts[self.active], encoding="utf-8") self.dirty.discard(self.active) self.notify(f"Saved {self.active}", "success") def toggle_folder(self, folder: str): self.expanded.symmetric_difference_update({folder}) self.selected = folder def notify(self, text: str, kind: str = "info"): self.notices = (self.notices + [(text, kind)])[-3:] # -- palette ----------------------------------------------------------- def commands(self) -> List[ui.PaletteItem]: def run(action): def handler(): self.palette = None return action() return handler def set_theme(theme): return run(lambda: setattr(self, "theme", theme)) return [ ui.PaletteItem("file finder: toggle", run(self.open_finder), keys="ctrl-p"), ui.PaletteItem("workspace: toggle left dock", run(self.toggle_left), keys="ctrl-b"), ui.PaletteItem("workspace: toggle bottom dock", run(self.toggle_bottom), keys="ctrl-j"), ui.PaletteItem("workspace: toggle right dock", run(self.toggle_right), keys="ctrl-alt-b"), ui.PaletteItem("workspace: save", run(self.save), keys="ctrl-s"), ui.PaletteItem("pane: close active item", run(lambda: self.active and self.close_tab(self.active)), keys="ctrl-w"), ui.PaletteItem("theme selector: one dark", set_theme(pz.themes.ONE_DARK)), ui.PaletteItem("theme selector: one light", set_theme(pz.themes.ONE_LIGHT)), ui.PaletteItem("theme selector: gruvbox dark", set_theme(pz.themes.GRUVBOX_DARK)), ui.PaletteItem("terminal panel: toggle focus", run(self.toggle_bottom), keys="ctrl-`"), ui.PaletteItem("outline panel: toggle", run(self.toggle_right)), ui.PaletteItem("zed: about", run(lambda: self.notify("Pyzed 0.1 · Iced 0.14 · Python " + sys.version.split()[0]))), ui.PaletteItem("zed: quit", run(Command.exit), keys="ctrl-q"), ] def palette_matches(self): if self.palette == "files": items = [ ui.PaletteItem(path, (lambda p=path: self.pick_file(p)), icon=pz.icons.for_file(path)[0]) for path in self.files ] return ui.fuzzy_filter(self.query, items, key=lambda i: i.label)[:200] return ui.fuzzy_filter(self.query, self.commands(), key=lambda i: i.label) def pick_file(self, path: str): self.palette = None self.open_file(path) def open_palette(self, kind: str = "commands"): self.palette, self.query, self.choice = kind, "", 0 return Command.focus("palette") def open_finder(self): return self.open_palette("files") def set_query(self, query: str): self.query, self.choice = query, 0 def submit(self): matches = self.palette_matches() if matches: return matches[min(self.choice, len(matches) - 1)][0].on_press() def toggle_left(self): self.show_left = not self.show_left def toggle_bottom(self): self.show_bottom = not self.show_bottom def toggle_right(self): self.show_right = not self.show_right def on_key(self, combo: str): combo = combo.replace("cmd-", "ctrl-") if combo == "ctrl-shift-p": if self.palette == "commands": self.palette = None return None return self.open_palette() if combo == "ctrl-p": return self.open_finder() if self.palette: if combo == "escape": self.palette = None elif combo in ("down", "ctrl-n"): self.choice += 1 elif combo in ("up", "ctrl-p"): self.choice = max(0, self.choice - 1) return None actions = { "ctrl-b": self.toggle_left, "ctrl-j": self.toggle_bottom, "ctrl-`": self.toggle_bottom, "ctrl-alt-b": self.toggle_right, "ctrl-s": self.save, "ctrl-w": lambda: self.active and self.close_tab(self.active), "ctrl-q": Command.exit, } if combo in actions: return actions[combo]() # -- view -------------------------------------------------------------- def view(self): t = self.theme layout = ui.workspace( self.center(), title_bar=ui.title_bar( [ ui.title_button(self.root.name, chevron=True), ui.title_button("main", icon="git-branch", muted=True), ], [ ui.icon_button("bell", tooltip="Notifications"), ui.icon_button("sparkles", tooltip="Agent Panel"), pz.space(width=4), ui.avatar("KV", index=0), pz.space(width=4), ], ), left=self.project_panel() if self.show_left else None, right=self.outline_panel() if self.show_right else None, bottom=self.terminal_panel() if self.show_bottom else None, status_bar=self.status_bar(), left_width=250, right_width=260, ) if self.notices: layout = ui.toasts(layout, [ ui.toast(text, kind=kind, on_dismiss=(lambda i=i: self.notices.pop(i))) for i, (text, kind) in enumerate(self.notices) ]) if self.palette: matches = self.palette_matches() self.choice = min(self.choice, max(len(matches) - 1, 0)) overlay = ui.picker( self.query, self.set_query, matches, selected=self.choice, input_id="palette", on_submit=self.submit, placeholder="Execute a command…" if self.palette == "commands" else "Search project files…", ) layout = ui.layer(layout, overlay, on_dismiss=lambda: setattr(self, "palette", None), backdrop=pz.theme.alpha("#000000", 0.12 if t.is_dark else 0.04)) return layout def project_panel(self): return pz.scrollable( pz.container( ui.tree(self.nodes, expanded=self.expanded, selected=self.selected, on_toggle=self.toggle_folder, on_select=self.open_file), padding=(4, 0), ), height="fill", scroller=self.theme.scrollbar, scrollbar_width=6, ) def outline_panel(self): symbols = outline(self.texts.get(self.active or "", "")) rows = [ ui.list_item( pz.row([ui.label(kind, color=self.theme.syntax["keyword"], size="sm", mono=True), ui.label(name, mono=True, size="sm")], spacing=6), lambda: None, height=24, indent=8, ) for _, kind, name in symbols ] return pz.column([ ui.panel_header("Outline", [ui.icon_button("x", self.toggle_right, size="sm")], icon="list-tree"), pz.scrollable(pz.column(rows or [ui.muted("No symbols", size="sm")], padding=(0, 0, 8, 0)), height="fill"), ]) def terminal_panel(self): tabs = pz.row([ pz.container( pz.row([pz.icon("terminal", size=14), ui.label("zsh", size="sm")], spacing=6, align="center"), padding=(3, 10), bg=self.theme.editor, radius=self.theme.radius_sm, ), pz.hspace(), ui.icon_button("plus", size="sm"), ui.icon_button("maximize-2", size="sm"), ui.icon_button("x", self.toggle_bottom, size="sm"), ], align="center", padding=(4, 6)) return pz.column([tabs, ui.terminal(TERMINAL_SAMPLE)]) def center(self): if not self.active: return self.welcome() symbols = outline(self.texts.get(self.active, "")) scope = [f"{kind} {name}" for line, kind, name in symbols if line <= self.cursor[0]][-1:] tabs = ui.tab_bar( [ui.Tab.for_file(p, dirty=p in self.dirty, status=self.status.get(p)) for p in self.tabs], self.active, on_select=self.select_tab, on_close=self.close_tab, start=[ui.icon_button("arrow-left", size="sm"), ui.icon_button("arrow-right", size="sm")], end=[ui.icon_button("plus", size="sm"), ui.icon_button("columns-2", size="sm")], ) toolbar = ui.toolbar( [ui.breadcrumbs([self.active, *scope])], [ ui.icon_button("sparkles", size="sm", tooltip="Inline Assist"), ui.icon_button("text-search", size="sm", tooltip="Buffer Search"), ui.icon_button("ellipsis", size="sm"), ], ) editor = ui.editor( self.active, self.texts[self.active], path=self.active, markers=self.markers.get(self.active), on_change=lambda text, p=self.active: self.edited(p, text), on_cursor=self.move_cursor, ) return pz.column([tabs, toolbar, editor]) def select_tab(self, path: str): self.active = self.selected = path def move_cursor(self, line: int, column: int): self.cursor = (line, column) def welcome(self): rows = [ ("Open a File", "file", "ctrl-p", self.open_finder), ("Command Palette", "command", "ctrl-shift-p", self.open_palette), ("Toggle Project Panel", "panel-left", "ctrl-b", self.toggle_left), ("Toggle Terminal", "terminal", "ctrl-j", self.toggle_bottom), ] return pz.center( pz.column( [ pz.row([pz.icon("zap", size=30, color=self.theme.accent), ui.heading("Pyzed")], spacing=10, align="center"), ui.muted("Code at the speed of thought — now in Python"), pz.space(height=18), *[ ui.button(label, action, icon=icon, variant="ghost", full_width=True, keys=keys) for label, icon, keys, action in rows ], ], spacing=4, width=340, align="center", ) ) def status_bar(self): language = pz.icons.language_for(self.active) if self.active else None return ui.status_bar( [ ui.status_item(icon="panel-left", on_press=self.toggle_left, selected=self.show_left, tooltip="Project Panel", keys="ctrl-b"), ui.status_item(icon="git-branch", on_press=lambda: None), ui.status_item(icon="search", on_press=self.open_finder, tooltip="File Finder", keys="ctrl-p"), pz.space(width=6), ui.status_item("0", icon="circle-x", on_press=lambda: None), ui.status_item("0", icon="triangle-alert", on_press=lambda: None), ], [ ui.status_item(f"{self.cursor[0]}:{self.cursor[1]}", on_press=lambda: None) if self.active else None, ui.status_item(language, on_press=lambda: None) if language else None, ui.status_item(icon="sparkles", on_press=lambda: None), ui.status_item(icon="bell", on_press=lambda: None), ui.status_item(icon="panel-bottom", on_press=self.toggle_bottom, selected=self.show_bottom, tooltip="Terminal", keys="ctrl-j"), ui.status_item(icon="panel-right", on_press=self.toggle_right, selected=self.show_right, tooltip="Outline", keys="ctrl-alt-b"), ], ) if __name__ == "__main__": Zed(sys.argv[1] if len(sys.argv) > 1 else ".").run() ```
**Sobre esta página:** Cada app de aquí se dibuja fuera de pantalla en la suite de tests, así que funcionan con la versión actual de Pyzed. Se están añadiendo grabaciones cortas de las apps que se mueven (Juego de la vida, descargas, varias ventanas). ### Formularios > Formularios de escritorio en Python hechos con Pyzed: inicio de sesión, registro con validación, un formulario de contacto, pantallas de bienvenida y de ajustes, diálogos, validación con Pydantic y formularios dibujados en el diseñador visual Pyzed Design. Source: https://albakore.github.io/pyzed/es/showcase/forms/ Los formularios son el punto de partida de la mayoría de las apps de escritorio: campos, botones, reglas y mensajes. Pyzed te da dos formas de hacerlos, y puedes mezclar las dos en una misma app: - **Dibujarlos** en [Pyzed Design](https://albakore.github.io/pyzed/es/editor/), el diseñador visual: arrastras controles a una ventana, configuras sus propiedades y escribes solo el Python que corre cuando pasa algo (un archivo `.pzform` más un `.py` con los manejadores). Si usaste Visual Basic, Qt Designer o los editores de Godot y Unreal Engine, te resultará familiar. - **Escribirlos** en Python con la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/): `ui.field`, `ui.input`, `ui.select`, `ui.switch`... en un método `view()`. **Abre un formulario en el diseñador:** Cada `.pzform` de esta página se abre en Pyzed Design: `pyzed design ruta/al/formulario.pzform`. Pulsa F5 allí para ejecutarlo y mira el panel **Eventos** para ver qué manejador llama cada control. #### Formulario de acceso Dibujado en el diseñador: un correo y una contraseña, una casilla "recordarme" y un botón que muestra una barra de progreso mientras "inicia sesión". El archivo de Python son solo cuatro funciones pequeñas, una por evento. - Los controles son atributos de `form`: `form.email.text`, `form.status.color = "error"`, `form.progress.show()`. - Un control **Temporizador** (`ticker`) llama a `ticker_tick` cada pocos milisegundos mientras corre; así se llena la barra de progreso sin bloquear la ventana.
Manejadores de eventos: examples/designer/login.py `examples/designer/login.py`: ```py """Event handlers for the login form. Each function receives the running ``form`` (controls are attributes: ``form.button1.text``) and an ``event`` with ``event.sender``, ``event.name`` and ``event.value``. """ def sign_in_click(form, event): """Runs when sign_in is clicked.""" if "@" not in form.email.text: form.status.text = "Enter a valid email address." form.status.color = "error" return form.email.focus() form.status.text = "" form.sign_in.enabled = False form.progress.value = 0 form.progress.show() form.ticker.running = True def ticker_tick(form, event): """Runs every ``interval`` milliseconds while ticker is enabled.""" form.progress.value += 4 if form.progress.value >= 100: form.ticker.running = False form.progress.hide() form.status.color = "success" form.status.text = f"Signed in as {form.email.text}" form.sign_in.enabled = True def password_submit(form, event): """Runs when Enter is pressed in password.""" return sign_in_click(form, event) def form_load(form, event): """Runs once when login opens.""" form.status.text = "Tip: any email works in this demo." return form.email.focus() ```
#### Registro con reglas Cada campo está **enlazado** a `form.data` y tiene **reglas** definidas en el panel Propiedades (obligatorio, un correo, al menos 8 caracteres, igual que otro campo). Una línea, `form.validate()`, las revisa todas y muestra los mensajes junto a los campos; el saludo y el resumen se actualizan solos mientras escribes.
Manejadores de eventos: examples/tutorial/designer/signup.py `examples/tutorial/designer/signup.py`: ```py """Event handlers for the sign-up form (tutorial: Data binding and validation). pyzed run examples/tutorial/designer/signup.pzform The controls are bound to fields of ``form.data`` (Properties → Data → Bind to) and carry their validation rules, so the handler only has to call ``form.validate()`` and read ``form.data``. """ def create_click(form, event): """Runs when create is clicked.""" if not form.validate(): # checks every rule and shows the messages under the controls form.status.color = "error" form.status.text = f"{len(form.errors)} field(s) need your attention." return data = form.data # {"name": ..., "email": ..., "password": ..., "age": ..., "terms": True} print("New account:", {k: v for k, v in data.items() if k != "password"}) form.status.color = "success" form.status.text = f"Welcome aboard, {data['name']}!" ```
Se construye paso a paso en [Enlace de datos y validación](https://albakore.github.io/pyzed/es/tutorials/intermediate/data-binding/). #### Formulario de contacto Nombre, correo, un desplegable de tema y un mensaje con un contador de caracteres en vivo. El botón **Send** revisa los campos y escribe el mensaje en la consola.
Manejadores de eventos: examples/tutorial/designer/contact.py `examples/tutorial/designer/contact.py`: ```py """Event handlers for the contact form (tutorial: A form with inputs and a button). pyzed run examples/tutorial/designer/contact.pzform Each function receives the running ``form`` and an ``event`` (``event.value`` holds the new value of a ``change`` event). """ LIMIT = 500 def form_load(form, event): """Runs once when contact opens: put the cursor in the first box.""" return form.your_name.focus() def message_change(form, event): """Runs on every keystroke in message: update the character counter.""" count = len(event.value) form.counter.text = f"{count} / {LIMIT}" form.counter.color = "error" if count > LIMIT else "text_muted" def send_click(form, event): """Runs when send is clicked: check the fields, then 'send' the message.""" if not form.your_name.text.strip(): form.status.color = "error" form.status.text = "Please tell us your name." return form.your_name.focus() if "@" not in form.email.text: form.status.color = "error" form.status.text = "That email does not look right." return form.email.focus() if len(form.message.text) > LIMIT: form.status.color = "error" form.status.text = f"Keep the message under {LIMIT} characters." return None # print() shows up in the designer's console (and in the terminal) print(f"New {form.topic.selected.lower()} from {form.email.text}: {form.message.text!r}") form.status.color = "success" form.status.text = f"Thanks, {form.your_name.text.split()[0]}! We got your message." form.message.text = "" form.counter.text = f"0 / {LIMIT}" return None ```
Se construye paso a paso en [Un formulario que hace algo](https://albakore.github.io/pyzed/es/tutorials/basic/first-form/). #### Valoraciones con componentes Dos tarjetas de perfil, cada una con un componente **Rating** de cinco estrellas, una al lado de la otra en una fila flexible que crece y se encoge con la ventana. `Rating`, `ProfileCard` y `TeamCard` son componentes dibujados en el modo componente del diseñador (archivos `.pzcomp`): tienen sus propias propiedades, eventos y código, y `TeamCard` hereda todo de `ProfileCard`.
Manejadores de eventos: examples/designer/components/reviews.py `examples/designer/components/reviews.py`: ```py """Event handlers for the Reviews form. Each function receives the running ``form`` (controls are attributes: ``form.button1.text``) and an ``event`` with ``event.sender``, ``event.name`` and ``event.value``. """ def ada_rated(form, event): """Runs when the card of ada is rated.""" event.sender.score = event.value form.status.text = f"{event.sender.person} now has {event.value} stars" def grace_rated(form, event): """Runs when the card of grace is rated.""" event.sender.score = event.value form.status.text = f"{event.sender.person} now has {event.value} stars" ```
Se construye paso a paso en [Un componente propio](https://albakore.github.io/pyzed/es/tutorials/intermediate/custom-component/). #### Pantalla de login Una pantalla de inicio de sesión completa escrita en código: un panel de marca, botones de inicio de sesión único, una opción de passkey y un formulario de correo. Es uno de los [Bloques](https://albakore.github.io/pyzed/es/blocks/), pantallas que puedes copiar en tu app y adaptar. El código y qué cambiar en él: [bloque Login](https://albakore.github.io/pyzed/es/blocks/login/). #### Asistente de bienvenida Una configuración en varios pasos: una barra de pasos arriba, tarjetas para elegir, validación en cada paso y un resumen antes de terminar. El código: [bloque Onboarding](https://albakore.github.io/pyzed/es/blocks/onboarding/). #### Ajustes Secciones a la izquierda y filas agrupadas a la derecha: campos de perfil, un selector de idioma, tema claro, oscuro o del sistema, muestras de color de acento, un deslizador de tamaño de letra, interruptores de notificaciones y una zona de peligro que pregunta antes de borrar. El código: [bloque Settings](https://albakore.github.io/pyzed/es/blocks/settings/). #### Diálogos e idiomas Cada botón abre uno de los diálogos listos de `pyzed.ui` (un aviso, una confirmación, una pregunta, una elección y "cambios sin guardar"). Elige un idioma en la barra de herramientas y cada texto, incluidos los botones de los diálogos, cambia mientras la app corre.
Código completo: examples/dialogs.py `examples/dialogs.py`: ```py """Dialogs and languages: ready-made dialogs and switching the app's language while it runs. python examples/dialogs.py - Pick a language in the toolbar; every text (and the dialogs' buttons) follows it. - Each button opens one of the dialogs in ``pyzed.ui``. """ import pyzed as pz from pyzed import i18n, ui i18n.add("en", { "title": "Dialogs", "language": "Language", "files": {"zero": "No files", "one": "{count} file", "other": "{count} files"}, "actions": {"alert": "Show an alert", "confirm": "Delete files", "prompt": "Rename", "choice": "Export", "unsaved": "Close document"}, "delete_question": "Delete {files}? This cannot be undone.", "saved": "Your changes were saved.", "rename": "Rename file", "new_name": "New name", "export": "Export the report as…", "last": "Last action: {action}", }) i18n.add("es", { "title": "Diálogos", "language": "Idioma", "files": {"zero": "Ningún archivo", "one": "{count} archivo", "other": "{count} archivos"}, "actions": {"alert": "Mostrar un aviso", "confirm": "Eliminar archivos", "prompt": "Renombrar", "choice": "Exportar", "unsaved": "Cerrar documento"}, "delete_question": "¿Eliminar {files}? No se puede deshacer.", "saved": "Tus cambios se guardaron.", "rename": "Renombrar archivo", "new_name": "Nombre nuevo", "export": "Exportar el informe como…", "last": "Última acción: {action}", }) LANGUAGES = {"en": "English", "es": "Español"} class DialogsDemo(pz.App): size = (720, 460) def __init__(self): self.dialog = None self.files = 3 self.name = "report.md" self.last = "-" def title(self): return pz.t("title") def open(self, kind): self.dialog = kind def done(self, action): self.last = action self.dialog = None def delete(self): self.files = 0 self.done("delete") def dialog_view(self): close = lambda: self.done("cancel") # noqa: E731 if self.dialog == "alert": return ui.alert(pz.t("saved"), close, kind="success") if self.dialog == "confirm": return ui.confirm(pz.t("delete_question", files=pz.t("files", count=self.files)), self.delete, close, danger=True) if self.dialog == "prompt": return ui.prompt(pz.t("rename"), self.name, lambda v: setattr(self, "name", v), lambda: self.done(f"rename → {self.name}"), close, message=pz.t("new_name")) if self.dialog == "choice": return ui.choice(pz.t("export"), [(f, lambda f=f: self.done(f"export {f}")) for f in ("CSV", "PDF")], close) if self.dialog == "unsaved": return ui.unsaved_changes(self.name, lambda: self.done("save"), lambda: self.done("discard"), close) return None def view(self): toolbar = pz.row([ ui.heading(pz.t("title")), pz.space(width="fill"), ui.label(pz.t("language"), color="text_muted"), ui.segmented([LANGUAGES[k] for k in LANGUAGES], LANGUAGES[i18n.language()[:2]], lambda name: i18n.set_language(next(k for k, v in LANGUAGES.items() if v == name))), ], align="center", spacing=10) buttons = pz.grid( [ui.button(pz.t(f"actions.{kind}"), lambda kind=kind: self.open(kind), full_width=True) for kind in ("alert", "confirm", "prompt", "choice", "unsaved")], columns=["1fr", "1fr"], spacing=8, ) body = pz.column([ toolbar, ui.label(f"{self.name} · {pz.t('files', count=self.files)}", color="text_muted"), buttons, ui.label(pz.t("last", action=self.last), mono=True, size="sm"), ], spacing=16, padding=24) return ui.dialog_layer(body, self.dialog_view(), on_dismiss=lambda: self.done("cancel")) if __name__ == "__main__": pz.run(DialogsDemo) ```
Para errores de campo, confirmaciones y toasts en tus propios formularios, mira [Diálogos, notificaciones y validación](https://albakore.github.io/pyzed/es/tutorials/intermediate/dialogs-feedback/). #### Registro validado con Pydantic Las reglas de este formulario viven en un modelo de [Pydantic](https://docs.pydantic.dev), el mismo tipo de modelo que usaría una API web. La ventana guarda lo que escribe el usuario como texto y se lo pasa a `SignUp.model_validate`; Pydantic convierte los valores (una fecha, un correo, un sí/no), revisa cada regla, y `pz.validation_errors` convierte sus errores en un mensaje por campo. Las cuentas se guardan en `accounts.json`. Es un pequeño proyecto con sus propias dependencias, así que ejecútalo desde su carpeta: **uv:** ```bash title="Windows, macOS y Linux" cd examples/integrations/pydantic_signup uv run main.py ``` **pip:** ```bash title="macOS / Linux" cd examples/integrations/pydantic_signup python3 -m venv .venv source .venv/bin/activate python -m pip install pyzed-ui "pydantic[email]>=2.6" python main.py ``` ```powershell title="Windows (PowerShell)" cd examples\integrations\pydantic_signup py -m venv .venv .venv\Scripts\Activate.ps1 python -m pip install pyzed-ui "pydantic[email]>=2.6" python main.py ``` **python:** ```bash title="Con pyzed-ui y pydantic[email] ya instalados" cd examples/integrations/pydantic_signup python main.py ```
Código completo: main.py y models.py `examples/integrations/pydantic_signup/models.py`: ```py """The form's rules, written as Pydantic models (https://docs.pydantic.dev/latest/concepts/models/).""" from __future__ import annotations from datetime import date from typing import Literal from pydantic import BaseModel, EmailStr, Field, SecretStr, TypeAdapter, field_validator, model_validator class SignUp(BaseModel): username: str = Field(min_length=3, max_length=20, pattern=r"^[a-z0-9_]+$") email: EmailStr birthday: date plan: Literal["free", "pro", "team"] = "free" password: SecretStr # repr() and str() show ******** password_repeat: SecretStr newsletter: bool = False @field_validator("username", mode="before") @classmethod def lowercase(cls, value: str) -> str: return value.strip().lower() @field_validator("birthday") @classmethod def old_enough(cls, value: date) -> date: today = date.today() age = today.year - value.year - ((today.month, today.day) < (value.month, value.day)) if age < 13: raise ValueError("You must be at least 13 years old") return value @field_validator("password") @classmethod def long_enough(cls, value: SecretStr) -> SecretStr: if len(value.get_secret_value()) < 8: raise ValueError("Use 8 characters or more") return value @model_validator(mode="after") def passwords_match(self) -> SignUp: if self.password.get_secret_value() != self.password_repeat.get_secret_value(): raise ValueError("The passwords don't match") return self class Account(BaseModel): """What gets saved: the sign-up without the passwords.""" username: str email: EmailStr birthday: date plan: Literal["free", "pro", "team"] newsletter: bool Accounts = TypeAdapter(list[Account]) # validates and serializes a whole list at once ``` `examples/integrations/pydantic_signup/main.py`: ```py """A sign-up form whose rules live in a Pydantic model; accounts are saved to accounts.json. uv run main.py The window keeps what the user types as plain strings and hands them to ``SignUp.model_validate``: Pydantic converts them (a date, an email, a bool), checks every rule and reports the errors per field, which ``pz.validation_errors`` turns into ``{field: message}``. Walkthrough: https://albakore.github.io/pyzed/integrations/pydantic/ """ from __future__ import annotations from pathlib import Path from pydantic import ValidationError import pyzed as pz from pyzed import ui from models import Account, Accounts, SignUp FILE = Path(__file__).with_name("accounts.json") EMPTY = {"username": "", "email": "", "birthday": "", "plan": "free", "password": "", "password_repeat": "", "newsletter": False} def load() -> list[Account]: return Accounts.validate_json(FILE.read_bytes()) if FILE.exists() else [] class SignUpForm(pz.App): title = "Sign up · Pydantic" size = (860, 600) def __init__(self): self.draft = dict(EMPTY) self.errors: dict[str, str] = {} self.accounts = load() self.preview = "" # the JSON Pydantic produced for the last account def edit(self, key): return lambda value: self.draft.__setitem__(key, value) def submit(self, *_): try: signup = SignUp.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) # the model validator's error has the key "" return account = Account.model_validate(signup.model_dump(exclude={"password", "password_repeat"})) if any(a.username == account.username for a in self.accounts): self.errors = {"username": "That username is taken"} return self.accounts.append(account) FILE.write_bytes(Accounts.dump_json(self.accounts, indent=2)) self.preview = account.model_dump_json(indent=2) self.draft, self.errors = dict(EMPTY), {} def form(self): e, d = self.errors.get, self.draft def text(key, title, placeholder="", **kwargs): return ui.field(title, ui.input(d[key], self.edit(key), placeholder=placeholder, on_submit=self.submit, **kwargs), error=e(key)) return pz.column([ ui.heading("Create an account", level=2), text("username", "Username", "lowercase letters, digits and _"), text("email", "Email", "you@example.com"), text("birthday", "Birthday", "YYYY-MM-DD"), ui.field("Plan", ui.segmented(["free", "pro", "team"], d["plan"], self.edit("plan"))), text("password", "Password", "8 characters or more", secure=True), text("password_repeat", "Repeat the password", secure=True), ui.checkbox(d["newsletter"], self.edit("newsletter"), label="Send me the newsletter"), ui.callout(e(""), tint="error") if e("") else None, ui.button("Sign up", self.submit, icon="user-plus", variant="solid", full_width=True), ], spacing=12, padding=24, width=420) def side(self, t): rows = [ui.list_item(a.username, None, icon="user", end=ui.badge(a.plan), inset=True, height=30) for a in reversed(self.accounts)] return pz.container(pz.column([ ui.muted("accounts.json", size="xs", mono=True), *rows, ui.card(ui.label(self.preview, mono=True, size="sm"), title="model_dump_json()") if self.preview else None, ], spacing=6), padding=16, width="fill", height="fill", bg=t.panel) def view(self): t = self.resolved_theme() return pz.row([pz.container(pz.scrollable(self.form()), height="fill", bg=t.editor), self.side(t)]) if __name__ == "__main__": pz.run(SignUpForm) ```
La explicación paso a paso está en [Pydantic](https://albakore.github.io/pyzed/es/integrations/pydantic/). **Sobre esta página:** Los formularios del diseñador y las apps de esta página se cargan y se dibujan en la suite de tests con la versión actual de Pyzed. Vienen grabaciones de cómo se rellenan. ### Dashboards y apps de datos > Apps de escritorio en Python con muchos datos hechas con Pyzed: un panel de administración con KPI y gráficos, una tabla de datos con filtros y páginas, un tablero kanban, un chat de equipo, un reproductor musical, un layout de IDE y una app de contactos sobre SQLite. Source: https://albakore.github.io/pyzed/es/showcase/dashboards/ Pantallas llenas de números, tablas y listas: el tipo de herramienta interna, panel de administración o app multimedia que son muchos programas de escritorio. Los seis primeros son [Bloques](https://albakore.github.io/pyzed/es/blocks/), pantallas completas escritas con la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/) que puedes copiar y adaptar; la página de cada bloque tiene su código completo y notas sobre qué cambiar. Cada captura sigue el tema de este sitio: cambia entre claro y oscuro arriba de la página para verlas en los dos. #### Dashboard Un panel de administración: navegación lateral, tarjetas de KPI con tendencias, un gráfico de ingresos, fuentes de tráfico y una tabla de pedidos recientes. - Las tarjetas de KPI son `ui.stat` con un `delta` y una pequeña línea de `trend`. Los gráficos también son componentes listos: `ui.bar_chart` para los ingresos (su rango es un control `ui.segmented`) y `ui.donut` para las fuentes de tráfico, con una barra `ui.progress` para el objetivo del mes. - La barra lateral cambia de página cambiando un atributo del estado: mira [Una app con varias vistas](https://albakore.github.io/pyzed/es/tutorials/intermediate/navigation/). El código: [bloque Dashboard](https://albakore.github.io/pyzed/es/blocks/dashboard/). #### Tabla de datos Una tabla de miembros de un equipo con búsqueda, filtros por estado, columnas ordenables, selección de filas, páginas y una acción de exportar. El código: [bloque Data table](https://albakore.github.io/pyzed/es/blocks/data-table/). Para aprender las partes una a una (buscar, ordenar, seleccionar, mostrar los detalles), sigue [Listas y tablas con datos](https://albakore.github.io/pyzed/es/tutorials/intermediate/lists-tables/), que construye esta pequeña biblioteca de libros: #### Tablero kanban Un tablero de proyecto con columnas, tarjetas con etiquetas, responsables, progreso y botones para mover las tarjetas. El código: [bloque Kanban](https://albakore.github.io/pyzed/es/blocks/kanban/). #### Chat de equipo Una mensajería con canales, mensajes directos, un hilo de mensajes y una caja para escribir. El código: [bloque Chat](https://albakore.github.io/pyzed/es/blocks/chat/). Para un chat que recibe respuestas en streaming de un modelo de IA, mira el [Chat con IA](https://albakore.github.io/pyzed/es/showcase/apps/#chat-con-ia). #### Reproductor musical Una barra lateral con la biblioteca, una página de álbum con su lista de canciones y una barra de reproducción cuyo progreso avanza mientras suena una canción. El código: [bloque Music player](https://albakore.github.io/pyzed/es/blocks/music-player/). #### Layout de IDE La disposición de un editor de código: un árbol de proyecto, pestañas con migas de pan, una terminal, un panel de estructura y una barra de estado. El código: [bloque File explorer](https://albakore.github.io/pyzed/es/blocks/file-explorer/). Un editor que funciona con la misma idea es el [Editor de proyectos](https://albakore.github.io/pyzed/es/showcase/apps/#editor-de-proyectos). #### Contactos sobre SQLite Una app de contactos sobre [SQLAlchemy](https://www.sqlalchemy.org) 2.0 con **20.000 filas** en una tabla virtual. La búsqueda y el orden corren como consultas SQL en un hilo de trabajo (`Command.perform`), así que la ventana nunca espera a la base de datos. **uv:** ```bash title="Windows, macOS y Linux" cd examples/integrations/sqlalchemy_contacts uv run main.py ``` **pip:** ```bash title="macOS / Linux" cd examples/integrations/sqlalchemy_contacts python3 -m venv .venv source .venv/bin/activate python -m pip install pyzed-ui "sqlalchemy>=2.0" python main.py ``` ```powershell title="Windows (PowerShell)" cd examples\integrations\sqlalchemy_contacts py -m venv .venv .venv\Scripts\Activate.ps1 python -m pip install pyzed-ui "sqlalchemy>=2.0" python main.py ``` **python:** ```bash title="Con pyzed-ui y SQLAlchemy ya instalados" cd examples/integrations/sqlalchemy_contacts python main.py ```
Código completo: db.py y main.py `examples/integrations/sqlalchemy_contacts/db.py`: ```py """The database: SQLAlchemy 2.0 models on SQLite, plus the queries the window runs.""" from __future__ import annotations import random from datetime import datetime from pathlib import Path from sqlalchemy import String, create_engine, func, or_, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column DATABASE = Path(__file__).with_name("contacts.db") engine = create_engine(f"sqlite:///{DATABASE}") class Base(DeclarativeBase): pass class Contact(Base): __tablename__ = "contacts" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(80), index=True) email: Mapped[str] = mapped_column(String(120), unique=True) company: Mapped[str] = mapped_column(String(80), default="") city: Mapped[str] = mapped_column(String(60), default="") created: Mapped[datetime] = mapped_column(default=datetime.now) # Sorting by a column title of the table is an ORDER BY on this column. COLUMNS = {"Name": Contact.name, "Email": Contact.email, "Company": Contact.company, "City": Contact.city} def setup(rows: int = 20_000) -> None: """Creates the table and fills it with made-up contacts the first time.""" Base.metadata.create_all(engine) with Session(engine) as session: if session.scalar(select(func.count()).select_from(Contact)): return rng = random.Random(7) first = ["Ada", "Alan", "Grace", "Linus", "Guido", "Margaret", "Ken", "Barbara", "Dennis", "Radia", "Tim", "Frances", "Bjarne", "Anita", "John", "Hedy", "Yukihiro", "Sophie", "Brendan", "Katherine"] last = ["Lovelace", "Turing", "Hopper", "Torvalds", "van Rossum", "Hamilton", "Thompson", "Liskov", "Ritchie", "Perlman", "Berners-Lee", "Allen", "Stroustrup", "Borg", "McCarthy", "Lamarr", "Matsumoto", "Wilson", "Eich", "Johnson"] companies = ["Acme", "Globex", "Initech", "Umbrella", "Hooli", "Stark", "Wayne", "Tyrell", "Cyberdyne", "Aperture"] cities = ["Buenos Aires", "Madrid", "Mexico City", "Bogotá", "Lima", "Santiago", "Montevideo", "Barcelona", "Berlin", "Tokyo", "Toronto", "Lisbon"] weights = [12, 10, 9, 7, 6, 5, 4, 4, 3, 3, 2, 2] session.add_all( Contact(name=f"{rng.choice(first)} {rng.choice(last)}", email=f"user{i}@example.com", company=rng.choice(companies), city=rng.choices(cities, weights)[0]) for i in range(rows) ) session.commit() def search(text: str, sort: str, descending: bool) -> list[Contact]: """Contacts whose name, email or company contains ``text``, sorted in SQL.""" column = COLUMNS[sort] query = select(Contact).order_by(column.desc() if descending else column, Contact.id) if text: pattern = f"%{text}%" query = query.where(or_(Contact.name.ilike(pattern), Contact.email.ilike(pattern), Contact.company.ilike(pattern))) with Session(engine) as session: return list(session.scalars(query)) # plain objects keep their loaded values after the session closes def stats() -> dict: """Totals and the five cities with more contacts, for the header.""" with Session(engine) as session: total = session.scalar(select(func.count()).select_from(Contact)) companies = session.scalar(select(func.count(func.distinct(Contact.company)))) cities = session.execute(select(Contact.city, func.count()).group_by(Contact.city) .order_by(func.count().desc()).limit(5)).all() return {"total": total, "companies": companies, "cities": [(city, count) for city, count in cities]} def save(contact_id: int | None, values: dict) -> Contact | dict: """Inserts or updates a contact. Returns it, or ``{field: message}`` when it can't be saved.""" errors = {} if not values["name"].strip(): errors["name"] = "A contact needs a name" if "@" not in values["email"]: errors["email"] = "Not an email address" if errors: return errors with Session(engine, expire_on_commit=False) as session: contact = session.get(Contact, contact_id) if contact_id else Contact() for key, value in values.items(): setattr(contact, key, value.strip()) session.add(contact) try: session.commit() except IntegrityError: return {"email": "Another contact already uses this email"} return contact def delete(contact_id: int) -> None: with Session(engine) as session: contact = session.get(Contact, contact_id) if contact is not None: session.delete(contact) session.commit() ``` `examples/integrations/sqlalchemy_contacts/main.py`: ```py """A contacts manager on SQLAlchemy 2.0: 20,000 rows in a virtual table, searched and sorted in SQL. uv run main.py The database (contacts.db) is created next to this file on the first run. Walkthrough: https://albakore.github.io/pyzed/integrations/sqlalchemy/ """ from __future__ import annotations import pyzed as pz from pyzed import Command, ui import db FIELDS = [("name", "Name", "Ada Lovelace"), ("email", "Email", "ada@example.com"), ("company", "Company", "Analytical Engines"), ("city", "City", "London")] class Contacts(pz.App): title = "Contacts · SQLAlchemy" size = (1180, 720) def __init__(self): self.rows: list[db.Contact] = [] self.stats = {"total": 0, "companies": 0, "cities": []} self.query = "" self.sort, self.descending = "Name", False self.loading = True self.editing = None # a Contact, "new" or None self.draft: dict[str, str] = {} self.errors: dict[str, str] = {} # -- database work runs in a thread with Command.perform ------------------------------ def on_start(self): return Command.perform(db.setup, lambda _: self.reload()) def reload(self): self.loading = True query, sort, descending = self.query, self.sort, self.descending return [ Command.perform(lambda: db.search(query, sort, descending), lambda rows: self.loaded(query, rows)), Command.perform(db.stats, lambda stats: setattr(self, "stats", stats)), ] def loaded(self, query: str, rows: list[db.Contact]): if query == self.query: # a newer search is on its way: keep waiting for it self.rows, self.loading = rows, False def search(self, text: str): self.query = text return self.reload() def sort_by(self, title: str): self.descending = not self.descending if title == self.sort else False self.sort = title return self.reload() # -- the edit panel ------------------------------------------------------------------ def open(self, contact): self.editing, self.errors = contact, {} self.draft = {key: "" if contact == "new" else getattr(contact, key) for key, _, _ in FIELDS} def save(self): contact_id = None if self.editing == "new" else self.editing.id values = dict(self.draft) return Command.perform(lambda: db.save(contact_id, values), self.saved) def saved(self, result): if isinstance(result, dict): self.errors = result return None self.editing = None return self.reload() def delete(self): contact_id = self.editing.id self.editing = None return Command.perform(lambda: db.delete(contact_id), lambda _: self.reload()) def on_key(self, combo): if combo == "escape": self.editing = None elif combo == "ctrl-n": self.open("new") # -- view ---------------------------------------------------------------------------- def header(self): cities = self.stats["cities"] return pz.row([ ui.stat("Contacts", f"{self.stats['total']:,}"), ui.stat("Companies", self.stats["companies"]), ui.card(pz.column([ui.muted("Top cities", size="sm"), ui.bar_chart([count for _, count in cities], labels=[c.split()[0] for c, _ in cities], width=360, height=90)], spacing=6)), ], spacing=12) def panel(self, t): if self.editing is None: return None rows = [ui.field(title, ui.input(self.draft[key], lambda v, k=key: self.draft.__setitem__(k, v), placeholder=hint, width=220), error=self.errors.get(key)) for key, title, hint in FIELDS] new = self.editing == "new" return pz.container( pz.column([ pz.row([ui.heading("New contact" if new else "Edit contact", level=3), pz.hspace(), ui.icon_button("x", lambda: setattr(self, "editing", None), tooltip="Close (Esc)")], align="center"), *rows, pz.row([None if new else ui.button("Delete", self.delete, icon="trash-2", variant="ghost", tint="error"), pz.hspace(), ui.button("Save", self.save, icon="check", variant="solid")], spacing=8), ], spacing=14), padding=16, width=420, height="fill", bg=t.panel, ) def view(self): t = self.resolved_theme() columns = [ ui.Column("Name", "name", width="fill:3"), ui.Column("Email", "email", width="fill:3"), ui.Column("Company", "company", width="fill:2"), ui.Column("City", "city", width="fill:2"), ] toolbar = pz.row([ ui.search_input(self.query, self.search, placeholder="Search by name, email or company…", width=360), ui.spinner() if self.loading else ui.muted(f"{len(self.rows):,} found", size="sm"), pz.hspace(), ui.button("New contact", lambda: self.open("new"), icon="user-plus", keys="ctrl-n"), ], spacing=10, align="center") # Rows come sorted from SQL; sort_by only draws the arrow (re-sorting a sorted list is cheap). table = ui.table(columns, self.rows, sort_by=self.sort, descending=self.descending, on_sort=self.sort_by, on_row_press=self.open, selected=self.editing, striped=True, height="fill") main = pz.column([self.header(), toolbar, table], spacing=14, padding=16) return pz.container(pz.row([main, self.panel(t)]), width="fill", height="fill", bg=t.editor) if __name__ == "__main__": pz.run(Contacts) ```
La explicación está en [SQLAlchemy](https://albakore.github.io/pyzed/es/integrations/sqlalchemy/). Sin ninguna biblioteca, el tutorial [Conectar un backend](https://albakore.github.io/pyzed/es/tutorials/advanced/backend/) construye una app de contactos más pequeña sobre el módulo `sqlite3` que viene con Python, con una clase repositorio, un spinner y errores amables: #### Más - [Datos en vivo](https://albakore.github.io/pyzed/es/showcase/apps/#datos-en-vivo): un panel de cotizaciones alimentado por un generador async. - [Centro de control de cristal](https://albakore.github.io/pyzed/es/showcase/glass-dashboard/): un dashboard con tarjetas de cristal esmerilado sobre un shader en movimiento. - [Integraciones](https://albakore.github.io/pyzed/es/integrations/overview/): FastAPI, Django, SQLModel, httpx y más, cada una con una app de escritorio. **Sobre esta página:** Los bloques se regeneran desde `examples/blocks` y las herramientas de la documentación los dibujan en los dos temas, así que las capturas coinciden con el código actual. ### Shaders y efectos > Efectos de GPU en apps de escritorio con Python: once grafos de shader hechos con nodos en Pyzed Design (plasma, fuego, nebulosa, synthwave, radar...) y los efectos de cristal, bloom, resplandor, desenfoque y animación que tiene cada widget. Source: https://albakore.github.io/pyzed/es/showcase/shaders/ Pyzed lo dibuja todo con la GPU, así que una app de escritorio puede tener lo que tienen los videojuegos: fondos en movimiento, luz que desborda de lo brillante, cristal esmerilado. Hay dos capas: - **Shaders**: un pequeño programa que la tarjeta gráfica ejecuta para cada píxel de un widget, en cada fotograma. En Pyzed los haces como **grafos de nodos** en la vista de shader de [Pyzed Design](https://albakore.github.io/pyzed/es/shading/), guardados como archivos `.pzshad`, o en Python con `pyzed.shading.Graph`. No hay que aprender un lenguaje de shaders. - **Efectos**: `glass`, `bloom`, `glow`, `blur`, rotación y animaciones, que cualquier widget tiene con `widget.style(...)` y `widget.animate(...)`. #### Seis shaders a la vez Seis grafos lado a lado, con un deslizador que cambia la variable `speed` de todos a la vez. Las variables (uniforms) son solo valores, así que cambiarlas nunca recompila el shader. ```python from pyzed import shading plasma = shading.load("shaders/plasma.pzshad") # se compila una vez pz.shader(plasma, uniforms={"speed": self.speed}, # variables por nombre, desde tu estado width=340, height=210, fallback=pz.Gradient(["#7c3aed", "#ec4899"])) # se muestra donde no hay shaders ```
Código completo: examples/showcase/shaders.py `examples/showcase/shaders.py`: ```py """A gallery of shader graphs: plasma, a ripple that follows the pointer, a tunnel, a CRT screen, Voronoi cells and a fire. Each is a ``.pzshad`` file in ``shaders/`` made of nodes (no WGSL to write): open one in Pyzed Design (``pyzed design shaders/fire.pzshad``) to see and change its graph. python examples/showcase/shaders.py ``pz.shader`` compiles the graph and runs it on the GPU for every pixel, every frame:: pz.shader("shaders/plasma.pzshad", uniforms={"speed": 2.0}, width=340, height=210) ``uniforms`` sets the shader's mutable variables by name; the slider below changes ``speed`` on every shader without recompiling. """ from pathlib import Path import pyzed as pz from pyzed import shading HERE = Path(__file__).parent / "shaders" NAMES = {"Plasma": "plasma", "Ripple": "ripple", "Tunnel": "tunnel", "CRT": "crt", "Voronoi": "voronoi", "Fire": "fire"} SHADERS = {name: shading.load(HERE / f"{file}.pzshad") for name, file in NAMES.items()} FALLBACKS = { "Plasma": pz.Gradient(["#7c3aed", "#ec4899", "#f59e0b"], angle=45), "Ripple": pz.Gradient(["#0c4a6e", "#0891b2"], angle=180), "Tunnel": pz.Gradient(["#1e1b4b", "#0e7490"], angle=135), "CRT": pz.Gradient(["#ef4444", "#22c55e", "#3b82f6"], angle=90), "Voronoi": pz.Gradient(["#111827", "#4c1d95"], angle=135), "Fire": pz.Gradient(["#000000", "#b91c1c", "#fbbf24"], angle=180), } TINTS = {"Ripple": "#7dd3fc", "Voronoi": "#a78bfa"} class Shaders(pz.App): title = "Shaders" size = (1140, 720) theme = pz.themes.ONE_DARK def __init__(self): self.speed = 1.0 def set_speed(self, value): self.speed = value def tile(self, name): shader = SHADERS[name] uniforms = {"speed": self.speed} if name in TINTS: uniforms["tint"] = TINTS[name] art = pz.shader(shader, uniforms=uniforms, width=340, height=210, fallback=FALLBACKS[name]) label = pz.row([ pz.text(name, size=14, weight="semibold", color="#ffffff"), pz.space(width="fill"), pz.text(f"{len(shader.graph.nodes)} nodes", size=11, color="#ffffff88", font="mono"), ], width=340, align="center") return pz.column([art, label], spacing=8).animate("lift", on="hover") def view(self): names = list(SHADERS) grid = pz.column([pz.row([self.tile(n) for n in names[i:i + 3]], spacing=20) for i in (0, 3)], spacing=22) header = pz.row([ pz.column([ pz.text("Shaders", size=28, weight="bold", color="#ffffff"), pz.text("Node graphs from Pyzed Design, running on the GPU", size=13, color="#ffffffaa"), ], spacing=2), pz.space(width="fill"), pz.text("Speed", size=13, color="#ffffffaa"), pz.slider(0.0, 3.0, self.speed, step=0.05, on_change=self.set_speed, width=200), pz.text(f"{self.speed:.2f}×", size=13, color="#ffffff", font="mono", width=48), ], spacing=12, align="center", width=1060) return pz.container(pz.column([header, pz.row(pz.stagger([grid], "fade_in_up"))], spacing=24), width="fill", height="fill", align_x="center", align_y="center").style( background="#0b0d14") if __name__ == "__main__": pz.run(Shaders) ```
#### Todos los grafos Los once grafos viven en [`examples/showcase/shaders`](https://github.com/albakore/pyzed/tree/main/examples/showcase/shaders). Haz clic en uno para ver su archivo, o ábrelo en el diseñador para ver y cambiar los nodos: `pyzed design examples/showcase/shaders/fire.pzshad`. .pzshad dibujado con pz.shader." size="wide" /> speed.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/plasma.pzshad', image: plasma, tag: '28 nodos' }, { title: 'Ripple', text: 'Anillos de ondas alrededor del puntero. Variables: speed, tint.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/ripple.pzshad', image: ripple, tag: 'Mouse' }, { title: 'Tunnel', text: 'Un túnel a cuadros que viene hacia ti. Variable: speed.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/tunnel.pzshad', image: tunnel, tag: '29 nodos' }, { title: 'CRT', text: 'Barras de color en una pantalla curva, con líneas de barrido y una banda que rueda. Variable: speed.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/crt.pzshad', image: crt, tag: '28 nodos' }, { title: 'Voronoi', text: 'Celdas que se desplazan con bordes brillantes. Variables: speed, tint.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/voronoi.pzshad', image: voronoi, tag: '14 nodos' }, { title: 'Fire', text: 'Llamas de ruido fractal que suben desde abajo. Variable: speed.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/fire.pzshad', image: fire, tag: '23 nodos' }, { title: 'Nebula', text: 'Nubes de ruido deformado y estrellas que titilan; el fondo del HUD. Variable: drift.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/nebula.pzshad', image: nebula, tag: '40 nodos' }, { title: 'Radar', text: 'Un barrido con estela, anillos y una cruz; el minimapa del HUD. Variables: accent, sweep.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/radar.pzshad', image: radar, tag: '41 nodos' }, { title: 'Embers', text: 'Chispas que suben desde un resplandor cálido; el fondo del inventario. Variable: rise.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/embers.pzshad', image: embers, tag: '100 nodos' }, { title: 'Synthwave', text: 'Cielo de atardecer, estrellas, un sol a rayas y una cuadrícula que se inclina hacia el puntero; el fondo del menú principal.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/synthwave.pzshad', image: synthwave, tag: 'Mouse' }, { title: 'Blobs', text: 'Manchas de color suaves que se desplazan lento, pensadas para ir detrás de paneles de cristal. Variable: speed.', href: 'https://github.com/albakore/pyzed/blob/main/examples/showcase/shaders/blobs.pzshad', image: blobs, tag: '74 nodos' }, ]} /> **Haz el tuyo:** Empieza desde una plantilla en Pyzed Design (Ctrl+N → **Shader**) y sigue [Un efecto de shader desde la vista de shader](https://albakore.github.io/pyzed/es/tutorials/intermediate/shader-effect/), o construye uno nodo a nodo en Python con [Un grafo de shader propio](https://albakore.github.io/pyzed/es/tutorials/advanced/shader-graph/). La sección [Shading](https://albakore.github.io/pyzed/es/shading/) documenta cada nodo. #### Efectos en cualquier widget Rotación, cristal esmerilado, bloom y desenfoque, aplicados con `style(...)` y animados. Los botones de abajo animan el desenfoque y el tamaño; el tamaño, el relleno y el tamaño de letra recolocan el layout mientras se mueven. ```python card.style(rotate=-8) # girarla card.style(glass=True) # un panel tintado que desenfoca lo que tiene detrás card.style(bloom=("#fbbf24", 0.8)) # luz alrededor de sus partes brillantes card.style(blur=4) # desenfocar el propio widget spinner.animate("spin") # un preset que se repite ```
Código completo: examples/effects.py `examples/effects.py`: ```py """Effects drawn by the renderer: rotation, blur, backdrop blur, glass and bloom, animated. python examples/effects.py - ``style(rotate=-8)`` turns a widget, ``blur=4`` blurs it and ``backdrop_blur=12`` blurs what is behind it; ``glass=True`` is a tinted panel with a backdrop blur. - ``bloom=(color, intensity)`` lights up the area around the bright parts of a widget. - ``animate("spin")`` and ``transition(rotate=..., blur=..., width=..., font_size=...)`` animate them; size, padding and font size reflow the layout as they move. """ import pyzed as pz from pyzed import ui BACKDROP = pz.Gradient(["#ff006e", "#3a86ff", "#06d6a0"], angle=45) class Effects(pz.App): title = "Effects" size = (900, 460) def __init__(self): self.focused = True self.wide = False def toggle_focus(self): self.focused = not self.focused def toggle_wide(self): self.wide = not self.wide def card(self, title, **style): return pz.container(pz.text(title, size=18), padding=20, width=150, height=100, align_x="center", align_y="center").style(radius=12, **style) def view(self): cards = pz.row([ self.card("Rotate", background="panel", rotate=-10).animate("tilt", on="hover"), self.card("Glass", glass=True), self.card("Bloom", background="#ffffff", color="#1f2937", bloom=("#ffbe0b", 1.0)), self.card("Blur", background="panel").transition(blur=0 if self.focused else 6), ], spacing=32, align="center") loader = pz.container(pz.icon("loader", size=28), padding=4).animate("spin") bar = pz.container(pz.text("Resizable", size=14), padding=10).style(background="panel", radius=8) \ .transition(width=420 if self.wide else 160, font_size=20 if self.wide else 14, duration=260, easing="ease_out_cubic") controls = pz.row([ ui.button("Blur" if self.focused else "Focus", self.toggle_focus, variant="outline"), ui.button("Resize", self.toggle_wide, variant="outline"), loader, bar, ], spacing=16, align="center") return pz.container(pz.column([cards, controls], spacing=48, align="center"), width="fill", height="fill", align_x="center", align_y="center").style(background=BACKDROP) if __name__ == "__main__": Effects().run() ```
#### Animaciones Cada preset, que se reproduce al hacer clic o al pasar el puntero; cristal, resplandor, bloom y opacidad sobre un degradado; y transiciones que animan un panel, una página y una contraseña incorrecta. ```python button.animate("lift", on="hover").animate("shrink", on="press") # con eventos title.animate("fade_in_up") # al aparecer panel.transition(x=0 if self.open else -260, opacity=1 if self.open else 0) # al cambiar un valor pz.presence(page, key=self.page) # sale y entra al cambiar la key ```
Código completo: examples/animations.py `examples/animations.py`: ```py """Animations and effects: presets, hover and press motion, transitions, presence and glass. python examples/animations.py - ``widget.animate("fade_in_up")`` plays a preset when the widget appears; ``on="hover"``, ``"press"``, ``"focus"``, ``"click"`` and ``"active"`` play it on events. - ``widget.transition(opacity=...)`` animates a value whenever it changes, like CSS. - ``pz.presence(content, key=...)`` animates content out and in when ``key`` changes. - ``style(glass=True)``, ``glow=``, ``bloom=`` and ``opacity=`` are visual effects. """ import pyzed as pz from pyzed import ui AURORA = pz.Gradient(["#0f172a", "#312e81", "#0e7490"], angle=135) PAGES = ("Overview", "Activity", "Settings") class Animations(pz.App): title = "Animations" size = (980, 700) transition = "zoom" def __init__(self): self.page = 0 self.panel = True self.errors = 0 self.recording = False def toggle_panel(self): self.panel = not self.panel def next_page(self): self.page = (self.page + 1) % len(PAGES) def fail(self): self.errors += 1 def toggle_recording(self): self.recording = not self.recording def tile(self, name): preset = pz.animation.PRESETS[name] on = preset.on if preset.on != "mount" else "click" repeat = 1 if preset.category == "loop" and on == "click" else None return pz.column([ pz.text(name.replace("_", " ")).style(weight="semibold"), pz.text(f"{preset.category} · {on}").style(size=11, color="text_muted"), ], spacing=2).style(background="elevated", border=(1, "border"), radius=8, padding=(10, 12), width=150).animate(name, on=on, repeat=repeat) def effects(self): card = lambda label, **style: pz.column([ # noqa: E731 pz.text(label).style(weight="semibold", color="#ffffff"), pz.text(", ".join(f"{k}={v!r}" for k, v in style.items())).style(size=11, color="#ffffffaa"), ], spacing=4).style(padding=14, width=200, radius=12, **style) return pz.row([ card("Glass", glass=True), card("Glow", glow=("#22d3ee", 18), background="#0b1220"), card("Bloom", bloom=("#f472b6", 0.8), background="#1a0b16"), card("Opacity", opacity=0.55, background="#0b1220", border=(1, "#ffffff33")), ], spacing=18).style(padding=24, background=AURORA, radius=14, width="fill") def view(self): tiles = [self.tile(n) for n in ("pulse", "shake", "bounce", "tada", "lift", "grow", "glow", "bloom", "float", "glow_pulse")] side = pz.column([ pz.text("Side panel").style(weight="semibold"), pz.text("transition(x=..., opacity=...)").style(size=11, color="text_muted"), ], spacing=4).style(background="elevated", radius=8, padding=12, width=220).transition( x=0 if self.panel else -24, opacity=1 if self.panel else 0, duration=220, easing="ease_out_cubic") page = pz.presence( pz.text(PAGES[self.page]).style(size=20, weight="bold"), key=self.page, enter="fade_in_right", exit="fade_out_left", id="pages") controls = pz.row([ ui.button("Toggle panel", self.toggle_panel), ui.button("Next page", self.next_page), ui.button("Wrong password", self.fail).animate("shake", key=self.errors), ui.button("Record" if not self.recording else "Stop", self.toggle_recording).animate( "glow_pulse", active=self.recording).animate("shrink"), ], spacing=10) return pz.column([ pz.text("Animations").style(size=24, weight="bold").animate("fade_in_down"), pz.row(pz.stagger(tiles, "fade_in_up", step=40), spacing=10, wrap=True, line_spacing=10), self.effects().animate("fade_in_up", delay=200), controls, pz.row([side, page], spacing=20), ], spacing=18).style(padding=24, background="background", width="fill", height="fill") if __name__ == "__main__": pz.run(Animations) ```
#### Estilos La parte parecida a CSS: bordes, radios, degradados, sombras, tipografía, estilos con nombre con `pz.define_style` y pinturas con máscara de opacidad. Más en [Animación y efectos](https://albakore.github.io/pyzed/es/animation/) y en la sección de [Temas y estilos](https://albakore.github.io/pyzed/es/theming/). #### Dónde se usan - [HUD de videojuego](https://albakore.github.io/pyzed/es/showcase/hud/): los grafos Nebula y Radar, paneles de cristal y bloom. - [Menú principal](https://albakore.github.io/pyzed/es/showcase/main-menu/): el grafo Synthwave y un título brillante. - [Inventario RPG](https://albakore.github.io/pyzed/es/showcase/inventory/): el grafo Embers, bloom según la rareza y estadísticas animadas. - [Centro de control de cristal](https://albakore.github.io/pyzed/es/showcase/glass-dashboard/): el grafo Blobs detrás de tarjetas de cristal esmerilado. **Cuando no hay GPU:** En máquinas sin una tarjeta gráfica compatible (algunas máquinas virtuales y escritorios remotos) los shaders no pueden correr, y Pyzed dibuja en su lugar el `fallback` del widget, un degradado o un color. Dale siempre uno a los shaders que van detrás de tu contenido. ### HUD de videojuego > Un HUD de videojuego en Python: barras de vida y maná, un minimapa de radar hecho con un shader, una barra de habilidades con enfriamientos y paneles de cristal. Source: https://albakore.github.io/pyzed/es/showcase/hud/ Un HUD completo sobre una nebulosa animada. Todo son widgets normales de Pyzed: la nebulosa y el barrido del radar son grafos de shader (archivos `.pzshad` hechos con nodos en Pyzed Design), los paneles son `glass`, y la habilidad lista y el cartel de subida de nivel usan `bloom`. (Image: HUD de videojuego) Pulsa de 1 a 6 (o haz clic en una casilla) para lanzar una habilidad. #### Cómo está hecho ##### Fondo con shader La nebulosa es un grafo de ruido deformado que la GPU redibuja en cada fotograma. El barrido del radar es otro grafo dentro de un panel de cristal redondo; su variable `accent` fija el color desde Python. ```python SHADERS = Path(__file__).parent / "shaders" pz.shader(SHADERS / "nebula.pzshad", width="fill", height="fill", fallback=pz.Gradient(["#05040f", "#2a0c46", "#0b3a4a"], angle=135)) pz.shader(SHADERS / "radar.pzshad", uniforms={"accent": "#34d399"}, width=176, height=176) ``` Mira los dos grafos en [Shaders y efectos](https://albakore.github.io/pyzed/es/showcase/shaders/#todos-los-grafos). ##### Paneles de cristal Cada panel desenfoca y tiñe lo que tiene detrás. ```python pz.row([...]).style(glass={"tint": "#0f172a", "strength": 0.35, "blur": 18}, radius=16) ``` ##### Luz y movimiento La vida baja palpita; la habilidad definitiva brilla con bloom y late cuando está lista. ```python health.animate("glow_pulse", repeat="infinite") # vida baja tile.animate("pulse", repeat="infinite", duration=1400) # la definitiva, cuando está lista ``` ##### Enfriamientos Pulsa del 1 al 6: `pz.every` descuenta los enfriamientos y la capa oscura se encoge con ellos. ```python def subscriptions(self): return [pz.every(0.1, self.tick, key="cooldowns")] if self.cooldowns else [] ``` #### Prueba esto - Cambia el `accent` del radar a `"#f472b6"` y su variable `sweep` a `1.0`. - Abre `examples/showcase/shaders/nebula.pzshad` en Pyzed Design y cambia los colores de su nodo Mix. - Haz que el enfriamiento de cada habilidad dure el doble. #### Código completo `examples/showcase/hud.py`: ```py """A game HUD: health and mana bars, a radar minimap, an ability bar and a quest tracker. python examples/showcase/hud.py - The space background and the minimap's radar sweep are shader graphs made of nodes in Pyzed Design (``shaders/nebula.pzshad`` and ``shaders/radar.pzshad``): ``pz.shader`` runs them on the GPU for every pixel, every frame. - Panels are ``glass=True`` (a tinted panel with a real backdrop blur), the ready ability and the level-up banner use ``bloom``, low health pulses with ``animate("glow_pulse")``. - Press 1 to 6 (or click a slot) to cast an ability; it goes on cooldown. """ from pathlib import Path import pyzed as pz SHADERS = Path(__file__).parent / "shaders" ABILITIES = ( ("sword", "Slash", 2.0), ("flame", "Fireball", 6.0), ("shield", "Ward", 10.0), ("zap", "Chain bolt", 8.0), ("wind", "Dash", 4.0), ("sparkles", "Ultimate", 30.0), ) QUESTS = ( ("Reach the Sunken Gate", True), ("Collect 3 star shards", False), ("Defeat the Warden", False), ) GOLD = "#fbbf24" CYAN = "#22d3ee" class Hud(pz.App): title = "Game HUD" size = (1280, 720) theme = pz.themes.ONE_DARK def __init__(self): self.health = 0.28 self.mana = 0.72 self.cooldowns = {1: 3.0} # slot -> seconds left self.casts = 0 def subscriptions(self): return [pz.every(0.1, self.tick, key="cooldowns")] if self.cooldowns else [] def tick(self): self.cooldowns = {slot: left - 0.1 for slot, left in self.cooldowns.items() if left > 0.1} def cast(self, slot): if slot in self.cooldowns: return self.cooldowns[slot] = ABILITIES[slot][2] self.mana = max(0.0, self.mana - 0.08) self.casts += 1 def on_key(self, combo): if combo in ("1", "2", "3", "4", "5", "6"): self.cast(int(combo) - 1) return True # -- pieces --------------------------------------------------------------------- def bar(self, value, colors, glow, width=260, height=14, label=None): fill = pz.container(None, width=max(height, width * value), height=height).style( background=pz.Gradient(colors, angle=90), radius=height / 2, glow=(glow, 10)) track = pz.container(fill, width=width, height=height).style( background="#ffffff14", radius=height / 2, border=(1, "#ffffff22")) if label is None: return track return pz.stack([track, pz.container(pz.text(label, size=10, weight="bold", color="#ffffffdd"), width=width, height=height, align_x="center", align_y="center")]) def player(self): portrait = pz.container(pz.icon("swords", size=34, color="#ffffff"), width=72, height=72, align_x="center", align_y="center").style( background=pz.Gradient(["#7c3aed", "#db2777"], angle=135), radius=36, border=(2, "#ffffffaa"), bloom=("#a855f7", 0.5)) health = self.bar(self.health, ["#991b1b", "#ef4444"], "#ef4444", label=f"{round(self.health * 1840)} / 1840") if self.health < 0.3: health = health.animate("glow_pulse", repeat="infinite") stats = pz.column([ pz.row([pz.text("Aria", size=18, weight="bold", color="#ffffff"), pz.text("Lv 27 · Stormblade", size=12, color="#ffffffaa")], spacing=8, align="end"), health, self.bar(self.mana, ["#1d4ed8", "#38bdf8"], "#38bdf8", label=f"{round(self.mana * 960)} / 960"), self.bar(0.64, ["#b45309", GOLD], GOLD, height=5), ], spacing=6) return pz.row([portrait, stats], spacing=14, align="center").style( glass={"tint": "#0f172a", "strength": 0.35, "blur": 18}, radius=16, padding=14) def minimap(self): radar = pz.shader(SHADERS / "radar.pzshad", uniforms={"accent": "#34d399"}, width=176, height=176) blips = pz.stack([ pz.container(pz.container(None, width=8, height=8).style(background="#f87171", radius=4, bloom=("#f87171", 1.0)), width=176, height=176, padding=(46, 0, 0, 120)), pz.container(pz.container(None, width=8, height=8).style(background=GOLD, radius=4, bloom=(GOLD, 1.0)), width=176, height=176, padding=(118, 0, 0, 52)), pz.container(pz.icon("navigation", size=16, color="#ffffff"), width=176, height=176, align_x="center", align_y="center"), ]) dial = pz.stack([radar, blips], width=176, height=176).style( radius=88, glass={"tint": "#022c22", "strength": 0.25, "blur": 14}, clip=True) return pz.column([ dial, pz.text("Sunken Coast · 412, -88", size=11, color="#ffffffaa"), ], spacing=6, align="center") def objective(self): return pz.row([ pz.icon("compass", size=16, color=GOLD), pz.text("Reach the Sunken Gate", size=14, weight="semibold", color="#ffffff"), pz.text("120 m", size=12, color="#ffffffaa"), ], spacing=10, align="center").style(glass={"tint": "#0f172a", "strength": 0.3}, radius=20, padding=(8, 16)).animate("fade_in_down") def quests(self): rows = [pz.text("QUESTS", size=11, weight="bold", color=GOLD)] for name, done in QUESTS: rows.append(pz.row([ pz.icon("circle-check" if done else "circle", size=14, color="#34d399" if done else "#ffffff88"), pz.text(name, size=13, color="#ffffff88" if done else "#ffffff"), ], spacing=8, align="center")) return pz.column(rows, spacing=8).style(glass={"tint": "#0f172a", "strength": 0.3}, radius=14, padding=14, width=230) def slot(self, index): icon, name, cooldown = ABILITIES[index] left = self.cooldowns.get(index) ready = left is None face = pz.container(pz.icon(icon, size=28, color="#ffffff" if ready else "#ffffff55"), width=64, height=64, align_x="center", align_y="center") layers = [face] if not ready: shade = 64 * left / cooldown layers.append(pz.container(pz.container(None, width=64, height=shade).style(background="#000000aa"), width=64, height=64, align_y="end")) layers.append(pz.container(pz.text(f"{left:.0f}" if left >= 1 else f"{left:.1f}", size=18, weight="bold", color="#ffffff"), width=64, height=64, align_x="center", align_y="center")) layers.append(pz.container(pz.text(str(index + 1), size=10, weight="bold", color="#ffffffcc"), width=64, height=64, padding=(3, 6), align_x="end")) style = dict(radius=12, clip=True, border=(1, "#ffffff33")) if ready and index == 5: style.update(background=pz.Gradient(["#7c2d12", "#ea580c"], angle=135), bloom=(GOLD, 0.9), border=(2, GOLD)) elif ready: style.update(background=pz.Gradient(["#1e293b", "#334155"], angle=135)) else: style.update(background="#0f172a") tile = pz.stack(layers, width=64, height=64).style(**style) tile = pz.mouse_area(tile, on_press=lambda: self.cast(index)) tile = tile.animate("shrink", on="press") if ready and index == 5: tile = tile.animate("pulse", repeat="infinite", duration=1400) return pz.column([tile, pz.text(name, size=10, color="#ffffffaa")], spacing=4, align="center") def abilities(self): return pz.row([self.slot(i) for i in range(len(ABILITIES))], spacing=10).style( glass={"tint": "#020617", "strength": 0.4, "blur": 16}, radius=18, padding=(12, 16, 8, 16)) def log(self): lines = ( ("You hit Tide Wraith for 312", "#ffffff"), ("Critical! Chain bolt 1,204", GOLD), ("Tide Wraith hits you for 188", "#f87171"), ("Ward absorbed 140 damage", CYAN), ) return pz.column([pz.text(t, size=12, color=c, font="mono") for t, c in lines], spacing=4).style( glass={"tint": "#020617", "strength": 0.3}, radius=12, padding=12, width=300) def level_up(self): return pz.column([ pz.text("LEVEL UP", size=40, weight="black", color="#fff7d6").style(bloom=(GOLD, 1.0)), pz.text("Stormblade · Level 27", size=14, color="#fde68a"), ], spacing=2, align="center").animate("pop_in").animate("float", repeat="infinite") def view(self): def at(widget, x, y, pad): return pz.container(widget, width="fill", height="fill", align_x=x, align_y=y, padding=pad) return pz.stack([ pz.shader(SHADERS / "nebula.pzshad", width="fill", height="fill", fallback=pz.Gradient(["#05040f", "#2a0c46", "#0b3a4a"], angle=135)), at(self.player(), "start", "start", 20), at(self.objective(), "center", "start", 24), at(self.minimap(), "end", "start", 20), at(self.quests(), "end", "center", 20), at(self.level_up(), "center", "center", (0, 0, 120, 0)), at(self.log(), "start", "end", 20), at(self.abilities(), "center", "end", 20), ]) if __name__ == "__main__": pz.run(Hud) ``` ### Menú principal de videojuego > Un menú principal synthwave en Python con un fondo de shader que sigue al ratón, un título brillante y botones de cristal que se elevan en hover. Source: https://albakore.github.io/pyzed/es/showcase/main-menu/ Una pantalla de título al estilo de un juego de carreras synthwave. El sol, las estrellas y la cuadrícula infinita son un solo grafo de shader, `shaders/synthwave.pzshad`, que avanza con el tiempo y se inclina hacia el puntero. (Image: Menú principal de videojuego) Usa ↑ y ↓ (o W y S) para elegir. #### Cómo está hecho ##### Shader interactivo El grafo tiene un nodo **Mouse**: la posición del puntero sobre el widget, de 0 a 1. Unos pocos nodos de operaciones mueven un poco el horizonte con él, así que la escena se inclina cuando mueves el puntero. En Python es un widget detrás del contenido: ```python pz.stack([ pz.shader(SHADER, width="fill", height="fill", fallback=pz.Gradient(["#08021a", "#4a0d5e", "#d9468f"], angle=180)), content, ]) ``` ##### Título brillante `bloom` en un texto sin caja lo ilumina desde sus propios trazos. ```python pz.text("NEON DRIFT", size=72, weight="black").style(bloom=("#f472b6", 1.0)) ``` ##### Movimiento del menú Los botones se elevan en hover, se encogen al pulsar y el seleccionado se desliza con una transición. ```python button.animate("lift", on="hover").animate("shrink", on="press").transition(x=6 if selected else 0) ``` #### Prueba esto - Añade una opción al menú y dale su propia acción. - Cambia el color y la intensidad del `bloom` del título. - Abre `shaders/synthwave.pzshad` en Pyzed Design y cambia los colores del sol. #### Código completo `examples/showcase/main_menu.py`: ```py """A game's main menu: a synthwave shader backdrop, a glowing title and glass buttons. python examples/showcase/main_menu.py - The sun, the grid and the stars are one shader graph, ``shaders/synthwave.pzshad``, made of nodes in Pyzed Design: it scrolls with Time and follows the pointer with Mouse. - The title uses ``bloom``; the menu is a ``glass`` panel whose buttons lift and glow on hover (``animate("lift", on="hover")``) and play ``shrink`` on press. - The arrow keys (or W and S) move the selection. """ from pathlib import Path import pyzed as pz SHADER = Path(__file__).parent / "shaders" / "synthwave.pzshad" ITEMS = ( ("play", "Continue", "Chapter 4 · The Neon Pier"), ("plus", "New game", "Start a fresh run"), ("users", "Multiplayer", "3 friends online"), ("settings", "Settings", "Graphics, audio, controls"), ("log-out", "Quit", "See you soon"), ) PINK = "#f472b6" class MainMenu(pz.App): title = "Main menu" size = (1280, 720) theme = pz.themes.ONE_DARK def __init__(self): self.selected = 0 def pick(self, index): self.selected = index def on_key(self, combo): if combo in ("down", "s"): self.selected = (self.selected + 1) % len(ITEMS) return True if combo in ("up", "w"): self.selected = (self.selected - 1) % len(ITEMS) return True def item(self, index): icon, label, hint = ITEMS[index] selected = index == self.selected body = pz.row([ pz.icon(icon, size=20, color="#ffffff" if selected else "#ffffffbb"), pz.column([ pz.text(label, size=18, weight="bold" if selected else "semibold", color="#ffffff"), pz.text(hint, size=12, color="#ffffffaa"), ], spacing=1), ], spacing=14, align="center") style = dict(radius=12, padding=(12, 18), width=330) if selected: style.update(background=pz.Gradient(["#db2777cc", "#7c3aedcc"], angle=90), border=(1, "#ffffff66"), bloom=(PINK, 0.6)) else: style.update(background="#ffffff0d", border=(1, "#ffffff1a")) button = pz.mouse_area(body.style(**style), on_press=lambda: self.pick(index)) return button.animate("lift", on="hover").animate("shrink", on="press").transition( x=6 if selected else 0, duration=160, easing="ease_out_cubic") def view(self): title = pz.column([ pz.text("NEON DRIFT", size=72, weight="black", color="#ffe4f3").style(bloom=(PINK, 1.0)), pz.text("A pyzed showcase", size=16, color="#fbcfe8").style(opacity=0.8), ], spacing=0).animate("fade_in_down", duration=600) menu = pz.column([self.item(i) for i in range(len(ITEMS))], spacing=10).style( glass={"tint": "#1e0b2e", "strength": 0.35, "blur": 22}, radius=20, padding=16) footer = pz.row([ pz.text("v1.4.2", size=12, color="#ffffff88", font="mono"), pz.space(width="fill"), pz.text("Press ↑ ↓ to choose", size=12, color="#ffffffaa").animate( "blink", repeat="infinite", duration=2200), ], width="fill") content = pz.column([ title, pz.row(pz.stagger([menu], "fade_in_left", step=80)), pz.space(height="fill"), footer, ], spacing=28, width="fill", height="fill", padding=(48, 56)) return pz.stack([ pz.shader(SHADER, width="fill", height="fill", fallback=pz.Gradient(["#08021a", "#4a0d5e", "#d9468f"], angle=180)), content, ]) if __name__ == "__main__": pz.run(MainMenu) ``` ### Inventario de RPG > Una pantalla de inventario de RPG en Python donde la rareza de los objetos brilla con glow y bloom, con un panel de detalles de cristal animado sobre un shader de brasas. Source: https://albakore.github.io/pyzed/es/showcase/inventory/ Un inventario donde la rareza es luz: los objetos raros tienen glow, los épicos y legendarios bloom, y los legendarios palpitan. Al hacer clic en un objeto, `presence` cambia el panel de detalles de cristal y sus barras crecen con `transition(width=...)`. (Image: Inventario de RPG) Las brasas del fondo son el grafo `embers.pzshad`; míralo en [Shaders y efectos](https://albakore.github.io/pyzed/es/showcase/shaders/#todos-los-grafos). #### Cómo está hecho ##### La rareza como luz Glow para los raros; bloom para épicos y legendarios. ```python style.update(bloom=(color, 0.8)) tile.animate("glow_pulse", repeat="infinite", duration=2000) ``` ##### Estadísticas animadas El ancho de las barras es una propiedad de layout, así que se anima sin descolocar nada. ```python bar.transition(width=240 * value, duration=320, easing="ease_out_cubic") ``` ##### Cambio de panel `pz.presence` saca el panel anterior y hace entrar el nuevo. ```python pz.presence(panel, key=self.selected, enter="fade_in_right", exit="fade_out") ``` #### Prueba esto - Añade un objeto con una rareza nueva, y un color y una luz para ella en la tabla de rarezas. - Haz que las barras de estadísticas crezcan más lento subiendo la `duration` de la transición. - Cambia la variable `rise` del shader de brasas para que las chispas vayan más rápido. #### Código completo `examples/showcase/inventory.py`: ```py """An RPG inventory: a grid of items whose rarity glows, and a glass details panel. python examples/showcase/inventory.py - Rarity is light: rare items ``glow``, epic and legendary ones ``bloom`` (and legendary ones pulse with ``animate("glow_pulse")``). - The details panel is ``glass`` over an ember backdrop, a shader graph made of nodes in Pyzed Design (``shaders/embers.pzshad``); stat bars and the selected slot animate with ``transition``. - Click an item to inspect it. """ from pathlib import Path import pyzed as pz SHADER = Path(__file__).parent / "shaders" / "embers.pzshad" RARITY = { "common": ("#9ca3af", None), "rare": ("#60a5fa", "glow"), "epic": ("#c084fc", "bloom"), "legendary": ("#fb923c", "bloom"), } ITEMS = ( ("sword", "Dawnbreaker", "legendary", "Weapon", {"Attack": 0.92, "Speed": 0.64, "Crit": 0.71}), ("shield", "Aegis of Tides", "epic", "Off-hand", {"Armor": 0.81, "Block": 0.66, "Weight": 0.40}), ("wand", "Ember Wand", "rare", "Weapon", {"Attack": 0.58, "Speed": 0.80, "Crit": 0.35}), ("gem", "Star Shard", "epic", "Material", {"Purity": 0.88, "Power": 0.62, "Value": 0.74}), ("flask-conical", "Health Potion", "common", "Consumable", {"Heal": 0.45, "Duration": 0.10, "Value": 0.08}), ("scroll", "Scroll of Return", "common", "Consumable", {"Range": 0.30, "Value": 0.12, "Weight": 0.05}), ("crown", "Crown of Ash", "legendary", "Helm", {"Armor": 0.55, "Wisdom": 0.95, "Charm": 0.86}), ("key", "Old Key", "common", "Quest", {"Value": 0.02, "Weight": 0.04, "Rust": 0.90}), ("feather", "Phoenix Plume", "rare", "Material", {"Power": 0.70, "Purity": 0.52, "Value": 0.61}), ("bow-arrow", "Windstring", "rare", "Weapon", {"Attack": 0.66, "Speed": 0.87, "Crit": 0.50}), ("shirt", "Silk Tunic", "common", "Chest", {"Armor": 0.20, "Speed": 0.60, "Weight": 0.10}), ("apple", "Golden Apple", "epic", "Consumable", {"Heal": 0.95, "Duration": 0.60, "Value": 0.70}), ) #: Each rarity's slot color. TINT = {"common": "#1f1d24", "rare": "#16233a", "epic": "#2a1a3d", "legendary": "#3a2112"} COLUMNS, ROWS = 6, 4 SLOT = 76 class Inventory(pz.App): title = "Inventory" size = (1180, 640) theme = pz.themes.ONE_DARK def __init__(self): self.selected = 0 def select(self, index): self.selected = index def slot(self, index): if index >= len(ITEMS): return pz.container(None, width=SLOT, height=SLOT).style( background="#ffffff08", radius=12, border=(1, "#ffffff12")) icon, name, rarity, _, _ = ITEMS[index] color, light = RARITY[rarity] selected = index == self.selected # A plain color, not a gradient: the GPU renderer draws halos only around solid boxes. style = dict(radius=12, background=TINT[rarity], border=(2 if selected else 1, color if selected else color + "66")) if light == "glow": style.update(glow=(color + "88", 10)) elif light == "bloom": style.update(bloom=(color, 0.55 if rarity == "epic" else 0.8)) tile = pz.container(pz.icon(icon, size=32, color="#ffffff"), width=SLOT, height=SLOT, align_x="center", align_y="center").style(**style) if rarity == "legendary": tile = tile.animate("glow_pulse", repeat="infinite", duration=2000) tile = pz.mouse_area(tile, on_press=lambda: self.select(index)) return tile.animate("grow", on="hover").animate("shrink", on="press") def grid(self): rows = [pz.row([self.slot(r * COLUMNS + c) for c in range(COLUMNS)], spacing=10) for r in range(ROWS)] gold = pz.row([pz.icon("coins", size=16, color="#fbbf24"), pz.text("12,480", size=14, weight="bold", color="#fde68a"), pz.space(width="fill"), pz.text(f"{len(ITEMS)} / {COLUMNS * ROWS}", size=12, color="#ffffff88")], spacing=8, align="center", width=COLUMNS * SLOT + (COLUMNS - 1) * 10) return pz.column([ pz.text("INVENTORY", size=13, weight="bold", color="#fdba74"), pz.column(rows, spacing=10), gold, ], spacing=14).style(glass={"tint": "#140b0b", "strength": 0.35, "blur": 16}, radius=20, padding=20) def details(self): icon, name, rarity, kind, stats = ITEMS[self.selected] color, _ = RARITY[rarity] bars = [] for label, value in stats.items(): bars.append(pz.column([ pz.row([pz.text(label, size=12, color="#ffffffbb"), pz.space(width="fill"), pz.text(str(round(value * 100)), size=12, weight="bold", color="#ffffff")], width=240), pz.container(pz.container(None, height=8).style( background=pz.Gradient([color + "88", color], angle=90), radius=4).transition( width=240 * value, duration=320, easing="ease_out_cubic"), width=240).style(background="#ffffff14", radius=4), ], spacing=4)) hero = pz.container(pz.icon(icon, size=56, color="#ffffff").style(bloom=(color, 0.9)), width=240, height=150, align_x="center", align_y="center").style( background=pz.Gradient([color + "44", "#00000000"], angle=180), radius=16) panel = pz.column([ hero, pz.text(name, size=22, weight="bold", color="#ffffff"), pz.row([pz.text(rarity.upper(), size=11, weight="bold", color=color), pz.text("·", size=11, color="#ffffff66"), pz.text(kind, size=11, color="#ffffffaa")], spacing=6), *bars, pz.row([ pz.text("Equip", size=13, weight="bold", color="#1c1917").style( background=color, radius=8, padding=(8, 18)).animate("lift", on="hover"), pz.text("Drop", size=13, color="#ffffffcc").style( background="#ffffff14", radius=8, padding=(8, 18), border=(1, "#ffffff22")), ], spacing=10), ], spacing=12, width=240) return pz.presence(panel.style(glass={"tint": "#1c1012", "strength": 0.3, "blur": 18}, radius=20, padding=20), key=self.selected, enter="fade_in_right", exit="fade_out", id="details") def view(self): body = pz.row([self.grid(), self.details()], spacing=24, align="start") return pz.stack([ pz.shader(SHADER, width="fill", height="fill", fallback=pz.Gradient(["#0f0707", "#2a0e05"], angle=180)), pz.container(body, width="fill", height="fill", align_x="center", align_y="center"), ]) if __name__ == "__main__": pz.run(Inventory) ``` ### Centro de control de cristal > Un panel profesional de cristal esmerilado en Python: reproductor, luces, clima y agenda con desenfoque real del fondo sobre colores en movimiento. Source: https://albakore.github.io/pyzed/es/showcase/glass-dashboard/ Un centro de control domótico con el estilo de cristal esmerilado de los sistemas operativos modernos. Las manchas de color detrás de las tarjetas son un grafo de shader (`shaders/blobs.pzshad`), así que el desenfoque que ves a través de cada tarjeta se mueve con ellas. (Image: Centro de control de cristal) #### Cómo está hecho ##### Desenfoque real del fondo `glass` desenfoca lo que hay detrás de la tarjeta; no es un degradado falso. ```python GLASS = {"tint": "#ffffff", "strength": 0.10, "blur": 28} card.style(glass=GLASS, radius=22, padding=20) ``` ##### Detalles vivos La portada gira mientras suena la música; las bombillas encendidas brillan con bloom. ```python cover.animate("spin", duration=12000) ``` ##### Controles reales Los sliders y switches cambian el estado como en cualquier app de Pyzed. ```python pz.slider(0, 100, self.volume, on_change=self.set_volume) ``` #### Prueba esto - Cambia `tint` y `strength` en `GLASS` y mira cuánto del fondo se ve a través. - Enciende todas las luces y mira el bloom. - Cambia las manchas por otro grafo de [Shaders y efectos](https://albakore.github.io/pyzed/es/showcase/shaders/#todos-los-grafos), como `nebula.pzshad`. **Tip:** El cristal luce mejor sobre algo que se mueve y tiene color. Sobre un fondo plano es solo un panel tintado. #### Código completo `examples/showcase/glass_dashboard.py`: ```py """A frosted-glass control center: music, lights, climate and a schedule over moving color. python examples/showcase/glass_dashboard.py - The background is a shader graph of soft color blobs that drift with time (``shaders/blobs.pzshad``, made of nodes in Pyzed Design). - Every card is ``glass``: a tinted panel that blurs what moves behind it (``backdrop_blur``). - Switches, sliders and the play button update the cards; values move with ``transition``. """ from pathlib import Path import pyzed as pz SHADER = Path(__file__).parent / "shaders" / "blobs.pzshad" from pyzed import ui GLASS = {"tint": "#ffffff", "strength": 0.10, "blur": 28} WHITE = "#ffffff" MUTED = "#ffffffaa" class Dashboard(pz.App): title = "Control center" size = (1180, 700) theme = pz.themes.ONE_DARK def __init__(self): self.playing = True self.volume = 64.0 self.lights = {"Living room": True, "Kitchen": False, "Studio": True} self.brightness = 72.0 self.temperature = 21.5 def toggle_play(self): self.playing = not self.playing def set_volume(self, value): self.volume = value def toggle_light(self, name): self.lights[name] = not self.lights[name] def set_brightness(self, value): self.brightness = value def warmer(self): self.temperature = min(28.0, self.temperature + 0.5) def cooler(self): self.temperature = max(16.0, self.temperature - 0.5) def card(self, title, icon, body, width): head = pz.row([pz.icon(icon, size=16, color=WHITE), pz.text(title, size=13, weight="semibold", color=WHITE)], spacing=8, align="center") return pz.column([head, body], spacing=14).style(glass=GLASS, radius=22, padding=20, width=width) def music(self): cover = pz.container(pz.icon("disc-3", size=44, color="#ffffffdd"), width=112, height=112, align_x="center", align_y="center").style( background=pz.Gradient(["#f472b6", "#8b5cf6", "#22d3ee"], angle=135), radius=18, rotate=0, bloom=("#c084fc", 0.4)) if self.playing: cover = cover.animate("spin", duration=12000) controls = pz.row([ pz.icon("skip-back", size=20, color=WHITE), pz.mouse_area(pz.container(pz.icon("pause" if self.playing else "play", size=22, color="#111827"), width=48, height=48, align_x="center", align_y="center").style( background=WHITE, radius=24, glow=("#ffffff88", 14)), on_press=self.toggle_play).animate( "shrink", on="press"), pz.icon("skip-forward", size=20, color=WHITE), ], spacing=22, align="center") info = pz.column([ pz.text("Midnight City", size=20, weight="bold", color=WHITE), pz.text("Neon Avenue · Afterglow", size=13, color=MUTED), pz.container(pz.container(None, height=4).style(background=WHITE, radius=2).transition( width=230 * (0.42 if self.playing else 0.41), duration=400), width=230).style( background="#ffffff33", radius=2), pz.row([pz.text("1:48", size=11, color=MUTED), pz.space(width="fill"), pz.text("4:16", size=11, color=MUTED)], width=230), controls, ], spacing=8) volume = pz.row([ pz.icon("volume-1", size=16, color=MUTED), pz.slider(0, 100, self.volume, on_change=self.set_volume, width="fill", accent=WHITE, bg="#ffffff33"), pz.icon("volume-2", size=16, color=MUTED), ], spacing=10, align="center") return self.card("Now playing", "music", pz.column([pz.row([cover, info], spacing=20, align="center"), volume], spacing=16), 440) def lights_card(self): rows = [] for name, on in self.lights.items(): rows.append(pz.row([ pz.container(pz.icon("lightbulb", size=16, color="#1f2937" if on else MUTED), width=34, height=34, align_x="center", align_y="center").style( background="#fde68a" if on else "#ffffff1a", radius=17, bloom=("#fbbf24", 0.6) if on else None), pz.column([pz.text(name, size=14, weight="semibold", color=WHITE), pz.text(f"{round(self.brightness)}%" if on else "Off", size=11, color=MUTED)], spacing=1, width="fill"), ui.switch(on, lambda _v, name=name: self.toggle_light(name)), ], spacing=12, align="center")) rows.append(pz.slider(0, 100, self.brightness, on_change=self.set_brightness, width="fill", accent="#fde68a", bg="#ffffff33")) return self.card("Lights", "sun", pz.column(rows, spacing=12), 330) def climate(self): ring = pz.column([ pz.text(f"{self.temperature:.1f}°", size=44, weight="bold", color=WHITE).transition( scale=1.0, duration=200), pz.text("Heating to 22°", size=12, color=MUTED), ], spacing=0, align="center") buttons = pz.row([ pz.mouse_area(pz.container(pz.icon("minus", size=18, color=WHITE), width=44, height=44, align_x="center", align_y="center").style( background="#ffffff1f", radius=22), on_press=self.cooler).animate("shrink", on="press"), pz.mouse_area(pz.container(pz.icon("plus", size=18, color=WHITE), width=44, height=44, align_x="center", align_y="center").style( background="#ffffff1f", radius=22), on_press=self.warmer).animate("shrink", on="press"), ], spacing=16) dial = pz.container(ring, width=170, height=170, align_x="center", align_y="center").style( radius=85, border=(6, "#fb923c"), bloom=("#fb923c", 0.5), background="#ffffff0d") return self.card("Climate", "thermometer", pz.column([dial, buttons], spacing=14, align="center", width="fill"), 330) def schedule(self): events = ( ("09:30", "Design review", "#22d3ee"), ("12:00", "Lunch with Sam", "#a3e635"), ("15:00", "Ship 1.2", "#f472b6"), ("18:30", "Climbing", "#fbbf24"), ) rows = [pz.row([ pz.text(time, size=12, color=MUTED, font="mono", width=48), pz.container(None, width=4, height=28).style(background=color, radius=2, bloom=(color, 0.6)), pz.text(title, size=14, color=WHITE), ], spacing=12, align="center") for time, title, color in events] return self.card("Today", "calendar", pz.column(rows, spacing=10), 440) def header(self): return pz.row([ pz.column([pz.text("Good evening, Kim", size=28, weight="bold", color=WHITE), pz.text("Saturday · 3 rooms on · 21.5° inside", size=14, color=MUTED)], spacing=2), pz.space(width="fill"), pz.row([pz.icon("search", size=16, color=MUTED), pz.text("Search devices", size=13, color=MUTED)], spacing=8, align="center").style(glass=GLASS, radius=20, padding=(10, 16), width=240), ], align="center", width=1124) def view(self): grid = pz.row([ pz.column([self.music(), self.schedule()], spacing=20), self.lights_card(), self.climate(), ], spacing=20, align="start") content = pz.column([self.header(), pz.row(pz.stagger([grid], "fade_in_up", step=60))], spacing=24) return pz.stack([ pz.shader(SHADER, width="fill", height="fill", fallback=pz.Gradient(["#0b1026", "#5b1a4a", "#0f4c5c"], angle=135)), pz.container(content, width="fill", height="fill", align_x="center", align_y="center"), ]) if __name__ == "__main__": pz.run(Dashboard) ``` ## Videojuegos ### Crear videojuegos > Aprende a hacer juegos 2D en Python con Pyzed, desde tu primer bucle de juego hasta plataformas, shooters y puzles completos: una ruta paso a paso con ejemplos animados, consejos y tres juegos terminados. Source: https://albakore.github.io/pyzed/es/games/ Pyzed es una librería de interfaces, pero ya trae todo lo que necesita un juego 2D: un `pz.canvas` dibujado por la GPU, un bucle que se ejecuta una vez por fotograma, las teclas que están pulsadas ahora mismo y toda la biblioteca de widgets para construir menús y HUD encima de la acción. Cada juego de estas páginas es un programa completo y jugable en [`examples/games/`](https://github.com/albakore/pyzed/tree/main/examples/games), comprobado por las pruebas en cada cambio. #### Aprende a crear juegos Una ruta desde cero hasta juegos completos. Cada paso es un tutorial con un objetivo, el tiempo que lleva, lo que necesitas saber antes, el código completo e ideas para seguir. Si nunca has escrito un juego, empieza por el 1; si ya lo has hecho, salta a lo que necesites. ```mermaid flowchart LR A["1 · Bucle y teclas"] --> B["2 · Sprites y animación"] --> C["3 · Colisiones y física"] C --> D["4 · Plataformas"] C --> E["5 · Asteroids"] B --> F["6 · Puzle: 2048"] D --> G["7 · Menús, HUD, ajustes"] E --> G F --> G G --> H["8 · Guardado y sonido"] ``` **¿Nunca has programado un juego?:** Empieza por [1. Tu primer juego](https://albakore.github.io/pyzed/es/games/first-game/): explica cada línea, y el juego se puede jugar desde el tercer paso. [Un bucle de juego](https://albakore.github.io/pyzed/es/tutorials/advanced/game-loop/) (Dodge, en los Tutoriales) es una buena segunda lectura. #### Juegos completos Tres juegos más grandes para leer, jugar y desmontar. Crecen en dificultad: - [Snake](https://albakore.github.io/pyzed/es/games/snake/): Movimiento en cuadrícula con un temporizador fijo, cola de teclas, game over y reinicio. ~150 líneas. - [Breakout](https://albakore.github.io/pyzed/es/games/breakout/): Un bucle a 60 fps con pz.frames, teclas mantenidas y ratón, física de la pelota, partículas y temblor. - [Neon Invaders](https://albakore.github.io/pyzed/es/games/neon-invaders/): Sprites pixel art, oleadas, power-ups, brillo neón y un HUD de cristal con bloom. Ejecuta cualquier juego desde la carpeta del repositorio; cambia el nombre del archivo para los demás: #### Las piezas | Qué | API | Para qué | | --- | --- | --- | | Bucle por fotograma | `pz.frames(callback)` en `subscriptions()` | `callback(dt)` una vez por fotograma (sincronizado con la pantalla); `dt` son los segundos desde el anterior. | | Temporizador fijo | `pz.every(segundos, callback)` | Juegos por turnos o en cuadrícula que avanzan a ritmo fijo (Snake). | | Pulsaciones | `on_key(combo)` | Acciones puntuales: saltar, pausar, reiniciar (`"space"`, `"enter"`, `"ctrl-r"`). | | Soltar teclas | `on_key_up(key)` | Mecánicas de cargar y soltar, parar un sonido. | | Teclas mantenidas | `pz.pressed()`, `pz.is_pressed("left", "a")` | Movimiento continuo y disparo automático, consultados en cada fotograma. | | Movimiento | `game.axis(neg, pos)`, `game.vector()` | -1/0/1 a partir de dos grupos de teclas; una dirección normalizada con flechas/WASD. | | Dibujo | `pz.canvas([...])` con `pyzed.draw` | Rectángulos, círculos, trazados, texto, transformaciones. | | Sprites | `draw.image(src, x, y, w, h)` y `game.sprite(filas, paleta)` | Imágenes (se decodifican una vez y quedan en caché) o pixel art escrito como texto. | | Colisiones | `game.Rect`, `game.circle_rect` | Solapamiento de cajas; por dónde golpea una pelota a una caja, para rebotar. | | Efectos | `game.glow`, `game.Particles`, `game.Shake` | Luces neón, explosiones y chispas, temblor de pantalla. | | HUD y menús | `pz.stack([canvas, widgets])` | Puntuaciones, barras de vida y pantallas de pausa con `glass`, `bloom` y animaciones. | ##### El bucle Un juego es un `pz.App` normal. Devuelve `pz.frames(self.tick)` desde `subscriptions()` y el runtime llama a `tick(dt)` antes de cada fotograma. Mueve las cosas *velocidad × dt* para que vayan igual de rápido a 60, 120 o 144 Hz: ```python import pyzed as pz from pyzed import draw, game class Ship(pz.App): size = (480, 320) def __init__(self): self.x, self.y = 240, 160 def subscriptions(self): return [pz.frames(self.tick)] def tick(self, dt): dx, dy = game.vector() # flechas / WASD, diagonales normalizadas self.x += dx * 300 * dt self.y += dy * 300 * dt def view(self): return pz.canvas([draw.background("#0b0e14"), *game.glow(self.x, self.y, 14, "#22d3ee")], width=480, height=320) pz.run(Ship) ``` `dt` se limita a un cuarto de segundo, así que arrastrar la ventana o parar en un breakpoint nunca hace que el juego salte hacia delante. Donde la pantalla no espera a la sincronización vertical (renderizado por software), `max_fps` (240 por defecto) evita que el bucle consuma toda la CPU. **Tip:** Para el bucle cuando nada se mueve: `subscriptions()` se evalúa tras cada actualización, así que `return [] if self.paused else [pz.frames(self.tick)]` pausa el juego y la CPU. ##### Entrada `on_key` es para acciones que ocurren una vez por pulsación; `pz.pressed()` para lo que ocurre *mientras* una tecla está pulsada. Ambos ven todas las teclas, tenga el foco el widget que lo tenga. ```python def on_key(self, combo): # "space", "p", "shift-r"... if combo == "p": self.paused = not self.paused def tick(self, dt): if pz.is_pressed("space") and self.cooldown <= 0: self.fire() # disparo automático mientras Space está pulsada ``` El ratón llega desde el propio canvas: `on_move(x, y)`, `on_press(x, y)`, `on_release()`, `on_right_press(x, y)` y `on_scroll(dx, dy)`, en coordenadas del canvas. ##### Sprites `draw.image` dibuja un PNG/JPEG (o bytes RGBA) dentro de una caja, opcionalmente girado y con transparencia. Se decodifica una vez por origen y queda en caché, así que dibujar el mismo sprite cientos de veces por fotograma es barato. Usa `pixelated=True` para pixel art ampliado. ```python draw.image("assets/ship.png", self.x - 16, self.y - 16, 32, 32, rotation=self.angle) ``` Para juegos retro ni siquiera hacen falta archivos: `game.sprite` convierte texto en pixel art, con una forma por color sea cual sea el tamaño del sprite. ```python INVADER = ["..#.....#..", "...#...#...", "..#######..", ".##.###.##.", "###########"] game.sprite(INVADER, {"#": "#a3e635"}, x, y, scale=4) ``` ##### El HUD y los menús son widgets Apila widgets sobre el canvas con `pz.stack`. Puntuaciones, barras de vida, insignias y menús de pausa son componentes normales de Pyzed, así que tienen temas, paneles `glass`, `bloom` y animaciones de entrada sin esfuerzo: la parte difícil de la interfaz de un juego ya está hecha. ```python hud = pz.container(pz.row([score, pz.hspace(), ui.badge(f"Wave {self.wave}")]), padding=(8, 14)).style(glass=True, radius=12) return pz.stack([self.scene(), hud], width=W, height=H) ``` #### Consejos de rendimiento Más en [Consejos para crear juegos](https://albakore.github.io/pyzed/es/games/tips/). - Haz poco trabajo Python por fotograma: listas y números simples, sin crear estructuras grandes cada vez. - Agrupa formas: `draw.rects` dibuja muchos rectángulos de un color como una sola forma (es lo que usa `game.sprite`). - Limita las partículas (`game.Particles(limit=...)`) y elimina las balas que salen de la pantalla. - Solo el canvas cambia en cada fotograma; los widgets del HUD son baratos de reconstruir, pero `pz.memo` puede cachear los costosos. - Prueba la lógica sin ventana: llama a `tick(1 / 60)` en un bucle y comprueba el estado, como hace [`tests/test_game.py`](https://github.com/albakore/pyzed/blob/main/tests/test_game.py). `App.screenshot` renderiza cualquier momento del juego a un PNG. ### 1. Tu primer juego: el bucle y las teclas > Haz un pequeño juego completo en Python con Pyzed: un bucle por fotograma con pz.frames, movimiento con velocidad × dt, teclas mantenidas, un temporizador, una puntuación y una pausa, en unas 100 líneas. Source: https://albakore.github.io/pyzed/es/games/first-game/ Un juego es un programa que **sigue funcionando aunque no pulses nada**. Cada pocos milisegundos mira las teclas, mueve un poco las cosas y lo vuelve a dibujar todo. En este primer paso de la ruta construyes *Coin Chase*: un orbe brillante que diriges con las flechas, una moneda que atrapar, un reloj de 30 segundos y una puntuación. Es pequeño, pero tiene todas las partes de un juego de verdad. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un juego completo con bucle, teclas mantenidas, temporizador y puntuación | 25 minutos | Principiante | Python y Pyzed instalados ([Instalación](https://albakore.github.io/pyzed/es/getting-started/installation/)); ninguna experiencia con juegos | #### Qué aprenderás - Las tres partes de todo juego: **estado**, **actualización** y **dibujo**. - Ejecutar código **una vez por fotograma** con `pz.frames(self.tick)`. - Mover cosas con **velocidad × `dt`**, para que el juego vaya igual de rápido en cualquier pantalla. - Leer las teclas **mantenidas** (`game.vector`) y las **pulsadas una vez** (`on_key`). - Pausar el juego (y la CPU) devolviendo cero suscripciones. Juégalo primero: #### Las tres partes ```mermaid flowchart LR S["Estado
x, y, score, time_left"] --> U["Actualizar: tick(dt)
leer teclas · mover · colisiones"] U --> D["Dibujar: view()
canvas + widgets del HUD"] D -->|siguiente fotograma| U ``` Pyzed llama a `tick(dt)` antes de cada fotograma que muestra la pantalla (60, 120 o 144 veces por segundo) y luego a `view()` para dibujar. Nunca escribes el bucle: solo dices qué pasa en un fotograma. 1. **El estado.** Todo lo que el juego necesita recordar vive en atributos de la app. `reset()` les da valor, así que el mismo método empieza la primera ronda y cada ronda nueva: ```python W, H = 560, 380 SPEED = 260 # pixels per second PLAYER_R = 14 COIN_R = 9 ROUND = 30.0 # seconds per round class CoinChase(pz.App): def __init__(self, seed=None): self.random = random.Random(seed) self.held = None # the keys held down; None reads the keyboard self.best = 0 self.reset() def reset(self): self.x, self.y = W / 2, H / 2 self.score = 0 self.time_left = ROUND self.paused = self.over = False self.pops = [] # [x, y, age]: rings that grow and fade where a coin was taken self.clock = 0.0 self.coin = self.new_coin() ``` `held` es para pruebas y demos: le asignan `{"right"}` para mover sin teclado. Cuando es `None`, el juego lee las teclas reales. 2. **Enciende el bucle.** `subscriptions()` devuelve lo que debe estar en marcha ahora. `pz.frames(self.tick)` significa "llama a `tick` en cada fotograma". Pyzed vuelve a preguntar tras cada cambio, así que no devolver nada pausa el juego, y la CPU descansa: ```python def subscriptions(self): if self.paused or self.over: return [] return [pz.frames(self.tick)] ``` 3. **Mueve con velocidad × dt.** `dt` son los segundos desde el fotograma anterior (unos `0.0167` a 60 Hz). Si movieras 4 píxeles fijos por fotograma, el orbe iría el doble de rápido en una pantalla de 120 Hz. Multiplicar por `dt` da *píxeles por segundo*: ```python def tick(self, dt): self.clock += dt self.time_left = max(0.0, self.time_left - dt) dx, dy = game.vector(held=self.held) # arrows / WASD, diagonals normalized self.x = game.clamp(self.x + dx * SPEED * dt, PLAYER_R, W - PLAYER_R) self.y = game.clamp(self.y + dy * SPEED * dt, PLAYER_R, H - PLAYER_R) ``` `game.vector()` convierte las flechas y WASD en una dirección: `(1, 0)` a la derecha, `(0.71, -0.71)` arriba a la derecha (así las diagonales no son más rápidas). `game.clamp` mantiene el orbe dentro del tablero. 4. **¿Tocó la moneda?** Dos círculos se tocan cuando la distancia entre sus centros es menor que la suma de sus radios. `math.hypot` mide esa distancia: ```python cx, cy = self.coin if math.hypot(cx - self.x, cy - self.y) < PLAYER_R + COIN_R: self.score += 1 self.pops.append([cx, cy, 0.0]) self.coin = self.new_coin() for pop in self.pops: pop[2] += dt self.pops = [p for p in self.pops if p[2] < 0.4] if self.time_left <= 0: self.over = True self.best = max(self.best, self.score) ``` Cada "pop" es un anillo que vive 0,4 segundos. Efectos así son la forma más barata de que un juego se sienta bien; la [página de consejos](https://albakore.github.io/pyzed/es/games/tips/#juice) tiene más. 5. **Teclas pulsadas una vez.** Mantener una tecla mueve el orbe; *pulsarla* hace algo una vez. `on_key` recibe cada pulsación como texto: ```python def on_key(self, combo): if combo == "space" and not self.over: self.paused = not self.paused elif combo == "enter" and self.over: self.reset() ``` 6. **Dibuja el fotograma.** `view()` convierte el estado en una imagen. El tablero es un `pz.canvas` con formas de `pyzed.draw`; la moneda estrecha su ancho con `cos` para parecer que gira, y `game.glow` añade una luz suave: ```python def board(self): shapes = [draw.background("#0b0e14")] ... for x, y, age in self.pops: k = age / 0.4 # 0 -> 1 over the ring's life shapes.append(draw.circle(x, y, COIN_R + k * 26, stroke=game.rgba("#fde68a", 1 - k), width=3)) shapes += game.glow(self.x, self.y, PLAYER_R, "#22d3ee", layers=4, strength=0.5) return pz.canvas(shapes, width=W, height=H) ``` 7. **El HUD son widgets.** La puntuación, el reloj y la tarjeta de pausa son componentes normales de Pyzed apilados sobre el canvas con `pz.stack`, así que reciben el tema, los iconos y el efecto `glass` gratis: ```python hud = pz.row([ ui.label("Coin Chase", weight="semibold"), pz.hspace(), ui.badge(f"{self.score} coins", icon="circle-dollar-sign", tint="warning"), ui.badge(f"{math.ceil(self.time_left)} s", icon="timer", tint="error" if self.time_left < 5 else "accent"), ], spacing=8, align="center") play = pz.stack([self.board(), self.overlay()], width=W, height=H) ``` **¿Por qué no mover 4 píxeles por fotograma?:** Muchos primeros juegos lo hacen, y van a media velocidad en un portátil a 30 fps y al doble en un monitor de 144 Hz. Multiplica siempre las velocidades por `dt`. Pyzed además limita `dt` a un cuarto de segundo, así que un punto de interrupción o arrastrar la ventana nunca teletransporta el orbe. **¿Y los fotogramas por segundo?:** `pz.frames` sigue la frecuencia de la pantalla (vsync). Donde la pantalla no puede esperar al vsync, se detiene en `max_fps` (240 por defecto): `pz.frames(self.tick, max_fps=60)` lo limita más. #### Prueba esto - Haz que el orbe vaya más rápido un segundo después de cada moneda (un temporizador `boost` que baja en `tick`). - Añade una segunda moneda, más rara, que vale 5 puntos y desaparece a los 3 segundos. - Guarda la mejor puntuación en un archivo; el [paso 8](https://albakore.github.io/pyzed/es/games/high-scores/) enseña cómo. #### Código completo `examples/games/coin_chase.py`: ```py """Coin Chase: your first game. A frame loop, keys held down and a 30-second timer. python examples/games/coin_chase.py Arrows or WASD move the orb, collect as many coins as you can in 30 seconds. Space pauses, Enter plays again. Shows the three parts of every real-time game: 1. **State**: plain attributes on the app (position, score, time left). 2. **Update**: ``tick(dt)``, called by ``pz.frames`` once per displayed frame, moves things by *speed x dt* and checks what happened. 3. **Draw**: ``view()`` turns the state into a canvas, with the HUD as ordinary widgets. Tutorial: https://albakore.github.io/pyzed/games/first-game/ """ import math import random import pyzed as pz from pyzed import draw, game, ui W, H = 560, 380 SPEED = 260 # pixels per second PLAYER_R = 14 COIN_R = 9 ROUND = 30.0 # seconds per round class CoinChase(pz.App): title = "Coin Chase" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) #: The keys held down; ``None`` reads the keyboard. Tests and demos set it to steer. self.held = None self.best = 0 self.reset() # -- 1. state --------------------------------------------------------------------------- def reset(self): self.x, self.y = W / 2, H / 2 self.score = 0 self.time_left = ROUND self.paused = self.over = False self.pops = [] # [x, y, age]: rings that grow and fade where a coin was taken self.clock = 0.0 # seconds since the round started (for the coin's spin) self.coin = self.new_coin() def new_coin(self): """A coin somewhere not too close to the player.""" while True: x = self.random.uniform(30, W - 30) y = self.random.uniform(30, H - 30) if math.hypot(x - self.x, y - self.y) > 120: return (x, y) # -- 2. update -------------------------------------------------------------------------- def subscriptions(self): # No frames while paused or over: the game stops and so does the CPU. if self.paused or self.over: return [] return [pz.frames(self.tick)] def tick(self, dt): self.clock += dt self.time_left = max(0.0, self.time_left - dt) # Move by speed x dt: the same speed at 60, 120 or 144 frames per second. dx, dy = game.vector(held=self.held) # arrows / WASD, diagonals normalized self.x = game.clamp(self.x + dx * SPEED * dt, PLAYER_R, W - PLAYER_R) self.y = game.clamp(self.y + dy * SPEED * dt, PLAYER_R, H - PLAYER_R) # Two circles touch when the distance between centers is less than the radii added. cx, cy = self.coin if math.hypot(cx - self.x, cy - self.y) < PLAYER_R + COIN_R: self.score += 1 self.pops.append([cx, cy, 0.0]) self.coin = self.new_coin() for pop in self.pops: pop[2] += dt self.pops = [p for p in self.pops if p[2] < 0.4] if self.time_left <= 0: self.over = True self.best = max(self.best, self.score) def on_key(self, combo): if combo == "space" and not self.over: self.paused = not self.paused elif combo == "enter" and self.over: self.reset() # -- 3. draw ---------------------------------------------------------------------------- def board(self): shapes = [draw.background("#0b0e14")] for gx in range(0, W, 40): # a faint grid, to feel the motion shapes.append(draw.line(gx, 0, gx, H, stroke="#ffffff08")) for gy in range(0, H, 40): shapes.append(draw.line(0, gy, W, gy, stroke="#ffffff08")) cx, cy = self.coin spin = abs(math.cos(self.clock * 4)) # squash the coin's width: it seems to spin shapes += game.glow(cx, cy, COIN_R * 0.6, "#fbbf24", layers=3, strength=0.5, core=False) shapes.append(draw.path([("M", cx, cy - COIN_R)] + [ ("L", cx + math.sin(a) * COIN_R * max(spin, 0.15), cy - math.cos(a) * COIN_R) for a in (i * math.pi / 8 for i in range(1, 17))], fill="#fbbf24", stroke="#fde68a", width=1.5)) for x, y, age in self.pops: k = age / 0.4 # 0 -> 1 over the ring's life shapes.append(draw.circle(x, y, COIN_R + k * 26, stroke=game.rgba("#fde68a", 1 - k), width=3)) shapes += game.glow(self.x, self.y, PLAYER_R, "#22d3ee", layers=4, strength=0.5) return pz.canvas(shapes, width=W, height=H) def overlay(self): if self.over: title, lines = "Time!", [f"{self.score} coins · best {self.best}", "Enter plays again"] elif self.paused: title, lines = "Paused", ["Space continues"] else: return None card = pz.container(pz.column([ui.heading(title, level=2), *[ui.muted(t) for t in lines]], spacing=8, align="center"), padding=(20, 32)).style(glass=True, radius=14) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def view(self): t = self.resolved_theme() hud = pz.row([ ui.label("Coin Chase", weight="semibold"), pz.hspace(), ui.badge(f"{self.score} coins", icon="circle-dollar-sign", tint="warning"), ui.badge(f"{math.ceil(self.time_left)} s", icon="timer", tint="error" if self.time_left < 5 else "accent"), ui.badge(f"Best {self.best}", icon="trophy", tint="text_muted"), ], spacing=8, align="center") play = pz.stack([self.board(), self.overlay()], width=W, height=H) return pz.container(pz.column([hud, pz.container(play, radius=10, clip=True), ui.muted("Arrows / WASD move · Space pauses · Enter restarts", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(CoinChase) ``` #### Siguiente - [2. Sprites y animación](https://albakore.github.io/pyzed/es/games/sprites-animation/): Personajes pixel art escritos como texto, animación por fotogramas, voltear y tweens. ### 2. Sprites y animación > Anima un personaje pixel art en Python con Pyzed: sprites escritos como texto con game.sprite, una hoja de sprites reproducida fotograma a fotograma, voltear, tweens con curvas de easing, un arco de salto y squash and stretch. Source: https://albakore.github.io/pyzed/es/games/sprites-animation/ En un juego las cosas casi nunca solo se deslizan: los personajes caminan, parpadean y aterrizan; las monedas flotan y estallan. Dos técnicas hacen casi todo. La **animación por fotogramas** muestra una serie de dibujos uno tras otro, como un folioscopio. El **tweening** lleva un número (una posición, un tamaño, una opacidad) de un valor a otro con el tiempo, con la forma de una *curva de easing*. Este paso construye *Walker*, un héroe pixel art que usa las dos. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un personaje animado con estados de caminar, reposo y salto | 35 minutos | Principiante | [1. Tu primer juego](https://albakore.github.io/pyzed/es/games/first-game/) | #### Qué aprenderás - Dibujar pixel art **desde texto** con `game.sprite`, y reflejarlo con `flip=True`. - Guardar una **hoja de sprites** (una lista de fotogramas) y reproducirla a unos pocos fotogramas por segundo. - Cambiar de animación con **estados** (`idle`, `walk`, `jump`). - Escribir una pequeña clase **`Tween`** y usar **curvas de easing** para el salto, el aplastamiento al aterrizar, un "+1" y un título. 1. **Un sprite es texto.** Cada cadena es una fila de píxeles; cada carácter elige un color de una paleta, y cualquier carácter que no esté en ella (como `.`) es transparente. `game.sprite` lo convierte en unas pocas formas, una por color, sea cual sea el tamaño del sprite: ```python PALETTE = {"h": "#7c4a2d", "s": "#f5c99b", "S": "#e9b385", "e": "#1b1f27", "b": "#3b82f6", "B": "#60a5fa", "p": "#334155", "k": "#111827"} HEAD = [ "...hhhh...", "..hhhhhh..", "..hhssss..", "..hsssess.", "..hsssssS.", "...ssss...", ] BODY = ["..bbBBbb..", ".sbbBBbbs.", ".sbbbbbbs.", "..bbbbbb.."] game.sprite(HEAD + BODY + LEGS["stand"], PALETTE, x, y, scale=4) # every pixel becomes 4x4 ``` 2. **Una hoja de sprites es una lista de fotogramas.** El ciclo de caminar reutiliza la cabeza y el cuerpo y cambia las piernas; la animación de reposo es el mismo fotograma seis veces y un parpadeo, así que el héroe parpadea cada par de segundos: ```python LEGS = { "stand": ["..pp..pp..", "..pp..pp..", "..kk..kk.."], "apart": ["..pp..pp..", ".pp....pp.", ".kk.....kk"], "pass": ["...pp.pp..", "...pp.pp..", "...kkkkk.."], "air": ["..pp..pp..", "..ppp.ppp.", "....k...k."], } IDLE = [HEAD + BODY + LEGS["stand"]] * 6 + [BLINK + BODY + LEGS["stand"]] WALK = [HEAD + BODY + LEGS["apart"], HEAD + BODY + LEGS["pass"], HEAD + BODY + LEGS["apart"], HEAD + BODY + LEGS["pass"]] JUMP = [HEAD + BODY + LEGS["air"]] ``` 3. **Reproduce los fotogramas.** Una animación solo necesita contar el tiempo; el fotograma a mostrar es *tiempo × fotogramas por segundo*, que vuelve al principio con `%` cuando se repite: ```python class FrameAnimation: def __init__(self, frames, fps, loop=True): self.frames, self.fps, self.loop = frames, fps, loop self.time = 0.0 def update(self, dt): self.time += dt @property def index(self): i = int(self.time * self.fps) return i % len(self.frames) if self.loop else min(i, len(self.frames) - 1) @property def frame(self): return self.frames[self.index] ``` 4. **Elige la animación según el estado.** En cada fotograma el héroe está en reposo, caminando o saltando. Cuando el estado cambia, la nueva animación empieza por su primer fotograma, y la dirección refleja el sprite: ```python self.animations = {"idle": FrameAnimation(IDLE, 3), "walk": FrameAnimation(WALK, 10), "jump": FrameAnimation(JUMP, 1)} def set_state(self, state): if state != self.state: self.state = state self.animations[state].time = 0.0 # in tick(dt): move = game.axis(("left", "a"), ("right", "d"), held=self.held) if move: self.facing = move self.set_state("jump" if self.jump else "walk" if move else "idle") self.animation.update(dt) # in board(): game.sprite(self.animation.frame, PALETTE, x, y, scale=PIXEL, flip=self.facing < 0) ``` 5. **Tweens: de A a B con el tiempo.** Un tween recuerda cuándo empezó y calcula su valor a partir del *progreso* (de 0 a 1) pasado por una función de easing. El easing es lo que hace natural el movimiento: las cosas casi nunca arrancan y paran a toda velocidad. ```python def ease_out_cubic(t): return 1 - (1 - t) ** 3 def ease_out_back(t): # goes a little past the end, then settles c = 1.70158 return 1 + (c + 1) * (t - 1) ** 3 + c * (t - 1) ** 2 class Tween: def __init__(self, start, end, duration, ease=ease_out_cubic, delay=0.0): self.start, self.end, self.duration, self.ease = start, end, duration, ease self.time = -delay def update(self, dt): self.time += dt @property def progress(self): return game.clamp(self.time / self.duration, 0.0, 1.0) @property def done(self): return self.time >= self.duration @property def value(self): return game.lerp(self.start, self.end, self.ease(self.progress)) ``` 6. **Un salto también es un tween.** El progreso del salto va de 0 a 1 en `JUMP_TIME` segundos; la altura es una parábola, `4t(1 − t)`, que vale 0 en los extremos y 1 en el medio: ```python def start_jump(self): if not self.jump: self.jump = Tween(0.0, 1.0, JUMP_TIME, linear) @property def height(self): if not self.jump: return 0.0 t = self.jump.value return 4 * t * (1 - t) * JUMP_HEIGHT ``` En el [paso 3](https://albakore.github.io/pyzed/es/games/collisions-physics/) lo cambiarás por gravedad real, que puede aterrizar en plataformas; un tween es perfecto cuando el movimiento siempre es el mismo. 7. **Squash and stretch.** Cuando el salto termina, un segundo tween va de 1 a 0 en un cuarto de segundo. Mientras dura, el sprite se dibuja más ancho y más bajo alrededor de sus pies, con una transformación `scale`: ```python s = self.squash.value if self.squash else 0.0 sx, sy = 1 + 0.25 * s, 1 - 0.2 * s feet_x, feet_y = self.x + SPRITE_W / 2, GROUND - self.height shapes += [draw.save(), draw.translate(feet_x, feet_y), draw.scale(sx, sy), *game.sprite(self.animation.frame, PALETTE, -SPRITE_W / 2, -SPRITE_H, scale=PIXEL, flip=self.facing < 0), draw.restore()] ``` Llevar primero el origen a los pies (`translate`) hace que el sprite se aplaste *contra el suelo* y no hacia su esquina superior izquierda. 8. **Pequeños tweens por todas partes.** El "+1" sobre una moneda sube 40 píxeles y se desvanece en 0,7 s; el título entra con `ease_out_back` 0,2 s después de empezar; las monedas flotan con una onda seno: ```python self.popups.append([cx, cy, Tween(0.0, 1.0, 0.7, ease_out_cubic)]) ... for x, y, tween in self.popups: shapes.append(draw.text(x, y - 40 * tween.value, "+1", size=18, weight="bold", color=game.rgba("#fde68a", 1 - tween.progress), align_x="center")) ``` **Archivos de imagen y hojas de sprites reales:** `draw.image("hero.png", x, y, w, h, pixelated=True)` dibuja un PNG en vez de píxeles de texto; se decodifica una vez y queda en caché, así que dibujarlo en cada fotograma es barato, y `rotation=` y `opacity=` lo giran y lo desvanecen. `draw.image` dibuja una imagen entera, así que para una hoja de sprites en un solo archivo, córtala en fotogramas una vez al empezar el juego (por ejemplo con `Image.crop` de Pillow, guardando los bytes de cada fotograma) y guarda la lista, igual que `WALK` arriba. **Los widgets se animan solos:** Los menús y los widgets del HUD tienen su propio sistema de animación: `.animate("fade_in_up")`, `.transition(...)` y `pz.AnimationPlayer` se reproducen de forma nativa, sin tu bucle (mira [Animación](https://albakore.github.io/pyzed/es/guides/animation/)). Dentro de un `pz.canvas` no hay widgets, así que animas los números tú mismo, como aquí. #### Prueba esto - Añade una animación de correr (6 fotogramas a 14 fps) que se use al mantener Shift, con más `WALK_SPEED`. - Haz girar las monedas estrechando su ancho con `abs(math.cos(time))`, como hace Coin Chase. - Dale al título un balanceo con `ease_in_out_sine` después de aterrizar. #### Código completo `examples/games/walker.py`: ```py """Walker: sprites, frame animation and tweening. python examples/games/walker.py Arrows or A/D walk, Space jumps, F shows or hides the sprite sheet. A pixel-art hero walks, blinks, jumps and squashes when it lands; coins bob and pop with a "+1" that eases up and fades. Shows: - **Sprites from text** with ``game.sprite`` (one string per row, one character per pixel), flipped with ``flip=True`` when the hero faces left. - **Frame animation**: a list of frames (a sprite sheet) played at a few frames per second by ``FrameAnimation``, which only counts time. - **Tweening**: a value going from A to B over a duration with an easing curve (``Tween``), for the jump, the landing squash, the "+1" and the title that slides in. Tutorial: https://albakore.github.io/pyzed/games/sprites-animation/ """ import math import pyzed as pz from pyzed import draw, game, ui W, H = 640, 300 GROUND = 236 PIXEL = 4 WALK_SPEED = 170 JUMP_HEIGHT = 70 JUMP_TIME = 0.55 PALETTE = {"h": "#7c4a2d", "s": "#f5c99b", "S": "#e9b385", "e": "#1b1f27", "b": "#3b82f6", "B": "#60a5fa", "p": "#334155", "k": "#111827"} # "." (or any character not here) is transparent # -- the sprite sheet: every frame is a list of rows, every character a pixel --------------- HEAD = [ "...hhhh...", "..hhhhhh..", "..hhssss..", "..hsssess.", "..hsssssS.", "...ssss...", ] BLINK = HEAD[:3] + ["..hsssshs."] + HEAD[4:] # the eye closed for a frame BODY = [ "..bbBBbb..", ".sbbBBbbs.", ".sbbbbbbs.", "..bbbbbb..", ] LEGS = { "stand": ["..pp..pp..", "..pp..pp..", "..kk..kk.."], "apart": ["..pp..pp..", ".pp....pp.", ".kk.....kk"], "pass": ["...pp.pp..", "...pp.pp..", "...kkkkk.."], "air": ["..pp..pp..", "..ppp.ppp.", "....k...k."], } IDLE = [HEAD + BODY + LEGS["stand"]] * 6 + [BLINK + BODY + LEGS["stand"]] # blink once in a while WALK = [HEAD + BODY + LEGS["apart"], HEAD + BODY + LEGS["pass"], HEAD + BODY + LEGS["apart"], HEAD + BODY + LEGS["pass"]] JUMP = [HEAD + BODY + LEGS["air"]] SPRITE_W = len(HEAD[0]) * PIXEL SPRITE_H = len(IDLE[0]) * PIXEL # -- easing: how a value moves from 0 to 1 ---------------------------------------------------- def linear(t): return t def ease_out_cubic(t): return 1 - (1 - t) ** 3 def ease_out_back(t): c = 1.70158 return 1 + (c + 1) * (t - 1) ** 3 + c * (t - 1) ** 2 def ease_in_out_sine(t): return -(math.cos(math.pi * t) - 1) / 2 class Tween: """A number going from ``start`` to ``end`` in ``duration`` seconds, shaped by ``ease``.""" def __init__(self, start, end, duration, ease=ease_out_cubic, delay=0.0): self.start, self.end, self.duration, self.ease = start, end, duration, ease self.time = -delay def update(self, dt): self.time += dt @property def progress(self): return game.clamp(self.time / self.duration, 0.0, 1.0) @property def done(self): return self.time >= self.duration @property def value(self): return game.lerp(self.start, self.end, self.ease(self.progress)) class FrameAnimation: """Plays a list of frames at ``fps`` frames per second, looping or not.""" def __init__(self, frames, fps, loop=True): self.frames, self.fps, self.loop = frames, fps, loop self.time = 0.0 def update(self, dt): self.time += dt @property def index(self): i = int(self.time * self.fps) return i % len(self.frames) if self.loop else min(i, len(self.frames) - 1) @property def frame(self): return self.frames[self.index] class Walker(pz.App): title = "Walker" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None): self.held = None # the keys held down; None reads the keyboard self.show_sheet = True self.x = 120.0 self.facing = 1 # 1 right, -1 left self.clock = 0.0 self.animations = {"idle": FrameAnimation(IDLE, 3), "walk": FrameAnimation(WALK, 10), "jump": FrameAnimation(JUMP, 1)} self.state = "idle" self.jump = None # a Tween from 0 to 1 while in the air self.squash = None # a Tween from 1 back to 0 after landing self.coins = [{"x": 260 + i * 70, "high": i % 2 == 1, "taken": None} for i in range(5)] self.popups = [] # [x, y, Tween] "+1" labels self.title_in = Tween(-80, 18, 0.8, ease_out_back, delay=0.2) self.score = 0 @property def animation(self): return self.animations[self.state] def set_state(self, state): if state != self.state: self.state = state self.animations[state].time = 0.0 # start the new animation from its first frame # -- update ------------------------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] def tick(self, dt): self.clock += dt self.title_in.update(dt) move = game.axis(("left", "a"), ("right", "d"), held=self.held) if move: self.facing = move self.x = game.clamp(self.x + move * WALK_SPEED * dt, 10, W - SPRITE_W - 10) if self.held is not None and "space" in self.held: self.start_jump() if self.jump: self.jump.update(dt) if self.jump.done: self.jump = None self.squash = Tween(1.0, 0.0, 0.25, ease_out_cubic) # land: squash, then recover if self.squash: self.squash.update(dt) if self.squash.done: self.squash = None self.set_state("jump" if self.jump else "walk" if move else "idle") self.animation.update(dt) self.collect() for popup in self.popups: popup[2].update(dt) self.popups = [p for p in self.popups if not p[2].done] def start_jump(self): if not self.jump: self.jump = Tween(0.0, 1.0, JUMP_TIME, linear) @property def height(self): """How high the hero is: a parabola, 4t(1-t), peaks at JUMP_HEIGHT halfway through.""" if not self.jump: return 0.0 t = self.jump.value return 4 * t * (1 - t) * JUMP_HEIGHT def collect(self): hero = game.Rect(self.x + 8, GROUND - SPRITE_H - self.height, SPRITE_W - 16, SPRITE_H) for coin in self.coins: if coin["taken"] is not None: if self.clock - coin["taken"] > 3: # coins come back after three seconds coin["taken"] = None continue cx, cy = coin["x"], self.coin_y(coin) if hero.overlaps(game.Rect(cx - 9, cy - 9, 18, 18)): coin["taken"] = self.clock self.score += 1 self.popups.append([cx, cy, Tween(0.0, 1.0, 0.7, ease_out_cubic)]) def coin_y(self, coin): base = GROUND - (110 if coin["high"] else 40) return base + math.sin(self.clock * 3 + coin["x"] * 0.05) * 5 # bob with a sine wave def on_key(self, combo): if combo in ("space", "up", "w"): self.start_jump() elif combo == "f": self.show_sheet = not self.show_sheet # -- draw --------------------------------------------------------------------------------- def board(self): shapes = [draw.background("#101521")] for i, (color, y) in enumerate((("#1a2236", 170), ("#202a40", 196))): # far hills shapes += [draw.circle(80 + j * 190 + i * 90, y + 70, 110, fill=color) for j in range(4)] shapes.append(draw.rect(0, GROUND, W, H - GROUND, fill="#1f2a1d")) shapes.append(draw.rect(0, GROUND, W, 6, fill="#4d7c3a")) for coin in self.coins: if coin["taken"] is None: cx, cy = coin["x"], self.coin_y(coin) shapes += game.glow(cx, cy, 8, "#fbbf24", layers=3, strength=0.45) shapes.append(draw.circle(cx, cy, 4, fill="#fde68a")) # shadow: smaller and fainter the higher the hero is k = 1 - self.height / JUMP_HEIGHT * 0.5 shapes.append(draw.rect(self.x + SPRITE_W / 2 - 18 * k, GROUND - 3, 36 * k, 6, radius=3, fill=game.rgba("#000000", 0.35 * k))) # squash and stretch: wider and shorter right after landing, around the feet s = self.squash.value if self.squash else 0.0 sx, sy = 1 + 0.25 * s, 1 - 0.2 * s feet_x, feet_y = self.x + SPRITE_W / 2, GROUND - self.height shapes += [draw.save(), draw.translate(feet_x, feet_y), draw.scale(sx, sy), *game.sprite(self.animation.frame, PALETTE, -SPRITE_W / 2, -SPRITE_H, scale=PIXEL, flip=self.facing < 0), draw.restore()] for x, y, tween in self.popups: shapes.append(draw.text(x, y - 40 * tween.value, "+1", size=18, weight="bold", color=game.rgba("#fde68a", 1 - tween.progress), align_x="center")) shapes.append(draw.text(W / 2, self.title_in.value, "WALKER", size=26, weight="bold", color="#e2e8f0", align_x="center")) if self.show_sheet: shapes += self.sheet() return pz.canvas(shapes, width=W, height=H) def sheet(self): """The current animation's frames, small, with the one on screen highlighted.""" frames = self.animation.frames unique = [] for f in frames: if not unique or unique[-1] is not f: unique.append(f) scale, gap = 2, 6 fw, fh = len(HEAD[0]) * scale, len(IDLE[0]) * scale x0, y0 = 12, 12 shapes = [draw.rect(x0 - 6, y0 - 6, max(len(frames) * (fw + gap) + 6, 160), fh + 30, radius=8, fill="#00000066")] for i, frame in enumerate(frames): x = x0 + i * (fw + gap) current = i == self.animation.index shapes.append(draw.rect(x - 2, y0 - 2, fw + 4, fh + 4, radius=3, stroke="#fbbf24" if current else "#ffffff22", width=1.5 if current else 1)) shapes += game.sprite(frame, PALETTE, x, y0, scale=scale) shapes.append(draw.text(x0, y0 + fh + 8, f"{self.state} · frame {self.animation.index + 1}/{len(frames)}", size=11, color="#cbd5e1", font="mono")) return shapes def view(self): t = self.resolved_theme() hud = pz.row([ui.label("Walker", weight="semibold"), pz.hspace(), ui.badge(self.state, icon="film", tint="accent"), ui.badge(f"{self.score} coins", icon="coins", tint="warning")], spacing=8, align="center") return pz.container(pz.column([hud, pz.container(self.board(), radius=10, clip=True), ui.muted("←/→ or A/D walk · Space jumps · F shows the sprite sheet", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(Walker) ``` #### Siguiente - [3. Colisiones y física](https://albakore.github.io/pyzed/es/games/collisions-physics/): Velocidad, gravedad, colisiones de cajas y un salto que se siente bien. ### 3. Colisiones y física > Física de juego sencilla en Python con Pyzed: velocidad y gravedad, colisiones de cajas AABB resueltas eje por eje, círculos que rebotan en cajas, y un salto de plataformas con coyote time y buffer de salto. Source: https://albakore.github.io/pyzed/es/games/collisions-physics/ No necesitas un motor de física para hacer un juego de plataformas. Unas pocas líneas te dan **velocidad** (lo rápido que se mueve algo), **gravedad** (un tirón constante hacia abajo) y **colisiones** con cajas. Este paso construye *Jump Lab*, un laboratorio donde una caja corre y salta entre plataformas mientras unas pelotas rebotan, con una vista de depuración que muestra las cajas de colisión, la velocidad y el arco de cada salto. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Una caja que corre, salta y aterriza en plataformas, y pelotas que rebotan | 40 minutos | Intermedio | [2. Sprites y animación](https://albakore.github.io/pyzed/es/games/sprites-animation/) | #### Qué aprenderás - **Velocidad y gravedad**: la posición cambia con velocidad × dt, la velocidad con gravedad × dt. - **Colisiones AABB** (cajas alineadas con los ejes) con `game.Rect.overlaps`, resueltas **eje por eje**. - **Círculos contra cajas** con `game.circle_rect`, que dice qué lado se golpeó. - **Sensación de juego**: aceleración, altura de salto variable, *coyote time* y *buffer de salto*. - Una **vista de depuración** que puedes encender y apagar. 1. **Números que se sienten bien.** Toda la física son unas pocas constantes, en píxeles y segundos. Cambiarlas es *afinar* la sensación: ```python RUN = 250 # top running speed, px/s ACCEL = 2200 # px/s² on the ground (how fast you reach RUN) AIR_ACCEL = 1300 # less control in the air GRAVITY = 1900 # px/s², added to vy every second JUMP_SPEED = 660 # vy right after a jump (upwards, so negative) LOW_JUMP = 2.6 # gravity multiplier while rising without the jump key: short hops MAX_FALL = 900 COYOTE = 0.10 # seconds after leaving a ledge when a jump still works BUFFER = 0.12 # seconds a jump press is remembered before landing ``` Con estos valores, el salto más alto es `JUMP_SPEED² / (2 × GRAVITY)` ≈ 115 píxeles. Las pruebas lo comprueban. 2. **Velocidad y gravedad.** `vx` y `vy` son velocidades en píxeles por segundo (`y` crece hacia abajo, así que arriba es negativo). En vez de poner `vx` a la velocidad objetivo de golpe, se acerca a ella como mucho `ACCEL × dt` por fotograma, lo que da un arranque y una parada cortos y suaves: ```python move = game.axis(("left", "a"), ("right", "d"), held=self.held) jump_held = game.is_pressed(*JUMP_KEYS, held=self.held) target = move * RUN accel = (ACCEL if self.on_ground else AIR_ACCEL) * dt self.vx += game.clamp(target - self.vx, -accel, accel) g = GRAVITY * (LOW_JUMP if self.vy < 0 and not jump_held else 1) self.vy = min(self.vy + g * dt, MAX_FALL) ``` La segunda línea de la gravedad es la **altura de salto variable**: mientras sube, soltar la tecla hace la gravedad más fuerte, así que un toque es un saltito y una pulsación larga un salto completo. 3. **Mueve un eje cada vez.** Muévete en horizontal y sal de lo que ahora solapes; luego haz lo mismo en vertical. Resolver cada eje por separado es lo que permite deslizarse por una pared al caer y aterrizar justo encima del suelo: ```python def move_x(self, dx): self.player.x += dx for i, p in self.solids_hit(self.player): self.player.x = p.left - self.player.w if dx > 0 else p.right self.vx = 0.0 def move_y(self, dy): self.player.y += dy for i, p in self.solids_hit(self.player): if dy > 0: # falling: land on top self.player.y = p.top - self.player.h self.on_ground = True else: # rising: bump the head self.player.y = p.bottom self.vy = 0.0 # in tick: self.move_x(self.vx * dt) self.on_ground = False self.move_y(self.vy * dt) ``` `on_ground` solo es cierto cuando el movimiento vertical chocó con algo debajo, así que siempre está al día. 4. **Coyote time y buffer de salto.** Dos temporizadores diminutos hacen los saltos indulgentes. El *coyote time* te deja saltar 0,1 s después de salir corriendo de un borde (como el coyote de los dibujos, que solo cae cuando mira abajo). El *buffer de salto* recuerda una pulsación hecha justo antes de aterrizar para que no se pierda: ```python def press_jump(self): # called by on_key for Space, Up and W self.buffer = BUFFER # in tick: self.coyote = COYOTE if self.on_ground else max(0.0, self.coyote - dt) self.buffer = max(0.0, self.buffer - dt) if self.buffer > 0 and self.coyote > 0: self.vy = -JUMP_SPEED self.buffer = self.coyote = 0.0 ``` Nadie nota que están, pero todo el mundo nota cuando faltan: el juego parece "comerse" saltos. 5. **Las pelotas rebotan en cajas.** `game.circle_rect(cx, cy, r, rect)` devuelve `None`, o la dirección desde la caja hacia el círculo por el lado golpeado: `(0, -1)` desde arriba, `(1, 0)` desde la derecha. Gira la velocidad hacia ese lado, saca la pelota y quítale un poco de energía: ```python normal = game.circle_rect(ball.x, ball.y, ball.r, p) if normal: nx, ny = normal if nx: ball.vx = abs(ball.vx) * nx ball.x = (p.right + ball.r) if nx > 0 else (p.left - ball.r) if ny: ball.vy = abs(ball.vy) * ny * 0.82 ball.y = (p.bottom + ball.r) if ny > 0 else (p.top - ball.r) ``` 6. **Una vista de depuración.** Dibujar lo que "ve" el código ahorra horas. Con H el juego dibuja el contorno de las cajas, la velocidad como una línea, las últimas 60 posiciones como puntos que se desvanecen (el arco del salto) y los números: ```python self.trail.append(self.player.center) del self.trail[:-60] ... if self.debug: shapes.append(p.shape(stroke="#34d399", width=1.5)) shapes.append(draw.line(cx, cy, cx + self.vx * 0.15, cy + self.vy * 0.15, stroke="#f472b6", width=2.5)) ``` **Atravesar paredes (tunneling):** Si algo se mueve más que su propio tamaño en un fotograma, puede atravesar una pared fina sin llegar a solaparla. Jump Lab limita `dt` a 1/30 s; para objetos rápidos, divide el movimiento en pasos más pequeños, como hace [Breakout](https://albakore.github.io/pyzed/es/games/breakout/) con su pelota (`steps = ceil(speed × dt / radius)`). **Qué usar en cada caso:** Cajas (`Rect.overlaps`) para personajes, plataformas, balas y objetos; círculos (`math.hypot`) para cosas redondas y para comprobar rápido si algo está "lo bastante cerca"; `circle_rect` cuando una pelota debe rebotar en una caja. La mayoría de juegos 2D no necesitan nada más. #### Prueba esto - Añade una plataforma móvil: un `Rect` cuya `x` sigue una onda seno; cuando el jugador está encima, suma su movimiento a la `x` del jugador. - Añade un doble salto: un salto extra en el aire, que se recupera al aterrizar. - Ajusta `GRAVITY` y `JUMP_SPEED` para un salto más flotante o más seco, y mira cómo cambia el rastro. #### Código completo `examples/games/jump_lab.py`: ```py """Jump Lab: velocity, gravity, box collisions and a jump that feels good. python examples/games/jump_lab.py Arrows or A/D run, Space (or Up/W) jumps, hold it to jump higher, H hides the debug view. A box runs and jumps between platforms while three balls bounce around. The debug view draws the hitboxes, the velocity and the trail of the last second, so you can see the jump arc. Shows: - **Velocity and gravity**: ``vx``/``vy`` in pixels per second; gravity adds to ``vy`` every frame, and the position moves by ``velocity x dt``. - **AABB collisions** (axis-aligned bounding boxes, ``game.Rect.overlaps``), resolved one axis at a time so the box slides along walls and lands on floors. - **Circle against box** with ``game.circle_rect``, which says which side was hit, to bounce. - **Game feel**: acceleration, coyote time, jump buffering and variable jump height. Tutorial: https://albakore.github.io/pyzed/games/collisions-physics/ """ import random import pyzed as pz from pyzed import draw, game, ui W, H = 720, 400 RUN = 250 # top running speed, px/s ACCEL = 2200 # px/s² on the ground (how fast you reach RUN) AIR_ACCEL = 1300 # less control in the air GRAVITY = 1900 # px/s², added to vy every second JUMP_SPEED = 660 # vy right after a jump (upwards, so negative) LOW_JUMP = 2.6 # gravity multiplier while rising without the jump key: short hops MAX_FALL = 900 COYOTE = 0.10 # seconds after leaving a ledge when a jump still works BUFFER = 0.12 # seconds a jump press is remembered before landing JUMP_KEYS = ("space", "up", "w") PLATFORMS = [ game.Rect(0, H - 36, W, 36), # the floor game.Rect(120, 280, 140, 16), game.Rect(330, 214, 130, 16), game.Rect(520, 150, 150, 16), game.Rect(560, H - 116, 40, 80), # a wall to bump into game.Rect(0, 0, 12, H), game.Rect(W - 12, 0, 12, H), # side walls ] class Ball: def __init__(self, x, y, vx, vy, r, color): self.x, self.y, self.vx, self.vy, self.r, self.color = x, y, vx, vy, r, color class JumpLab(pz.App): title = "Jump Lab" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None): rnd = random.Random(seed) self.held = None # the keys held down; None reads the keyboard self.debug = True self.player = game.Rect(60, H - 36 - 34, 26, 34) self.vx = self.vy = 0.0 self.on_ground = False self.coyote = 0.0 # time left to jump after walking off a ledge self.buffer = 0.0 # time left of a remembered jump press self.trail = [] # the player's center, one point per frame self.hits = {} # platform index -> seconds to keep it highlighted self.balls = [Ball(rnd.uniform(100, 600), rnd.uniform(40, 120), rnd.choice((-1, 1)) * rnd.uniform(120, 220), 0.0, r, color) for r, color in ((10, "#f472b6"), (14, "#a78bfa"), (8, "#34d399"))] # -- input ------------------------------------------------------------------------------ def on_key(self, combo): if combo in JUMP_KEYS: self.press_jump() elif combo == "h": self.debug = not self.debug def press_jump(self): self.buffer = BUFFER # remember the press for a moment (jump buffering) # -- update ----------------------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] def tick(self, dt): dt = min(dt, 1 / 30) # big steps would let things pass through walls move = game.axis(("left", "a"), ("right", "d"), held=self.held) jump_held = game.is_pressed(*JUMP_KEYS, held=self.held) # Horizontal: accelerate toward the target speed instead of jumping to it. target = move * RUN accel = (ACCEL if self.on_ground else AIR_ACCEL) * dt self.vx += game.clamp(target - self.vx, -accel, accel) # Vertical: gravity, stronger while rising without the key held (variable jump height). g = GRAVITY * (LOW_JUMP if self.vy < 0 and not jump_held else 1) self.vy = min(self.vy + g * dt, MAX_FALL) # Coyote time and jump buffer: both are small timers counting down. self.coyote = COYOTE if self.on_ground else max(0.0, self.coyote - dt) self.buffer = max(0.0, self.buffer - dt) if self.buffer > 0 and self.coyote > 0: self.vy = -JUMP_SPEED self.buffer = self.coyote = 0.0 # Move one axis at a time and push out of whatever we hit. self.move_x(self.vx * dt) self.on_ground = False self.move_y(self.vy * dt) self.trail.append(self.player.center) del self.trail[:-60] self.hits = {i: t - dt for i, t in self.hits.items() if t - dt > 0} for ball in self.balls: self.bounce(ball, dt) def solids_hit(self, rect): return [(i, p) for i, p in enumerate(PLATFORMS) if rect.overlaps(p)] def move_x(self, dx): self.player.x += dx for i, p in self.solids_hit(self.player): self.player.x = p.left - self.player.w if dx > 0 else p.right self.vx = 0.0 self.hits[i] = 0.2 def move_y(self, dy): self.player.y += dy for i, p in self.solids_hit(self.player): if dy > 0: # falling: land on top self.player.y = p.top - self.player.h self.on_ground = True else: # rising: bump the head self.player.y = p.bottom self.vy = 0.0 self.hits[i] = 0.2 def bounce(self, ball, dt): ball.vy += GRAVITY * 0.5 * dt ball.x += ball.vx * dt ball.y += ball.vy * dt for i, p in enumerate(PLATFORMS): normal = game.circle_rect(ball.x, ball.y, ball.r, p) if normal: nx, ny = normal if nx: ball.vx = abs(ball.vx) * nx # reflect the speed on that side ball.x = (p.right + ball.r) if nx > 0 else (p.left - ball.r) if ny: ball.vy = abs(ball.vy) * ny * 0.82 # lose a little energy on each bounce ball.y = (p.bottom + ball.r) if ny > 0 else (p.top - ball.r) if ny < 0 and abs(ball.vy) < 120: # nearly still: kick it again ball.vy = -620 self.hits[i] = 0.2 # -- draw ------------------------------------------------------------------------------- def board(self): shapes = [draw.background("#0f141c")] for i, p in enumerate(PLATFORMS): lit = i in self.hits and self.debug shapes.append(p.shape(fill="#2a3446", radius=3)) shapes.append(draw.rect(p.x, p.y, p.w, 3, fill="#4b5d7c" if not lit else "#fbbf24")) if self.debug: shapes.append(p.shape(stroke="#fbbf24" if lit else "#ffffff1f", width=1.5 if lit else 1)) for ball in self.balls: shapes += game.glow(ball.x, ball.y, ball.r, ball.color, layers=2, strength=0.35) if self.debug: shapes.append(draw.line(ball.x, ball.y, ball.x + ball.vx * 0.08, ball.y + ball.vy * 0.08, stroke=game.rgba(ball.color, 0.7), width=1.5)) if self.debug and len(self.trail) > 1: # the trail: fading dots, one per frame n = len(self.trail) for i, (x, y) in enumerate(self.trail[::2]): a = (i * 2 + 1) / n shapes.append(draw.circle(x, y, 2.2, fill=game.rgba("#22d3ee", a * 0.8))) p = self.player squash = 1 + game.clamp(abs(self.vy) / MAX_FALL, 0, 1) * 0.15 if not self.on_ground else 1 w, h = p.w / squash, p.h * squash # stretch while flying fast shapes.append(draw.rect(p.center[0] - w / 2, p.bottom - h, w, h, radius=6, fill="#22d3ee")) eye = 1 if self.vx >= 0 else -1 shapes.append(draw.rect(p.center[0] + eye * 4 - 3, p.bottom - h + 9, 6, 7, radius=2, fill="#0b0e14")) if self.debug: cx, cy = p.center shapes.append(p.shape(stroke="#34d399", width=1.5)) shapes.append(draw.line(cx, cy, cx + self.vx * 0.15, cy + self.vy * 0.15, stroke="#f472b6", width=2.5, cap="round")) info = (f"vx {self.vx:6.0f} vy {self.vy:6.0f} ground {'yes' if self.on_ground else 'no '} " f"coyote {self.coyote:.2f} buffer {self.buffer:.2f}") shapes.append(draw.rect(16, 16, 520, 24, radius=6, fill="#00000088")) shapes.append(draw.text(26, 21, info, size=12, font="mono", color="#cbd5e1")) return pz.canvas(shapes, width=W, height=H) def view(self): t = self.resolved_theme() hud = pz.row([ui.label("Jump Lab", weight="semibold"), pz.hspace(), ui.badge("on the ground" if self.on_ground else "in the air", icon="footprints" if self.on_ground else "wind", tint="success" if self.on_ground else "accent"), ui.badge("debug on" if self.debug else "debug off", icon="bug", tint="text_muted")], spacing=8, align="center") return pz.container(pz.column([hud, pz.container(self.board(), radius=10, clip=True), ui.muted("←/→ run · Space jumps (hold for higher) · H toggles the debug view", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(JumpLab) ``` #### Siguiente - [4. Un juego de plataformas](https://albakore.github.io/pyzed/es/games/platformer/): Un mapa de tiles, una cámara que te sigue, enemigos que pisar, monedas y una meta. ### 4. Un juego de plataformas > Crea un juego de plataformas con scroll lateral en Python con Pyzed: un mapa de tiles escrito como texto, colisiones solo con los tiles cercanos, una cámara suave con parallax, enemigos que patrullan y se pueden pisar, monedas, pinchos, vidas y una bandera de meta. Source: https://albakore.github.io/pyzed/es/games/platformer/ Con la física de Jump Lab tienes un personaje que corre y salta. Ahora dale un mundo. Este paso construye *Pixel Run*: un nivel de tres mil píxeles de ancho escrito en unas pocas líneas de texto, una cámara que sigue al jugador, slimes que patrullan y se pueden pisar, monedas, pinchos y una bandera al final. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un nivel completo con cámara, enemigos, monedas, vidas y pantalla de victoria | 60 minutos | Intermedio | [3. Colisiones y física](https://albakore.github.io/pyzed/es/games/collisions-physics/) | #### Qué aprenderás - Escribir un nivel como **mapa de tiles** en texto y convertirlo en objetos. - Colisionar solo con los **tiles cerca del jugador**, y dibujar solo los **tiles en pantalla**. - Una **cámara** que se acerca suave al jugador y se detiene en los bordes del nivel, y **parallax**. - **Enemigos** que patrullan, se dan la vuelta en paredes y bordes, y se pueden **pisar**. - **Vidas, reaparición y pantalla de victoria**. 1. **El nivel es texto.** Cada carácter es un tile de 32 × 32: `#` suelo, `=` una plataforma, `o` una moneda, `E` un slime, `^` pinchos, `P` el inicio y `F` la bandera. Editar el nivel es editar estas cadenas: ```python TILE = 32 LEVEL = [ "................................................................................................", ... "...P.ooo......ooo.......Eoo.######............Eoo...##...^^.ooE...............E.ooo.......F.....", "##################...####################...######################...###########################", "##################...####################...######################...###########################", ] COLS, ROWS = len(LEVEL[0]), len(LEVEL) SOLID = "#=" ``` Al empezar una partida, `new_game` lo lee una vez: las monedas van a un conjunto de `(col, row)`, cada `E` se convierte en un `Enemy`, `P` fija el inicio y `F` la caja de la bandera. 2. **Solo los tiles que importan.** El nivel tiene más de mil tiles, pero el jugador solo puede tocar los pocos que hay bajo su caja. `tiles_near` convierte la caja del jugador en un rango de columnas y filas y devuelve los tiles sólidos que hay ahí; la colisión es la misma de Jump Lab, eje por eje: ```python def solid(col, row): if col < 0 or col >= COLS: return True # the sides of the level are walls if row < 0 or row >= ROWS: return False # above and below are open (falling out is a lost life) return LEVEL[row][col] in SOLID def tiles_near(rect): out = [] for row in range(int(rect.top // TILE), int(rect.bottom // TILE) + 1): for col in range(int(rect.left // TILE), int(rect.right // TILE) + 1): if solid(col, row): out.append(game.Rect(col * TILE, row * TILE, TILE, TILE)) return out ``` 3. **Una cámara.** La cámara es un número: cuánto se ha desplazado el mundo. Apunta un poco por delante del jugador (al 40% de la pantalla), no sale del nivel y se acerca a su objetivo con `lerp`, así que se desliza en vez de dar tirones: ```python target = game.clamp(self.player.center[0] - W * 0.4, 0, LEVEL_W - W) self.camera = game.lerp(self.camera, target, min(1.0, 8 * dt)) ``` Dibujar el mundo a través de la cámara es una sola transformación: `draw.translate(-cam, 0)` antes del mundo, dentro de `save()`/`restore()` para no mover el cielo. Las colinas se mueven `cam × 0.2` y `cam × 0.45`, lo que las hace parecer lejanas (**parallax**). 4. **Dibuja solo lo que está en pantalla.** Con la cámara sabes la primera y la última columna visibles. Los tiles de un mismo color se juntan y se dibujan con un solo `draw.rects`, así que todo el nivel cuesta tres formas: ```python first, last = cam // TILE, (cam + W) // TILE + 1 dirt, grass, stone = [], [], [] for row, line in enumerate(LEVEL): for col in range(max(0, first), min(COLS, last + 1)): ... shapes += [draw.rects(dirt, fill="#5b3a29"), draw.rects(grass, fill="#65a30d"), draw.rects(stone, fill="#64748b")] ``` 5. **Enemigos que patrullan.** Un slime camina hasta que el tile de delante es una pared o el de abajo-delante está vacío; entonces retrocede y se da la vuelta. Esa única regla lo mantiene en su plataforma: ```python r.x += e.vx * dt ahead = r.right if e.vx > 0 else r.left col = int(ahead // TILE) row = int((r.bottom - 1) // TILE) if solid(col, row) or not solid(col, row + 1): r.x -= e.vx * dt e.vx = -e.vx ``` 6. **Pisar o hacerse daño.** Tocar un slime está bien *si caes sobre él*: la base del jugador estaba por encima de la parte superior del slime en el fotograma anterior. Entonces el slime se aplasta y el jugador rebota. Cualquier otro contacto cuesta una vida: ```python if self.vy > 0 and p.bottom - self.vy / 60 <= e.rect.top + 8: # coming down on it: stomp e.squashed = 0.001 self.vy = -JUMP_SPEED * 0.6 self.sparks.burst(e.rect.center[0], e.rect.bottom, count=14, color="#a3e635", angle=-90, spread=160) self.shake.kick(3) elif not self.invincible: return self.hurt() ``` Después de un golpe el jugador reaparece en el inicio y parpadea un segundo (`invincible`), así que un error nunca cuesta dos vidas. 7. **Ganar y perder.** Caer fuera del nivel, los pinchos y los slimes llaman a `hurt()`; llegar a la bandera pone `won`. Las tarjetas son los mismos widgets de cristal de antes, y Enter llama a `new_game()`. **Diseña niveles para el salto que tienes:** Con `JUMP_SPEED = 680` y `GRAVITY = 1900` el héroe sube unos 120 píxeles (casi cuatro tiles) y recorre unos cinco tiles en un salto completo a toda velocidad. Deja los huecos en tres tiles y las paredes en tres tiles de alto y todo salto será justo. Cambia la física y el nivel cambia con ella. **Mundos más grandes:** Los mapas de texto son perfectos para unos pocos niveles. Para muchos, guarda cada nivel en su propio `.txt` y léelo con `Path("levels/1.txt").read_text().splitlines()`, o exporta como CSV desde un editor de tiles como Tiled. #### Prueba esto - Añade una bandera de control a mitad de nivel: al tocarla, `respawn()` usa su posición. - Haz que las plataformas `=` sean de un solo sentido: solo colisionan al caer, si la base del jugador estaba por encima de la plataforma. - Añade un segundo nivel: una lista de mapas y `level += 1` al llegar a la bandera. #### Código completo `examples/games/platformer.py`: ```py """Pixel Run: a side-scrolling platformer with a tile map, a camera, enemies and coins. python examples/games/platformer.py Arrows or A/D run, Space (or Up/W) jumps, hold it to jump higher. Stomp the slimes, grab the coins, avoid the spikes and reach the flag. Enter plays again. Builds on the physics of Jump Lab and adds what turns it into a level: - **A tile map written as text**: ``#`` ground, ``=`` platforms, ``o`` coins, ``E`` enemies, ``^`` spikes, ``P`` the start and ``F`` the flag. Only the tiles near the player are checked for collisions, and only the ones on screen are drawn. - **A camera** that follows the player smoothly and stops at the edges of the level, with hills that scroll slower than the ground (parallax). - **Enemies** that patrol and turn at walls and ledges, and can be stomped from above. - **Lives, respawn, a win screen** and a HUD made of ordinary widgets. Tutorial: https://albakore.github.io/pyzed/games/platformer/ """ import math import pyzed as pz from pyzed import draw, game, ui TILE = 32 LEVEL = [ "................................................................................................", "................................................................................................", "................................................................................................", "................................................................................................", "....................................ooo.........................................................", "...................................=====........................................................", ".........ooo.............................................................ooo....................", "........=====...........................................................=====...................", "...................oo.....................oo........##.............oo...........................", "...............................###..................##..........................................", "...P.ooo......ooo.......Eoo.######............Eoo...##...^^.ooE...............E.ooo.......F.....", "##################...####################...######################...###########################", "##################...####################...######################...###########################", ] COLS, ROWS = len(LEVEL[0]), len(LEVEL) LEVEL_W, LEVEL_H = COLS * TILE, ROWS * TILE W, H = 704, LEVEL_H # the window shows 22 of the 96 columns RUN, ACCEL, AIR_ACCEL = 240, 2200, 1400 GRAVITY, JUMP_SPEED, LOW_JUMP, MAX_FALL = 1900, 680, 2.6, 900 COYOTE, BUFFER = 0.1, 0.12 JUMP_KEYS = ("space", "up", "w") ENEMY_SPEED = 55 SOLID = "#=" SLIME = ["..####..", ".######.", "##w##w##", "##k##k##", "########", "########", ".#.##.#."] SLIME_FLAT = ["........", "........", "........", "..####..", "##w##w##", "########", "########"] def solid(col, row): """Whether the tile at ``(col, row)`` blocks; outside the map, the sides block and the bottom is open.""" if col < 0 or col >= COLS: return True if row < 0 or row >= ROWS: return False return LEVEL[row][col] in SOLID def tiles_near(rect): """The solid tiles ``rect`` could touch: only the few cells under it, never the whole map.""" out = [] for row in range(int(rect.top // TILE), int(rect.bottom // TILE) + 1): for col in range(int(rect.left // TILE), int(rect.right // TILE) + 1): if solid(col, row): out.append(game.Rect(col * TILE, row * TILE, TILE, TILE)) return out class Enemy: def __init__(self, x, y): self.rect = game.Rect(x + 2, y + TILE - 24, 28, 24) self.vx = -ENEMY_SPEED self.squashed = 0.0 # seconds since it was stomped (0 while alive) class Platformer(pz.App): title = "Pixel Run" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None): self.held = None # the keys held down; None reads the keyboard self.sparks = game.Particles(gravity=600, seed=seed) self.shake = game.Shake(seed=seed) self.best_time = None self.new_game() # -- state ------------------------------------------------------------------------------ def new_game(self): self.coins, self.enemies = set(), [] for row, line in enumerate(LEVEL): for col, ch in enumerate(line): if ch == "o": self.coins.add((col, row)) elif ch == "E": self.enemies.append(Enemy(col * TILE, row * TILE)) elif ch == "P": self.start = (col * TILE + 4, row * TILE + TILE - 30) elif ch == "F": self.flag = game.Rect(col * TILE + 10, row * TILE - 4 * TILE, 12, 5 * TILE) self.total_coins = len(self.coins) self.lives, self.time = 3, 0.0 self.won = self.over = False self.camera = 0.0 self.respawn() def respawn(self): self.player = game.Rect(*self.start, 22, 30) self.vx = self.vy = 0.0 self.on_ground = False self.coyote = self.buffer = 0.0 self.facing = 1 self.invincible = 1.0 # a second of blinking after (re)spawning @property def collected(self): return self.total_coins - len(self.coins) # -- input ------------------------------------------------------------------------------ def on_key(self, combo): if combo in JUMP_KEYS: self.buffer = BUFFER elif combo == "enter" and (self.won or self.over): self.new_game() # -- update ----------------------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] def tick(self, dt): dt = min(dt, 1 / 30) self.sparks.update(dt) self.shake.update(dt) if self.won or self.over: return self.time += dt self.invincible = max(0.0, self.invincible - dt) self.move_player(dt) self.move_enemies(dt) self.pick_coins() self.check_dangers() # The camera looks a little ahead of the player and eases toward it. target = game.clamp(self.player.center[0] - W * 0.4, 0, LEVEL_W - W) self.camera = game.lerp(self.camera, target, min(1.0, 8 * dt)) def move_player(self, dt): move = game.axis(("left", "a"), ("right", "d"), held=self.held) if move: self.facing = move accel = (ACCEL if self.on_ground else AIR_ACCEL) * dt self.vx += game.clamp(move * RUN - self.vx, -accel, accel) rising_without_key = self.vy < 0 and not game.is_pressed(*JUMP_KEYS, held=self.held) self.vy = min(self.vy + GRAVITY * (LOW_JUMP if rising_without_key else 1) * dt, MAX_FALL) self.coyote = COYOTE if self.on_ground else max(0.0, self.coyote - dt) self.buffer = max(0.0, self.buffer - dt) if self.buffer > 0 and self.coyote > 0: self.vy, self.buffer, self.coyote = -JUMP_SPEED, 0.0, 0.0 p = self.player p.x += self.vx * dt for tile in tiles_near(p): if p.overlaps(tile): p.x = tile.left - p.w if self.vx > 0 else tile.right self.vx = 0.0 p.y += self.vy * dt self.on_ground = False for tile in tiles_near(p): if p.overlaps(tile): if self.vy > 0: p.y, self.on_ground = tile.top - p.h, True else: p.y = tile.bottom self.vy = 0.0 def move_enemies(self, dt): for e in self.enemies: if e.squashed: e.squashed += dt continue r = e.rect r.x += e.vx * dt ahead = r.right if e.vx > 0 else r.left col = int(ahead // TILE) row = int((r.bottom - 1) // TILE) if solid(col, row) or not solid(col, row + 1): # a wall, or no floor ahead: r.x -= e.vx * dt # step back and turn around e.vx = -e.vx self.enemies = [e for e in self.enemies if e.squashed < 0.6] def pick_coins(self): p = self.player for col, row in list(self.coins): if p.overlaps(game.Rect(col * TILE + 8, row * TILE + 8, 16, 16)): self.coins.discard((col, row)) self.sparks.burst(col * TILE + 16, row * TILE + 16, count=12, color=["#fde68a", "#fbbf24"], speed=(40, 160), life=(0.2, 0.5)) def check_dangers(self): p = self.player for e in self.enemies: if e.squashed or not p.overlaps(e.rect): continue if self.vy > 0 and p.bottom - self.vy / 60 <= e.rect.top + 8: # coming down on it: stomp e.squashed = 0.001 self.vy = -JUMP_SPEED * 0.6 self.sparks.burst(e.rect.center[0], e.rect.bottom, count=14, color="#a3e635", angle=-90, spread=160) self.shake.kick(3) elif not self.invincible: return self.hurt() for row in range(int(p.top // TILE), int(p.bottom // TILE) + 1): for col in range(int(p.left // TILE), int(p.right // TILE) + 1): if 0 <= row < ROWS and 0 <= col < COLS and LEVEL[row][col] == "^": spike = game.Rect(col * TILE + 4, row * TILE + 14, TILE - 8, TILE - 14) if p.overlaps(spike) and not self.invincible: return self.hurt() if p.top > LEVEL_H + 40: return self.hurt() if p.overlaps(self.flag): self.won = True self.best_time = self.time if self.best_time is None else min(self.best_time, self.time) self.sparks.burst(self.flag.center[0], self.flag.top + 20, count=60, color=["#f472b6", "#fbbf24", "#22d3ee", "#a3e635"], speed=(80, 320)) def hurt(self): self.lives -= 1 self.shake.kick(10) self.sparks.burst(*self.player.center, count=30, color=["#f87171", "#fecaca"], speed=(80, 260)) if self.lives <= 0: self.over = True else: self.respawn() # -- draw ------------------------------------------------------------------------------- def board(self): cam = round(self.camera) shapes = [draw.background("#1e1b3a")] for i, color in enumerate(("#2a2550", "#352d63", "#43367a")): # sky bands shapes.append(draw.rect(0, 70 + i * 60, W, 60, fill=color)) shapes.append(draw.circle(560, 90, 34, fill="#fde68a")) for layer, (speed, color, base) in enumerate(((0.2, "#2c2a55", 300), (0.45, "#24344d", 340))): off = -(cam * speed) % 240 # parallax hills for i in range(-1, W // 240 + 2): cx = i * 240 + off + layer * 120 shapes.append(draw.circle(cx, base, 150 - layer * 30, fill=color)) shapes += [draw.save(), *self.shake.shapes(), draw.translate(-cam, 0)] first, last = cam // TILE, (cam + W) // TILE + 1 dirt, grass, stone = [], [], [] for row, line in enumerate(LEVEL): for col in range(max(0, first), min(COLS, last + 1)): # only the visible columns ch = line[col] x, y = col * TILE, row * TILE if ch == "#": dirt.append((x, y, TILE, TILE)) if row == 0 or LEVEL[row - 1][col] not in SOLID: grass.append((x, y, TILE, 8)) elif ch == "=": stone.append((x, y, TILE, TILE / 2)) elif ch == "^": for k in range(2): sx = x + 2 + k * 14 shapes.append(draw.polygon([(sx, y + TILE), (sx + 7, y + 12), (sx + 14, y + TILE)], fill="#cbd5e1")) shapes += [draw.rects(dirt, fill="#5b3a29"), draw.rects(grass, fill="#65a30d"), draw.rects(stone, fill="#64748b")] for col, row in self.coins: cx, cy = col * TILE + 16, row * TILE + 16 + math.sin(self.time * 4 + col) * 3 spin = 0.35 + 0.65 * abs(math.cos(self.time * 3 + col * 0.7)) shapes += game.glow(cx, cy, 6, "#fbbf24", layers=2, strength=0.35, core=False) shapes.append(draw.rect(cx - 7 * spin, cy - 8, 14 * spin, 16, radius=7 * spin, fill="#fbbf24")) fx, fy = self.flag.x, self.flag.y shapes.append(draw.rect(fx + 4, fy, 4, self.flag.h, fill="#e2e8f0")) wave = math.sin(self.time * 5) * 4 shapes.append(draw.polygon([(fx + 8, fy + 2), (fx + 52, fy + 14 + wave), (fx + 8, fy + 28)], fill="#f472b6")) for e in self.enemies: frame = SLIME_FLAT if e.squashed else SLIME bob = 0 if e.squashed else abs(math.sin(self.time * 8 + e.rect.x * 0.1)) * -2 shapes += game.sprite(frame, {"#": "#84cc16", "w": "#f8fafc", "k": "#111827"}, e.rect.x - 2, e.rect.y - 4 + bob, scale=4, flip=e.vx > 0) if not self.over and (not self.invincible or int(self.time * 14) % 2): p = self.player shapes.append(p.shape(fill="#22d3ee", radius=5)) ex = p.center[0] + self.facing * 4 shapes.append(draw.rect(ex - 2, p.y + 8, 4, 7, radius=2, fill="#0b0e14")) shapes += self.sparks.shapes(glow=True) shapes.append(draw.restore()) return pz.canvas(shapes, width=W, height=H) def overlay(self): if self.won: title, lines = "Level clear!", [f"{self.collected}/{self.total_coins} coins in {self.time:.1f} s", "Enter plays again"] elif self.over: title, lines = "Game over", [f"{self.collected}/{self.total_coins} coins", "Enter plays again"] else: return None card = pz.container(pz.column([ui.heading(title, level=2), *[ui.muted(t) for t in lines]], spacing=8, align="center"), padding=(20, 32)).style(glass=True, radius=14) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def view(self): t = self.resolved_theme() hearts = pz.row([pz.icon("heart", size=15, color="#f87171" if i < self.lives else t.border) for i in range(3)], spacing=3) hud = pz.row([ ui.label("Pixel Run", weight="semibold"), pz.hspace(), ui.badge(f"{self.collected}/{self.total_coins}", icon="coins", tint="warning"), ui.badge(f"{self.time:.1f} s", icon="timer", tint="accent"), ui.badge(f"Best {self.best_time:.1f} s", icon="trophy", tint="text_muted") if self.best_time else None, hearts, ], spacing=8, align="center") play = pz.stack([self.board(), self.overlay()], width=W, height=H) return pz.container(pz.column([hud, pz.container(play, radius=10, clip=True), ui.muted("←/→ run · Space jumps · stomp the slimes · reach the flag", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(Platformer) ``` #### Siguiente - [5. Asteroids con partículas](https://albakore.github.io/pyzed/es/games/asteroids/): Rotación y empuje, espacio que da la vuelta, colisiones de círculos y muchas partículas. ### 5. Asteroids con partículas > Haz un shooter espacial al estilo Asteroids en Python con Pyzed: rotación y empuje con sin y cos, rozamiento, espacio que da la vuelta, colisiones de círculos, rocas que se parten, estelas del motor, explosiones con game.Particles y temblor de pantalla. Source: https://albakore.github.io/pyzed/es/games/asteroids/ *Rock Storm* es un shooter con el espíritu del clásico de 1979: una nave que gira y acelera, deriva con inercia y dispara a rocas que se parten en rocas más pequeñas. Es un buen sitio para aprender un poco de trigonometría (solo son `cos` y `sin`) y para llenar la pantalla de vida con **partículas**. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un shooter con inercia, rocas que se parten, oleadas, vidas y efectos de partículas | 50 minutos | Intermedio | [1. Tu primer juego](https://albakore.github.io/pyzed/es/games/first-game/); [3. Colisiones](https://albakore.github.io/pyzed/es/games/collisions-physics/) ayuda | #### Qué aprenderás - Convertir un **ángulo** en una dirección con `cos` y `sin`; empuje y **rozamiento**. - Hacer que las cosas **den la vuelta** por los bordes de la pantalla. - **Colisiones de círculos** para rocas, balas y la nave. - **Partículas** con `game.Particles`: estela del motor, fogonazos, escombros y explosiones. - **Temblor de pantalla**, invulnerabilidad tras un golpe y oleadas. 1. **Un ángulo se convierte en dirección.** La nave guarda hacia dónde apunta en grados. `cos` y `sin` de ese ángulo dan una dirección de longitud 1: a 0° es `(1, 0)` (derecha), a -90° es `(0, -1)` (arriba). El empuje suma esa dirección a la velocidad; el rozamiento le quita una fracción cada segundo: ```python def fly(self, dt): self.angle += game.axis(("left", "a"), ("right", "d"), held=self.held) * TURN * dt rad = math.radians(self.angle) fx, fy = math.cos(rad), math.sin(rad) if game.is_pressed("up", "w", held=self.held): self.vx += fx * THRUST * dt self.vy += fy * THRUST * dt damping = max(0.0, 1 - DRAG * dt) self.vx, self.vy = self.vx * damping, self.vy * damping ``` La nave sigue a la deriva cuando sueltas: esa inercia es lo que hace que Asteroids se sienta como el espacio. 2. **Dar la vuelta.** Lo que sale por un borde vuelve por el contrario. El operador módulo `%` lo hace en una línea; el margen deja que las rocas grandes salgan del todo antes de reaparecer: ```python def wrap(x, y, margin=0): return ((x + margin) % (W + 2 * margin) - margin, (y + margin) % (H + 2 * margin) - margin) ``` 3. **Rocas con carácter.** Una roca es un círculo para las colisiones, pero se dibuja como un polígono dentado: puntos alrededor de un círculo a distancias un poco aleatorias, girados con su propio ángulo cada fotograma: ```python n = 9 + size * 2 self.outline = [(math.cos(i / n * math.tau), math.sin(i / n * math.tau), rnd.uniform(0.75, 1.1)) for i in range(n)] ``` 4. **Colisiones de círculos.** Una bala da a una roca cuando está más cerca de su centro que su radio; la nave choca con una roca cuando la distancia es menor que los dos radios sumados: ```python for b in self.bullets[:]: for rock in self.rocks: if math.hypot(b[0] - rock.x, b[1] - rock.y) < rock.r: self.bullets.remove(b) self.break_rock(rock) break ``` Recorre una copia (`self.bullets[:]`) cuando quites elementos de la lista dentro del bucle. 5. **Parte la roca.** Una roca grande (tamaño 3) se convierte en dos medianas, una mediana en dos pequeñas, y una pequeña desaparece. Cada trozo sale en una dirección aleatoria, algo más rápido que su madre: ```python def break_rock(self, rock): self.rocks.remove(rock) self.score += POINTS[rock.size] self.sparks.burst(rock.x, rock.y, count=10 + rock.size * 10, color=["#cbd5e1", "#94a3b8", "#fbbf24"], speed=(40, 120 + rock.size * 60), life=(0.3, 0.6 + rock.size * 0.25), size=(1, 3)) self.shake.kick(rock.size * 2.5) if rock.size > 1: for _ in range(2): self.rocks.append(Rock(self.random, rock.x, rock.y, rock.size - 1, math.hypot(rock.vx, rock.vy) * 1.3 + self.random.uniform(20, 60))) ``` 6. **Partículas por todas partes.** `game.Particles` es una lista de puntos de vida corta. `burst` añade unos cuantos, `update(dt)` los mueve y quita los muertos, `shapes()` los dibuja desvaneciéndose. La estela del motor es un pequeño burst *en cada fotograma* mientras aceleras, apuntado hacia atrás: ```python self.trail = game.Particles(drag=3.0, limit=400, seed=seed) ... self.trail.burst(self.x - fx * SHIP_R, self.y - fy * SHIP_R, count=3, angle=self.angle + 180, spread=30, speed=(80, 180), life=(0.15, 0.4), size=(1.5, 3), color=["#fbbf24", "#f97316", "#fde68a"]) ``` `angle` y `spread` apuntan un burst (`angle=-90, spread=40` es una fuente), `gravity` y `drag` dan forma a la caída, `life` y `size` son rangos que se eligen al azar por partícula, y `color` puede ser una lista para mezclar. 7. **Recibir un golpe.** Un choque cuesta una vida, sacude fuerte la pantalla y esparce la nave en partículas. Luego la nave vuelve al centro y parpadea dos segundos (`safe`), en los que las rocas no le hacen daño. **Las partículas son baratas, pero no gratis:** Cada partícula es un círculo (dos con `glow=True`). Unos cientos vivas a la vez no es nada; decenas de miles son mucho Python por fotograma. Usa `limit=` para limitar cada emisor, y prefiere una `life` más corta a un `count` más pequeño. #### Prueba esto - Añade un OVNI que cruce la pantalla de vez en cuando y dispare a la nave. - Añade una tecla de hiperespacio que lleve la nave a un sitio aleatorio (con un burst donde estaba). - Da a las rocas un color por tamaño y haz que las pequeñas destellen en blanco un fotograma al recibir un disparo. #### Código completo `examples/games/asteroids.py`: ```py """Rock Storm: an Asteroids-style shooter with thrust, wrap-around space and lots of particles. python examples/games/asteroids.py Left/Right (or A/D) turn, Up (or W) thrusts, hold Space to fire, P pauses, Enter plays again. Big rocks split in two when hit, small ones burst into sparks. Shows: - **Rotation and thrust**: the ship has an angle; thrust adds ``cos``/``sin`` of it to the velocity, and a little drag slows it down. Everything wraps around the screen edges. - **Circle collisions**: two circles touch when the distance between their centers is smaller than the sum of their radii. - **Particles** everywhere: an engine trail emitted every frame, explosions, debris and sparks, with ``game.Particles`` (``burst``, ``update``, ``shapes``). - **Screen shake**, invulnerability after a hit, waves that get harder. Tutorial: https://albakore.github.io/pyzed/games/asteroids/ """ import math import random import pyzed as pz from pyzed import draw, game, ui W, H = 720, 480 TURN = 260 # degrees per second THRUST = 380 # px/s² while thrusting DRAG = 0.6 # fraction of the speed lost per second MAX_SPEED = 420 BULLET_SPEED = 620 BULLET_LIFE = 0.9 FIRE_DELAY = 0.18 SHIP_R = 13 SIZES = {3: 46, 2: 26, 1: 14} # rock size -> radius POINTS = {3: 20, 2: 50, 1: 100} def wrap(x, y, margin=0): """The point moved back onto the screen when it leaves through one edge.""" return ((x + margin) % (W + 2 * margin) - margin, (y + margin) % (H + 2 * margin) - margin) class Rock: def __init__(self, rnd, x, y, size, speed): self.x, self.y, self.size = x, y, size a = rnd.uniform(0, math.tau) self.vx, self.vy = math.cos(a) * speed, math.sin(a) * speed self.angle, self.spin = rnd.uniform(0, 360), rnd.uniform(-60, 60) # A jagged outline: points around a circle at slightly random distances. n = 9 + size * 2 self.outline = [(math.cos(i / n * math.tau), math.sin(i / n * math.tau), rnd.uniform(0.75, 1.1)) for i in range(n)] @property def r(self): return SIZES[self.size] def points(self): rad = math.radians(self.angle) c, s = math.cos(rad), math.sin(rad) return [(self.x + (ux * c - uy * s) * k * self.r, self.y + (ux * s + uy * c) * k * self.r) for ux, uy, k in self.outline] class Asteroids(pz.App): title = "Rock Storm" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) self.held = None # the keys held down; None reads the keyboard self.sparks = game.Particles(drag=1.2, seed=seed) # explosions and debris self.trail = game.Particles(drag=3.0, limit=400, seed=seed) # the engine's exhaust self.shake = game.Shake(seed=seed) self.stars = [(self.random.uniform(0, W), self.random.uniform(0, H), self.random.uniform(0.4, 1.4)) for _ in range(90)] self.best = 0 self.new_game() # -- state ------------------------------------------------------------------------------ def new_game(self): self.score, self.lives, self.wave = 0, 3, 0 self.over = self.paused = False self.clock = 0.0 self.reset_ship() self.next_wave() def reset_ship(self): self.x, self.y, self.vx, self.vy = W / 2, H / 2, 0.0, 0.0 self.angle = -90.0 # pointing up self.bullets = [] # [x, y, vx, vy, age] self.cooldown = 0.0 self.safe = 2.0 # seconds of invulnerability def next_wave(self): self.wave += 1 self.rocks = [] for _ in range(2 + self.wave): # Spawn on the edges, never on top of the ship. x, y = (self.random.uniform(0, W), -40) if self.random.random() < 0.5 else (-40, self.random.uniform(0, H)) self.rocks.append(Rock(self.random, x, y, 3, self.random.uniform(30, 60) + self.wave * 8)) # -- input ------------------------------------------------------------------------------ def on_key(self, combo): if combo == "p" and not self.over: self.paused = not self.paused elif combo == "enter" and self.over: self.new_game() # -- update ----------------------------------------------------------------------------- def subscriptions(self): return [] if self.paused else [pz.frames(self.tick)] def tick(self, dt): dt = min(dt, 1 / 30) self.clock += dt self.sparks.update(dt) self.trail.update(dt) self.shake.update(dt) for rock in self.rocks: rock.x, rock.y = wrap(rock.x + rock.vx * dt, rock.y + rock.vy * dt, rock.r) rock.angle += rock.spin * dt if self.over: return self.safe = max(0.0, self.safe - dt) self.fly(dt) self.move_bullets(dt) self.collide() if not self.rocks: self.next_wave() def fly(self, dt): self.angle += game.axis(("left", "a"), ("right", "d"), held=self.held) * TURN * dt rad = math.radians(self.angle) fx, fy = math.cos(rad), math.sin(rad) if game.is_pressed("up", "w", held=self.held): self.vx += fx * THRUST * dt self.vy += fy * THRUST * dt # Exhaust: a few particles every frame, shot backwards from the tail. self.trail.burst(self.x - fx * SHIP_R, self.y - fy * SHIP_R, count=3, angle=self.angle + 180, spread=30, speed=(80, 180), life=(0.15, 0.4), size=(1.5, 3), color=["#fbbf24", "#f97316", "#fde68a"]) damping = max(0.0, 1 - DRAG * dt) self.vx, self.vy = self.vx * damping, self.vy * damping speed = math.hypot(self.vx, self.vy) if speed > MAX_SPEED: self.vx, self.vy = self.vx / speed * MAX_SPEED, self.vy / speed * MAX_SPEED self.x, self.y = wrap(self.x + self.vx * dt, self.y + self.vy * dt) self.cooldown -= dt if game.is_pressed("space", held=self.held) and self.cooldown <= 0: self.fire() def fire(self): self.cooldown = FIRE_DELAY rad = math.radians(self.angle) fx, fy = math.cos(rad), math.sin(rad) self.bullets.append([self.x + fx * SHIP_R, self.y + fy * SHIP_R, self.vx + fx * BULLET_SPEED, self.vy + fy * BULLET_SPEED, 0.0]) self.sparks.burst(self.x + fx * SHIP_R, self.y + fy * SHIP_R, count=4, angle=self.angle, spread=40, speed=(60, 140), life=(0.05, 0.15), color="#a5f3fc") def move_bullets(self, dt): for b in self.bullets: b[0], b[1] = wrap(b[0] + b[2] * dt, b[1] + b[3] * dt) b[4] += dt self.bullets = [b for b in self.bullets if b[4] < BULLET_LIFE] def collide(self): for b in self.bullets[:]: for rock in self.rocks: if math.hypot(b[0] - rock.x, b[1] - rock.y) < rock.r: # a point inside a circle self.bullets.remove(b) self.break_rock(rock) break if self.safe <= 0: for rock in self.rocks: if math.hypot(self.x - rock.x, self.y - rock.y) < rock.r + SHIP_R * 0.8: # circle vs circle self.crash() break def break_rock(self, rock): self.rocks.remove(rock) self.score += POINTS[rock.size] self.sparks.burst(rock.x, rock.y, count=10 + rock.size * 10, color=["#cbd5e1", "#94a3b8", "#fbbf24"], speed=(40, 120 + rock.size * 60), life=(0.3, 0.6 + rock.size * 0.25), size=(1, 3)) self.shake.kick(rock.size * 2.5) if rock.size > 1: for _ in range(2): child = Rock(self.random, rock.x, rock.y, rock.size - 1, math.hypot(rock.vx, rock.vy) * 1.3 + self.random.uniform(20, 60)) self.rocks.append(child) def crash(self): self.lives -= 1 self.shake.kick(16) self.sparks.burst(self.x, self.y, count=70, color=["#22d3ee", "#a5f3fc", "#f472b6", "#ffffff"], speed=(60, 340), life=(0.4, 1.2), size=(1.5, 4)) if self.lives <= 0: self.over = True self.best = max(self.best, self.score) else: self.reset_ship() # -- draw ------------------------------------------------------------------------------- def board(self): shapes = [draw.background("#05060d"), *self.shake.shapes()] for x, y, r in self.stars: shapes.append(draw.circle(x, y, r, fill=game.rgba("#e2e8f0", 0.25 + r * 0.3))) shapes += self.trail.shapes(glow=True) for rock in self.rocks: shapes.append(draw.polygon(rock.points(), fill="#1e2433", stroke="#94a3b8", width=2)) for x, y, vx, vy, age in self.bullets: speed = math.hypot(vx, vy) or 1 tx, ty = x - vx / speed * 10, y - vy / speed * 10 shapes.append(draw.line(tx, ty, x, y, stroke=game.rgba("#22d3ee", 0.35), width=6, cap="round")) shapes.append(draw.line(tx, ty, x, y, stroke="#e0fbff", width=2, cap="round")) if not self.over and (self.safe <= 0 or int(self.clock * 12) % 2): rad = math.radians(self.angle) pts = [] for a, d in ((0, 1.4), (140, 1.0), (180, 0.45), (220, 1.0)): # nose, wing, tail notch, wing pa = rad + math.radians(a) pts.append((self.x + math.cos(pa) * SHIP_R * d, self.y + math.sin(pa) * SHIP_R * d)) shapes += game.glow(self.x, self.y, SHIP_R, "#22d3ee", layers=3, strength=0.18, core=False) shapes.append(draw.polygon(pts, fill="#0b1220", stroke="#22d3ee", width=2.5)) shapes += self.sparks.shapes(glow=True) return pz.canvas(shapes, width=W, height=H) def overlay(self): if self.over: title, lines = "Game over", [f"Score {self.score:,} · wave {self.wave}", "Enter plays again"] elif self.paused: title, lines = "Paused", ["P continues"] else: return None card = pz.container(pz.column([ui.heading(title, level=2), *[ui.muted(t) for t in lines]], spacing=8, align="center"), padding=(20, 32)).style(glass=True, radius=14) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def view(self): t = self.resolved_theme() ships = pz.row([pz.icon("rocket", size=15, color="#22d3ee" if i < self.lives else t.border) for i in range(3)], spacing=3) hud = pz.row([ ui.label("Rock Storm", weight="semibold"), pz.hspace(), ui.badge(f"Wave {self.wave}", icon="orbit", tint="accent"), ui.badge(f"{self.score:,}", icon="star", tint="warning"), ui.badge(f"Best {self.best:,}", icon="trophy", tint="text_muted"), ships, ], spacing=8, align="center") play = pz.stack([self.board(), self.overlay()], width=W, height=H) return pz.container(pz.column([hud, pz.container(play, radius=10, clip=True), ui.muted("←/→ turn · ↑ thrust · hold Space to fire · P pauses", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(Asteroids) ``` #### Siguiente - [6. Un puzle: 2048](https://albakore.github.io/pyzed/es/games/puzzle-2048/): Reglas como funciones sencillas, fichas con tweens, gestos con el ratón y una capa de widgets. ### 6. Un puzle: 2048 > Haz 2048 en Python con Pyzed: las reglas como funciones sencillas y comprobables, fichas que se deslizan y crecen con tweens, un bucle que solo corre mientras algo se mueve, gestos con el ratón, y marcadores y diálogos hechos con widgets. Source: https://albakore.github.io/pyzed/es/games/puzzle-2048/ No todos los juegos son una carrera contra el reloj. En un puzle no pasa nada hasta que mueves, y entonces pasa mucho a la vez: las fichas se deslizan, se juntan, crecen y aparece una nueva. Este paso construye **2048** y enseña a separar las reglas de la animación, y cómo un juego puede **no usar nada de CPU** mientras el jugador piensa. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un 2048 completo con animaciones, gestos y diálogos de victoria y derrota | 45 minutos | Intermedio | [2. Sprites y animación](https://albakore.github.io/pyzed/es/games/sprites-animation/) (tweens) | #### Qué aprenderás - Escribir las **reglas como funciones** fáciles de leer y de probar. - Separar el **estado del juego** (los números) de su **animación** (dónde se dibujan las fichas). - Ejecutar el bucle **solo mientras algo se mueve**. - Leer **flechas y gestos con el ratón**. - Poner **marcadores, botones y un diálogo** hechos con widgets alrededor y encima de un canvas. 1. **La regla, como función.** Cada movimiento desliza cada fila hacia un lado y junta los vecinos iguales, una sola vez. Eso es una función de una lista de cuatro números, sin ventana y sin dibujo: ```python def slide_line(values): """[2, 2, 4, 0] -> ([4, 4, 0, 0], 4): the new row and the points gained.""" tiles = [v for v in values if v] out, gained, i = [], 0, 0 while i < len(tiles): if i + 1 < len(tiles) and tiles[i] == tiles[i + 1]: out.append(tiles[i] * 2) gained += tiles[i] * 2 i += 2 # a tile merges only once per move else: out.append(tiles[i]) i += 1 return out + [0] * (len(values) - len(out)), gained ``` Deslizar a la derecha, arriba o abajo es la misma función sobre las celdas en otro orden: `lines(direction)` devuelve cada fila o columna empezando por el borde hacia el que van las fichas. Las pruebas comprueban los casos difíciles: `[2, 2, 2, 2]` da `[4, 4, 0, 0]`, no `[8, 0, 0, 0]`. 2. **Las fichas recuerdan de dónde vienen.** Para animar, cada ficha es un objeto con su valor, su celda y la celda de la que viene. Una ficha que se junta con otra se convierte en un *fantasma*: se desliza a la misma celda y desaparece cuando termina el deslizamiento: ```python class Tile: def __init__(self, value, cell): self.value = value self.cell = self.came_from = cell self.merged = False # doubled in the last move: shows the old value while sliding self.new = True # just appeared: grows from nothing ``` `move()` recorre cada línea exactamente como `slide_line`, mueve las fichas y se comprueba a sí mismo contra `slide_line` con un `assert`. Luego suma la puntuación, añade una ficha nueva y reinicia el reloj de la animación: `self.anim = 0.0`. 3. **Animación desde un reloj.** Todo se calcula a partir de `anim`, los segundos desde el último movimiento: las fichas se dibujan entre `came_from` y `cell` con un ease-out durante los primeros 0,11 s; después las fichas juntadas crecen un 18% y vuelven, y las nuevas crecen desde nada: ```python slide = game.clamp(self.anim / SLIDE_TIME, 0, 1) ease = 1 - (1 - slide) ** 3 x, y = game.lerp(x0, x1, ease), game.lerp(y0, y1, ease) pop = (self.anim - SLIDE_TIME) / POP_TIME # 0 -> 1 after the slide if tile.new: scale = 0.0 if pop < 0 else min(1.0, 0.3 + pop * 0.9) elif tile.merged and 0 <= pop < 1: scale = 1 + 0.18 * (1 - abs(2 * pop - 1)) # up and back down ``` Las reglas ya movieron los números al pulsar la tecla; la animación solo dibuja el camino. Por eso puedes pulsar más rápido que la animación sin perder ningún movimiento. 4. **Sin bucle mientras piensas.** `subscriptions()` devuelve el bucle por fotograma solo mientras algo se anima. Entre movimientos el juego no dibuja nada ni usa CPU, como cualquier app de escritorio: ```python @property def animating(self): return self.anim < SLIDE_TIME + POP_TIME or bool(self.floats) def subscriptions(self): return [pz.frames(self.tick)] if self.animating else [] ``` 5. **Teclas y gestos.** Las flechas y WASD se convierten en direcciones en `on_key`. Para el ratón, el canvas avisa dónde empieza un arrastre (`on_press`), por dónde va (`on_move`) y cuándo termina (`on_release`); un arrastre lo bastante largo es un gesto en su dirección principal: ```python def release(self): x0, y0, x1, y1 = self.swipe self.swipe = None dx, dy = x1 - x0, y1 - y0 if max(abs(dx), abs(dy)) > 30: # a short click is not a swipe self.move((0, 1 if dx > 0 else -1) if abs(dx) > abs(dy) else (1 if dy > 0 else -1, 0)) pz.canvas(shapes, width=BOARD, height=BOARD, on_press=self.press, on_move=self.drag, on_release=self.release, cursor="grab") ``` 6. **Widgets alrededor del tablero.** Los marcadores son contenedores con el color `surface` del tema; los puntos que acabas de ganar suben y se desvanecen con `.style(opacity=..., translate=(0, ...))`. Cuando ganas o te quedas sin movimientos, aparece sobre el tablero una tarjeta de cristal con botones y un `.animate("zoom_in")` nativo: ```python card = pz.container(pz.column([ui.heading(title, level=2), ui.muted(line), pz.row(buttons, spacing=8)], spacing=10, align="center"), padding=(22, 30)) card = card.style(glass=True, radius=14).animate("zoom_in", duration=250) ``` **Primero las reglas, después los píxeles:** Tener las reglas en funciones sencillas (`slide_line`, `can_move`) te permite probarlas en una fracción de segundo, cambiar el dibujo sin romper el juego e incluso escribir un solucionador o un jugador automático sobre las mismas funciones. #### Prueba esto - Añade **Deshacer**: guarda los `values` y la puntuación antes de cada movimiento, y un botón que los restaure. - Haz que el tamaño del tablero sea un ajuste (`SIZE = 5`) y comprueba que todo sigue funcionando. - Guarda la mejor puntuación entre sesiones, como en el [paso 8](https://albakore.github.io/pyzed/es/games/high-scores/). #### Código completo `examples/games/game_2048.py`: ```py """2048: the sliding-tiles puzzle, with smooth moves, merges that pop and a widget overlay. python examples/games/game_2048.py Arrows or WASD (or a swipe with the mouse) slide every tile; two equal tiles that meet merge into one. Reach 2048. Shows how a turn-based game differs from an action game: - **The rules are plain functions** (``slide_line``), easy to read and to test without a window. - **Animation only between turns**: ``pz.frames`` runs while tiles slide and pop, and ``subscriptions()`` returns nothing the rest of the time, so the game uses no CPU while you think. - **Tweens** for sliding (each tile remembers where it came from), popping merges and the "+points" that float up. - **UI on top of the game**: score boxes, a New game button and a glass card with buttons when you win or lose. Tutorial: https://albakore.github.io/pyzed/games/puzzle-2048/ """ import random import pyzed as pz from pyzed import draw, game, ui SIZE = 4 CELL, GAP = 96, 12 BOARD = SIZE * CELL + (SIZE + 1) * GAP SLIDE_TIME = 0.11 POP_TIME = 0.16 DIRECTIONS = {"left": (0, -1), "a": (0, -1), "right": (0, 1), "d": (0, 1), "up": (-1, 0), "w": (-1, 0), "down": (1, 0), "s": (1, 0)} COLORS = {2: ("#3b4252", "#e5e9f0"), 4: ("#434c5e", "#e5e9f0"), 8: ("#d08770", "#ffffff"), 16: ("#e07a5f", "#ffffff"), 32: ("#e76f51", "#ffffff"), 64: ("#ef4444", "#ffffff"), 128: ("#e9c46a", "#1f2937"), 256: ("#f4d35e", "#1f2937"), 512: ("#facc15", "#1f2937"), 1024: ("#a3e635", "#1f2937"), 2048: ("#22d3ee", "#0b1220")} # -- the rules: plain functions ----------------------------------------------------------------- def slide_line(values): """Slides one row toward its start and merges equal neighbours once. ``[2, 2, 4, 0] -> ([4, 4, 0, 0], 4)``: the new row and the points gained. """ tiles = [v for v in values if v] out, gained, i = [], 0, 0 while i < len(tiles): if i + 1 < len(tiles) and tiles[i] == tiles[i + 1]: out.append(tiles[i] * 2) gained += tiles[i] * 2 i += 2 # both tiles are used up: a tile merges only once per move else: out.append(tiles[i]) i += 1 return out + [0] * (len(values) - len(out)), gained def lines(direction): """The cells of each row (or column) in the order tiles slide, the leading edge first.""" dr, dc = direction out = [] for i in range(SIZE): cells = [(i, j) if dr == 0 else (j, i) for j in range(SIZE)] if dr == 1 or dc == 1: cells.reverse() out.append(cells) return out def can_move(values): """Whether any move changes the board (``values`` maps ``(row, col)`` to a number).""" for r in range(SIZE): for c in range(SIZE): v = values.get((r, c), 0) if not v or (c + 1 < SIZE and values.get((r, c + 1)) == v) or (r + 1 < SIZE and values.get((r + 1, c)) == v): return True return False class Tile: def __init__(self, value, cell): self.value = value self.cell = self.came_from = cell self.merged = False # doubled in the last move: shows the old value while sliding self.new = True # just appeared: grows from nothing class Game2048(pz.App): title = "2048" size = (BOARD + 48, BOARD + 150) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) self.best = 0 self.new_game() # -- state ------------------------------------------------------------------------------ def new_game(self): self.tiles = {} # (row, col) -> Tile self.ghosts = [] # tiles that merged into another: they slide, then vanish self.score = 0 self.won = self.keep_going = False self.anim = 1.0 # seconds since the last move (animations end at SLIDE + POP) self.floats = [] # [text, age]: "+8" rising from the score self.swipe = None self.add_tile() self.add_tile() def add_tile(self): free = [(r, c) for r in range(SIZE) for c in range(SIZE) if (r, c) not in self.tiles] if free: cell = self.random.choice(free) self.tiles[cell] = Tile(4 if self.random.random() < 0.1 else 2, cell) @property def values(self): return {cell: tile.value for cell, tile in self.tiles.items()} @property def over(self): return not can_move(self.values) @property def animating(self): return self.anim < SLIDE_TIME + POP_TIME or bool(self.floats) # -- a move ----------------------------------------------------------------------------- def move(self, direction): if self.over or (self.won and not self.keep_going): return False for tile in self.tiles.values(): tile.came_from, tile.merged, tile.new = tile.cell, False, False self.ghosts = [] moved, gained, new_tiles = False, 0, {} for cells in lines(direction): row = [self.tiles[c] for c in cells if c in self.tiles] new_values, points = slide_line([self.tiles[c].value if c in self.tiles else 0 for c in cells]) gained += points # Walk the row the same way slide_line did, to know where each tile goes. k = 0 i = 0 while i < len(row): target = cells[k] tile = row[i] if i + 1 < len(row) and row[i + 1].value == tile.value: other = row[i + 1] other.cell = target # it slides to the same cell, then disappears self.ghosts.append(other) tile.value *= 2 tile.merged = True i += 2 else: i += 1 moved |= tile.cell != target or tile.merged tile.cell = target new_tiles[target] = tile k += 1 assert [new_tiles[c].value if c in new_tiles else 0 for c in cells] == new_values if not moved: return False self.tiles = new_tiles self.score += gained self.best = max(self.best, self.score) if gained: self.floats.append([f"+{gained}", 0.0]) if any(t.value >= 2048 for t in self.tiles.values()) and not self.keep_going: self.won = True self.add_tile() self.anim = 0.0 return True # -- input ------------------------------------------------------------------------------ def on_key(self, combo): if combo in DIRECTIONS: self.move(DIRECTIONS[combo]) def press(self, x, y): self.swipe = (x, y, x, y) def drag(self, x, y): if self.swipe: self.swipe = (*self.swipe[:2], x, y) def release(self): if not self.swipe: return x0, y0, x1, y1 = self.swipe self.swipe = None dx, dy = x1 - x0, y1 - y0 if max(abs(dx), abs(dy)) > 30: # a short click is not a swipe self.move((0, 1 if dx > 0 else -1) if abs(dx) > abs(dy) else (1 if dy > 0 else -1, 0)) def continue_playing(self): self.keep_going = True # -- animation -------------------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] if self.animating else [] # idle between moves def tick(self, dt): self.anim += dt for f in self.floats: f[1] += dt self.floats = [f for f in self.floats if f[1] < 0.8] if self.anim >= SLIDE_TIME: self.ghosts = [] # -- draw ------------------------------------------------------------------------------- @staticmethod def cell_xy(cell): r, c = cell return GAP + c * (CELL + GAP), GAP + r * (CELL + GAP) def tile_shapes(self, tile, value): slide = game.clamp(self.anim / SLIDE_TIME, 0, 1) ease = 1 - (1 - slide) ** 3 (x0, y0), (x1, y1) = self.cell_xy(tile.came_from), self.cell_xy(tile.cell) x, y = game.lerp(x0, x1, ease), game.lerp(y0, y1, ease) scale = 1.0 pop = (self.anim - SLIDE_TIME) / POP_TIME # 0 -> 1 after the slide if tile.new: scale = 0.0 if pop < 0 else min(1.0, 0.3 + pop * 0.9) elif tile.merged and 0 <= pop < 1: scale = 1 + 0.18 * (1 - abs(2 * pop - 1)) # up and back down if scale <= 0: return [] bg, fg = COLORS.get(value, ("#22d3ee", "#0b1220")) size = CELL * scale cx, cy = x + CELL / 2, y + CELL / 2 shapes = [] if value >= 128: shapes += game.glow(cx, cy, CELL * 0.42, bg, layers=2, strength=0.2, core=False) shapes.append(draw.rect(cx - size / 2, cy - size / 2, size, size, radius=10 * scale, fill=bg)) font = (40 if value < 100 else 34 if value < 1000 else 28) * scale shapes.append(draw.text(cx, cy, value, size=font, weight="bold", color=fg, align_x="center", align_y="center")) return shapes def board(self): shapes = [draw.background("#1b1f27")] for r in range(SIZE): for c in range(SIZE): x, y = self.cell_xy((r, c)) shapes.append(draw.rect(x, y, CELL, CELL, radius=10, fill="#262b35")) sliding = self.anim < SLIDE_TIME for ghost in self.ghosts: shapes += self.tile_shapes(ghost, ghost.value) for tile in self.tiles.values(): shown = tile.value // 2 if tile.merged and sliding else tile.value shapes += self.tile_shapes(tile, shown) return pz.canvas(shapes, width=BOARD, height=BOARD, on_press=self.press, on_move=self.drag, on_release=self.release, cursor="grab") def score_box(self, label, value, floats=()): texts = [pz.text(f"{value:,}", size=20, weight="bold")] box = pz.container(pz.column([ui.muted(label, size="sm"), *texts], spacing=0, align="center"), padding=(6, 16)).style(background="surface", radius=8) risers = [pz.container(pz.text(text, size=14, weight="bold", color="#fbbf24"), padding=(0, 0, 0, 0)).style(opacity=1 - age / 0.8, translate=(0, -28 * age / 0.8)) for text, age in floats] return pz.stack([box, *[pz.container(r, width="fill", align_x="center") for r in risers]]) def overlay(self): if self.won and not self.keep_going: title, line = "You made 2048!", f"Score {self.score:,}" buttons = [ui.button("Keep going", self.continue_playing, variant="solid", icon="play"), ui.button("New game", self.new_game, icon="rotate-ccw")] elif self.over and not self.animating: title, line = "No more moves", f"Score {self.score:,} · best {self.best:,}" buttons = [ui.button("Try again", self.new_game, variant="solid", icon="rotate-ccw")] else: return None card = pz.container(pz.column([ui.heading(title, level=2), ui.muted(line), pz.row(buttons, spacing=8)], spacing=10, align="center"), padding=(22, 30)) card = card.style(glass=True, radius=14).animate("zoom_in", duration=250) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def view(self): t = self.resolved_theme() header = pz.row([ pz.text("2048", size=40, weight="bold"), pz.hspace(), self.score_box("SCORE", self.score, self.floats), self.score_box("BEST", self.best), ui.button("New game", self.new_game, icon="rotate-ccw"), ], spacing=10, align="center") play = pz.stack([self.board(), self.overlay()], width=BOARD, height=BOARD) return pz.container(pz.column([header, pz.container(play, radius=12, clip=True), ui.muted("Arrows / WASD or swipe with the mouse · join the numbers to get to 2048", size="sm")], spacing=12), padding=24, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(Game2048) ``` #### Siguiente - [7. Menús, HUD, pausa y ajustes](https://albakore.github.io/pyzed/es/games/menus-hud/): Una pantalla de título, transiciones, un menú de pausa y una pantalla de ajustes con los componentes de Pyzed. ### 7. Menús, HUD, pausa y ajustes > Envuelve un juego de Python en una interfaz de verdad con Pyzed: escenas, un menú de título que funciona con teclado y ratón, transiciones animadas, un HUD de cristal con bloom, un menú de pausa y una pantalla de ajustes con sliders, interruptores y controles segmentados. Source: https://albakore.github.io/pyzed/es/games/menus-hud/ Los jugadores ven tus menús antes que tu juego. Aquí es donde Pyzed brilla: es una librería de interfaces, así que los botones, sliders, interruptores, paneles de cristal y textos que brillan ya están ahí y ya se ven bien. Este paso envuelve un juego diminuto, *Orb Rush*, en una pantalla de título, un HUD, un menú de pausa y una pantalla de ajustes, con transiciones entre ellos. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un menú de título, HUD, menú de pausa y pantalla de ajustes alrededor de un juego | 45 minutos | Intermedio | [1. Tu primer juego](https://albakore.github.io/pyzed/es/games/first-game/); la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/) ayuda | #### Qué aprenderás - Organizar un juego en **escenas** (`title`, `play`, `pause`, `settings`, `over`). - Animar **transiciones** con un reloj por escena y `.style(opacity=..., translate=...)`. - Construir menús que funcionan con **flechas y Enter** y con **clics**. - Un **HUD** con bloom, una barra de vida, un temporizador y un contador de FPS. - Una **pantalla de ajustes** con `ui.slider`, `ui.switch` y `ui.segmented` cuyos valores lee el juego. ```mermaid stateDiagram-v2 [*] --> title title --> play: Play title --> settings: Settings play --> pause: Esc / P pause --> play: Resume / Esc pause --> settings: Settings settings --> title: Back (desde title) settings --> pause: Back (desde pause) pause --> title: Main menu play --> over: vida = 0 over --> play: Play again over --> title: Main menu ``` 1. **Un atributo decide la escena.** `go()` cambia de escena y reinicia `since`, los segundos desde que empezó la escena, que mueve todas las transiciones. Settings recuerda desde dónde se abrió, así que Back vuelve al título o al menú de pausa: ```python def go(self, scene): if scene == "settings": self.came_from = self.scene self.scene = scene self.since = 0.0 self.selected = 0 ``` `tick` solo ejecuta el juego en su escena, así que pausar no cuesta nada: `if self.scene == "play": self.play(dt)`. Las estrellas siguen moviéndose detrás de los menús, para que la pantalla nunca parezca congelada. 2. **Menús como datos.** El menú de cada escena es una lista de `(texto, acción)`. Un solo método hace lo que significa cada acción, venga de una tecla o de un clic, y *Quit* devuelve `self.close()`, un comando que cierra la ventana: ```python MENUS = { "title": [("Play", "play"), ("Settings", "settings"), ("Quit", "quit")], "pause": [("Resume", "resume"), ("Settings", "settings"), ("Main menu", "menu")], "over": [("Play again", "play"), ("Main menu", "menu")], "settings": [("Back", "back")], } def choose(self, action): if action == "play": self.reset_round() self.go("play") ... elif action == "quit": return self.close() ``` 3. **Navegar con el teclado.** Arriba y abajo mueven `selected` (dando la vuelta con `%`), Enter elige. El botón seleccionado se dibuja `solid` con una flecha; los demás son `ghost`. Hacer clic en cualquiera llama al mismo `choose`: ```python items = MENUS[self.scene] if combo in ("down", "s"): self.selected = (self.selected + 1) % len(items) elif combo in ("up", "w"): self.selected = (self.selected - 1) % len(items) elif combo in ("enter", "space"): return self.choose(items[self.selected][1]) ``` 4. **Transiciones desde un reloj.** `appear(delay)` va de 0 a 1 en 0,35 s, `delay` segundos después de empezar la escena, con un ease-out. Cualquier widget puede aparecer y deslizarse con él, y dar a cada elemento del menú un retraso un poco mayor hace que lleguen uno tras otro: ```python def appear(self, delay=0.0): return ease_out(game.clamp((self.since - delay) / FADE, 0.0, 1.0)) def slide_in(self, widget, delay=0.0, distance=24): a = self.appear(delay) return widget.style(opacity=a, translate=(0, (1 - a) * distance)) # each item of a menu: items.append(self.slide_in(pz.container(button, width=220), delay + i * 0.06, 16)) ``` Como los valores salen de tu propio reloj, la transición es exactamente igual cada vez, también en capturas y en pruebas. 5. **Un título que brilla y un HUD de cristal.** `bloom` hace brillar las partes claras de un widget; sin borde ni sombra, solo brilla el texto. El HUD es una fila dentro de un contenedor `glass` (un panel borroso y teñido) y baja desde arriba cuando empieza el juego: ```python def title_text(self, text, color, size=48): return pz.container(pz.text(text, size=size, weight="bold", color="#ffffff"), padding=(4, 16)).style( bloom=(color, 0.9), border_width=0, shadow=("#00000000", (0, 0), 0)) bits = [ self.title_text(f"{self.score:05d}", NEON["cyan"], size=22), pz.hspace(), pz.row([pz.icon("heart", size=14, color=NEON["red"]), pz.container(ui.progress(self.health, max=3, tint=NEON["red"], height=6), width=90)], spacing=6, align="center"), ui.badge(f"{self.time:4.1f} s", icon="timer", tint="text_muted"), ui.badge(f"{self.fps:.0f} fps", icon="gauge", tint="success") if self.settings["fps"] else None, pz.row([ui.kbd("Esc"), ui.muted("pause", size="sm")], spacing=4, align="center"), ] ``` Un `None` en una fila se omite: es la forma fácil de mostrar algo solo a veces (el contador de FPS). 6. **El menú de pausa.** El juego se sigue dibujando debajo; encima van el HUD, una capa oscura que aparece y el panel del menú. `pz.stack` necesita el tamaño del tablero para que el panel se pueda centrar: ```python return pz.stack([self.hud(), pz.container(width="fill", height="fill").style( background=game.rgba("#000000", 0.35 * self.appear())), self.panel(body)], width=W, height=H) ``` 7. **Ajustes que lee el juego.** Los ajustes son un diccionario normal. Cada control escribe una clave, y el juego las lee donde importan: la dificultad escala la velocidad, el interruptor de temblor decide si un golpe sacude la pantalla: ```python self.settings = {"music": 0.6, "effects": 0.8, "difficulty": "Normal", "shake": True, "fps": False} rows = [ ui.field("Music", pz.container(ui.slider(s["music"], setter("music"), show_value=True), width=180)), ui.field("Effects", pz.container(ui.slider(s["effects"], setter("effects"), show_value=True), width=180)), ui.field("Difficulty", ui.segmented(list(DIFFICULTY), s["difficulty"], setter("difficulty"), size="sm")), ui.field("Screen shake", ui.switch(s["shake"], setter("shake"))), ui.field("Show FPS", ui.switch(s["fps"], setter("fps"))), ] ``` **Sliders de volumen:** Aquí los sliders de música y efectos solo guardan un valor, porque Orb Rush no tiene sonido. El [paso 8](https://albakore.github.io/pyzed/es/games/high-scores/#efectos-de-sonido) añade efectos de sonido con `pyzed.audio`, enseña cómo un slider cambia su volumen y guarda estos ajustes en disco. **La accesibilidad también es un ajuste:** Ofrece un interruptor para el temblor y los destellos; a algunas personas les marean. `pz.system_reduced_motion()` dice si el sistema pide menos movimiento, un buen valor inicial para ese interruptor. **Mira también:** El showcase del [Menú principal de un juego](https://albakore.github.io/pyzed/es/showcase/main-menu/) es una pantalla de título con un fondo de shader interactivo, y el del [HUD de un juego](https://albakore.github.io/pyzed/es/showcase/hud/) un HUD de ciencia ficción completo. #### Prueba esto - Añade una página **Controles** en los ajustes que liste las teclas con `ui.kbd`. - Añade una escena de cuenta atrás 3-2-1 entre el título y el juego. - Anima el título de Game over con `.animate("pulse", repeat="infinite")`. #### Código completo `examples/games/menus.py`: ```py """Orb Rush: a title screen, a HUD, a pause menu and a settings screen around a small game. python examples/games/menus.py Up/Down (or W/S) choose, Enter confirms, Escape (or P) pauses and goes back. In the game, arrows or WASD fly; collect the blue orbs and dodge the red mines. Shows the parts of a game that are not the game itself: - **Scenes**: one ``scene`` attribute (``"title"``, ``"play"``, ``"pause"``, ``"settings"``, ``"over"``) decides what ``tick`` updates and what ``view`` shows. - **Transitions**: every scene change restarts a clock; panels fade and slide in from it (``.style(opacity=..., translate=...)``) and menu items follow one after another. - **Menus that work with the keyboard and the mouse**: a selected index for the keys, ``ui.button`` for clicks. - **A HUD** of widgets (score with bloom, a health bar, a timer, an FPS counter) and a **settings screen** with ``ui.slider``, ``ui.switch`` and ``ui.segmented`` whose values the game reads. Tutorial: https://albakore.github.io/pyzed/games/menus-hud/ """ import math import random import pyzed as pz from pyzed import draw, game, ui W, H = 720, 460 FADE = 0.35 # seconds a scene takes to come in NEON = {"cyan": "#22d3ee", "pink": "#f472b6", "violet": "#a78bfa", "red": "#f87171", "amber": "#fbbf24"} DIFFICULTY = {"Easy": 0.7, "Normal": 1.0, "Hard": 1.45} #: The menu of each scene: (label, action) pairs. MENUS = { "title": [("Play", "play"), ("Settings", "settings"), ("Quit", "quit")], "pause": [("Resume", "resume"), ("Settings", "settings"), ("Main menu", "menu")], "over": [("Play again", "play"), ("Main menu", "menu")], "settings": [("Back", "back")], } def ease_out(t): return 1 - (1 - t) ** 3 class OrbRush(pz.App): title = "Orb Rush" size = (W + 32, H + 32) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) self.held = None # the keys held down; None reads the keyboard self.settings = {"music": 0.6, "effects": 0.8, "difficulty": "Normal", "shake": True, "fps": False} self.stars = [(self.random.uniform(0, W), self.random.uniform(0, H), self.random.choice((1, 2, 3))) for _ in range(110)] self.sparks = game.Particles(drag=1.5, seed=seed) self.shake = game.Shake(seed=seed) self.clock, self.fps = 0.0, 60.0 self.best = 0 self.reset_round() self.go("title") # -- scenes ----------------------------------------------------------------------------- def go(self, scene): """Switches scene and restarts the transition clock.""" if scene == "settings": self.came_from = self.scene self.scene = scene self.since = 0.0 # seconds since this scene started: drives every transition self.selected = 0 def choose(self, action): if action == "play": self.reset_round() self.go("play") elif action == "resume": self.go("play") elif action == "settings": self.go("settings") elif action == "back": self.go(self.came_from) elif action == "menu": self.go("title") elif action == "quit": return self.close() def reset_round(self): self.x, self.y = W / 2, H - 80 self.orbs, self.mines = [], [] self.score, self.health, self.time = 0, 3, 0.0 self.spawn = 0.0 # -- input ------------------------------------------------------------------------------ def on_key(self, combo): if self.scene == "play": if combo in ("escape", "p"): self.go("pause") return None if combo in ("escape", "p") and self.scene == "pause": return self.go("play") if combo == "escape" and self.scene == "settings": return self.go(self.came_from) items = MENUS[self.scene] if combo in ("down", "s"): self.selected = (self.selected + 1) % len(items) elif combo in ("up", "w"): self.selected = (self.selected - 1) % len(items) elif combo in ("enter", "space"): return self.choose(items[self.selected][1]) return None # -- update ----------------------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] # the stars move in every scene def tick(self, dt): dt = min(dt, 1 / 30) self.clock += dt self.since += dt self.fps = game.lerp(self.fps, 1 / max(dt, 1e-3), 0.1) # smoothed, so the number is readable self.stars = [(x, (y + 18 * layer * dt * (3 if self.scene == "play" else 1)) % H, layer) for x, y, layer in self.stars] self.sparks.update(dt) self.shake.update(dt) if self.scene == "play": # the game only runs in its scene: pausing is free self.play(dt) def play(self, dt): self.time += dt speed = DIFFICULTY[self.settings["difficulty"]] dx, dy = game.vector(held=self.held) self.x = game.clamp(self.x + dx * 320 * dt, 16, W - 16) self.y = game.clamp(self.y + dy * 320 * dt, 80, H - 16) self.spawn -= dt if self.spawn <= 0: self.spawn = 0.45 / speed kind = self.mines if self.random.random() < 0.35 else self.orbs kind.append([self.random.uniform(20, W - 20), -20.0]) for item in self.orbs + self.mines: item[1] += 170 * speed * dt for orb in self.orbs[:]: if math.hypot(orb[0] - self.x, orb[1] - self.y) < 24: self.orbs.remove(orb) self.score += 10 self.sparks.burst(orb[0], orb[1], count=14, color=[NEON["cyan"], "#e0fbff"]) for mine in self.mines[:]: if math.hypot(mine[0] - self.x, mine[1] - self.y) < 24: self.mines.remove(mine) self.health -= 1 self.sparks.burst(mine[0], mine[1], count=30, color=[NEON["red"], NEON["amber"]], speed=(80, 300)) if self.settings["shake"]: self.shake.kick(10) if self.health <= 0: self.best = max(self.best, self.score) self.go("over") self.orbs = [o for o in self.orbs if o[1] < H + 20] self.mines = [m for m in self.mines if m[1] < H + 20] # -- transitions ------------------------------------------------------------------------ def appear(self, delay=0.0): """0 -> 1 over FADE seconds, ``delay`` seconds after the scene started.""" return ease_out(game.clamp((self.since - delay) / FADE, 0.0, 1.0)) def slide_in(self, widget, delay=0.0, distance=24): a = self.appear(delay) return widget.style(opacity=a, translate=(0, (1 - a) * distance)) # -- draw ------------------------------------------------------------------------------- def world(self): shapes = [draw.background("#070817"), *self.shake.shapes()] for x, y, layer in self.stars: shapes.append(draw.circle(x, y, layer * 0.6, fill=game.rgba("#c7d2fe", 0.25 * layer))) if self.scene != "title": for x, y in self.orbs: shapes += game.glow(x, y, 7, NEON["cyan"], layers=3, strength=0.5) for x, y in self.mines: spikes = [(x + math.cos(a) * (13 if i % 2 else 7), y + math.sin(a) * (13 if i % 2 else 7)) for i, a in enumerate(i * math.pi / 6 + self.clock * 3 for i in range(12))] shapes += game.glow(x, y, 6, NEON["red"], layers=2, strength=0.3, core=False) shapes.append(draw.polygon(spikes, fill=NEON["red"])) if self.scene != "over": shapes += game.glow(self.x, self.y, 12, NEON["violet"], layers=4, strength=0.4) shapes += self.sparks.shapes(glow=True) return pz.canvas(shapes, width=W, height=H) def title_text(self, text, color, size=48): return pz.container(pz.text(text, size=size, weight="bold", color="#ffffff"), padding=(4, 16)).style( bloom=(color, 0.9), border_width=0, shadow=("#00000000", (0, 0), 0)) def menu(self, delay=0.15): """The current scene's menu: a button per item, the selected one solid, each sliding in a little later.""" items = [] for i, (label, action) in enumerate(MENUS[self.scene]): selected = i == self.selected button = ui.button(label, lambda a=action: self.choose(a), variant="solid" if selected else "ghost", full_width=True, icon="chevron-right" if selected else None) items.append(self.slide_in(pz.container(button, width=220), delay + i * 0.06, 16)) return pz.column(items, spacing=6, align="center") def panel(self, content, delay=0.0): card = pz.container(content, padding=(24, 32)).style(glass=True, radius=16, border=(1, "#ffffff22")) return pz.container(self.slide_in(card, delay, 30), width="fill", height="fill", align_x="center", align_y="center") def hud(self): bits = [ self.title_text(f"{self.score:05d}", NEON["cyan"], size=22), pz.hspace(), pz.row([pz.icon("heart", size=14, color=NEON["red"]), pz.container(ui.progress(self.health, max=3, tint=NEON["red"], height=6), width=90)], spacing=6, align="center"), ui.badge(f"{self.time:4.1f} s", icon="timer", tint="text_muted"), ui.badge(f"{self.fps:.0f} fps", icon="gauge", tint="success") if self.settings["fps"] else None, pz.row([ui.kbd("Esc"), ui.muted("pause", size="sm")], spacing=4, align="center"), ] bar = pz.container(pz.row(bits, spacing=10, align="center"), padding=(6, 14)).style( glass=True, radius=12, border=(1, "#ffffff1f")) a = self.appear() return pz.container(bar.style(opacity=a, translate=(0, -(1 - a) * 40)), padding=12, width="fill") def settings_panel(self): s = self.settings def setter(key): return lambda value: s.__setitem__(key, value) rows = [ ui.field("Music", pz.container(ui.slider(s["music"], setter("music"), show_value=True), width=180)), ui.field("Effects", pz.container(ui.slider(s["effects"], setter("effects"), show_value=True), width=180)), ui.field("Difficulty", ui.segmented(list(DIFFICULTY), s["difficulty"], setter("difficulty"), size="sm")), ui.field("Screen shake", ui.switch(s["shake"], setter("shake"))), ui.field("Show FPS", ui.switch(s["fps"], setter("fps"))), ] body = pz.column([ ui.heading("Settings", level=2), *[self.slide_in(r, 0.08 + i * 0.05, 12) for i, r in enumerate(rows)], self.menu(0.4), ], spacing=10, width=380) return self.panel(body) def overlay(self): if self.scene == "title": pulse = 0.85 + 0.15 * math.sin(self.clock * 2.5) body = pz.column([ self.slide_in(self.title_text("ORB RUSH", NEON["violet"]).style(scale=pulse), 0.0, -30), self.slide_in(ui.muted("Collect the orbs. Dodge the mines."), 0.08, 12), pz.vspace(12), self.menu(), self.slide_in(ui.muted(f"Best {self.best}", size="sm"), 0.4, 8), ], spacing=6, align="center") return pz.container(body, width="fill", height="fill", align_x="center", align_y="center") if self.scene == "play": return self.hud() if self.scene == "pause": body = pz.column([ui.heading("Paused", level=2), ui.muted(f"Score {self.score}"), self.menu()], spacing=10, align="center") return pz.stack([self.hud(), pz.container(width="fill", height="fill").style( background=game.rgba("#000000", 0.35 * self.appear())), self.panel(body)], width=W, height=H) if self.scene == "settings": return self.settings_panel() body = pz.column([self.title_text("GAME OVER", NEON["pink"], size=34), ui.muted(f"Score {self.score} · best {self.best}"), self.menu()], spacing=10, align="center") return self.panel(body) def view(self): board = pz.stack([self.world(), self.overlay()], width=W, height=H) return pz.container(pz.container(board, radius=12, clip=True), padding=16, width="fill", height="fill", bg="#05060d", align_x="center", align_y="center") if __name__ == "__main__": pz.run(OrbRush) ``` #### Siguiente - [8. Récords, guardado y sonido](https://albakore.github.io/pyzed/es/games/high-scores/): Guarda una tabla de récords de forma segura, pide un nombre y añade efectos de sonido. ### 8. Récords, guardado y sonido > Guarda la tabla de récords y los ajustes de un juego en Python con Pyzed: un archivo JSON en la carpeta correcta de Windows, macOS y Linux, escritura atómica, nombre con ui.input, una tabla de récords, efectos de sonido sintetizados con pyzed.audio y empaquetar el juego. Source: https://albakore.github.io/pyzed/es/games/high-scores/ Una puntuación vale más si mañana sigue ahí. Este último paso de la ruta construye *Target Rush*, un juego de clics de 20 segundos, y le da lo que tienen los juegos terminados: una **tabla de récords guardada en disco**, un **nombre** cuando batas un récord, **efectos de sonido** y una forma de **compartirlo** con tus amigos. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un juego con sonido que guarda una tabla de los 10 mejores | 35 minutos | Intermedio | [7. Menús, HUD y ajustes](https://albakore.github.io/pyzed/es/games/menus-hud/) | #### Qué aprenderás - Encontrar la **carpeta correcta** para las partidas guardadas en Windows, macOS y Linux. - **Guardar con seguridad**: escribir un archivo temporal y cambiarlo de golpe, y sobrevivir a un archivo dañado. - Pedir un **nombre** con `ui.input` (Enter confirma) y una **tabla de récords** hecha de filas. - **Efectos de sonido**: un método al que se llama donde va un sonido, que toca percusión sintetizada con `pyzed.audio`. - **Empaquetar** el juego como app. 1. **Dónde van las partidas guardadas.** Cada sistema tiene una carpeta para los datos de las apps, y los juegos no deberían escribir junto a su código (esa carpeta puede ser de solo lectura una vez instalado): ```python def data_dir(app_name): if sys.platform == "win32": base = Path(os.environ.get("APPDATA") or Path.home() / "AppData" / "Roaming") elif sys.platform == "darwin": base = Path.home() / "Library" / "Application Support" else: base = Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share") return base / app_name ``` | Sistema | Carpeta de "Target Rush" | | --- | --- | | Windows | `C:\Users\tú\AppData\Roaming\Target Rush` | | macOS | `~/Library/Application Support/Target Rush` | | Linux | `~/.local/share/Target Rush` | 2. **Cargar sin miedo.** La primera vez no hay archivo, y un archivo se puede dañar (un disco lleno, alguien que lo edita a mano). Las dos cosas deberían significar "aún no hay récords", nunca un error: ```python def load(self): try: data = json.loads(self.path.read_text(encoding="utf-8")) entries = [e for e in data.get("scores", []) if isinstance(e.get("score"), int)] except (OSError, ValueError, AttributeError): return [] return sorted(entries, key=lambda e: -e["score"])[:KEEP] ``` 3. **Guardar con seguridad.** Si el juego falla mientras escribe, un archivo a medias perdería todos los récords. Así que escribe un archivo temporal en la misma carpeta y sustituye el real en un solo paso; `os.replace` es atómico en todos los sistemas: ```python def save(self): self.path.parent.mkdir(parents=True, exist_ok=True) fd, tmp = tempfile.mkstemp(dir=self.path.parent, prefix=".scores-", suffix=".json") with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump({"version": 1, "scores": self.entries}, f, indent=2) os.replace(tmp, self.path) ``` El `"version": 1` no cuesta nada y permite que una versión futura del juego lea de otra forma las partidas antiguas. 4. **¿Entra en la tabla?** Una puntuación entra si la tabla aún no está llena o supera a la última. `add` la inserta, se queda con las 10 mejores, guarda y devuelve el puesto: ```python def qualifies(self, score): return score > 0 and (len(self.entries) < KEEP or score > self.entries[-1]["score"]) def add(self, name, score): entry = {"name": name.strip()[:16] or "Player", "score": score, "date": time.strftime("%Y-%m-%d")} self.entries.append(entry) self.entries.sort(key=lambda e: -e["score"]) del self.entries[KEEP:] self.save() return self.entries.index(entry) + 1 if entry in self.entries else None ``` 5. **Pedir el nombre.** Cuando una ronda termina con una puntuación que entra, la escena pasa a `"name"`. `ui.input` mantiene el texto en tu estado con `on_input`, y `on_submit` se ejecuta con Enter, así que basta con escribir un nombre y pulsar Enter: ```python ui.input(self.name, self.set_name, placeholder="Your name", icon="user", on_submit=self.submit_name, id="name", max_length=16) ``` La tabla es una columna de filas; la fila recién añadida tiene un fondo teñido, y las tres primeras tienen números de oro, plata y bronce. 6. **Ganchos de sonido.** Decide *dónde* van los sonidos antes de decidir *cómo* suenan. Target Rush llama a `self.sound("hit")`, `"miss"`, `"combo"`, `"start"`, `"end"` y `"record"` en el momento justo, y solo este método sabe qué hacer con ellos: ```python def sound(self, name): """Called wherever a sound belongs: "start", "hit", "miss", "combo", "end", "record".""" self.last_sound, self.sound_age = name, 0.0 if self.sounds is None and self.sound_backend: self.sounds = Sounds(self.sound_backend) # the first sound starts the engine if self.sounds: self.sounds.play(name) ``` Además muestra el nombre en el HUD un momento, algo útil para comprobar el ritmo de tus sonidos. 7. **Crea los sonidos.** `Sounds` monta un pequeño patch con [`pyzed.audio`](https://albakore.github.io/pyzed/es/audio/): cuatro voces `drum` que entran en un `mixer` y van a los altavoces. Cada efecto es una voz con su propia afinación, en una tabla al principio del archivo, así que no hay archivos de sonido que distribuir: ```python SOUNDS = { # effect: (drum voice, tune in semitones, decay in seconds) "hit": ("tom", 7, 0.12), "miss": ("kick", -6, 0.25), ... } def play(self, name): kind, tune, decay = SOUNDS[name] voice = self.voices[kind] voice.set(tune=tune, decay=decay) voice.trigger() # returns at once: the sound plays on the audio thread ``` #### Efectos de sonido `pyzed.audio` calcula el sonido en Rust, en su propio hilo de tiempo real, así que `trigger()` nunca frena el bucle de fotogramas, y sin tarjeta de sonido el motor suena en silencio en vez de fallar. Algunas formas de ir más lejos: - **Sonidos grabados**: un módulo `sampler` reproduce un archivo WAV. Llama a `node.load("coin.wav")` una vez al empezar y a `node.trigger()` en `sound()`. - **Ajustes de volumen**: el módulo `audio_out` tiene un mando `volume`, y el mezclador un `level_1`…`level_4` por canal. Cámbialos desde los sliders de [Orb Rush](https://albakore.github.io/pyzed/es/games/menus-hud/): `self.sounds.out["volume"] = valor`. - **Música**: un `clock` y un `sequencer` tocan un bucle solos; mira el [motor de audio](https://albakore.github.io/pyzed/es/audio/engine/getting-started/). - **Pruebas**: `sound_backend="offline"` nunca abre un dispositivo; las pruebas llaman a `engine.render(segundos)` para comprobar que cada efecto suena, y `None` silencia el juego. **Tip:** Que los sonidos sean cortos y un poco distintos cada vez (varía `tune` un semitono o dos), para que un sonido que se oye cien veces en una partida no canse. Guarda los ajustes igual que los récords: un `settings.json` junto a `scores.json`, escrito con el mismo `save()` al cerrar la pantalla de ajustes, y cargado al empezar con valores por defecto para las claves que falten. #### Comparte tu juego Un juego está terminado cuando otra persona puede jugarlo. Pyzed empaqueta una app como un programa que se abre con doble clic en Windows, macOS y Linux, con su icono y su nombre: - [Empaqueta tu app](https://albakore.github.io/pyzed/es/tutorials/advanced/packaging/): Un tutorial paso a paso: compilar, icono y compilaciones para los tres sistemas desde CI. - [Distribuir](https://albakore.github.io/pyzed/es/packaging/): Todo sobre compilar, firmar y actualizar apps en cada sistema. **Tip:** Una vez empaquetado, la carpeta del juego puede ser de solo lectura: un motivo más para guardar las partidas en `data_dir`. #### Prueba esto - Guarda los ajustes de [Orb Rush](https://albakore.github.io/pyzed/es/games/menus-hud/) con el mismo patrón. - Lleva una tabla de récords por dificultad: una clave por dificultad en el JSON. - Añade un botón "Borrar récords" detrás de un diálogo `ui.confirm`. #### Código completo `examples/games/high_scores.py`: ```py """Target Rush: a 20-second clicking game that keeps a top-10 table on disk. python examples/games/high_scores.py Click the targets before they shrink away: the sooner, the more points, and hits in a row build a combo. A good score asks for your name and goes into the leaderboard, which is saved as JSON in your user data folder and is still there next time. Shows: - **Saving data** the safe way: a JSON file in the right folder for each system, written to a temporary file first and then swapped in, so a crash never leaves half a file. A missing or damaged file just means an empty table. - **Name entry** with ``ui.input`` (Enter submits) and a leaderboard made of rows. - **Sound effects** from one ``sound(name)`` method called wherever a sound belongs. The sounds are synthesized live by ``pyzed.audio`` (drum voices, no files); with no sound card the game simply stays silent. Tutorial: https://albakore.github.io/pyzed/games/high-scores/ """ import json import math import os import random import sys import tempfile import time from pathlib import Path import pyzed as pz from pyzed import audio, draw, game, ui W, H = 640, 420 ROUND = 20.0 TARGET_LIFE = 1.4 # seconds a target lives (it shrinks meanwhile) TARGET_R = 34 KEEP = 10 # entries in the leaderboard SOUNDS = { # effect: (drum voice, tune in semitones, decay in seconds) "start": ("tom", 0, 0.3), "hit": ("tom", 7, 0.12), "combo": ("clap", 0, 0.25), "miss": ("kick", -6, 0.25), "end": ("tom", -7, 0.6), "record": ("hihat", 5, 0.5), } # -- sound ---------------------------------------------------------------------------------------- class Sounds: """The game's sound effects, synthesized live by ``pyzed.audio``: there are no files to load. Four drum voices go through a mixer to the speakers, and each effect strikes one voice with its own tuning. Without a sound card the engine plays silently, so the game runs anywhere. """ def __init__(self, backend="auto", volume=0.8): self.engine = audio.Engine(backend) mixer = self.engine.add("mixer") self.out = self.engine.add("audio_out", volume=volume) self.engine.connect(mixer, "left", self.out, "left") self.engine.connect(mixer, "right", self.out, "right") self.voices = {} for i, kind in enumerate(("tom", "clap", "kick", "hihat"), start=1): self.voices[kind] = self.engine.add("drum", sound=kind) self.engine.connect(self.voices[kind], "out", mixer, f"in_{i}") if backend != "offline": # "offline" never plays: tests render() it instead self.engine.start() def play(self, name): kind, tune, decay = SOUNDS[name] voice = self.voices[kind] voice.set(tune=tune, decay=decay) voice.trigger() # returns at once: the sound plays on the audio thread # -- saving -------------------------------------------------------------------------------------- def data_dir(app_name): """The folder where a game keeps its saves, following each system's convention.""" if sys.platform == "win32": base = Path(os.environ.get("APPDATA") or Path.home() / "AppData" / "Roaming") elif sys.platform == "darwin": base = Path.home() / "Library" / "Application Support" else: base = Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share") return base / app_name class HighScores: """The best ``KEEP`` scores, as a list of ``{"name", "score", "date"}`` saved as JSON.""" def __init__(self, path): self.path = Path(path) self.entries = self.load() def load(self): try: data = json.loads(self.path.read_text(encoding="utf-8")) entries = [e for e in data.get("scores", []) if isinstance(e.get("score"), int)] except (OSError, ValueError, AttributeError): return [] # no file yet, or a damaged one: start empty return sorted(entries, key=lambda e: -e["score"])[:KEEP] def qualifies(self, score): return score > 0 and (len(self.entries) < KEEP or score > self.entries[-1]["score"]) def add(self, name, score): """Adds a score, saves, and returns its place (1 is the best).""" entry = {"name": name.strip()[:16] or "Player", "score": score, "date": time.strftime("%Y-%m-%d")} self.entries.append(entry) self.entries.sort(key=lambda e: -e["score"]) # a stable sort: older equal scores stay first del self.entries[KEEP:] self.save() return self.entries.index(entry) + 1 if entry in self.entries else None def save(self): self.path.parent.mkdir(parents=True, exist_ok=True) # Write next to the real file, then replace it in one step (atomic on every system). fd, tmp = tempfile.mkstemp(dir=self.path.parent, prefix=".scores-", suffix=".json") with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump({"version": 1, "scores": self.entries}, f, indent=2) os.replace(tmp, self.path) @property def best(self): return self.entries[0]["score"] if self.entries else 0 # -- the game ------------------------------------------------------------------------------------ class TargetRush(pz.App): title = "Target Rush" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None, path=None, sound_backend="auto"): self.random = random.Random(seed) self.scores = HighScores(path or data_dir("Target Rush") / "scores.json") self.sparks = game.Particles(gravity=500, seed=seed) self.scene = "ready" # ready -> play -> name (if it made the table) -> board self.name = "" self.rank = None self.last_sound, self.sound_age = None, 9.0 self.sound_backend = sound_backend # "auto", "offline" (tests) or None (muted) self.sounds = None # made on the first sound, so a menu costs nothing self.reset() def reset(self): self.targets = [] # [x, y, age] self.score, self.combo, self.hits, self.misses = 0, 0, 0, 0 self.time_left = ROUND self.spawn = 0.0 self.clock = 0.0 def start(self): self.reset() self.scene = "play" self.sound("start") # -- sound hooks -------------------------------------------------------------------------- def sound(self, name): """Called wherever a sound belongs: "start", "hit", "miss", "combo", "end", "record". Plays the effect and shows its name in the HUD for a moment. """ self.last_sound, self.sound_age = name, 0.0 if self.sounds is None and self.sound_backend: self.sounds = Sounds(self.sound_backend) if self.sounds: self.sounds.play(name) # -- update ------------------------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] if self.scene == "play" or len(self.sparks) or self.sound_age < 1 else [] def tick(self, dt): dt = min(dt, 1 / 30) self.sparks.update(dt) self.sound_age += dt if self.scene != "play": return self.clock += dt self.time_left = max(0.0, self.time_left - dt) self.spawn -= dt if self.spawn <= 0: self.spawn = max(0.35, 0.8 - self.clock * 0.02) self.targets.append([self.random.uniform(50, W - 50), self.random.uniform(50, H - 50), 0.0]) for t in self.targets: t[2] += dt expired = [t for t in self.targets if t[2] >= TARGET_LIFE] if expired: self.combo = 0 self.misses += len(expired) self.targets = [t for t in self.targets if t[2] < TARGET_LIFE] if self.time_left <= 0: self.finish() def finish(self): self.targets = [] if self.scores.qualifies(self.score): self.scene = "name" self.sound("record") else: self.scene = "board" self.rank = None self.sound("end") # -- input -------------------------------------------------------------------------------- def click(self, x, y): if self.scene != "play": return for t in reversed(self.targets): # the newest (on top) first r = self.radius(t) if math.hypot(x - t[0], y - t[1]) <= r: self.targets.remove(t) self.combo += 1 self.hits += 1 freshness = 1 - t[2] / TARGET_LIFE # earlier clicks are worth more self.score += int(10 + 40 * freshness) * min(self.combo, 5) self.sparks.burst(t[0], t[1], count=18, color=["#f472b6", "#fbbf24", "#ffffff"], speed=(60, 260)) self.sound("combo" if self.combo in (5, 10, 15) else "hit") return self.combo = 0 self.misses += 1 self.sound("miss") def set_name(self, value): self.name = value def submit_name(self, *_): self.rank = self.scores.add(self.name, self.score) self.scene = "board" def on_key(self, combo): if combo in ("enter", "space") and self.scene in ("ready", "board"): self.start() # -- draw --------------------------------------------------------------------------------- @staticmethod def radius(t): return TARGET_R * (1 - t[2] / TARGET_LIFE * 0.75) def board(self): shapes = [draw.background("#12101c")] for i in range(0, W, 32): shapes.append(draw.line(i, 0, i, H, stroke="#ffffff06")) for t in self.targets: r = self.radius(t) appear = min(1.0, t[2] / 0.12) # a quick grow-in r *= appear for k, color in enumerate(("#f472b6", "#ffffff", "#f472b6")): shapes.append(draw.circle(t[0], t[1], r * (1 - k * 0.3), fill=color)) # the remaining time as an arc around the target left = 1 - t[2] / TARGET_LIFE shapes.append(draw.arc(t[0], t[1], TARGET_R + 6, -90, -90 + 360 * left, stroke="#fbbf24", width=3, cap="round")) shapes += self.sparks.shapes(glow=True) return pz.canvas(shapes, width=W, height=H, on_press=self.click, cursor="crosshair") def leaderboard(self): rows = [] for i, e in enumerate(self.scores.entries): mine = self.rank == i + 1 medal = {0: "#fbbf24", 1: "#cbd5e1", 2: "#d97706"}.get(i) rows.append(pz.container(pz.row([ pz.container(pz.text(f"{i + 1}", weight="bold", color=medal) if medal else ui.muted(f"{i + 1}"), width=28), pz.text(e["name"], weight="semibold" if mine else None), pz.hspace(), ui.muted(e["date"], size="sm"), pz.container(pz.text(f"{e['score']:,}", font="mono"), width=70, align_x="end"), ], spacing=10, align="center"), padding=(4, 10)).style(background="#fbbf2430" if mine else None, radius=6)) if not rows: rows = [ui.muted("No scores yet. Be the first!")] return pz.column(rows, spacing=2, width=340) def card(self, *children): card = pz.container(pz.column(list(children), spacing=10, align="center"), padding=(20, 28)).style( glass=True, radius=14).animate("fade_in_up", duration=260) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def overlay(self): if self.scene == "ready": return self.card(ui.heading("Target Rush", level=2), ui.muted("Click the targets before they vanish."), self.leaderboard(), ui.button("Start", self.start, variant="solid", icon="play")) if self.scene == "name": return self.card( ui.heading("New high score!", level=2), pz.text(f"{self.score:,}", size=28, weight="bold"), pz.container(ui.input(self.name, self.set_name, placeholder="Your name", icon="user", on_submit=self.submit_name, id="name", max_length=16), width=240), ui.button("Save", self.submit_name, variant="solid", icon="save")) if self.scene == "board": place = f"You placed #{self.rank}!" if self.rank else f"Score {self.score:,}" return self.card(ui.heading("Leaderboard", level=2), ui.muted(place), self.leaderboard(), ui.button("Play again", self.start, variant="solid", icon="rotate-ccw")) return None def view(self): t = self.resolved_theme() sound = (ui.badge(f"♪ {self.last_sound}", icon="volume-2", tint="hint") if self.sound_age < 0.8 and self.last_sound else None) hud = pz.row([ ui.label("Target Rush", weight="semibold"), pz.hspace(), sound, ui.badge(f"x{self.combo}", icon="zap", tint="warning") if self.combo > 1 else None, ui.badge(f"{self.score:,}", icon="target", tint="accent"), ui.badge(f"{math.ceil(self.time_left)} s", icon="timer", tint="error" if self.time_left < 5 else "text_muted"), ui.badge(f"Best {self.scores.best:,}", icon="trophy", tint="text_muted"), ], spacing=8, align="center") play = pz.stack([self.board(), self.overlay()], width=W, height=H) return pz.container(pz.column([hud, pz.container(play, radius=10, clip=True), ui.muted(f"Scores are saved in {self.scores.path}", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(TargetRush) ``` #### Siguiente - [Consejos para crear juegos](https://albakore.github.io/pyzed/es/games/tips/): Rendimiento, pasos de tiempo fijos, buffer de entrada, juice, organizar el estado y probar sin ventana. ### Consejos para crear juegos > Consejos prácticos para hacer juegos con Pyzed en Python: delta time y pasos de tiempo fijos, rendimiento, buffer de entrada, juice y temblor de pantalla, organizar el estado del juego, vistas de depuración y probar juegos sin ventana. Source: https://albakore.github.io/pyzed/es/games/tips/ Consejos breves y prácticos sacados de los juegos de esta sección. Cada uno explica el *porqué*, con las pocas líneas de código que lo hacen en Pyzed. #### Tiempo ##### Multiplica siempre por dt `pz.frames(self.tick)` llama a `tick(dt)` una vez por fotograma mostrado, y `dt` son los segundos desde el anterior. Las velocidades son *por segundo* y cada cambio es `velocidad × dt`, así el juego va igual en un portátil de 60 Hz que en un monitor de 144 Hz. Los temporizadores también bajan con `dt`: ```python self.x += self.vx * dt self.cooldown = max(0.0, self.cooldown - dt) ``` ##### Limita dt Tras una pausa (arrastrar la ventana, un punto de interrupción, un fotograma lento) `dt` puede ser grande, y un paso grande deja que las cosas atraviesen paredes. Pyzed ya lo limita a 0,25 s; los juegos con mucha física lo limitan más: ```python def tick(self, dt): dt = min(dt, 1 / 30) ``` ##### Un paso de tiempo fijo, cuando lo necesites Algunos juegos necesitan que todos los pasos tengan el mismo tamaño: física que no debe depender de los fotogramas por segundo, repeticiones, juego en red, o una simulación que debe dar exactamente el mismo resultado dos veces. Acumula tiempo y avanza en trozos fijos: ```python STEP = 1 / 120 def tick(self, dt): self.acc += dt while self.acc >= STEP: self.simulate(STEP) # the game's logic, always with the same dt self.acc -= STEP self.blend = self.acc / STEP # 0..1: how far between two steps, to interpolate drawing ``` **Caution:** No llames `update` a ese método: en una `pz.App`, `update(message)` es donde llegan los mensajes de los widgets. Para juegos por turnos o en cuadrícula, usa `pz.every(segundos, callback)` en lugar de un bucle por fotograma, como hace [Snake](https://albakore.github.io/pyzed/es/games/snake/). ##### Para el bucle cuando nada se mueve `subscriptions()` se evalúa tras cada actualización. Devuelve `[]` en pausa, en un menú sin nada animado o entre movimientos de un puzle, y el juego no usa CPU: ```python def subscriptions(self): return [pz.frames(self.tick)] if self.animating else [] ``` #### Rendimiento - **Que `tick` sea pequeño.** Números, listas y clases pequeñas son rápidos; evita construir estructuras grandes en cada fotograma, y no leas archivos ni la red dentro del bucle. - **Agrupa formas.** `draw.rects([...], fill=color)` dibuja muchos rectángulos de un color como una sola forma; así se mantienen baratos `game.sprite` y los tiles del juego de plataformas. - **Dibuja solo lo que está en pantalla**, y colisiona solo con lo que está cerca (mira el [juego de plataformas](https://albakore.github.io/pyzed/es/games/platformer/)). - **Limita las partículas** con `game.Particles(limit=...)`, y quita las balas y los enemigos que salen de la pantalla. - **Las imágenes se guardan en caché.** `draw.image("ship.png", ...)` decodifica un archivo una vez, así que dibujarlo cientos de veces por fotograma está bien. - **El HUD es barato**, pero `pz.memo` puede guardar un widget caro de construir que cambia poco. - **Mide.** Muestra un contador de FPS (mira [Orb Rush](https://albakore.github.io/pyzed/es/games/menus-hud/)) y lee la guía de [Rendimiento](https://albakore.github.io/pyzed/es/guides/performance/). #### Entrada - **Mantener vs pulsar.** Usa `game.axis`, `game.vector` y `pz.is_pressed` en `tick` para lo que pasa *mientras* una tecla está pulsada (moverse, disparo automático), y `on_key` para lo que pasa *una vez* por pulsación (saltar, pausar). - **Guarda las pulsaciones un momento.** Recuerda una pulsación un rato corto para que no se pierda: un salto pulsado justo antes de aterrizar, o dos giros rápidos en Snake. Basta un temporizador pequeño o una cola: ```python def on_key(self, combo): if combo == "space": self.jump_buffer = 0.12 # seconds; tick counts it down and jumps when possible ``` - **Sé indulgente.** El coyote time, cajas de colisión del jugador un poco más pequeñas que su sprite y un poco más grandes para los objetos hacen que un juego parezca justo. - **Deja elegir.** Acepta flechas y WASD (`game.vector` lo hace por defecto), y el ratón donde tenga sentido. #### Juice El "juice" es la respuesta que hace que una acción se sienta bien: el mismo golpe, con y sin ella: | Efecto | Cómo | | --- | --- | | Temblor de pantalla | `shake = game.Shake()`, `shake.kick(8)` en un golpe, `shake.update(dt)` en cada fotograma, y empieza el canvas con `*shake.shapes()` | | Partículas | `game.Particles(...)`, `burst(x, y, ...)` en los eventos, `update(dt)` y `shapes(glow=True)` | | Pausa de impacto (hit-stop) | no actualices el mundo durante 50–100 ms tras un golpe fuerte (deja moverse a las partículas) | | Destello | dibuja en blanco lo golpeado durante uno o dos fotogramas | | Squash and stretch | `draw.scale(sx, sy)` alrededor de los pies al aterrizar, o en la dirección del movimiento | | Easing | tweens con curvas `ease_out` en vez de movimientos lineales (mira [Sprites y animación](https://albakore.github.io/pyzed/es/games/sprites-animation/)) | | Brillo | `game.glow(x, y, r, color)` en el canvas; `bloom` y `glow` en los widgets | **Tip:** Añade un ajuste para quitar el temblor y los destellos. Hay personas que lo necesitan, y `pz.system_reduced_motion()` dice si el sistema ya pide menos movimiento. #### Organizar un juego - **El estado en un sitio.** Los atributos de la app son el estado del juego; `reset()` o `new_game()` los fija todos, así que volver a empezar es una llamada. - **Clases pequeñas para las cosas.** `Enemy`, `Rock`, `Tile`: unos pocos atributos cada una. Las listas de ellas son el mundo. - **Reglas como funciones.** El código que decide *qué pasa* (`slide_line`, `can_move`, las colisiones) no debería dibujar; el código que dibuja no debería cambiar el estado. Así los dos son fáciles de cambiar y de probar. - **Escenas.** Un atributo `scene` (`"title"`, `"play"`, `"pause"`...) decide qué ejecuta `tick` y qué muestra `view` (mira [Menús, HUD y ajustes](https://albakore.github.io/pyzed/es/games/menus-hud/)). - **Constantes arriba.** Velocidades, tamaños y tiempos como constantes con nombre son los mandos para afinar el juego. - **Azar con semilla.** `self.random = random.Random(seed)` hace un juego reproducible para pruebas e informes de errores; con `seed=None` es aleatorio como siempre. #### Depurar - **Dibuja lo que ve el código**: cajas de colisión, velocidades, caminos, el tile bajo el ratón. Actívalo con una tecla, como hace [Jump Lab](https://albakore.github.io/pyzed/es/games/collisions-physics/) con H. - **Escribe los números en el canvas** con `draw.text(..., font="mono")` en vez de en la consola, para verlos en su contexto. - **Cámara lenta**: multiplica `dt` por 0,25 mientras mantienes una tecla para ver un momento rápido. #### Probar juegos sin ventana Un juego de Pyzed es un objeto de Python normal: puedes crearlo, llamar a `tick` en un bucle y comprobar el resultado, sin ventana. Los juegos de esta sección dejan que las pruebas fijen las teclas que leen con un atributo `held`: ```python from coin_chase import CoinChase, SPEED def test_moves_right(): app = CoinChase(seed=1) app.held = {"right"} # instead of the real keyboard x = app.x for _ in range(30): app.tick(1 / 60) # half a second, frame by frame assert app.x == pytest.approx(x + SPEED * 0.5) def test_draws(tmp_path): app = CoinChase(seed=1) app.screenshot(tmp_path / "frame.png", scale=1) # renders off-screen, no window needed ``` `game.axis`, `game.vector` y `game.is_pressed` aceptan `held=` para esto. `App.screenshot` (o `pz.screenshot(app, path)`) dibuja cualquier momento del juego en un PNG, que es también como se grabaron las animaciones de estas páginas. Las pruebas de estos juegos están en [`tests/test_game_tutorials.py`](https://github.com/albakore/pyzed/blob/main/tests/test_game_tutorials.py). **Lee también:** [Pruebas](https://albakore.github.io/pyzed/es/guides/testing/) para probar apps de Pyzed en general, y [Dibujar con canvas](https://albakore.github.io/pyzed/es/guides/canvas/) para todas las formas y transformaciones. - [Volver a la introducción de juegos](https://albakore.github.io/pyzed/es/games/): La ruta de aprendizaje y los juegos completos. ### Tutorial: Snake > Crea el clásico Snake con un temporizador fijo, una cola de teclas, un tablero en canvas y una tarjeta de cristal para el game over. Source: https://albakore.github.io/pyzed/es/games/snake/ Snake se mueve en una cuadrícula, celda a celda, así que no necesita un bucle por fotograma: basta un temporizador que salte unas cuantas veces por segundo. Aprenderás a guardar el estado del juego en la app, avanzarlo con `pz.every`, girar con `on_key`, dibujar el tablero en un canvas y poner un menú encima con `pz.stack`. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | El Snake clásico: una cuadrícula, un temporizador, una cola de teclas y una tarjeta de game over | 30 minutos | Intermedio | El tutorial de la [Lista de tareas](https://albakore.github.io/pyzed/es/tutorials/basic/todo-app/), o algunas apps de Pyzed propias | Juégalo primero: 1. **El estado.** La serpiente es una lista de celdas, empezando por la cabeza. La dirección es un par `(dx, dy)` y `turns` guarda las pulsaciones hasta el siguiente paso (si no, dos pulsaciones rápidas entre pasos perderían la primera): ```python CELL = 22 COLS, ROWS = 24, 18 class Snake(pz.App): title = "Snake" size = (COLS * CELL + 32, ROWS * CELL + 96) def __init__(self, seed=None): self.random = random.Random(seed) # una semilla hace las partidas reproducibles en tests self.best = 0 self.reset() def reset(self): middle = (COLS // 2, ROWS // 2) self.body = [middle, (middle[0] - 1, middle[1]), (middle[0] - 2, middle[1])] self.direction = (1, 0) self.turns = [] self.food = self.free_cell() self.score = 0 self.paused = self.over = False ``` 2. **Un paso.** Mueve la cabeza; si cae en la comida la serpiente crece (la cola se queda), si no, la cola avanza también. Las paredes dan la vuelta con `%` y morderse a sí misma termina la partida: ```python def step(self): if self.turns: self.direction = self.turns.pop(0) dx, dy = self.direction x, y = self.body[0] head = ((x + dx) % COLS, (y + dy) % ROWS) if head in self.body[:-1]: self.over = True self.best = max(self.best, self.score) return self.body.insert(0, head) if head == self.food: self.score += 1 self.food = self.free_cell() else: self.body.pop() ``` 3. **El temporizador.** `subscriptions()` se vuelve a leer tras cada actualización, así que el juego decide cuándo corre el reloj. Poner la velocidad en la `key` reinicia el temporizador cuando la serpiente acelera, y no devolver nada lo pausa: ```python @property def speed(self): return min(8 + self.score // 3, 20) # pasos por segundo def subscriptions(self): if self.over or self.paused: return [] return [pz.every(1 / self.speed, self.step, key=f"snake-{self.speed}")] ``` 4. **Girar.** `on_key` recibe cada pulsación como texto. Se ignora dar media vuelta (o pulsar dos veces la misma dirección): ```python DIRECTIONS = {"up": (0, -1), "w": (0, -1), "down": (0, 1), "s": (0, 1), "left": (-1, 0), "a": (-1, 0), "right": (1, 0), "d": (1, 0)} def on_key(self, combo): if combo in DIRECTIONS: last = self.turns[-1] if self.turns else self.direction new = DIRECTIONS[combo] if new != last and (new[0] + last[0], new[1] + last[1]) != (0, 0): self.turns.append(new) elif combo == "space" and not self.over: self.paused = not self.paused elif combo == "enter" and self.over: self.reset() ``` 5. **El tablero.** Un canvas es una lista de formas de `pyzed.draw`. El damero es una sola forma `draw.rects` (rápida incluso con cientos de celdas), la comida tiene un halo suave hecho con un círculo translúcido y el cuerpo son rectángulos redondeados: ```python def board(self, t): shapes = [draw.background(t.editor)] shapes.append(draw.rects(((x * CELL, y * CELL, CELL, CELL) for x in range(COLS) for y in range(ROWS) if (x + y) % 2), fill=t.surface)) fx, fy = self.food shapes.append(draw.circle(fx * CELL + CELL / 2, fy * CELL + CELL / 2, CELL / 2 - 3, fill="#d07277")) for i, (x, y) in enumerate(self.body): shapes.append(draw.rect(x * CELL + 2, y * CELL + 2, CELL - 4, CELL - 4, radius=6, fill=t.accent if i == 0 else "#a1c181")) return pz.canvas(shapes, width=COLS * CELL, height=ROWS * CELL) ``` 6. **HUD y game over.** La puntuación es una fila de `ui.badge`. La tarjeta de game over es un contenedor normal con `glass=True` (un panel tintado que desenfoca lo de detrás), apilado sobre el tablero: ```python def overlay(self, t): if not self.over: return None card = pz.container(pz.column([ ui.heading("Game over", level=2), ui.muted(f"Score {self.score} · Best {self.best}"), ui.button("Play again", self.reset, variant="solid", icon="rotate-ccw"), ], spacing=10, align="center"), padding=24).style(glass=True, radius=14) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def view(self): game = pz.stack([self.board(t), self.overlay(t)], width=COLS * CELL, height=ROWS * CELL) ``` (Image: Game over, en una tarjeta de cristal) **Tip:** Ideas para seguir: paredes que matan en lugar de dar la vuelta, manzanas doradas que valen tres puntos y desaparecen a los pocos segundos (un segundo `pz.every`), o un modo de dos jugadores con WASD y las flechas. #### Código completo `examples/games/snake.py`: ```py """Snake: the classic, in about 150 lines of Pyzed. python examples/games/snake.py Arrows or WASD steer, Space pauses, Enter starts again. Shows a fixed-rate game loop with ``pz.every``, steering from ``on_key`` (with a queue so quick turns are not lost), a ``pz.canvas`` board and a HUD made of ordinary widgets stacked on top of it. Tutorial: https://albakore.github.io/pyzed/games/snake/ """ import random import pyzed as pz from pyzed import draw, ui CELL = 22 COLS, ROWS = 24, 18 DIRECTIONS = { "up": (0, -1), "w": (0, -1), "down": (0, 1), "s": (0, 1), "left": (-1, 0), "a": (-1, 0), "right": (1, 0), "d": (1, 0), } class Snake(pz.App): title = "Snake" size = (COLS * CELL + 32, ROWS * CELL + 96) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) self.best = 0 self.reset() # -- game state ----------------------------------------------------------- def reset(self): middle = (COLS // 2, ROWS // 2) self.body = [middle, (middle[0] - 1, middle[1]), (middle[0] - 2, middle[1])] self.direction = (1, 0) self.turns = [] # key presses waiting for the next step self.food = self.free_cell() self.score = 0 self.paused = False self.over = False def free_cell(self): while True: cell = (self.random.randrange(COLS), self.random.randrange(ROWS)) if cell not in self.body: return cell @property def speed(self): """Steps per second: starts at 8 and speeds up as the snake grows.""" return min(8 + self.score // 3, 20) def step(self): if self.turns: self.direction = self.turns.pop(0) dx, dy = self.direction x, y = self.body[0] head = ((x + dx) % COLS, (y + dy) % ROWS) # walls wrap around if head in self.body[:-1]: self.over = True self.best = max(self.best, self.score) return self.body.insert(0, head) if head == self.food: self.score += 1 self.food = self.free_cell() else: self.body.pop() # -- input and timing ----------------------------------------------------- def subscriptions(self): if self.over or self.paused: return [] # The key includes the speed so the timer restarts when it changes. return [pz.every(1 / self.speed, self.step, key=f"snake-{self.speed}")] def on_key(self, combo): if combo in DIRECTIONS: last = self.turns[-1] if self.turns else self.direction new = DIRECTIONS[combo] # Ignore turning back on itself and repeated presses. if new != last and (new[0] + last[0], new[1] + last[1]) != (0, 0) and len(self.turns) < 3: self.turns.append(new) elif combo == "space" and not self.over: self.paused = not self.paused elif combo == "enter" and self.over: self.reset() # -- drawing -------------------------------------------------------------- def board(self, t): shapes = [draw.background(t.editor)] # A subtle checkerboard makes the grid readable without lines. shapes.append(draw.rects(((x * CELL, y * CELL, CELL, CELL) for x in range(COLS) for y in range(ROWS) if (x + y) % 2), fill=t.surface)) fx, fy = self.food shapes.append(draw.circle(fx * CELL + CELL / 2, fy * CELL + CELL / 2, CELL * 0.62, fill=(0.82, 0.45, 0.47, 0.18))) shapes.append(draw.circle(fx * CELL + CELL / 2, fy * CELL + CELL / 2, CELL / 2 - 3, fill="#d07277")) for i, (x, y) in enumerate(self.body): inset = 2 if i else 1 shapes.append(draw.rect(x * CELL + inset, y * CELL + inset, CELL - 2 * inset, CELL - 2 * inset, radius=6 if i else 7, fill=t.accent if i == 0 else "#a1c181")) # Eyes looking where the snake goes. hx, hy = self.body[0] dx, dy = self.direction cx, cy = hx * CELL + CELL / 2 + dx * 4, hy * CELL + CELL / 2 + dy * 4 for side in (-1, 1): shapes.append(draw.circle(cx + dy * 5 * side, cy + dx * 5 * side, 2.2, fill=t.editor)) return pz.canvas(shapes, width=COLS * CELL, height=ROWS * CELL) def overlay(self, t): if self.over: title, hint = "Game over", "Press Enter to play again" elif self.paused: title, hint = "Paused", "Press Space to continue" else: return None card = pz.container(pz.column([ ui.heading(title, level=2), ui.muted(f"Score {self.score} · Best {self.best}"), ui.button(hint.split(" to ")[1].capitalize(), self.reset if self.over else (lambda: setattr(self, "paused", False)), variant="solid", icon="rotate-ccw" if self.over else "play"), ui.muted(hint, size="sm"), ], spacing=10, align="center"), padding=24).style(glass=True, radius=14, border=(1, "border")) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def view(self): t = self.resolved_theme() hud = pz.row([ pz.icon("worm", size=18, color=t.accent), ui.label("Snake", weight="semibold"), pz.hspace(), ui.badge(f"Score {self.score}", icon="apple"), ui.badge(f"Best {self.best}", tint="warning", icon="trophy"), ui.badge(f"{self.speed}/s", tint="text_muted", icon="gauge"), ], spacing=8, align="center") game = pz.stack([self.board(t), self.overlay(t)], width=COLS * CELL, height=ROWS * CELL) return pz.container( pz.column([hud, pz.container(game, radius=10, clip=True), ui.muted( "Arrows / WASD steer · Space pauses · Enter restarts", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background, ) if __name__ == "__main__": pz.run(Snake) ``` ### Tutorial: Breakout > Un Breakout a 60 fps con pz.frames, teclas mantenidas y ratón, física de la pelota, partículas, combos y temblor de pantalla. Source: https://albakore.github.io/pyzed/es/games/breakout/ Breakout necesita movimiento fluido, así que en lugar de un temporizador usa `pz.frames`: una función que se ejecuta una vez por fotograma con el tiempo transcurrido desde el anterior. Aprenderás el bucle por fotograma, la entrada continua con `pz.pressed()`, las colisiones con `pyzed.game` y los detalles que hacen que un juego se sienta bien: partículas, combos y temblor de pantalla. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Breakout a 60 fps: paleta, física de la pelota, ladrillos, partículas y temblor | 45 minutos | Intermedio · bucle de juego | [Snake](https://albakore.github.io/pyzed/es/games/snake/), o [Un bucle de juego](https://albakore.github.io/pyzed/es/tutorials/advanced/game-loop/) | Juégalo primero: 1. **Ladrillos como cajas.** `game.Rect` es una caja con ayudas de colisión. Cada ladrillo guarda su rectángulo, color, puntos y cuántos golpes aguanta: ```python from pyzed import draw, game, ui def build_level(self): self.bricks = [] for row in range(BRICK_ROWS): for col in range(BRICK_COLS): rect = game.Rect(left + col * (BRICK_W + BRICK_GAP), BRICK_TOP + row * (BRICK_H + BRICK_GAP), BRICK_W, BRICK_H) hits = 2 if self.level > 1 and row < self.level - 1 else 1 self.bricks.append({"rect": rect, "color": ROW_COLORS[row], "hits": hits, "points": (BRICK_ROWS - row) * 10}) ``` 2. **El bucle por fotograma.** Devuelve `pz.frames(self.tick)` y Pyzed llama a `tick(dt)` antes de cada fotograma. Todo se mueve *velocidad × dt*, así que el juego va igual de rápido en un portátil a 60 Hz que en un monitor de 144 Hz: ```python def subscriptions(self): return [pz.frames(self.tick)] def tick(self, dt): self.sparks.update(dt) self.shake.update(dt) if self.paused or self.over: return move = game.axis(("left", "a"), ("right", "d")) # -1, 0 o 1 según las teclas pulsadas self.paddle_x = game.clamp(self.paddle_x + move * 520 * dt, PADDLE_W / 2, W - PADDLE_W / 2) ... ``` `game.axis` lee `pz.pressed()`, el conjunto de teclas pulsadas ahora mismo. Eso hace que el movimiento sea continuo: `on_key` solo saltaría al pulsar (y con la repetición del teclado). 3. **También con ratón.** El canvas informa del puntero en sus propias coordenadas, así que la pala puede seguirlo y un clic puede lanzar la pelota: ```python pz.canvas(shapes, width=W, height=H, on_move=self.mouse, on_press=lambda x, y: self.launch(), cursor="crosshair") def mouse(self, x, y): self.paddle_x = game.clamp(x, PADDLE_W / 2, W - PADDLE_W / 2) ``` 4. **Mover la pelota sin atravesar ladrillos.** Una pelota rápida podría saltarse un ladrillo fino en un fotograma. Dividir el fotograma en subpasos no más largos que el radio lo evita: ```python steps = max(1, math.ceil(self.speed * dt / BALL_R)) for _ in range(steps): if self.advance(dt / steps): # True si se perdió la pelota break ``` 5. **Rebotar.** `game.circle_rect` dice si un círculo toca una caja y por qué lado, como una dirección `(nx, ny)`. Rebotar es entonces una línea por eje. En la pala, el punto de contacto elige el ángulo, que es lo que permite apuntar: ```python for brick in self.bricks: normal = game.circle_rect(self.ball_x, self.ball_y, BALL_R, brick["rect"]) if normal: nx, ny = normal if nx: self.vx = abs(self.vx) * nx if ny: self.vy = abs(self.vy) * ny ... if self.vy > 0 and game.circle_rect(self.ball_x, self.ball_y, BALL_R, self.paddle): offset = (self.ball_x - self.paddle_x) / (PADDLE_W / 2) # -1 … 1 angle = math.radians(-90 + 60 * game.clamp(offset, -1, 1)) self.vx, self.vy = math.cos(angle) * self.speed, math.sin(angle) * self.speed ``` 6. **Sensaciones: partículas, combos y temblor.** `game.Particles` es un pequeño sistema de partículas: `burst` cuando pasa algo, `update(dt)` en cada fotograma y `shapes()` para dibujar. Un combo multiplica los puntos de los ladrillos rotos sin tocar la pala, y `game.Shake` mueve toda la escena unos píxeles en cada golpe: ```python self.sparks = game.Particles(gravity=420, drag=1.5) self.shake = game.Shake() # cuando se rompe un ladrillo: self.combo += 1 self.score += brick["points"] * self.combo self.sparks.burst(cx, cy, count=22, color=[brick["color"], "#ffffff"]) self.shake.kick(2 + min(self.combo, 6)) # en la escena, el translate va primero para que tiemble todo lo que sigue: shapes = [draw.background("#101216"), *self.shake.shapes()] ... shapes += self.sparks.shapes(glow=True) shapes += game.glow(self.ball_x, self.ball_y, BALL_R, "#ffffff", layers=3) ``` 7. **HUD.** Las vidas son tres iconos `heart`, el combo es una insignia que solo aparece a partir de ×2 y el aviso "Level 1 · Space to launch" es una tarjeta `glass` apilada sobre el canvas: ```python hearts = pz.row([pz.icon("heart", size=15, color="#d07277" if i < self.lives else t.border) for i in range(3)], spacing=3) board = pz.stack([self.scene(t), self.banner()], width=W, height=H) ``` **Tip:** La lógica que solo depende de `dt` es fácil de probar: crea la app, llama a `tick(1 / 60)` unos cientos de veces y comprueba la puntuación, las vidas o los ladrillos que quedan. No hace falta ninguna ventana. #### Código completo `examples/games/breakout.py`: ```py """Breakout: a paddle, a ball and a wall of bricks, running at the display's frame rate. python examples/games/breakout.py Move with the mouse or the arrows/A-D, Space launches the ball, P pauses, Enter starts again. Shows ``pz.frames`` (a callback per frame with the elapsed time), ``pz.pressed`` for keys held down, ball-against-box collisions with ``game.circle_rect``, particles and screen shake from ``pyzed.game``. Tutorial: https://albakore.github.io/pyzed/games/breakout/ """ import math import random import pyzed as pz from pyzed import draw, game, ui W, H = 640, 480 PADDLE_W, PADDLE_H, PADDLE_Y = 96, 12, H - 40 BALL_R = 7 BRICK_COLS, BRICK_ROWS = 10, 6 BRICK_W, BRICK_H, BRICK_GAP, BRICK_TOP = 58, 20, 4, 56 ROW_COLORS = ["#d07277", "#dfa067", "#e5c07b", "#a1c181", "#6eb4bf", "#74ade8"] class Breakout(pz.App): title = "Breakout" size = (W + 32, H + 92) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) self.sparks = game.Particles(gravity=420, drag=1.5, seed=seed) self.shake = game.Shake(seed=seed) self.best = 0 self.new_game() # -- state ---------------------------------------------------------------- def new_game(self): self.level, self.score, self.lives = 1, 0, 3 self.over = self.won = self.paused = False self.build_level() def build_level(self): left = (W - (BRICK_COLS * (BRICK_W + BRICK_GAP) - BRICK_GAP)) / 2 self.bricks = [] for row in range(BRICK_ROWS): for col in range(BRICK_COLS): rect = game.Rect(left + col * (BRICK_W + BRICK_GAP), BRICK_TOP + row * (BRICK_H + BRICK_GAP), BRICK_W, BRICK_H) # Top rows are worth more and, from level 2, take two hits. hits = 2 if self.level > 1 and row < self.level - 1 else 1 self.bricks.append({"rect": rect, "color": ROW_COLORS[row], "hits": hits, "points": (BRICK_ROWS - row) * 10}) self.paddle_x = W / 2 self.reset_ball() def reset_ball(self): self.stuck = True # the ball rides on the paddle until Space self.ball_x, self.ball_y = self.paddle_x, PADDLE_Y - BALL_R - 1 self.vx = self.vy = 0.0 self.speed = 330 + 30 * self.level self.combo = 0 def launch(self): if self.stuck and not (self.over or self.won): angle = math.radians(self.random.uniform(-120, -60)) self.vx, self.vy = math.cos(angle) * self.speed, math.sin(angle) * self.speed self.stuck = False @property def paddle(self): return game.Rect(self.paddle_x - PADDLE_W / 2, PADDLE_Y, PADDLE_W, PADDLE_H) # -- the loop ------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] # particles keep moving even when paused def tick(self, dt): self.sparks.update(dt) self.shake.update(dt) if self.paused or self.over or self.won: return move = game.axis(("left", "a"), ("right", "d")) self.paddle_x = game.clamp(self.paddle_x + move * 520 * dt, PADDLE_W / 2, W - PADDLE_W / 2) if self.stuck: self.ball_x = self.paddle_x return # Small sub-steps so a fast ball can't jump through a brick in one frame. steps = max(1, math.ceil(self.speed * dt / BALL_R)) for _ in range(steps): if self.advance(dt / steps): break def advance(self, dt): """Moves the ball once; returns True when it was lost.""" self.ball_x += self.vx * dt self.ball_y += self.vy * dt if self.ball_x < BALL_R or self.ball_x > W - BALL_R: self.vx = -self.vx self.ball_x = game.clamp(self.ball_x, BALL_R, W - BALL_R) if self.ball_y < BALL_R: self.vy = abs(self.vy) if self.ball_y > H + BALL_R: self.lose_life() return True if self.vy > 0 and game.circle_rect(self.ball_x, self.ball_y, BALL_R, self.paddle): # Where it hits the paddle decides the angle: the edges send it sideways. offset = (self.ball_x - self.paddle_x) / (PADDLE_W / 2) angle = math.radians(-90 + 60 * game.clamp(offset, -1, 1)) self.vx, self.vy = math.cos(angle) * self.speed, math.sin(angle) * self.speed self.combo = 0 self.sparks.burst(self.ball_x, PADDLE_Y, count=6, color="#dce0e5", speed=(40, 120), angle=-90, spread=120, life=(0.15, 0.3)) for brick in self.bricks: normal = game.circle_rect(self.ball_x, self.ball_y, BALL_R, brick["rect"]) if normal: self.hit(brick, normal) break return False def hit(self, brick, normal): nx, ny = normal if nx: self.vx = abs(self.vx) * nx if ny: self.vy = abs(self.vy) * ny brick["hits"] -= 1 cx, cy = brick["rect"].center if brick["hits"] > 0: self.sparks.burst(cx, cy, count=8, color="#ffffff", speed=(40, 120), life=(0.15, 0.35)) return self.bricks.remove(brick) self.combo += 1 self.score += brick["points"] * self.combo self.sparks.burst(cx, cy, count=22, color=[brick["color"], "#ffffff"], life=(0.4, 0.9)) self.shake.kick(2 + min(self.combo, 6)) self.speed = min(self.speed + 4, 640) if not self.bricks: self.level += 1 if self.level > 3: self.won = True self.best = max(self.best, self.score) else: self.build_level() def lose_life(self): self.lives -= 1 self.shake.kick(14) self.sparks.burst(self.ball_x, H - 4, count=40, color=["#d07277", "#dfa067"], angle=-90, spread=90, speed=(120, 380)) if self.lives <= 0: self.over = True self.best = max(self.best, self.score) self.reset_ball() # -- input ---------------------------------------------------------------- def on_key(self, combo): if combo == "space": self.launch() elif combo == "p" and not (self.over or self.won): self.paused = not self.paused elif combo == "enter" and (self.over or self.won): self.new_game() def mouse(self, x, y): if not self.paused: self.paddle_x = game.clamp(x, PADDLE_W / 2, W - PADDLE_W / 2) # -- drawing -------------------------------------------------------------- def scene(self, t): shapes = [draw.background("#101216"), *self.shake.shapes()] for brick in self.bricks: r = brick["rect"] shapes.append(r.shape(fill=brick["color"], radius=4)) if brick["hits"] > 1: # armored: a lighter inner plate shapes.append(draw.rect(r.x + 4, r.y + 4, r.w - 8, r.h - 8, radius=3, fill="#ffffff55")) shapes.append(draw.rect(r.x, r.y, r.w, 4, radius=2, fill="#ffffff33")) # highlight p = self.paddle shapes += game.glow(p.center[0], p.center[1], 10, t.accent, layers=3, strength=0.35, core=False) shapes.append(p.shape(fill=t.accent, radius=PADDLE_H / 2)) shapes += self.sparks.shapes(glow=True) shapes += game.glow(self.ball_x, self.ball_y, BALL_R, "#ffffff", layers=3, strength=0.6) return pz.canvas(shapes, width=W, height=H, on_move=self.mouse, on_press=lambda x, y: self.launch(), cursor="crosshair") def banner(self): if self.over: text, hint = "Game over", "Enter plays again" elif self.won: text, hint = "You cleared every level!", "Enter plays again" elif self.paused: text, hint = "Paused", "P continues" elif self.stuck: text, hint = f"Level {self.level}", "Space or click to launch" else: return None card = pz.container(pz.column([ui.heading(text, level=2), ui.muted(hint)], spacing=6, align="center"), padding=(16, 28)).style(glass=True, radius=12, border=(1, "border")) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center", padding=(220, 0, 0, 0)) def view(self): t = self.resolved_theme() hearts = pz.row([pz.icon("heart", size=15, color="#d07277" if i < self.lives else t.border) for i in range(3)], spacing=3) hud = pz.row([ ui.label("Breakout", weight="semibold"), ui.badge(f"Level {self.level}", tint="text_muted"), pz.hspace(), ui.badge(f"x{self.combo} combo", tint="warning", icon="zap") if self.combo > 1 else None, ui.badge(f"{self.score:,}", icon="star"), ui.badge(f"Best {self.best:,}", tint="text_muted", icon="trophy"), hearts, ], spacing=8, align="center") board = pz.stack([self.scene(t), self.banner()], width=W, height=H) return pz.container(pz.column([hud, pz.container(board, radius=10, clip=True), ui.muted("Mouse or ←/→ moves · Space launches · P pauses", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background) if __name__ == "__main__": pz.run(Breakout) ``` ### Tutorial: Neon Invaders > Un shooter espacial completo con sprites pixel art, oleadas, power-ups, brillo neón, partículas y un HUD de cristal con bloom. Source: https://albakore.github.io/pyzed/es/games/neon-invaders/ La última demo junta todo en un shooter con aspecto de juego indie moderno: un fondo de estrellas con paralaje, invasores pixel art que marchan y se lanzan en picado, disparo automático, un power-up de triple disparo, luces neón por todas partes, explosiones, temblor de pantalla y un HUD de paneles de cristal y texto brillante dibujado por la biblioteca de widgets. | Objetivo | Tiempo | Nivel | Necesitas | | --- | --- | --- | --- | | Un matamarcianos con sprites de pixel art, oleadas, power-ups, brillo de neón y un HUD de cristal | 1 hora | Avanzado | [Breakout](https://albakore.github.io/pyzed/es/games/breakout/) | Juégalo primero: 1. **Pixel art en texto plano.** Los sprites son listas de cadenas; `game.sprite` asigna un color a cada carácter y dibuja una forma por color, así que una nave de 9×7 cuesta dos formas: ```python SHIP = ["....#....", "...###...", "...#@#...", "..#####..", ".##.#.##.", "###.#.###", "#.......#"] game.sprite(SHIP, {"#": NEON["cyan"], "@": "#ffffff"}, self.x - 13.5, self.y - 10.5, scale=3) ``` ¿Prefieres imágenes? `draw.image("ship.png", x, y, w, h, rotation=angle)` las dibuja, decodificadas una vez y en caché. 2. **Volar y disparo automático.** `game.vector()` convierte las flechas y WASD en una dirección (con las diagonales normalizadas) y `pz.is_pressed("space")` dispara mientras la tecla está pulsada, con un tiempo de recarga: ```python def fly(self, dt): dx, dy = game.vector() self.x = game.clamp(self.x + dx * PLAYER_SPEED * dt, 20, W - 20) self.y = game.clamp(self.y + dy * PLAYER_SPEED * dt, H * 0.55, H - 30) self.cooldown -= dt if game.is_pressed("space") and self.cooldown <= 0: self.fire() def fire(self): self.cooldown = FIRE_DELAY for a in ((-12, 0, 12) if self.triple else (0,)): # el power-up abre el disparo en abanico r = math.radians(a - 90) self.shots.append([self.x, self.y - 14, math.cos(r) * 640, math.sin(r) * 640]) ``` 3. **Una formación que marcha y ataca en picado.** La formación se desliza de lado a lado y baja un poco en cada borde; de vez en cuando un invasor la abandona y se lanza hacia la nave, corrigiendo el rumbo en cada fotograma: ```python self.march += self.direction * speed * dt if abs(self.march) > 60: self.direction = -self.direction self.drop = min(self.drop + 10, 140) for inv in self.invaders: if inv.diving: inv.vx += (self.x - inv.x) * 1.4 * dt # persigue a la nave inv.x += inv.vx * dt inv.y += inv.vy * dt elif self.random.random() < dt * (0.02 + 0.01 * self.wave): inv.diving, inv.vx, inv.vy = True, 0.0, 170 + 20 * self.wave ``` Usar `random.random() < tasa * dt` mantiene la misma frecuencia media de eventos sea cual sea la tasa de fotogramas. 4. **Luces neón.** Un canvas no tiene desenfoque, pero la luz es fácil de imitar: `game.glow` dibuja varios círculos translúcidos cada vez más grandes detrás de un núcleo brillante. Los láseres son dos líneas, una ancha y transparente bajo otra fina y blanca: ```python shapes += game.glow(cx, cy, 10, color, layers=2, strength=0.25, core=False) # halo del invasor shapes += game.glow(x, y, 3.5, NEON["pink"], layers=2, strength=0.6) # bala enemiga shapes.append(draw.line(x, y, x, y + 12, stroke=game.rgba(NEON["cyan"], 0.3), width=7, cap="round")) shapes.append(draw.line(x, y, x, y + 12, stroke="#e0fbff", width=2.5, cap="round")) ``` El fondo de estrellas son tres capas de puntos que caen a distinta velocidad (paralaje), y la cuadrícula en perspectiva de abajo es un puñado de `draw.line`. 5. **Explosiones y respuesta.** Cada baja suelta partículas brillantes del color del invasor y los golpes fuertes hacen temblar la pantalla. Tras recibir un impacto la nave parpadea y es invulnerable un momento: ```python def explode(self, inv, big=False): cx, cy = inv.rect.center color = INVADER_COLORS[inv.kind] self.sparks.burst(cx, cy, count=40 if big else 26, color=[color, "#ffffff", color], speed=(60, 300)) self.shake.kick(10 if big else 3) ``` 6. **Un HUD con efectos de interfaz reales.** Aquí brilla una librería de UI. La puntuación es texto con `bloom` (luz a partir de sus píxeles brillantes), la barra es un panel `glass` que desenfoca las estrellas de detrás, y el título y los avisos "WAVE 2" usan animaciones de entrada: ```python def neon(widget, color, intensity=1.0): """Bloom solo en las partes brillantes, sin el borde iluminado de la caja.""" return pz.container(widget, padding=(4, 12)).style( bloom=(color, intensity), border_width=0, shadow=("#00000000", (0, 0), 0)) score = neon(pz.text(f"{self.score:06d}", size=22, font="mono", color="#ffffff"), NEON["cyan"], 0.8) bar = pz.row([score, pz.hspace(), ui.badge(f"Wave {self.wave}", icon="crosshair"), shield]) hud = pz.container(bar, padding=(8, 14)).style(glass=True, radius=12, border=(1, "#ffffff22")) banner = neon(pz.text(f"WAVE {self.wave}", size=30, weight="bold"), NEON["lime"]).animate("zoom_in") board = pz.stack([self.scene(), banner, hud], width=W, height=H) ``` (Image: La pantalla de título: una tarjeta de cristal con un título neón que late) **Tip:** Todo lo que se ve se reconstruye en cada fotograma a partir del estado del juego, como cualquier vista de Pyzed. Eso mantiene el código simple: no hay objetos sprite que crear, mover o borrar, solo listas que `view()` convierte en formas. #### Código completo `examples/games/neon_invaders.py`: ```py """Neon Invaders: a space shooter with a glowing HUD, particles, bloom and screen shake. python examples/games/neon_invaders.py Arrows/WASD fly, hold Space to fire, P pauses, Enter starts. Waves of pixel-art invaders march and dive; shoot the purple capsules for a triple shot. Shows a complete game built from Pyzed parts: ``pz.frames`` for the loop, ``pz.pressed`` for flying and auto-fire, ``game.sprite`` pixel art, ``game.glow`` neon lights, ``game.Particles`` explosions, ``game.Shake``, and a HUD of regular widgets with ``glass`` panels, ``bloom`` and animations stacked over the canvas. Tutorial: https://albakore.github.io/pyzed/games/neon-invaders/ """ import math import random import pyzed as pz from pyzed import draw, game, ui W, H = 760, 560 PLAYER_SPEED = 330 FIRE_DELAY = 0.16 NEON = {"cyan": "#22d3ee", "pink": "#f472b6", "lime": "#a3e635", "amber": "#fbbf24", "violet": "#a78bfa"} SHIP = [ "....#....", "...###...", "...#@#...", "..#####..", ".##.#.##.", "###.#.###", "#.......#", ] INVADERS = [ ["..#.....#..", "...#...#...", "..#######..", ".##.###.##.", "###########", "#.#######.#", "#.#.....#.#", "...##.##..."], ["...####...", ".########.", "##########", "###..##..#", "##########", "..##..##..", ".##.##.##.", "##......##"], ["...##...", "..####..", ".######.", "##.##.##", "########", "..#..#..", ".#.##.#.", "#.#..#.#"], ] INVADER_COLORS = [NEON["lime"], NEON["pink"], NEON["amber"]] PIXEL = 3 def neon(widget, color, intensity=1.0): """Bloom on the bright parts only: no lit edge or box shadow, so text glows by itself.""" return pz.container(widget, padding=(4, 12)).style(bloom=(color, intensity), border_width=0, shadow=("#00000000", (0, 0), 0)) class Invader: def __init__(self, kind, col, row): self.kind, self.col, self.row = kind, col, row self.hp = 1 + (kind == 0) self.diving = False self.flash = 0.0 # seconds left of the white flash after a hit self.x = self.y = 0.0 self.vx = self.vy = 0.0 @property def w(self): return len(INVADERS[self.kind][0]) * PIXEL @property def rect(self): return game.Rect(self.x, self.y, self.w, 8 * PIXEL) class NeonInvaders(pz.App): title = "Neon Invaders" size = (W + 32, H + 32) theme = "One Dark" def __init__(self, seed=None): self.random = random.Random(seed) self.sparks = game.Particles(drag=2.2, seed=seed) self.shake = game.Shake(seed=seed) self.stars = [(self.random.uniform(0, W), self.random.uniform(0, H), layer) for layer in (1, 2, 3) for _ in range(40 // layer)] self.best = 0 self.started = False # the title screen until Enter self.time = 0.0 self.new_game() # -- state ---------------------------------------------------------------- def new_game(self): self.x, self.y = W / 2, H - 60 self.shield, self.score, self.wave = 3, 0, 0 self.shots, self.enemy_shots, self.powerups = [], [], [] self.cooldown = 0.0 self.triple = 0.0 # seconds of triple shot left self.hurt = 0.0 # invulnerability after a hit self.over = self.paused = False self.next_wave() def next_wave(self): self.wave += 1 self.invaders = [Invader(min(row // 2, 2), col, row) for row in range(4) for col in range(9)] self.march, self.direction, self.drop = 0.0, 1, 0.0 self.banner = 2.0 # "Wave N" shown for two seconds self.place_formation() def place_formation(self): left = 70 + self.march for inv in self.invaders: if not inv.diving: inv.x = left + inv.col * 64 + (33 - inv.w) / 2 inv.y = 70 + inv.row * 44 + self.drop # -- the loop ------------------------------------------------------------- def subscriptions(self): return [pz.frames(self.tick)] def tick(self, dt): self.time += dt self.sparks.update(dt) self.shake.update(dt) self.stars = [(x, (y + 20 * layer * dt) % H, layer) for x, y, layer in self.stars] if not self.started or self.paused or self.over: return self.banner = max(0.0, self.banner - dt) self.hurt = max(0.0, self.hurt - dt) self.triple = max(0.0, self.triple - dt) self.fly(dt) self.move_invaders(dt) self.move_shots(dt) if not self.invaders: self.next_wave() def fly(self, dt): dx, dy = game.vector() self.x = game.clamp(self.x + dx * PLAYER_SPEED * dt, 20, W - 20) self.y = game.clamp(self.y + dy * PLAYER_SPEED * dt, H * 0.55, H - 30) self.cooldown -= dt if game.is_pressed("space") and self.cooldown <= 0: self.fire() def fire(self): self.cooldown = FIRE_DELAY angles = (-12, 0, 12) if self.triple else (0,) for a in angles: r = math.radians(a - 90) self.shots.append([self.x, self.y - 14, math.cos(r) * 640, math.sin(r) * 640]) def move_invaders(self, dt): speed = 40 + 10 * self.wave + (36 - len(self.invaders)) * 3 self.march += self.direction * speed * dt if abs(self.march) > 60: self.direction = -self.direction self.march = game.clamp(self.march, -60, 60) self.drop = min(self.drop + 10, 140) self.place_formation() for inv in self.invaders: inv.flash = max(0.0, inv.flash - dt) if inv.diving: inv.vx += (self.x - inv.x) * 1.4 * dt # home in on the ship inv.x += inv.vx * dt inv.y += inv.vy * dt if inv.y > H + 30: inv.diving = False # back to the formation from the top elif self.random.random() < dt * (0.02 + 0.01 * self.wave): inv.diving, inv.vx, inv.vy = True, 0.0, 170 + 20 * self.wave if self.random.random() < dt * (0.06 + 0.02 * self.wave): cx, cy = inv.rect.center self.enemy_shots.append([cx, cy + 12, 0, 230 + 15 * self.wave]) if self.hurt <= 0 and inv.rect.overlaps(self.ship_rect): self.invaders.remove(inv) self.explode(inv, big=True) self.damage() break def move_shots(self, dt): for shot in self.shots[:]: shot[0] += shot[2] * dt shot[1] += shot[3] * dt if shot[1] < -10: self.shots.remove(shot) continue for inv in self.invaders: if inv.rect.contains(shot[0], shot[1]): self.shots.remove(shot) inv.hp -= 1 if inv.hp <= 0: self.invaders.remove(inv) self.score += (30 - 10 * inv.kind) * (2 if inv.diving else 1) self.explode(inv) if self.random.random() < 0.06: self.powerups.append(list(inv.rect.center)) else: inv.flash = 0.08 self.sparks.burst(shot[0], shot[1], count=6, color="#ffffff", speed=(40, 140)) break for shot in self.enemy_shots[:]: shot[1] += shot[3] * dt if shot[1] > H + 10: self.enemy_shots.remove(shot) elif self.hurt <= 0 and self.ship_rect.contains(shot[0], shot[1]): self.enemy_shots.remove(shot) self.damage() for p in self.powerups[:]: p[1] += 90 * dt if self.ship_rect.contains(*p) or self.ship_rect.moved(-8, -8).contains(*p): self.powerups.remove(p) self.triple = 8.0 self.sparks.burst(p[0], p[1], count=30, color=NEON["violet"], speed=(60, 200)) elif p[1] > H + 10: self.powerups.remove(p) @property def ship_rect(self): return game.Rect(self.x - 13, self.y - 10, 27, 21) def explode(self, inv, big=False): cx, cy = inv.rect.center color = INVADER_COLORS[inv.kind] self.sparks.burst(cx, cy, count=40 if big else 26, color=[color, "#ffffff", color], speed=(60, 300), life=(0.3, 0.8), size=(1.5, 3.2)) self.shake.kick(10 if big else 3) def damage(self): self.shield -= 1 self.hurt = 1.5 self.shake.kick(16) self.sparks.burst(self.x, self.y, count=50, color=[NEON["cyan"], "#ffffff"], speed=(80, 320)) if self.shield <= 0: self.over = True self.best = max(self.best, self.score) # -- input ---------------------------------------------------------------- def on_key(self, combo): if combo == "enter" and (not self.started or self.over): self.started = True self.new_game() elif combo == "p" and self.started and not self.over: self.paused = not self.paused # -- drawing -------------------------------------------------------------- def scene(self): shapes = [draw.background("#05060d"), *self.shake.shapes()] for x, y, layer in self.stars: shapes.append(draw.circle(x, y, 0.5 * layer, fill=game.rgba("#c7d2fe", 0.25 * layer))) # A neon horizon grid at the bottom. for i in range(9): y = H - 120 + i * i * 1.6 shapes.append(draw.line(0, y, W, y, stroke=game.rgba(NEON["violet"], 0.05 + i * 0.012))) for i in range(-8, 9): shapes.append(draw.line(W / 2 + i * 30, H - 120, W / 2 + i * 120, H, stroke=game.rgba(NEON["violet"], 0.08))) bob = math.sin(self.time * 6) for inv in self.invaders: color = "#ffffff" if inv.flash > 0 else INVADER_COLORS[inv.kind] cx, cy = inv.rect.center shapes += game.glow(cx, cy, 10, color, layers=2, strength=0.25, core=False) shapes += game.sprite(INVADERS[inv.kind], {"#": color}, inv.x, inv.y + bob * (0 if inv.diving else 1.5), scale=PIXEL) for x, y in self.powerups: shapes += game.glow(x, y, 7, NEON["violet"], layers=3, strength=0.7) shapes.append(draw.text(x, y, "3", color="#05060d", size=10, weight="bold", align_x="center", align_y="center")) for x, y, *_ in self.shots: shapes.append(draw.line(x, y, x, y + 12, stroke=game.rgba(NEON["cyan"], 0.3), width=7, cap="round")) shapes.append(draw.line(x, y, x, y + 12, stroke="#e0fbff", width=2.5, cap="round")) for x, y, *_ in self.enemy_shots: shapes += game.glow(x, y, 3.5, NEON["pink"], layers=2, strength=0.6) if self.started and not self.over and (self.hurt <= 0 or int(self.time * 12) % 2): flame = 6 + 3 * math.sin(self.time * 40) shapes += game.glow(self.x, self.y + 14, 4, NEON["amber"], layers=2, strength=0.8) shapes.append(draw.polygon([(self.x - 4, self.y + 11), (self.x + 4, self.y + 11), (self.x, self.y + 11 + flame)], fill=NEON["amber"])) shapes += game.glow(self.x, self.y, 16, NEON["cyan"], layers=3, strength=0.22, core=False) shapes += game.sprite(SHIP, {"#": NEON["cyan"], "@": "#ffffff"}, self.x - 13.5, self.y - 10.5, scale=PIXEL) shapes += self.sparks.shapes(glow=True) return pz.canvas(shapes, width=W, height=H) def hud(self, t): shield = pz.row([ pz.icon("shield", size=14, color=NEON["cyan"]), pz.container(ui.progress(self.shield, max=3, tint=NEON["cyan"], height=6), width=90), ], spacing=6, align="center") score = neon(pz.text(f"{self.score:06d}", size=22, font="mono", color="#ffffff"), NEON["cyan"], 0.8) chips = [ ui.badge(f"Wave {self.wave}", tint=NEON["pink"], icon="crosshair"), ui.badge(f"Triple {self.triple:.0f}s", tint=NEON["violet"], icon="sparkles") if self.triple else None, ui.badge(f"Best {self.best:,}", tint="text_muted", icon="trophy"), ] bar = pz.row([score, pz.hspace(), *chips, shield], spacing=10, align="center") return pz.container(bar, padding=(8, 14)).style(glass=True, radius=12, border=(1, "#ffffff22")) def center_card(self, title, lines, color): heading = neon(pz.text(title, size=40, weight="bold", color="#ffffff"), color).animate("pulse") body = [ui.muted(line) for line in lines] card = pz.container(pz.column([heading, *body], spacing=10, align="center"), padding=(28, 40)).style(glass=True, radius=16, border=(1, "#ffffff26")) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center") def overlay(self): if not self.started: return self.center_card("NEON INVADERS", ["Arrows / WASD fly · hold Space to fire", "Press Enter to start"], NEON["pink"]) if self.over: return self.center_card("GAME OVER", [f"Score {self.score:,} · wave {self.wave}", "Press Enter to play again"], NEON["pink"]) if self.paused: return self.center_card("PAUSED", ["Press P to continue"], NEON["cyan"]) if self.banner: text = neon(pz.text(f"WAVE {self.wave}", size=30, weight="bold", color="#ffffff"), NEON["lime"]).animate("zoom_in") return pz.container(text, width="fill", height="fill", align_x="center", align_y="center") return None def view(self): t = self.resolved_theme() hud = pz.container(self.hud(t), padding=12, width="fill") if self.started else None board = pz.stack([self.scene(), self.overlay(), hud], width=W, height=H) return pz.container(pz.container(board, radius=12, clip=True), padding=16, width="fill", height="fill", bg="#0b0d14", align_x="center", align_y="center") if __name__ == "__main__": pz.run(NeonInvaders) ``` ## Audio ### Audio > Crea apps de música en Python con Pyzed — un motor de audio modular, perillas, faders y pads al estilo de FL Studio, formas de onda, analizadores de espectro, una vista de webcam y un rack al estilo de VCV Rack con cables animados. Source: https://albakore.github.io/pyzed/es/audio/ (Image: Un rack modular: módulos en rieles, cables conectándose y una perilla girando el filtro) Pyzed tiene todo lo necesario para crear un sintetizador, una caja de ritmos, un controlador MIDI o un visualizador de música en Python. Se divide en dos capas que puedes usar juntas o por separado: - **El motor** (`pyzed.audio`) genera el sonido. Añades módulos (osciladores, filtros, envolventes, efectos, secuenciadores, batería), los unes con cables y los reproduce en tu tarjeta de sonido con baja latencia. No necesita ventana: un script, un test o un servidor pueden usarlo solo. - **La interfaz** (`pyzed.ui.audio`, `pyzed.ui.rack`, `pyzed.ui.camera`) lo muestra y lo controla: perillas, faders, pads, un piano, medidores, formas de onda, analizadores de espectro y un rack donde conectas módulos con cables que arrastras, como en VCV Rack o Reaktor. **En pocas palabras:** El motor es el instrumento y la interfaz su panel frontal. Puedes tocar el instrumento solo con código o darle un panel que el usuario pueda tocar. ```python import pyzed as pz from pyzed import audio, ui engine = audio.Engine() osc = engine.add("oscillator", wave="saw", freq=110) lpf = engine.add("filter", cutoff=800, resonance=0.5) engine.chain(osc, lpf, engine.add("audio_out")) class Synth(pz.App): def on_start(self): engine.start() def subscriptions(self): return [pz.frames(lambda dt: None)] # redibuja en cada fotograma: los medidores se mueven def view(self): return pz.row([ ui.audio.Knob(value=engine.get(lpf, "cutoff"), min=20, max=20000, curve="log", unit="Hz", caption="Cutoff", variant="fl", on_change=lambda e: engine.set(lpf, "cutoff", e.value)), ui.audio.Spectrum(magnitudes=engine.spectrum(), width=360, height=120), ui.audio.VuMeter(level=engine.level()), ], spacing=16, align="center") pz.run(Synth) ``` #### Por dónde empezar - [El motor](https://albakore.github.io/pyzed/es/audio/engine/getting-started/): Módulos, cables, MIDI y latencia: generar sonido desde Python. - [Componentes](https://albakore.github.io/pyzed/es/audio/components/knob/): Perillas, faders, pads, piano, medidores, formas de onda y analizadores. - [El rack](https://albakore.github.io/pyzed/es/audio/rack/rack/): Un rack al estilo de VCV Rack con módulos que conectas con cables. - [Demos](https://albakore.github.io/pyzed/es/audio/demos/synth/): Un sintetizador, un visualizador, un controlador MIDI, una caja de ritmos y más. #### Los componentes Todos los componentes están en la caja de herramientas del diseñador, en el grupo **Audio** (la webcam está en **Media**), y tienen su propia página con una animación: | Componente | Qué es | | --- | --- | | [`Knob`](https://albakore.github.io/pyzed/es/audio/components/knob/) | Un control giratorio: cinco estilos, rangos logarítmicos, selectores por pasos. | | [`Fader`](https://albakore.github.io/pyzed/es/audio/components/fader/) | Un deslizador como el de una mesa de mezclas, con medidor de nivel opcional. | | [`Pad`, `LedButton`](https://albakore.github.io/pyzed/es/audio/components/pads-and-buttons/) | Pads con velocidad y botones con luz. | | [`StepSequencer`](https://albakore.github.io/pyzed/es/audio/components/step-sequencer/) | Una cuadrícula de pasos como el channel rack de FL Studio. | | [`Piano`](https://albakore.github.io/pyzed/es/audio/components/piano/) | Un teclado que se toca con el ratón o con las teclas del ordenador. | | [`VuMeter`](https://albakore.github.io/pyzed/es/audio/components/vu-meter/) | Medidores LED, de barra y de aguja analógica con retención de picos. | | [`Waveform`](https://albakore.github.io/pyzed/es/audio/components/waveform/) | La forma de una grabación, con cabezal y selección. | | [`Scope`](https://albakore.github.io/pyzed/es/audio/components/scope/) | Un osciloscopio con disparo, modo XY y persistencia. | | [`Spectrum`](https://albakore.github.io/pyzed/es/audio/components/spectrum/) | Un analizador de espectro: barras, bloques, líneas, espejo, radial. | | [`Spectrogram`](https://albakore.github.io/pyzed/es/audio/components/spectrogram/) | El espectro a lo largo del tiempo, como una imagen que se desplaza. | | [`Webcam`](https://albakore.github.io/pyzed/es/audio/components/webcam/) | La imagen en vivo de una cámara. | | [`Rack`](https://albakore.github.io/pyzed/es/audio/rack/rack/) | Módulos en rieles unidos con [cables animados](https://albakore.github.io/pyzed/es/audio/rack/cables/). | Los colores y paletas se explican en [Colores y comportamiento](https://albakore.github.io/pyzed/es/audio/components/colors/). ### El motor de audio > Crea sintetizadores, efectos, cajas de ritmos y secuenciadores en Python con pyzed.audio — módulos unidos por cables como en VCV Rack, calculados en Rust con baja latencia, con o sin ventana. Source: https://albakore.github.io/pyzed/es/audio/engine/getting-started/ `pyzed.audio` es un sintetizador modular que se programa desde Python. Añades **módulos** (osciladores, filtros, envolventes, efectos, secuenciadores, batería, samplers), unes sus **puertos** con **cables**, giras sus **knobs** y el motor toca el resultado en tu tarjeta de sonido con baja latencia. Funciona solo, sin ninguna ventana: un script, un servidor o un test pueden sonar. La parte de interfaz (knobs, faders, formas de onda y el rack con cables animados) se construye encima.
Seis módulos unidos por cables que brillan: MIDI hacia un oscilador y una envolvente, por un filtro y un amplificador hasta la salida
Una voz de sintetizador: los puntos son la señal que viaja por los cables y las barras, el nivel de cada módulo.
#### Tu primer sonido ```python from pyzed import audio engine = audio.Engine() # la tarjeta por defecto, con ajustes de baja latencia osc = engine.add("oscillator", freq=220, wave="saw") lpf = engine.add("filter", cutoff=800, resonance=0.5) out = engine.add("audio_out") engine.chain(osc, lpf, out) # osc → filtro → altavoces with engine: # suena mientras está dentro del bloque input("Sonando. Pulsa Enter para parar.") ``` **Compilar desde el código en Linux:** Las wheels de PyPI no necesitan nada más. Una build de Pyzed desde el código en Linux deja fuera los dispositivos de audio y el MIDI salvo que los pidas, así que nunca necesita paquetes extra: `Engine("auto")` suena entonces con el reloj silencioso y `engine.info["error"]` explica por qué. Para tener sonido, instala las cabeceras de ALSA (`sudo apt install libasound2-dev pkg-config`; Fedora: `sudo dnf install alsa-lib-devel`) y compila con `maturin develop --release --features alsa` (con uv, deja exportado `MATURIN_PEP517_ARGS="--features alsa"` y ejecuta `uv sync --reinstall-package pyzed-ui`). Cada línea del patch es Python normal, así que puedes cambiarlo mientras suena: ```python lpf["cutoff"] = 2000 # gira un knob (se ajusta a su rango) osc["wave"] = "square" # una elección acepta el nombre de una opción... osc.set(wave=1, fine=0.1) # ...o su índice; set() gira varios knobs a la vez ``` #### Cómo funciona 1. **Python describe el patch.** `engine.add`, `engine.connect` y `node["knob"] = valor` solo anotan lo que pediste y lo mandan al hilo de audio por una cola sin bloqueos. Nunca esperan al sonido, y el sonido nunca los espera a ellos. 2. **Rust calcula el sonido.** Un hilo de tiempo real ejecuta los módulos en orden, de 64 en 64 muestras, sin reservar memoria y sin bloqueos. Ni el recolector de basura de Python ni una interfaz ocupada pueden hacerle perder su plazo. 3. **El sistema lo reproduce** con su propia API de baja latencia: WASAPI (o ASIO) en Windows, CoreAudio en macOS, PulseAudio/PipeWire o ALSA en Linux. Mira [Dispositivos y latencia](https://albakore.github.io/pyzed/es/audio/engine/latency/). 4. **El motor informa**: cada módulo publica su señal reciente, su nivel y su estado, así una interfaz puede dibujar osciloscopios, espectros, vúmetros y luces de pasos a 60 fps sin tocar el hilo de audio. #### Señales Como en un sintetizador modular de verdad (y en VCV Rack), cada cable lleva **una señal a frecuencia de audio**, y cualquier salida puede ir a cualquier entrada. El tipo del puerto solo dice cómo leerla: | Tipo | Qué lleva | Rango | | --- | --- | --- | | `audio` | Sonido | -1 … 1 | | `cv` | Un voltaje de control: altura en **V/oct** o una cantidad | altura: 0 = C4, +1 = una octava arriba | | `gate` | Encendido o apagado: una tecla pulsada, un paso que suena | 0 o 1 | Altura en voltios por octava quiere decir que sumar 1 a la entrada `pitch` de un oscilador lo sube una octava, y 1/12 lo sube un semitono. `audio.volts("A4")` da `0.75`, así una constante afina un oscilador en cualquier nota: ```python pitch = engine.add("constant", value=audio.volts("E2")) engine.connect(pitch, "out", osc, "pitch") ```
Las ondas seno, triángulo, sierra y cuadrada del oscilador desplazándose mientras suenan
Las cuatro salidas del oscilador, leídas en vivo del motor con node.scope().
#### Módulos, puertos y cables `engine.add(kind, id=None, **params)` añade un módulo y devuelve un `Node`. Su `id` (`"oscillator1"` si no le das nombre) es como lo nombran los cables y los patches guardados. El [catálogo de módulos](https://albakore.github.io/pyzed/es/audio/engine/modules/) lista cada tipo con sus puertos y knobs. ```python keys = engine.add("midi_in", id="keys") vco = engine.add("oscillator", id="vco", wave="saw") vcf = engine.add("filter", id="vcf", cutoff=600, resonance=0.6) env = engine.add("adsr", id="env", attack=0.01, decay=0.3, sustain=0.4, release=0.5) vca = engine.add("vca", id="vca") engine.connect(keys, "pitch", vco, "pitch") # el teclado toca el oscilador engine.connect(keys, "gate", env, "gate") # cada tecla abre la envolvente engine.connect(env, "out", vca, "cv") # la envolvente da forma al volumen... engine.connect(env, "out", vcf, "cutoff") # ...y abre el filtro engine.chain(vco, vcf, vca, engine.add("audio_out")) ``` - **Una entrada acepta cualquier número de cables**; sus señales se suman. - **`engine.chain(a, b, c)`** conecta la primera salida de cada módulo a la primera entrada del siguiente (los dos lados de un par estéreo cuando ambos tienen `left` y `right`). - **Se permiten bucles.** Un cable que vuelve a un módulo anterior (realimentación por un delay, un oscilador que se modula a sí mismo) lee el bloque anterior, como en un rack de hardware. - `engine.disconnect(...)` recibe lo mismo que `connect`; `engine.cables` los lista todos; `node.remove()` quita un módulo con sus cables.
Una envolvente ADSR dibujada mientras se mantiene y se suelta una tecla: ataque, caída, sostenido y liberación
El módulo adsr mientras se mantiene una tecla y se suelta: ataque, caída, sostenido, liberación.
#### Tocar notas El módulo `midi_in` convierte las notas en `pitch`, `gate` y `velocity`. Llegan de un teclado MIDI o de tu código: ```python engine.note_on("C4", velocity=100) # un número MIDI (60) o un nombre engine.note_off("C4") engine.note("E4", duration=0.5) # la liberación ocurre con un temporizador engine.cc(1, 64) # la rueda de modulación engine.pitch_bend(0.5) # medio rango de pitch bend hacia arriba engine.open_midi_input() # el primer teclado MIDI; o open_midi_input("Arturia") engine.open_midi_input(virtual="Pyzed") # un puerto donde tu DAW puede tocar (Linux y macOS) ``` `audio.midi_inputs()` y `audio.midi_outputs()` listan los puertos. El módulo `midi_out` toca una puerta y una altura en la salida abierta con `engine.open_midi_output(...)`, así un secuenciador de Pyzed puede controlar un sintetizador de hardware o un DAW. #### Secuenciar Un `clock` marca el tempo; un `sequencer` recorre hasta 16 alturas y puertas en cada pulso del reloj (o con su propio `bpm` cuando `clock` no tiene cable): ```python clock = engine.add("clock", bpm=124, division="1/16", swing=0.1) seq = engine.add("sequencer", steps=8, pitch_1=0, pitch_2=7, pitch_3=3, pitch_4=12, gate_3=0) engine.connect(clock, "clock", seq, "clock") engine.connect(seq, "pitch", vco, "pitch") engine.connect(seq, "gate", env, "gate") ```
Ocho pasos de secuenciador que se encienden por turnos, con la señal de puerta que envían dibujada debajo
El paso actual sale de seq.state["step"]; la puerta de abajo es seq.probe("gate").
La batería también son módulos: `drum` sintetiza un bombo, una caja, un charles, una palmada o un tom con cada flanco de subida de su `trigger`. El [ejemplo de caja de ritmos](https://github.com/albakore/pyzed/blob/main/examples/audio/engine_drum_machine.py) construye un ritmo completo con un bajo que el bombo atenúa por el `sidechain` de un compresor. #### Samples El `sampler` reproduce un archivo WAV o una lista de muestras, una vez, mientras se mantiene su puerta o en bucle: ```python smp = engine.add("sampler", mode="one-shot") smp.load("caja.wav") # o smp.load([0.0, 0.3, ...], sample_rate=48000) engine.connect(seq, "trigger", smp, "gate") smp.trigger() # o tócalo desde el código ``` #### Ver el sonido Cada módulo graba su señal para la interfaz: léela tan a menudo como quieras, desde cualquier hilo. | Llamada | Devuelve | | --- | --- | | `node.scope(n=1024)` | las últimas `n` muestras, de la más antigua a la más nueva (una forma de onda) | | `node.spectrum(bins=256, size=2048, log=True)` | `bins` bandas en dB (-120 … 0), repartidas por octavas desde 20 Hz | | `node.level()` | `(rms, peak)` de 0 a 1 (un vúmetro) | | `node.probe("out")` | el último valor de una salida (un cable puede brillar con él) | | `node.state` | lo que informa el módulo: el paso de un secuenciador, la etapa de una envolvente… | | `engine.scope()`, `engine.spectrum()`, `engine.level()` | lo mismo para lo que va a los altavoces |
Una forma de onda y un espectro de barras que se mueven con un ritmo y una línea de bajo
Un ritmo leído a 20 fps con engine.scope() y engine.spectrum(bins=48).
El espectro de una onda de sierra antes y después de un filtro paso bajo cuyo corte sube y baja
vco.spectrum() (gris) y vcf.spectrum() (azul) mientras se barre el corte del filtro.
En una `pz.App`, léelos en cada fotograma con `pz.frames` o `pz.every` y dibújalos con los componentes de audio de `pz.ui`. #### Renderizar sin tarjeta de sonido `audio.Engine("offline")` nunca abre un dispositivo: `engine.render(segundos)` calcula el patch tan rápido como permite el procesador y devuelve `(left, right)`. Así se hacen los tests y estas imágenes, y así se exporta un patch a un archivo: ```python engine = audio.Engine("offline") ... # construye el patch engine.note_on("A3") engine.render(1.0) # un segundo con la tecla pulsada engine.note_off("A3") engine.render_wav("pad.wav", 3.0) # tres segundos más en un archivo WAV ``` `audio.write_wav(ruta, left, right, sample_rate)` guarda cualquier lista de muestras. El backend `"null"` ejecuta el patch en tiempo real sin sonido, para máquinas sin tarjeta. #### Guardar patches ```python import json json.dump(engine.to_dict(), open("patch.json", "w")) engine.load(json.load(open("patch.json"))) copia = audio.Engine.from_dict(datos) ``` Los datos guardan los módulos con sus knobs, los cables y los archivos de samples. Lo que pongas en `engine.ui` o `node.ui` (posiciones en un rack, colores) viaja con ellos sin tocarse. #### El catálogo `audio.modules()` describe cada tipo de módulo, con los mismos datos que la [página del catálogo](https://albakore.github.io/pyzed/es/audio/engine/modules/): sus `inputs` y `outputs` (`Port` con `name`, `label` y `type`), sus `params` y su `state`. Un `Param` conoce su rango, su unidad y su curva, que es todo lo que necesita una interfaz para dibujar un knob: ```python cutoff = audio.module("filter").param("cutoff") cutoff.to_unit(1000) # 0.57: dónde cae 1 kHz en el knob (logarítmico, por octavas) cutoff.from_unit(0.25) # 112.5 Hz cutoff.format(1500) # "1.50 kHz" ``` #### Ejemplos La carpeta [`examples/audio/`](https://github.com/albakore/pyzed/tree/main/examples/audio) tiene scripts completos, cada uno con `--render salida.wav` para oírlos sin dispositivo: - `engine_synth.py`: un sintetizador sustractivo con suboscilador, filtro, envolvente, LFO, delay y reverb, tocado por tu teclado MIDI o por una melodía. - `engine_drum_machine.py`: batería, un bajo ácido y bombeo por sidechain a 124 BPM. - `engine_microphone.py`: tu micrófono con distorsión, chorus, delay y reverb, en vivo. ### El rack modular > Crea un sintetizador modular al estilo de VCV Rack en Python con Pyzed — módulos en rieles, perillas y conectores colocados a partir de la descripción de cada módulo, un explorador de módulos y patches guardados en JSON. Source: https://albakore.github.io/pyzed/es/audio/rack/rack/ (Image: Un rack de módulos — secuenciador, oscilador, filtro, envolvente, delay, osciloscopio y salida — conectados con cables que se balancean y llevan puntos de señal) **En pocas palabras:** Un sintetizador modular es un conjunto de cajas (módulos) que hacen una cosa cada una (generar un tono, filtrarlo, dar forma a su volumen, hacer eco) unidas por cables. `ui.rack.Rack` pone en pantalla los módulos del motor de audio como hardware en rieles: gira sus perillas y arrastra cables entre sus conectores. ```python import pyzed as pz from pyzed import audio, ui class Modular(pz.App): def __init__(self): self.engine = audio.Engine() self.layout = {} # dónde está cada módulo; se guarda con el patch seq = self.engine.add("sequencer", bpm=118) osc = self.engine.add("oscillator", wave="saw") vcf = self.engine.add("filter", cutoff=900, resonance=0.6) out = self.engine.add("audio_out") self.engine.connect(seq, "pitch", osc, "pitch") self.engine.chain(osc, vcf, out) self.engine.start() def subscriptions(self): return [pz.frames(lambda dt: None)] # los cables se balancean, osciloscopios y LEDs se mueven def view(self): return ui.rack.Rack(engine=self.engine, layout=self.layout, rows=2) pz.run(Modular) ``` #### Dos capas: lógica y gráfica El rack es solo la **capa gráfica**. El sonido sale del [motor de audio](https://albakore.github.io/pyzed/es/audio/engine/getting-started/) (`pyzed.audio`), que funciona sin ninguna ventana: un script puede montar el mismo patch, reproducirlo o renderizarlo a un archivo WAV. El rack lee tres cosas del motor y las cambia a través de él: | El rack lee | De | Y lo cambia con | | --- | --- | --- | | Los módulos | `engine.nodes` | `engine.add(kind)`, `engine.remove(id)` | | Qué tiene cada módulo | `audio.modules()`: puertos y parámetros | | | Las perillas | `node.params` | `engine.set(id, param, value)` | | Los cables | `engine.cables` | `engine.connect(...)`, `engine.disconnect(...)` | | Pantallas, LEDs, medidores | `node.scope()`, `node.spectrum()`, `node.state`, `node.level()` | | Así, cualquier cosa que tu código haga en el motor aparece en el rack en el siguiente fotograma, y cualquier cosa que el usuario haga en el rack es una llamada normal al motor. #### Los paneles se colocan solos Nunca dibujas un módulo. Cada panel se construye a partir de la descripción del módulo: - **Perillas** para sus parámetros, en columnas desde arriba (un parámetro con `options`, como la forma de onda de un oscilador, se convierte en un selector por pasos). Los parámetros numerados, como `pitch_1`…`pitch_16` y `gate_1`…`gate_16` de un secuenciador, se convierten en una rejilla de perillas e interruptores pequeños, una columna por paso. - **Conectores** para sus entradas abajo, y para sus salidas debajo de ellas sobre una **placa más oscura**, la convención que siguen todos los fabricantes de Eurorack para distinguir las salidas. - Una **pantalla** para los módulos que la tienen: un osciloscopio en vivo en `scope`, LEDs de paso en el `sequencer` (se enciende el paso actual) y medidores de nivel en `audio_out`, `mixer`, `vca` y `compressor`. - Una franja de color arriba según la categoría (fuentes en naranja, filtros en azul, efectos en rosa...), el título, cuatro tornillos y una × para quitarlo. Los anchos son unidades de rack enteras (HP, 15 px) y todos los módulos miden 380 px de alto, como el Eurorack de 3U. #### Cómo se usa 1. **Añade módulos** desde el explorador de la izquierda (escribe para buscar); el módulo va al final de la primera fila con sitio. 2. **Conecta un cable**: arrastra desde un conector y suéltalo en otro. Una salida va a una entrada; mientras arrastras, el cable se engancha al conector más cercano a su alcance. 3. **Mueve un cable**: arrastra una entrada conectada; levantas el cable por su clavija, como en VCV Rack. Suéltalo en otra entrada, o en cualquier otro sitio para desconectarlo. 4. **Desconecta**: clic derecho en un conector. 5. **Mueve un módulo**: arrastra su título. Se ajusta a la rejilla de HP y a una fila, y se aparta si cae sobre otro módulo. 6. **Gira perillas**: arrastra arriba y abajo (Shift para ajuste fino), doble clic para reiniciar. #### Guardar patches ```python ui.rack.save(self.engine, self.layout, "bass.json") # engine.to_dict() + la disposición self.layout = ui.rack.load(self.engine, "bass.json") # carga ambos de vuelta ``` El archivo es el formato de patch propio del motor con la disposición del rack bajo `"ui"`, así un patch hecho en el rack suena igual desde un script sin el rack. | Propiedad | Por defecto | Qué hace | | --- | --- | --- | | `engine` | `None` | El `audio.Engine` que se muestra y edita. | | `layout` | `{}` | Tu dict de posiciones: `{id del nodo: {"row": r, "x": hp}}`. | | `catalog` | `None` | Descripciones de módulos (`audio.modules()` por defecto). | | `rows` | `2` | Filas de rieles. | | `browser` | `True` | El explorador de módulos de la izquierda. | | `tension` | `0.45` | Lo tensos que cuelgan los cables (0 sueltos, 1 rectos). | | `cable_opacity` | `0.9` | Para ver los módulos a través de los cables. | | `flow` | `True` | Puntos de señal moviéndose por los cables. | | `light` | `False` | Paneles claros, como el aspecto clásico de VCV Rack. | Evento: `on_change` con lo que cambió: `"add"`, `"remove"`, `"move"`, `"connect"`, `"disconnect"` o `"set"` (para marcar un documento como no guardado, por ejemplo). **Tip:** `ui.rack.panel_layout(audio.module("filter"))` te dice dónde va cada perilla y conector de un módulo (`knobs`, `inputs`, `outputs`, `width`), por si quieres dibujar tus propios paneles con la misma geometría. ### Knob (perilla) > Una perilla giratoria para apps de audio en Python — arrastra, usa la rueda o haz doble clic para reiniciar, cinco estilos de FL Studio a Eurorack, rangos logarítmicos y selectores por pasos. Source: https://albakore.github.io/pyzed/es/audio/components/knob/ (Image: Seis perillas girando: arco, FL Studio, vintage, rack, mínima y una bipolar) **En pocas palabras:** Una perilla es el control redondo de todo sintetizador y mezclador. Arrástrala hacia arriba para girarla a la derecha y hacia abajo para girarla a la izquierda; mantén Shift para ajustes finos; haz doble clic para devolverla a su sitio. ```python import pyzed as pz from pyzed import ui class Filter(pz.App): cutoff = 1200.0 def view(self): return ui.audio.Knob( value=self.cutoff, min=20, max=20000, curve="log", unit="Hz", caption="Cutoff", on_change=lambda e: setattr(self, "cutoff", e.value), ) pz.run(Filter) ``` #### Cómo funciona - **Arrastra** hacia arriba o abajo: `travel` píxeles (200 por defecto) recorren todo el rango. Con Shift pulsado, el mismo arrastre la mueve diez veces menos. - **Gira la rueda** del ratón encima: un 2 % del rango por muesca, o un `step`. - **Doble clic** para volver a `default` (o a `min` si no hay). - `change` se dispara en cada movimiento con el nuevo valor en `event.value`. La perilla conserva el valor que pone el usuario aunque tu app no lo guarde. Cuando tu app pasa un `value` distinto (se cargó un preset, se movió un controlador MIDI), gana ese. #### Estilos `variant` elige uno de cinco dibujos; todos son formas vectoriales dibujadas en un canvas, así que se ven nítidas con cualquier `diameter`. | `variant` | Se parece a | | --- | --- | | `"arc"` | Un anillo que se llena con `color` alrededor de un dial pequeño. El de por defecto, sigue el tema. | | `"fl"` | FL Studio: una perilla oscura sombreada con un anillo luminoso. | | `"vintage"` | Una perilla crema con marcas, como el hardware antiguo. | | `"rack"` | Una perilla Eurorack pequeña con un indicador de color (la usa el rack). | | `"minimal"` | Solo el anillo, con el valor escrito en el centro. | `bipolar=True` dibuja el anillo desde el centro de arriba en lugar del extremo izquierdo: a la derecha para valores positivos y a la izquierda para negativos. Úsalo para el paneo, la desafinación o la cantidad de envolvente de un filtro. #### Rangos, unidades y opciones - `curve="log"` gira la perilla por proporciones en vez de por pasos: el centro de 20 Hz–20 kHz queda en unos 630 Hz, que es como oímos el tono. Úsalo para frecuencias y tiempos. - `unit` da formato al valor: `"Hz"` (pasa a kHz), `"ms"` (pasa a segundos), `"dB"`, `"%"` (0..1 mostrado como porcentaje) o cualquier texto. `decimals` fija los decimales. - `step` redondea el valor (`step=1` para números enteros). - `options=["Sine", "Triangle", "Saw", "Square"]` la convierte en un **selector por pasos**: el valor es el índice de la opción y la etiqueta muestra su nombre. ```python ui.audio.Knob(value=self.wave, options=["Sine", "Tri", "Saw", "Square"], caption="Wave", variant="fl", on_change=lambda e: engine.set(osc, "wave", e.value)) ``` #### Propiedades | Propiedad | Por defecto | Qué hace | | --- | --- | --- | | `value` | `0.5` | El valor mostrado. | | `min`, `max` | `0`, `1` | El rango. | | `default` | `None` | Adónde la lleva un doble clic (`min` si es `None`). | | `step` | `None` | Redondea el valor a múltiplos de `step`. | | `curve` | `"lin"` | `"log"` para frecuencias y tiempos. | | `caption` | `""` | Texto bajo la perilla. | | `unit`, `decimals` | `""`, `None` | Cómo se escribe el valor. | | `options` | `None` | Nombres de opciones: la convierte en selector por pasos. | | `diameter` | `48` | Tamaño en píxeles. | | `variant` | `"arc"` | `"arc"`, `"fl"`, `"vintage"`, `"rack"` o `"minimal"`. | | `color` | `"accent"` | Un token del tema o un color hex. | | `bipolar` | `False` | Dibuja el anillo desde el centro. | | `show_value` | `True` | Escribe el valor bajo la etiqueta. | | `travel` | `200` | Píxeles de arrastre para todo el rango. | | `disabled` | `False` | La atenúa e ignora el puntero. | Evento: `on_change` (`event.value` es el nuevo valor). En Pyzed Design la perilla está en el grupo **Audio** de la caja de herramientas. **Tip:** Conectar una perilla al motor de audio es una línea: `on_change=lambda e: engine.set(node, "cutoff", e.value)`. `engine.set` solo pasa un número al hilo de audio, así que llamarlo en cada movimiento no es problema. ### Sintetizador > Un sintetizador tocable con efectos en Python — dos osciladores, un filtro resonante, una envolvente, distorsión, chorus, delay y reverb, con perillas al estilo de FL Studio y un piano. Source: https://albakore.github.io/pyzed/es/audio/demos/synth/ (Image: Un panel de sintetizador: perillas en cuatro secciones, un osciloscopio, un espectro, un fader de volumen y un piano que se está tocando) Un sintetizador completo en unas 200 líneas: [`examples/audio/synth.py`](https://github.com/albakore/pyzed/blob/main/examples/audio/synth.py). ```bash python examples/audio/synth.py ``` Tócalo con el ratón en el piano o con las teclas del ordenador: A W S E D… son las teclas blancas y negras de una octava, y Z y X bajan y suben la octava. Los tres botones de arriba cargan presets (Bass, Lead, Pad). **En pocas palabras:** El sonido lo genera el motor; este archivo es solo el panel. Cada perilla fija un valor de un módulo y las pantallas leen lo que sale. #### Cómo está hecho El patch es una cadena de módulos del motor: ``` MIDI in → 2 osciladores → mezclador → filtro → amplificador → distorsión → chorus → delay → reverb → altavoces ↑ ↑ └ envolvente┘ ``` `midi_in` convierte `engine.note_on()` en un tono y una puerta; la envolvente abre el filtro y el amplificador en cada nota. El panel guarda un diccionario de valores y una función, `put`, que envía cada uno al módulo que controla: ```python def put(self, key, value): self.values[key] = value if key in ("cutoff", "resonance"): self.engine.set(self.vcf, key, value) elif key in ("attack", "decay", "sustain", "release"): self.engine.set(self.env, key, value) ... ``` La vista está hecha de [perillas](https://albakore.github.io/pyzed/es/audio/components/knob/) agrupadas en secciones, un [osciloscopio](https://albakore.github.io/pyzed/es/audio/components/scope/) y un [espectro](https://albakore.github.io/pyzed/es/audio/components/spectrum/) que leen `engine.scope()` y `engine.spectrum()`, un [vúmetro](https://albakore.github.io/pyzed/es/audio/components/vu-meter/), un [fader](https://albakore.github.io/pyzed/es/audio/components/fader/) para el volumen y un [piano](https://albakore.github.io/pyzed/es/audio/components/piano/) cuyos eventos `note_on`/`note_off` van al motor. `pz.frames()` redibuja la ventana en cada fotograma para que las pantallas se muevan. ### Cables animados > Cómo funcionan los cables de Pyzed — curvas de Bézier que cuelgan por la gravedad, un muelle que los asienta al conectarlos, puntos de señal que fluyen y cómo dibujarlos tú mismo. Source: https://albakore.github.io/pyzed/es/audio/rack/cables/ (Image: Cables conectándose uno a uno: cada clavija viaja a su conector y el cable cae, se pasa un poco y se asienta mientras los puntos fluyen por él) **En pocas palabras:** Los cables del rack se comportan como los de verdad: cuelgan, rebotan un poco al conectarlos y unos puntitos corren por ellos en la dirección en que viaja la señal. #### La forma: una curva que cuelga Un cable entre dos conectores es una **curva de Bézier cúbica**: empieza en un conector, termina en el otro y dos puntos de control tiran de ella hacia abajo. Los puntos de control quedan un poco hacia dentro de los extremos y más abajo según la **comba**, que crece con la distancia entre los conectores: ``` comba = (18 + 0.3 × distancia) × (1.15 − tension) ``` así un cable corto queda casi recto y uno largo cuelga, como lo haría por la gravedad. `tension` (0..1) los tensa todos a la vez. ```python from pyzed.ui import rack c0, c1 = rack.cable_path((40, 60), (300, 80), tension=0.45) shape = draw.path([("M", 40, 60), ("C", *c0, *c1, 300, 80)], stroke="#f5c542", width=5) ``` #### La caída: un muelle amortiguado Al conectar un cable, su comba no aparece de golpe. Sigue un **muelle amortiguado**: ``` caída(t) = 1 − e^(−5.5 t) · cos(13 t) ``` En `t = 0` el cable está recto; cae, se pasa un poco de su comba de reposo (el coseno hace que se pase) y se asienta en aproximadamente medio segundo. El rack guarda cuándo se conectó cada cable y multiplica su comba por `caída(ahora − conectado)` en cada fotograma. No se simula nada paso a paso, así que el movimiento es el mismo con cualquier tasa de fotogramas. #### El aspecto Cada cable se dibuja en cinco capas, de abajo arriba: 1. Una **sombra** suave, desplazada abajo a la derecha, para que el cable flote sobre los paneles. 2. Un **contorno** más oscuro. 3. El **cuerpo** del color del cable (`cable_opacity` deja ver los paneles a través). 4. Un **brillo** blanco fino por el borde superior, que hace que se vea redondo. 5. Dos **clavijas**: un aro, un centro más claro y un reflejo. Los cables nuevos toman el siguiente color de `ui.rack.CABLE_COLORS` (amarillo, rojo, verde, azul, morado...), como en VCV Rack. Un cable conserva su color cuando lo levantas y lo mueves. #### El flujo de la señal Con `flow=True` unos puntos de luz viajan de la salida a la entrada, separados unos 38 px. Su posición en la curva es el tiempo multiplicado por una velocidad, dando la vuelta al llegar al final, así se mueven con suavidad a cualquier tasa de fotogramas y cada cable muestra hacia dónde va su señal. #### Mientras arrastras Un cable que se arrastra sigue al puntero con una curva más tensa y ligera y se **engancha** al conector compatible más cercano a menos de 18 px (las salidas solo llegan a entradas y al revés), así ves dónde caerá antes de soltarlo. Una entrada admite un solo cable: soltar sobre una entrada conectada reemplaza su cable. **Tip:** El rack necesita redibujarse en cada fotograma para la caída y el flujo: `pz.frames(lambda dt: None)` en `subscriptions()`. Pon `flow=False` y no devuelvas ninguna suscripción cuando nada se mueva para que la app descanse. ### Catálogo de módulos > Todos los módulos del motor de audio de Pyzed: osciladores, filtros, envolventes, LFO, efectos, secuenciadores, batería, samplers y MIDI, con sus puertos, rangos y valores. Source: https://albakore.github.io/pyzed/es/audio/engine/modules/ Todos los módulos del motor, agrupados como en un rack. Añade uno con `engine.add(kind, **params)` y conecta sus puertos con `engine.connect(src, output, dst, input)`. Los mismos datos están en Python como [`audio.modules()`](https://albakore.github.io/pyzed/es/audio/engine/getting-started/#el-catálogo), que una interfaz puede usar para dibujar cualquier módulo. Las señales siguen las [convenciones de los sintetizadores modulares](https://albakore.github.io/pyzed/es/audio/engine/getting-started/#señales): el audio va de -1 a 1, la altura está en V/oct (0 = C4) y las puertas valen 0 o 1. #### Fuentes ##### Oscilador · `oscillator` Un VCO sin aliasing: seno, triángulo, sierra y cuadrada (con ancho de pulso), afinado por `freq` más la entrada `pitch` en V/oct. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `pitch` | Altura (V/oct) | cv | | `fm` | FM | cv | | `pwm` | PWM | cv | | `sync` | Sync | gate | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `out` | Salida | audio | | `sine` | Seno | audio | | `triangle` | Triángulo | audio | | `saw` | Sierra | audio | | `square` | Cuadrada | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `freq` | Frecuencia | 8 … 20000 Hz (logarítmico) | 261.63 | | `wave` | Onda | opciones: `sine`, `triangle`, `saw`, `square` | `saw` | | `fine` | Ajuste fino | -1 … 1 st | 0 | | `pw` | Ancho de pulso | 0.05 … 0.95 | 0.5 | | `fm_amount` | Cantidad de FM | 0 … 1 | 0 | | `level` | Nivel | 0 … 1 | 0.8 | ##### Ruido · `noise` Ruido blanco, rosa o marrón. **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `out` | Salida | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `color` | Color | opciones: `white`, `pink`, `brown` | `white` | | `level` | Nivel | 0 … 1 | 0.5 | ##### Sampler · `sampler` Reproduce un archivo WAV o una lista de muestras (`node.load`) cuando se abre su puerta: una vez, mientras se mantiene o en bucle. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `gate` | Puerta | gate | | `pitch` | Altura (V/oct) | cv | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `left` | Izquierda | audio | | `right` | Derecha | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `mode` | Modo | opciones: `one-shot`, `gate`, `loop` | `one-shot` | | `tune` | Afinación | -24 … 24 st | 0 | | `start` | Inicio | 0 … 1 | 0 | | `end` | Fin | 0 … 1 | 1 | | `level` | Nivel | 0 … 1 | 0.8 | **Estado** (`node.state`): `position`, `playing` ##### Batería · `drum` Una voz de batería sintetizada (bombo, caja, charles, palmada o tom) que suena con cada flanco de subida de `trigger`. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `trigger` | Disparo | gate | | `accent` | Acento | cv | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `out` | Salida | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `sound` | Sonido | opciones: `kick`, `snare`, `hihat`, `clap`, `tom` | `kick` | | `tune` | Afinación | -12 … 12 st | 0 | | `decay` | Caída | 0.02 … 2 s (logarítmico) | 0.4 | | `tone` | Tono | 0 … 1 | 0.5 | | `level` | Nivel | 0 … 1 | 0.8 | **Estado** (`node.state`): `hit` #### Modulación ##### LFO · `lfo` Un oscilador lento para mover otros parámetros: seno, triángulo, sierra, cuadrada o pasos aleatorios (sample and hold). **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `rate` | Velocidad | cv | | `reset` | Reinicio | gate | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `out` | Salida | cv | | `unipolar` | Unipolar | cv | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `rate` | Velocidad | 0.01 … 50 Hz (logarítmico) | 1 | | `wave` | Onda | opciones: `sine`, `triangle`, `saw`, `square`, `random` | `sine` | | `amount` | Cantidad | 0 … 1 | 1 | | `offset` | Desplazamiento | -1 … 1 | 0 | **Estado** (`node.state`): `phase` ##### VCA · `vca` Un amplificador cuya ganancia sigue a `cv` (una envolvente o un LFO); sin nada en `cv` es un volumen normal. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `in` | Entrada | audio | | `cv` | CV | cv | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `out` | Salida | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `gain` | Ganancia | 0 … 2 | 1 | | `response` | Respuesta | opciones: `linear`, `exponential` | `linear` | ##### Envolvente · `adsr` Una envolvente ADSR: sube cuando se abre la puerta, se queda en sustain y se apaga cuando se cierra. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `gate` | Puerta | gate | | `retrig` | Redisparo | gate | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `out` | Salida | cv | | `inverted` | Invertida | cv | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `attack` | Ataque | 0.001 … 10 s (logarítmico) | 0.01 | | `decay` | Caída | 0.001 … 10 s (logarítmico) | 0.2 | | `sustain` | Sostenido | 0 … 1 | 0.7 | | `release` | Liberación | 0.001 … 10 s (logarítmico) | 0.3 | **Estado** (`node.state`): `stage`, `level` #### Filtros ##### Filtro · `filter` Un filtro de estado variable resonante (VCF) con paso bajo, paso alto, paso banda y notch; `cutoff` recibe V/oct, así que una envolvente o un LFO lo barren. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `in` | Entrada | audio | | `cutoff` | Corte | cv | | `resonance` | Resonancia | cv | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `out` | Salida | audio | | `lowpass` | Paso bajo | audio | | `bandpass` | Paso banda | audio | | `highpass` | Paso alto | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `mode` | Modo | opciones: `lowpass`, `highpass`, `bandpass`, `notch` | `lowpass` | | `cutoff` | Corte | 20 … 20000 Hz (logarítmico) | 1200 | | `resonance` | Resonancia | 0 … 1 | 0.2 | | `cv_amount` | Cantidad de CV | -1 … 1 | 1 | | `drive` | Saturación | 0 … 1 | 0 | #### Mezcla ##### Mezclador · `mixer` Cuatro canales con nivel y panorama hacia un par estéreo. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `in_1` | Entrada 1 | audio | | `in_2` | Entrada 2 | audio | | `in_3` | Entrada 3 | audio | | `in_4` | Entrada 4 | audio | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `left` | Izquierda | audio | | `right` | Derecha | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `level_1` | Nivel 1 | 0 … 1.5 | 0.8 | | `level_2` | Nivel 2 | 0 … 1.5 | 0.8 | | `level_3` | Nivel 3 | 0 … 1.5 | 0.8 | | `level_4` | Nivel 4 | 0 … 1.5 | 0.8 | | `pan_1` | Panorama 1 | -1 … 1 | 0 | | `pan_2` | Panorama 2 | -1 … 1 | 0 | | `pan_3` | Panorama 3 | -1 … 1 | 0 | | `pan_4` | Panorama 4 | -1 … 1 | 0 | | `master` | Maestro | 0 … 1.5 | 1 | #### Efectos ##### Delay · `delay` Un eco con realimentación y un control de tono en las repeticiones. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `in` | Entrada | audio | | `time` | Tiempo | cv | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `out` | Salida | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `time` | Tiempo | 0.001 … 2 s (logarítmico) | 0.35 | | `feedback` | Realimentación | 0 … 0.98 | 0.4 | | `tone` | Tono | 200 … 20000 Hz (logarítmico) | 8000 | | `mix` | Mezcla | 0 … 1 | 0.35 | ##### Reverb · `reverb` Una sala estéreo (Freeverb): tamaño, amortiguación y anchura. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `left` | Izquierda | audio | | `right` | Derecha | audio | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `left` | Izquierda | audio | | `right` | Derecha | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `size` | Tamaño | 0 … 1 | 0.6 | | `damping` | Amortiguación | 0 … 1 | 0.4 | | `width` | Anchura | 0 … 1 | 1 | | `mix` | Mezcla | 0 … 1 | 0.3 | ##### Distorsión · `distortion` Satura la señal con un recorte suave o duro, un wavefolder o un fuzz. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `in` | Entrada | audio | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `out` | Salida | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `type` | Tipo | opciones: `soft`, `hard`, `fold`, `fuzz` | `soft` | | `drive` | Saturación | 0 … 1 | 0.4 | | `tone` | Tono | 200 … 20000 Hz (logarítmico) | 6000 | | `mix` | Mezcla | 0 … 1 | 1 | | `level` | Nivel | 0 … 1 | 0.7 | ##### Chorus · `chorus` Un chorus estéreo; con realimentación y un retardo corto se vuelve un flanger. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `in` | Entrada | audio | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `left` | Izquierda | audio | | `right` | Derecha | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `rate` | Velocidad | 0.05 … 5 Hz (logarítmico) | 0.8 | | `depth` | Profundidad | 0 … 1 | 0.5 | | `delay` | Retardo | 1 … 30 ms (logarítmico) | 12 | | `feedback` | Realimentación | 0 … 0.9 | 0 | | `mix` | Mezcla | 0 … 1 | 0.5 | ##### Bitcrusher · `bitcrusher` Lo-fi: menos bits y una frecuencia de muestreo más baja. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `in` | Entrada | audio | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `out` | Salida | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `bits` | Bits | 1 … 16 bit | 8 | | `downsample` | Reducción | 1 … 32 x | 4 | | `mix` | Mezcla | 0 … 1 | 1 | ##### Compresor · `compressor` Iguala el nivel; una señal en `sidechain` atenúa la entrada (el «bombeo» de la música de baile). **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `in` | Entrada | audio | | `sidechain` | Sidechain | audio | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `out` | Salida | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `threshold` | Umbral | -60 … 0 dB | -18 | | `ratio` | Ratio | 1 … 20 :1 | 4 | | `attack` | Ataque | 0.1 … 100 ms (logarítmico) | 10 | | `release` | Liberación | 10 … 1000 ms (logarítmico) | 120 | | `makeup` | Compensación | 0 … 24 dB | 0 | **Estado** (`node.state`): `reduction` #### Secuenciación ##### Reloj · `clock` El tempo de un patch: puertas en la subdivisión elegida del pulso, en cada pulso y en cada compás. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `reset` | Reinicio | gate | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `clock` | Reloj | gate | | `beat` | Pulso | gate | | `bar` | Compás | gate | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `bpm` | Tempo | 20 … 300 BPM | 120 | | `division` | Subdivisión | opciones: `1/4`, `1/8`, `1/16`, `1/32` | `1/16` | | `swing` | Swing | 0 … 0.5 | 0 | | `run` | En marcha | opciones: `off`, `on` | `on` | **Estado** (`node.state`): `tick`, `beat` ##### Secuenciador · `sequencer` Hasta 16 pasos de altura y puerta, avanzados por `clock` (o por su propio `bpm` en semicorcheas si `clock` no tiene cable). **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `clock` | Reloj | gate | | `reset` | Reinicio | gate | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `pitch` | Altura (V/oct) | cv | | `gate` | Puerta | gate | | `trigger` | Disparo | gate | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `steps` | Pasos | 1 … 16 | 8 | | `gate_length` | Duración de la puerta | 0.05 … 1 | 0.5 | | `bpm` | Tempo | 0 … 300 BPM | 120 | | `pitch_1` … `pitch_16` | Altura | -24 … 24 st | 0 | | `gate_1` … `gate_16` | Puerta | opciones: `off`, `on` | `on` | **Estado** (`node.state`): `step` #### Utilidades ##### Constante · `constant` Un voltaje fijo, para mover una entrada desde un knob. **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `out` | Salida | cv | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `value` | Valor | -10 … 10 V | 1 | ##### Osciloscopio · `scope` Deja pasar su entrada sin tocarla, para ver una señal en cualquier punto del patch. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `in` | Entrada | audio | **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `out` | Salida | audio | #### Entrada y salida ##### Entrada de audio · `audio_in` El sonido del dispositivo de entrada (un micrófono o una línea), abierto con `Engine(input_device=...)` o `input=True`. **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `left` | Izquierda | audio | | `right` | Derecha | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `gain` | Ganancia | 0 … 4 | 1 | ##### MIDI a CV · `midi_in` Convierte las notas de un teclado MIDI (o de `engine.note_on`) en altura, puerta y velocidad, tocando la última nota pulsada. **Salidas** | Puerto | | Tipo | | --- | --- | --- | | `pitch` | Altura (V/oct) | cv | | `gate` | Puerta | gate | | `velocity` | Velocidad de nota | cv | | `mod` | Rueda de modulación | cv | | `retrigger` | Redisparo | gate | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `channel` | Canal | 0 … 16 | 0 | | `glide` | Portamento | 0.0001 … 2 s (logarítmico) | 0.0001 | | `bend_range` | Rango de pitch bend | 0 … 24 st | 2 | **Estado** (`node.state`): `note`, `gate` ##### Salida de audio · `audio_out` Los altavoces. Sin nada en `right`, `left` suena por los dos lados. **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `left` | Izquierda | audio | | `right` | Derecha | audio | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `volume` | Volumen | 0 … 1 | 0.8 | ##### CV a MIDI · `midi_out` Toca las notas de una puerta y una altura en una salida MIDI (`engine.open_midi_output`). **Entradas** | Puerto | | Tipo | | --- | --- | --- | | `pitch` | Altura (V/oct) | cv | | `gate` | Puerta | gate | | `velocity` | Velocidad de nota | cv | **Parámetros** | Parámetro | | Rango | Por defecto | | --- | --- | --- | --- | | `channel` | Canal | 1 … 16 | 1 | **Estado** (`node.state`): `note` ### Fader > Un fader de mezclador para Python — tapa de consola, estilo FL Studio o tirador plano, vertical o como crossfader, con un medidor de nivel opcional junto al recorrido. Source: https://albakore.github.io/pyzed/es/audio/components/fader/ (Image: Tres faders verticales con medidores de nivel y un crossfader horizontal moviéndose) **En pocas palabras:** Un fader es el deslizador de una mesa de mezclas: uno por canal, normalmente para el volumen. Arrastra la tapa, haz clic en cualquier punto del recorrido para saltar ahí o usa la rueda encima. ```python ui.audio.Fader(value=self.volume, caption="Master", level=engine.level()[1], on_change=lambda e: engine.set(out, "volume", e.value)) ``` #### Cómo funciona - **Arrastra la tapa** para moverla; Shift hace el movimiento diez veces más fino. - **Haz clic en el recorrido** (no en la tapa) para saltar a ese punto. - **Gira la rueda** para moverla un poco y **haz doble clic** para devolverla a `default`. - `level` (0..1) dibuja un pequeño medidor junto al recorrido: pasa el pico del canal en cada fotograma y el fader hace también de medidor, como en una consola real. #### Estilos | `variant` | Se parece a | | --- | --- | | `"console"` | Una tapa metálica con ranuras, como una mesa de mezclas analógica. | | `"fl"` | La tapa oscura de FL Studio con una línea de color y el recorrido lleno hasta ella en `color`. | | `"flat"` | Un tirador redondo sobre un recorrido lleno, a juego con el resto del tema. | `vertical=False` lo coloca en horizontal (un crossfader). `ticks` fija cuántas marcas se dibujan a lo largo del recorrido (`0` para ninguna) y `length` su tamaño en píxeles. #### Propiedades | Propiedad | Por defecto | Qué hace | | --- | --- | --- | | `value`, `min`, `max` | `0.75`, `0`, `1` | El valor y su rango. | | `default`, `step`, `curve` | `None`, `None`, `"lin"` | Como en la [perilla](https://albakore.github.io/pyzed/es/audio/components/knob/). | | `caption`, `unit`, `decimals` | | Texto debajo y cómo se escribe el valor. | | `length` | `160` | Longitud del recorrido en píxeles. | | `vertical` | `True` | `False` para un fader horizontal. | | `variant` | `"console"` | `"console"`, `"fl"` o `"flat"`. | | `ticks` | `11` | Marcas a lo largo del recorrido. | | `level` | `None` | Un nivel 0..1 dibujado como medidor junto al recorrido. | | `color` | `"accent"` | El relleno y la línea de la tapa. | | `show_value`, `disabled` | `True`, `False` | | Evento: `on_change` con el nuevo valor. **Tip:** Un canal de mezclador es una columna: una [perilla](https://albakore.github.io/pyzed/es/audio/components/knob/) para el paneo, dos [botones LED](https://albakore.github.io/pyzed/es/audio/components/pads-and-buttons/) para mute y solo y un fader con su `level`. La [demo del controlador MIDI](https://albakore.github.io/pyzed/es/audio/demos/midi-controller/) pone ocho uno al lado del otro. ### Visualizador de música > Un visualizador de música en Python — analizadores de espectro, un espectro radial, un espectrograma, un osciloscopio y vúmetros cuyo aspecto cambia en vivo, alimentados por una canción o el micrófono. Source: https://albakore.github.io/pyzed/es/audio/demos/visualizer/ (Image: Un visualizador pasando por varios estilos: espectros de bloques, barras, relleno, espejo y línea, uno radial, un espectrograma, un osciloscopio y un vúmetro analógico) Todos los analizadores de Pyzed en una ventana: [`examples/audio/visualizer.py`](https://github.com/albakore/pyzed/blob/main/examples/audio/visualizer.py). ```bash python examples/audio/visualizer.py ``` Reproduce un pequeño groove hecho con el motor (un bajo secuenciado, bombo, caja, charles y acordes). Cambia la fuente a **Microphone** para ver tu propia voz. El panel de la izquierda cambia el espectro principal en vivo: su aspecto (`variant`), sus colores (`colors`), cuántas bandas tiene, lo rápido que caen las barras y si los picos se quedan. #### Cómo está hecho - `demo_song(engine)` monta el groove: un `clock` mueve un `sequencer` para el bajo y otro para la caja, y su salida de pulso golpea el bombo. Todo pasa por un módulo `scope` antes de los altavoces, para poder analizarlo. - En cada fotograma, la vista pide a ese módulo su análisis, calculado en Rust: ```python mags = tap.spectrum(512) # 512 bandas, de 20 Hz a 24 kHz, en dB rms, peak = tap.level() wave = tap.scope(2048) # las últimas 2048 muestras ``` - Las mismas `mags` alimentan un [`Spectrum`](https://albakore.github.io/pyzed/es/audio/components/spectrum/) en dos estilos y un [`Spectrogram`](https://albakore.github.io/pyzed/es/audio/components/spectrogram/); `wave` alimenta un [`Scope`](https://albakore.github.io/pyzed/es/audio/components/scope/); los niveles alimentan dos [`VuMeter`](https://albakore.github.io/pyzed/es/audio/components/vu-meter/). Cuando la fuente es el micrófono, la salida de la canción se silencia para que los altavoces no se realimenten en el micrófono. ### Controlador MIDI > Un controlador MIDI en Python como el de FL Studio — canales con perilla de paneo, faders, mute y solo, pads con velocidad, ruedas de pitch y modulación y un puerto MIDI virtual para tu DAW. Source: https://albakore.github.io/pyzed/es/audio/demos/midi-controller/ (Image: Un controlador MIDI tocando un ritmo: ocho canales con faders moviéndose, pads iluminándose y mensajes MIDI en un registro) Un controlador que puedes usar con cualquier programa de música: [`examples/audio/midi_controller.py`](https://github.com/albakore/pyzed/blob/main/examples/audio/midi_controller.py). ```bash python examples/audio/midi_controller.py ``` Tiene ocho canales (una [perilla](https://albakore.github.io/pyzed/es/audio/components/knob/) de paneo, un [fader](https://albakore.github.io/pyzed/es/audio/components/fader/) con medidor de nivel, mute y solo), ocho [pads](https://albakore.github.io/pyzed/es/audio/components/pads-and-buttons/) con velocidad, ruedas de pitch y modulación, botones de transporte y un [piano](https://albakore.github.io/pyzed/es/audio/components/piano/) de tres octavas. **PLAY** toca un ritmo en los pads. Toca su propia batería y su sintetizador con el motor, así oyes todo. Activa **MIDI OUT** para enviar además cada movimiento a otros programas: | Control | Mensaje MIDI | | --- | --- | | Fader 1–8 | Control change 70–77 | | Paneo 1–8 | Control change 10–17 | | Pad 1–8 | Nota 36–43 en el canal 10 (batería) | | Piano | Note on / note off en el canal 1 | | Rueda de pitch | Pitch bend | | Rueda de modulación | Control change 1 | **Usarlo con un DAW:** En Linux y macOS el controlador abre un puerto virtual llamado **Pyzed Controller**: elígelo como entrada MIDI en tu DAW. En Windows usa la primera salida MIDI que encuentre (instala un driver de loopback como loopMIDI para llegar a un DAW). #### Cómo está hecho ```python def set_level(self, i, value): self.levels[i] = value self.apply_mix() # el canal del mezclador del motor self.send(0xB0, 70 + i, int(value * 127)) # y un control change MIDI def toggle_midi_out(self, on): if on: self.engine.open_midi_output(virtual="Pyzed Controller") self.midi_out = on ``` `send` escribe los bytes en el registro de la derecha y, con MIDI OUT activado, llama a `engine.send_midi(message)`. ### Dispositivos y latencia > Cómo elige pyzed.audio la tarjeta de sonido, la API de audio, el buffer y la frecuencia de muestreo en Windows, macOS y Linux, cómo medir la latencia y cómo bajarla. Source: https://albakore.github.io/pyzed/es/audio/engine/latency/ La **latencia** es el tiempo entre que algo ocurre (una tecla, un knob que gira, tu voz en el micrófono) y lo oyes. El motor calcula 64 muestras cada vez, pero la tarjeta de sonido pide un **buffer** entero de golpe: ese buffer, más lo que añade el sistema, es la latencia. Buffers más pequeños dan menos retraso pero menos tiempo para calcular cada uno; si es demasiado pequeño, el sonido cruje.
Bloques de 64 muestras llenando las cuatro casillas de un buffer de 256 que luego va a los altavoces
Un buffer de 256 muestras son 5,3 ms a 48 kHz: cuatro bloques del motor.
#### Lo que elige el motor Sin ajustes, `audio.Engine()` usa la mejor API de audio del sistema, su dispositivo por defecto a su propia frecuencia de muestreo y el buffer más pequeño que suena sin cortes en una máquina normal: | Sistema | API de audio (host) | Buffer | Latencia a 48 kHz | | --- | --- | --- | --- | | Windows | WASAPI (ASIO si se compila con él) | 256 muestras (WASAPI en modo compartido puede elegir el suyo) | ~5–10 ms | | macOS | CoreAudio | 128 muestras | ~3 ms | | Linux | PulseAudio, que también llega a PipeWire; ALSA si no hay servidor de sonido | 256 muestras | ~5 ms | `audio.recommended_settings()` devuelve estas elecciones para la máquina actual. #### Elegir tú ```python from pyzed import audio audio.hosts() # ['PulseAudio', 'ALSA']: las APIs disponibles aquí audio.default_host() # 'PulseAudio' for d in audio.devices(): # los dispositivos del host por defecto (o devices("ALSA")) print(d.name, d.outputs, d.inputs, d.sample_rate, d.buffer_range) engine = audio.Engine( host="ALSA", # una API de audio.hosts() device="USB Audio", # el nombre o el id de un dispositivo (None: el de por defecto) sample_rate=48000, # None: la frecuencia propia del dispositivo buffer_size=128, # muestras; None: el tamaño recomendado input=True, # abre también la entrada por defecto (para el módulo audio_in) input_device=None, # o nombra otra ) engine.start() print(engine.info) ``` `engine.start(**ajustes)` también recibe estos ajustes, así puedes parar y volver a empezar con un buffer más pequeño u otro dispositivo; el patch se queda como está. #### Medir `engine.info` dice qué se abrió y cómo funciona: | Clave | Significado | | --- | --- | | `backend` | `"device"`, `"null"` (sin tarjeta: un reloj que lleva el tiempo) o `"offline"` | | `host`, `device`, `input_device` | la API y los dispositivos en uso | | `sample_rate`, `buffer_size`, `channels` | el formato del stream (`buffer_size` es `None` cuando eligió el sistema) | | `latency_ms` | desde que se calcula un buffer hasta que sale por los altavoces, según el sistema | | `load` | la parte del tiempo de cada buffer que se gasta en calcularlo: por encima de 1.0 el sonido se corta | | `underruns` | buffers que salieron en silencio | | `error` | el último problema (un dispositivo desconectado, o por qué no hay sonido) | Un `load` que se queda por debajo de 0.5 deja margen para un buffer más pequeño. #### Cada sistema **Windows:** - **WASAPI** es la opción por defecto. En modo compartido Windows mezcla todos los programas y decide el buffer (normalmente 10 ms); el motor pide 256 muestras y acepta lo que dé Windows. - **ASIO** da la latencia más baja con interfaces de audio que traen driver ASIO. Necesita el SDK de ASIO de Steinberg al compilar, así que no está en las wheels de PyPI: compila Pyzed con `maturin develop --release --features asio` (con `CPAL_ASIO_DIR` apuntando al SDK) y luego usa `audio.Engine(host="ASIO")`. - Para puertos MIDI virtuales, instala [loopMIDI](https://www.tobias-erichsen.de/software/loopmidi.html). **macOS:** - **CoreAudio** ejecuta el hilo de audio con prioridad de tiempo real por sí mismo y lleva bien los buffers pequeños: 128 muestras por defecto, 64 funciona en la mayoría de los Mac. - Un dispositivo agregado (Configuración de Audio MIDI) une una entrada y una salida que son dispositivos distintos. - `engine.open_midi_input(virtual="Pyzed")` crea un puerto MIDI donde pueden tocar otras apps. **Linux:** - **PulseAudio** es la opción por defecto cuando hay un servidor de sonido. En los escritorios actuales ese servidor es **PipeWire**, que entiende el protocolo de PulseAudio; el motor le pide una latencia de 256 muestras. - **ALSA** (`host="ALSA"`) habla directamente con la tarjeta cuando no hay servidor, o con PipeWire a través de su plugin de ALSA. Los nombres tipo `device="hw:0,0"` se saltan cualquier mezcla para la latencia más baja, pero dejan la tarjeta solo para Pyzed. - **JACK** está disponible si compilas Pyzed con `--features jack`. - El hilo de audio pide prioridad de tiempo real. Si tu usuario no la tiene permitida, el motor funciona igual; añádete al grupo `audio` (con un límite `rtprio`, como lo configuran casi todas las distribuciones) para buffers pequeños más estables. #### Sin tarjeta de sonido | Backend | Para qué | | --- | --- | | `audio.Engine("auto")` | Por defecto: la tarjeta de sonido, o el reloj `null` si no hay ninguna (`info["error"]` dice por qué) | | `audio.Engine("device")` | La tarjeta de sonido o un error | | `audio.Engine("null")` | Tiempo real sin sonido: servidores, CI, una interfaz que solo muestra las señales | | `audio.Engine("offline")` | Sin reloj: `engine.render(segundos)` calcula lo más rápido posible, para tests y archivos | **Auriculares para el micrófono:** Con un módulo `audio_in` sonando por los altavoces, el micrófono los oye y el sonido se realimenta. Usa auriculares o deja baja la `gain` de la entrada. ### Pads y botones LED > Pads de batería sensibles a la velocidad y botones LED (mute, solo, grabar) para apps de música en Python, al estilo de FL Studio y las cajas de ritmos. Source: https://albakore.github.io/pyzed/es/audio/components/pads-and-buttons/ (Image: Una cuadrícula de 4×3 pads que se iluminan con un ritmo, junto a botones PLAY, REC, MUTE y SOLO) **En pocas palabras:** Los pads son los cuadrados de goma de una caja de ritmos: golpeas uno y suena. Los botones LED son los interruptores pequeños con luz, como mute, solo o grabar. #### Pad ```python ui.audio.Pad(text="KICK", color="#ff9f3d", on_press=lambda e: engine.node("drums").trigger(), on_release=lambda e: None) ``` - `press` se dispara al pulsar el pad, con una **velocidad** de 1 a 127 en `event.value` (como en MIDI): cuanto más abajo del pad haces clic, más fuerte es el golpe. `velocity=False` envía siempre 100. - `release` se dispara al soltar el botón o cuando el puntero sale del pad. - `lit=True` lo enciende desde fuera: el paso actual de un secuenciador, una nota que llega de un teclado. El pad brilla en su `color`, así que una cuadrícula de pads muestra el ritmo de un vistazo. | Propiedad | Por defecto | Qué hace | | --- | --- | --- | | `text` | `""` | La etiqueta del centro. | | `color` | `"accent"` | El color con el que se enciende. | | `lit` | `False` | Encendido desde fuera. | | `velocity` | `True` | Velocidad según dónde se pulsa. | | `width`, `height` | `72`, `72` | Tamaño en píxeles. | #### Botón LED ```python ui.audio.LedButton(text="MUTE", on=self.muted, color="warning", on_toggle=lambda e: setattr(self, "muted", e.value)) ``` Una tecla oscura con una lucecita. **Alterna** con cada clic (`toggle` da `True` o `False`), o con `momentary=True` solo se enciende mientras lo mantienes (`press` / `release`), como una tecla de tap tempo. | Propiedad | Por defecto | Qué hace | | --- | --- | --- | | `text` | `""` | La etiqueta. | | `on` | `False` | Si está encendido. | | `color` | `"success"` | El color de la luz (un token del tema o hex). | | `variant` | `"fl"` | `"fl"` (tecla cuadrada), `"round"` (LED más grande, tecla redonda) o `"pill"`. | | `momentary` | `False` | Encendido solo mientras se mantiene. | | `icon_name` | `""` | Un icono de Lucide junto a la etiqueta (`"play"`, `"circle"`...). | Eventos: `on_toggle`, `on_press`, `on_release`. **Tip:** Toca los pads también desde el teclado del ordenador: asigna teclas en `on_key` y pon `lit` según el estado de tu app, así el pad se enciende tanto si se hizo clic como si se tecleó. ### Caja de ritmos > Una caja de ritmos en Python como el channel rack de FL Studio — una cuadrícula de pasos que pintas, un pad por sonido, tempo y swing, con la precisión de muestra del motor de audio. Source: https://albakore.github.io/pyzed/es/audio/demos/drum-machine/ (Image: Una caja de ritmos sonando: el cabezal recorre una cuadrícula de pasos, un puntero pinta nuevos pasos de tom y los pads destellan) Una caja de ritmos de seis sonidos: [`examples/audio/drum_machine.py`](https://github.com/albakore/pyzed/blob/main/examples/audio/drum_machine.py). ```bash python examples/audio/drum_machine.py ``` Haz clic o arrastra en la [cuadrícula de pasos](https://albakore.github.io/pyzed/es/audio/components/step-sequencer/) para escribir el ritmo, toca cada sonido con su [pad](https://albakore.github.io/pyzed/es/audio/components/pads-and-buttons/) y cambia el tempo y el swing con las perillas. #### Por qué el ritmo es exacto El ritmo corre dentro del motor, no en Python. Un módulo `clock` marca cada semicorchea y mueve un `sequencer` por fila; el disparo de cada secuenciador golpea su `drum`: ```python clock = engine.add("clock", bpm=124, division="1/16") for sound in ("kick", "snare", "clap", "hihat", "hihat", "tom"): drum = engine.add("drum", sound=sound) seq = engine.add("sequencer", steps=16) engine.connect(clock, "clock", seq, "clock") engine.connect(seq, "trigger", drum, "trigger") ``` Así el ritmo se mantiene a tiempo por muy ocupada que esté la ventana. La cuadrícula solo cambia las puertas de los secuenciadores (`engine.set(seq, "gate_5", 1)`) y el cabezal lee el paso en el que están (`seq.state["step"]`). ### Secuenciador por pasos > Una cuadrícula de pasos como el channel rack de FL Studio para Python — haz clic o pinta pasos, filas con nombre, agrupación por pulsos y un cabezal que avanza. Source: https://albakore.github.io/pyzed/es/audio/components/step-sequencer/ (Image: Un patrón de batería de cinco filas con el cabezal recorriendo dieciséis pasos) **En pocas palabras:** Cada fila es un instrumento y cada cuadrado un momento del compás. Los cuadrados encendidos suenan y los oscuros no. Haz clic en un cuadrado para cambiarlo o arrastra por una fila para pintar varios a la vez. ```python class Beat(pz.App): pattern = ["x...x...x...x...", "....x.......x...", "x.x.x.x.x.x.x.x."] step = 0 def view(self): return ui.audio.StepSequencer( pattern=self.pattern, labels=["Kick", "Snare", "Hat"], current=self.step, on_change=lambda e: setattr(self, "pattern", e.value), ) ``` #### Patrones `pattern` es una lista de filas. Cada fila es una lista (`True`/`False`, o una velocidad de 0 a 1 que fija el brillo del paso) o un texto donde `x` (o `1`) es un paso que suena y cualquier otro carácter es un silencio: `"x...x...x...x..."` es un bombo en cada pulso. `change` entrega el patrón nuevo completo como lista de listas, listo para guardar. Un arrastre que empieza en un paso vacío pinta pasos; uno que empieza en un paso encendido los borra. #### Cabezal `current` es el paso que suena: el secuenciador enmarca esa columna y resalta sus pasos. Muévelo con un temporizador (`pz.every`) o con el secuenciador del motor (`node.state["step"]`). Los pasos se sombrean en grupos de `group` (4 por defecto) para contar los pulsos fácilmente. | Propiedad | Por defecto | Qué hace | | --- | --- | --- | | `pattern` | `()` | Filas de pasos. | | `labels` | `()` | Un nombre por fila (a la izquierda). | | `current` | `-1` | El paso que suena (`-1` para ninguno). | | `color` | `"accent"` | Color de los pasos que suenan. | | `group` | `4` | Pasos por grupo sombreado. | | `cell` | `22` | Tamaño de un paso en píxeles. | Evento: `on_change` con el patrón nuevo. **Tip:** La [demo de la caja de ritmos](https://albakore.github.io/pyzed/es/audio/demos/drum-machine/) une un secuenciador por pasos, una fila de pads y el módulo `drum` del motor: los pads se encienden con `lit=` mientras suenan los pasos. ### Sintetizador modular > Un sintetizador modular en Python como VCV Rack — módulos en rieles, cables que arrastras entre conectores, un explorador de módulos y patches guardados en JSON. Source: https://albakore.github.io/pyzed/es/audio/demos/modular/ (Image: Un rack modular: un cable arrastrado del delay al osciloscopio y después la perilla de corte del filtro girando) VCV Rack en unas 90 líneas: [`examples/audio/modular.py`](https://github.com/albakore/pyzed/blob/main/examples/audio/modular.py). ```bash python examples/audio/modular.py # un patch de inicio python examples/audio/modular.py patch.json # uno guardado ``` Se abre con una línea de bajo ácida: un secuenciador toca un oscilador a través de un filtro, una envolvente, un amplificador y un delay, con un osciloscopio y la salida. Después: - Añade módulos desde el explorador de la izquierda (busca o elige una categoría). - Arrastra de un conector a otro para conectar un cable; las salidas están en las placas oscuras. - Clic derecho en un conector para desconectarlo; arrastra una entrada conectada para mover su cable. - Arrastra un módulo por su título para moverlo; la × lo quita. - Ctrl+S guarda el patch y Ctrl+O lo vuelve a abrir. #### Cómo está hecho Toda la ventana es un componente [`Rack`](https://albakore.github.io/pyzed/es/audio/rack/rack/) sobre el motor: ```python engine = audio.Engine() starter_patch(engine) # engine.add(...) y engine.connect(...) class Modular(pz.App): layout = {} def view(self): return ui.rack.Rack(engine=self.engine, layout=self.layout, rows=2) ``` El rack lee los módulos y los cables del motor, dibuja un panel para cada módulo a partir de su descripción en el catálogo y escribe de vuelta cada cambio que haces. `ui.rack.save()` guarda el patch y la posición de cada módulo en un único archivo JSON. Mira [Cables animados](https://albakore.github.io/pyzed/es/audio/rack/cables/) para ver cómo se dibujan y animan los cables. ### Teclado de piano > Un teclado de piano clicable para sintetizadores en Python — glissando, velocidad, notas encendidas desde MIDI o un secuenciador y teclas del ordenador para tocar. Source: https://albakore.github.io/pyzed/es/audio/components/piano/ (Image: Un teclado de cuatro octavas iluminando una progresión de acordes y un arpegio) **En pocas palabras:** Un teclado que se toca con el ratón: haz clic en una tecla para tocar una nota y arrastra por las teclas para deslizarte de una a otra. Las notas que llegan de fuera (un teclado MIDI, un secuenciador) se iluminan. ```python from pyzed import audio, ui engine = audio.Engine() synth = engine.add("midi_in") # las notas de note_on/note_off llegan a los módulos midi_in ... ui.audio.Piano(start=48, octaves=3, pressed=sorted(self.held), on_note_on=lambda e: engine.note_on(e.value.note, e.value.velocity), on_note_off=lambda e: engine.note_off(e.value)) ``` #### Notas y velocidad Las notas son números MIDI: 60 es el do central (C4) y 69 el la de 440 Hz. `note_on` entrega un `Note(note, velocity)`; cuanto más abajo de una tecla haces clic, más fuerte suena (velocidad de 30 a 127). `note_off` entrega el número de nota. Al arrastrar por las teclas se suelta una y se pulsa la siguiente. `ui.audio.note_name(61)` devuelve `"C#4"`, útil para etiquetas. #### Tocar con el teclado del ordenador `ui.audio.KEYBOARD` asigna teclas a semitonos como la mayoría de apps de música: la fila `a s d f g h j k` son las teclas blancas y `w e t y u o p` las negras. ```python def on_key(self, combo): if combo in ui.audio.KEYBOARD and combo not in self.keys: self.keys.add(combo) note = self.octave * 12 + ui.audio.KEYBOARD[combo] engine.note_on(note) def on_key_up(self, key): if key in self.keys: self.keys.discard(key) engine.note_off(self.octave * 12 + ui.audio.KEYBOARD[key]) ``` | Propiedad | Por defecto | Qué hace | | --- | --- | --- | | `start` | `48` | La primera nota (redondeada hacia abajo a un do). | | `octaves` | `2` | Cuántas octavas (más el último do). | | `pressed` | `()` | Notas que se iluminan, tocadas desde fuera. | | `color` | `"accent"` | Color de las teclas pulsadas. | | `key_width`, `height` | `24`, `110` | Tamaño de una tecla blanca. | | `labels` | `True` | Escribe el nombre de cada do. | Eventos: `on_note_on` (`Note(note, velocity)`), `on_note_off` (el número de nota). ### Visor de webcam > Un visor de webcam en Python — la imagen en vivo, un botón de captura que guarda archivos PNG y los fotogramas por segundo medidos, con OpenCV, ffmpeg o un patrón de prueba. Source: https://albakore.github.io/pyzed/es/audio/demos/webcam/ (Image: La imagen en vivo de una cámara con una insignia de fotogramas por segundo) Una app de cámara en 50 líneas: [`examples/audio/webcam.py`](https://github.com/albakore/pyzed/blob/main/examples/audio/webcam.py). ```bash python examples/audio/webcam.py # la cámara por defecto python examples/audio/webcam.py test # barras de colores en movimiento, sin cámara ``` **Snapshot** guarda la imagen actual como PNG junto al script y **Pause** la congela. ```python from pyzed import camera, ui cam = camera.Camera(0, width=1280, height=720, mirror=True) cam.start() def view(self): return pz.column([ ui.camera.Webcam(camera=cam, width=720, height=405, radius=10), ui.button("Snapshot", lambda: cam.snapshot("photo.png")), ]) ``` La cámara se lee en su propio hilo; cada imagen nueva pide a la ventana que se redibuje, así que la app no necesita temporizador. Usa OpenCV si está instalado (`pip install opencv-python`), si no el programa `ffmpeg` y si no el patrón de prueba. Mira [Webcam](https://albakore.github.io/pyzed/es/audio/components/webcam/) para todas las opciones. ### Vúmetro > Medidores de nivel para apps de audio en Python — segmentos LED, una barra con degradado o un vúmetro analógico de aguja con inercia, en decibelios, con retención de picos y cualquier color. Source: https://albakore.github.io/pyzed/es/audio/components/vu-meter/ (Image: Medidores LED, de degradado y de cuatro canales saltando con el ritmo junto a un vúmetro analógico de aguja) **En pocas palabras:** Un medidor muestra lo fuerte que suena algo en este momento. El verde está bien, el amarillo es fuerte y el rojo está a punto de distorsionar. La rayita que se queda arriba un momento es el último pico. ```python rms, peak = engine.level() # la salida principal, 0..1 ui.audio.VuMeter(level=[rms, peak]) # una barra por valor ``` #### Niveles y escala `level` es un número o una lista (una barra por canal), como amplitud lineal de 0 a 1, que es lo que devuelven `node.level()` y `engine.level()`. Con `scale="db"` (por defecto) el medidor lo muestra en decibelios desde `floor` (−60 dB) hasta 0 dB, como los mezcladores: los sonidos suaves también lo mueven. `scale="linear"` muestra el número tal cual (útil para señales de control). Cada canal retiene su pico durante `hold` segundos y luego lo deja caer. #### Estilos | `variant` | Se parece a | | --- | --- | | `"led"` | Segmentos que se encienden (`segments` en total), apagados en tenue. | | `"bar"` | Una barra continua con los colores en degradado. | | `"analog"` | Un vúmetro de aguja (−20 a +3 VU) que se mueve con la inercia de unos 300 ms de uno real. | `colors` es el nombre de una paleta (`"vu"`, `"fl"`, `"neon"`, `"fire"`... ver [Colores](https://albakore.github.io/pyzed/es/audio/components/colors/)) o una lista de colores de suave a fuerte. `vertical=False` lo coloca en horizontal. | Propiedad | Por defecto | Qué hace | | --- | --- | --- | | `level` | `0` | Un nivel o una lista de niveles (0..1). | | `variant` | `"led"` | `"led"`, `"bar"` o `"analog"`. | | `vertical` | `True` | Dirección de las barras. | | `length`, `thickness` | `160`, `10` | Tamaño de cada barra en píxeles. | | `segments` | `24` | LEDs por barra. | | `scale`, `floor` | `"db"`, `-60` | Cómo se traduce un nivel al medidor. | | `hold` | `1.2` | Segundos que se retiene un pico. | | `colors` | `"vu"` | Un nombre de paleta o una lista de colores. | **Tip:** Los medidores se tienen que redibujar en cada fotograma. Añade `pz.frames(lambda dt: None)` a las `subscriptions()` de tu app mientras suena el motor y lee los niveles en `view()`. ### Forma de onda > Dibuja formas de onda de audio en Python — vista de barras de app de música con parte reproducida y por reproducir, envolvente, línea luminosa, cabezal, selecciones y salto de posición. Source: https://albakore.github.io/pyzed/es/audio/components/waveform/ (Image: Tres formas de onda: barras redondeadas con cabezal, una envolvente en espejo con una selección y una línea luminosa en vivo) **En pocas palabras:** Una forma de onda es el dibujo de un sonido a lo largo del tiempo: alta donde suena fuerte y plana donde suena bajo. Los reproductores de música la usan para mostrar dónde estás en una canción y dejarte hacer clic para saltar. ```python left, right = engine.render(5.0) # cinco segundos renderizados sin tarjeta de sonido ui.audio.Waveform(samples=left, variant="bars", playhead=self.position, on_seek=lambda e: self.seek(e.value)) ``` #### Muestras `samples` son números de −1 a 1: un archivo entero o el último búfer de una señal en vivo (`node.scope(1024)`). Una lista larga se reduce a un par mínimo/máximo por columna de píxeles, así que un minuto de audio se dibuja tan rápido como un búfer corto. `gain` la amplía cuando el sonido es suave. #### Estilos | `variant` | Se parece a | | --- | --- | | `"mirror"` | La envolvente, simétrica alrededor de la línea central (por defecto). | | `"filled"` | La forma real, rellena entre sus puntos más bajos y más altos. | | `"line"` | Una línea por cada muestra; `glow=True` añade un halo suave. Ideal para búferes cortos. | | `"bars"` | Barras redondeadas como una app de música: la parte reproducida en `color` y el resto atenuado. | | `"dots"` | Un punto por muestra. | #### Cabezal, selección y salto - `playhead` (0..1) dibuja la posición de reproducción (con `"bars"` separa lo reproducido de lo pendiente). - `selection=(0.2, 0.4)` resalta un rango: un bucle, una región para cortar. - Con `on_seek`, hacer clic o arrastrar sobre ella informa la posición (0..1) en `event.value`. | Propiedad | Por defecto | Qué hace | | --- | --- | --- | | `samples` | `()` | Valores de −1 a 1. | | `width`, `height` | `360`, `96` | Tamaño en píxeles. | | `variant` | `"mirror"` | Ver la tabla de arriba. | | `color`, `background` | `"accent"`, `"#0f1012"` | Colores. | | `gain`, `thickness`, `glow` | `1`, `1.5`, `False` | Escala, grosor de línea y halo. | | `grid` | `True` | Líneas verticales tenues y una línea central. | | `playhead`, `selection` | `None` | Posición (0..1) y rango `(inicio, fin)`. | | `bar` | `3` | Ancho de una barra con `variant="bars"`. | Evento: `on_seek` (0..1). ### Osciloscopio > Un osciloscopio para Python — varias trazas sobre una rejilla, un disparo que mantiene quietas las ondas periódicas, modo XY de Lissajous, persistencia de fósforo y brillo. Source: https://albakore.github.io/pyzed/es/audio/components/scope/ (Image: Un osciloscopio de dos trazas con persistencia que se desvanece junto a un osciloscopio XY dibujando una figura de Lissajous que gira) **En pocas palabras:** Un osciloscopio dibuja una señal como un instrumento de laboratorio: unos pocos milisegundos sobre una rejilla verde. Así ves la forma de una onda (senoidal, sierra, cuadrada) mientras conectas. ```python ui.audio.Scope(traces=[osc.scope(1024), lfo.scope(1024)], width=320, height=180) ``` #### Trazas y disparo `traces` es una lista de señales (o una sola lista de números), cada una dibujada con el siguiente color de `colors`. Con `trigger=True` cada traza empieza en el primer punto donde la onda sube cruzando el cero, así un tono estable se queda quieto en lugar de desplazarse. `window` es cuántas muestras se muestran. #### Modo XY `xy=True` dibuja la primera traza contra la segunda: la primera mueve el punto a izquierda y derecha y la segunda arriba y abajo. Dos senoidales forman **figuras de Lissajous**; un par estéreo (izquierda, derecha) muestra lo ancha que es la imagen estéreo: una línea vertical es mono y una nube es amplia. #### Fósforo `persistence=3` deja copias que se desvanecen de los tres últimos fotogramas, como el brillo de una pantalla CRT antigua; `glow=True` añade un halo suave alrededor de la traza más reciente. | Propiedad | Por defecto | Qué hace | | --- | --- | --- | | `traces` | `()` | Señales a dibujar (−1..1). | | `width`, `height` | `320`, `180` | Tamaño en píxeles. | | `colors` | `"scope"` | Un nombre de paleta o una lista (un color por traza). | | `background` | `"#0b120e"` | La pantalla. | | `columns`, `rows` | `8`, `6` | Divisiones de la rejilla. | | `gain` | `1` | Zoom vertical. | | `trigger`, `window` | `True`, `512` | Mantiene quietas las ondas periódicas; muestras visibles. | | `xy` | `False` | Modo Lissajous. | | `persistence`, `glow` | `0`, `True` | Copias que se desvanecen y halo. | ### Analizador de espectro > Un analizador de espectro en tiempo real para Python — barras, bloques LED, líneas, en espejo o radial, bandas logarítmicas, caída suave, picos y paletas de colores. Source: https://albakore.github.io/pyzed/es/audio/components/spectrum/ (Image: Seis analizadores de espectro bailando con la misma canción: barras, bloques, línea, relleno, espejo y radial) **En pocas palabras:** Un analizador de espectro separa un sonido en sus tonos: graves a la izquierda, agudos a la derecha y, cuanto más alta la barra, más fuerte suena ese tono. Es el visualizador de música clásico. ```python class Visualizer(pz.App): def subscriptions(self): return [pz.frames(lambda dt: None)] # redibuja en cada fotograma def view(self): return ui.audio.Spectrum(magnitudes=engine.spectrum(512), variant="blocks", colors="fl") ``` #### De bins a bandas `magnitudes` son decibelios (−120 a 0), uno por bin de frecuencia: justo lo que devuelven `node.spectrum()` y `engine.spectrum()`. El analizador los agrupa en `bands` bandas espaciadas de forma **logarítmica** (cada banda un poco más ancha que la anterior), como oye el oído: una octava ocupa el mismo ancho en 100 Hz que en 5 kHz. Cada banda muestra su bin más fuerte. `spacing` indica cómo están espaciados tus bins: - `"log"` (por defecto): ya espaciados de forma logarítmica de 20 Hz a la mitad de la frecuencia de muestreo, como el espectro del motor. Solo se reagrupan en `bands`. - `"linear"`: espaciados por igual desde 0 Hz, como la salida de una FFT simple (`numpy.fft.rfft`). El analizador calcula qué bins van en cada banda a partir de `sample_rate`. `min_db` y `max_db` son la parte de abajo y de arriba de la pantalla. #### Movimiento Las barras suben de golpe y caen a `falloff` decibelios por segundo (0 hace que sigan la entrada exactamente), así la pantalla se ve suave aunque la entrada parpadee. Con `peaks=True` una pequeña tapa se queda encima de cada barra y baja más despacio. La velocidad se mide en tiempo real, así que es la misma a 30 o a 144 fotogramas por segundo. #### Estilos y colores | `variant` | Se parece a | | --- | --- | | `"bars"` | Barras sólidas. | | `"blocks"` | Segmentos LED, como un equipo de alta fidelidad. | | `"line"` | Una línea luminosa por la parte alta de las bandas. | | `"filled"` | La misma línea con el área de debajo rellena. | | `"mirror"` | Barras centradas en vertical, con un aspecto simétrico. | | `"radial"` | Barras alrededor de un círculo, como un visualizador de música. | `colors` es una paleta (`"fl"`, `"neon"`, `"fire"`, `"ice"`, `"magma"`, `"viridis"`...) o una lista de colores. `color_by="level"` pinta cada barra de suave (abajo) a fuerte (arriba); `color_by="frequency"` da a cada banda su propio color de graves a agudos. `labels=True` marca 100 Hz, 1 kHz y 10 kHz. | Propiedad | Por defecto | Qué hace | | --- | --- | --- | | `magnitudes` | `()` | dB por bin de frecuencia. | | `width`, `height` | `360`, `140` | Tamaño en píxeles. | | `variant` | `"bars"` | Ver la tabla de arriba. | | `bands` | `48` | Número de bandas. | | `spacing` | `"log"` | Cómo están espaciadas las `magnitudes`: `"log"` (el motor) o `"linear"` (una FFT simple). | | `min_db`, `max_db` | `-90`, `0` | Rango mostrado. | | `colors`, `color_by` | `"fl"`, `"level"` | Paleta y qué sigue. | | `falloff`, `peaks` | `48`, `True` | Velocidad de caída (dB/s) y tapas de pico. | | `sample_rate` | `48000` | Para las etiquetas y los bordes de las bandas. | | `labels`, `gap`, `background` | `False`, `2`, `"#0f1012"` | | **Tip:** `ui.audio.bands(magnitudes, 16)` te da las mismas bandas logarítmicas como números, para mover cualquier otra cosa con la música: el tamaño de un logo, el color de la ventana, un sistema de partículas. ### Espectrograma > Un espectrograma desplazable (cascada) para Python — frecuencia a lo largo del tiempo en color, con magma, viridis o cualquier paleta, dibujado lo bastante rápido para audio en vivo. Source: https://albakore.github.io/pyzed/es/audio/components/spectrogram/ (Image: Dos espectrogramas desplazándose a la izquierda mientras suena una canción: los bombos como columnas abajo y los acordes como líneas brillantes) **En pocas palabras:** Un espectrograma es un analizador de espectro con memoria: el tiempo corre de izquierda a derecha, el tono de abajo arriba y el color dice lo fuerte que sonó cada tono. En él se ven el ritmo y los acordes de una canción. ```python ui.audio.Spectrogram(magnitudes=engine.spectrum(512), history=160, colors="magma") ``` Cada render que recibe una lista `magnitudes` **nueva** añade una columna a la derecha; redibujar por cualquier otro motivo (pasar el ratón, otro widget) no la añade, así que la velocidad sigue a tus actualizaciones. Conserva las últimas `history` columnas. El volumen se reduce a `levels` colores y todas las celdas de un mismo color se dibujan en una sola forma, así que incluso 160 columnas × 64 bandas se dibujan con poco coste. | Propiedad | Por defecto | Qué hace | | --- | --- | --- | | `magnitudes` | `()` | dB por bin de frecuencia (como `node.spectrum()`). | | `width`, `height` | `360`, `140` | Tamaño en píxeles. | | `history` | `120` | Columnas que se conservan. | | `bands` | `48` | Filas (bandas logarítmicas). | | `min_db`, `max_db` | `-90`, `0` | Rango de los colores. | | `colors`, `levels` | `"magma"`, `24` | Paleta y cuántos pasos de color. | | `sample_rate`, `spacing` | `48000`, `"log"` | Como en [`Spectrum`](https://albakore.github.io/pyzed/es/audio/components/spectrum/): cómo están espaciados los bins. | ### Colores y comportamiento > Personaliza los componentes de audio de Pyzed — paletas de colores, tokens del tema, estilos, tamaños y cómo se mueven los medidores — y crea tus propios visuales con las mismas utilidades. Source: https://albakore.github.io/pyzed/es/audio/components/colors/ (Image: Un espectro pasando por todas las paletas incluidas) **En pocas palabras:** Cada componente de audio puede cambiar sus colores y su aspecto sin tocar su código: elige una paleta por nombre, pasa tus propios colores o deja que siga el tema de la app. #### Colores Cualquier `color=` acepta un **token del tema** (`"accent"`, `"success"`, `"warning"`, `"error"`...) o un color hex (`"#ff9f3d"`). Los tokens siguen al tema: cambia a un tema claro y las perillas lo siguen. Los medidores y analizadores aceptan `colors=`: un nombre de paleta o una lista de colores, de suave a fuerte (o de graves a agudos con `color_by="frequency"`). | Paleta | Colores | | --- | --- | | `"fl"` | Verde → amarillo → naranja → rojo, como los medidores de FL Studio. | | `"vu"` | Casi todo verde, amarillo cerca de arriba y rojo en lo más alto. | | `"neon"` | Cian → violeta → rosa. | | `"fire"` | Morado oscuro → rojo → naranja → amarillo → blanco. | | `"ice"` | Azul marino → azul → cian claro. | | `"magma"`, `"viridis"` | Los mapas de color científicos, ideales para espectrogramas. | | `"mono"` | Gris a blanco. | | `"scope"` | Los colores de las trazas del osciloscopio. | Añade las tuyas a `ui.audio.PALETTES` y úsalas por nombre en cualquier parte: ```python ui.audio.PALETTES["sunset"] = ["#2d1b69", "#f72585", "#ffbe0b"] ui.audio.Spectrum(magnitudes=mags, colors="sunset") ``` #### Comportamiento | Qué | Dónde | | --- | --- | | Aspecto | `variant=` en cada componente ([perilla](https://albakore.github.io/pyzed/es/audio/components/knob/), [fader](https://albakore.github.io/pyzed/es/audio/components/fader/), [medidor](https://albakore.github.io/pyzed/es/audio/components/vu-meter/), [espectro](https://albakore.github.io/pyzed/es/audio/components/spectrum/)...). | | Cuánto gira un control al arrastrar | `travel=` en las perillas, `length=` en los faders; Shift siempre es diez veces más fino. | | Cómo se traduce un valor al control | `curve="log"`, `step=`, `options=`, `bipolar=True`. | | Cómo se mueven los medidores | `hold=` (retención de pico), `falloff=` y `peaks=` (analizadores), la inercia de `variant="analog"`. | | Cómo lee un osciloscopio | `trigger=`, `window=`, `persistence=`, `xy=`. | Todos son `pz.Component`, así que `style=`, `class_=`, las hojas de estilo y `animate=` funcionan como en cualquier otro componente (ver [Componentes](https://albakore.github.io/pyzed/es/guides/components/)). #### Crear los tuyos Las utilidades son públicas: `ui.audio.bands()` (de bins de FFT a bandas logarítmicas), `ui.audio.format_value()` (`1.2 kHz`, `-6.0 dB`), `ui.audio.note_name()` y `ui.audio.PALETTES`. Cada componente tiene además un método `shapes()` que devuelve su dibujo como formas de `pyzed.draw`, así puedes poner una perilla o un medidor dentro de tu propio `pz.canvas`. **Tip:** Los visuales de audio se mueven en cada fotograma. Devuelve `pz.frames(lambda dt: None)` desde `subscriptions()` mientras algo suena, y nada cuando se detiene, así la app descansa cuando está inactiva. ### Webcam > Muestra una webcam en vivo en una app de escritorio en Python con Pyzed — OpenCV o ffmpeg en Windows, macOS y Linux, capturas, fotogramas RGBA y un patrón de prueba. Source: https://albakore.github.io/pyzed/es/audio/components/webcam/ (Image: Una vista de webcam con una insignia de grabación mostrando el patrón de prueba en movimiento) **En pocas palabras:** Pon la imagen en vivo de una cámara en tu ventana con una línea. Abre la cámara cuando aparece y la cierra cuando desaparece. ```python ui.camera.Webcam(device=0, width=480, height=360, mirror=True) ``` #### Cómo llegan los fotogramas La cámara se lee en un hilo en segundo plano. Cada fotograma nuevo pide a la ventana que se redibuje, así la vista se mantiene en vivo sin temporizador y sin frenar el resto de la app. Dos `Webcam` del mismo dispositivo comparten una única captura. La captura usa lo primero que esté disponible, en este orden (`backend="auto"`): | Backend | Necesita | Notas | | --- | --- | --- | | `"opencv"` | `pip install opencv-python` | El más compatible en todos los sistemas. | | `"ffmpeg"` | el programa `ffmpeg` | Video4Linux en Linux, AVFoundation en macOS, DirectShow en Windows. | | `"test"` | nada | Barras de colores en movimiento: para capturas, tests y equipos sin cámara. | #### Usar los fotogramas `pyzed.camera.Camera` te da los fotogramas para procesarlos (detectar caras, medir el brillo, grabar): ```python from pyzed import camera cam = camera.Camera(0, width=640, height=480, mirror=True) cam.start() frame = cam.read() # espera al siguiente Frame(width, height, pixels, index, time) cam.snapshot("photo.png") # se guarda sin Pillow cam.on_frame.append(lambda f: print(f.index)) # se ejecuta en el hilo de la cámara cam.stop() ``` `frame.pixels` son bytes RGBA (`width * height * 4`): `numpy.frombuffer(frame.pixels, "uint8")` los convierte en un array y `pz.image(frame.pixels, size=(frame.width, frame.height))` los dibuja. `camera.devices()` lista las cámaras encontradas. | Propiedad | Por defecto | Qué hace | | --- | --- | --- | | `device` | `0` | Índice de la cámara, una ruta (`"/dev/video2"`) o, en Windows con ffmpeg, su nombre. | | `camera` | `None` | Un `Camera` tuyo para mostrar en su lugar. | | `width`, `height` | `320`, `240` | Tamaño de la vista. | | `capture_width`, `capture_height`, `fps` | `640`, `480`, `30` | Lo que se le pide a la cámara. | | `mirror` | `True` | La voltea como un espejo. | | `fit` | `"cover"` | `"cover"` recorta para llenar, `"contain"` la muestra entera. | | `radius` | `8` | Esquinas redondeadas. | | `overlay` | `True` | Una insignia con un punto de grabación y los fotogramas por segundo. | | `paused` | `False` | Congela el último fotograma. | | `backend` | `"auto"` | Ver arriba. | **Caution:** En macOS la primera vez se pide permiso para usar la cámara; lo recibe el programa que ejecuta Python (la Terminal, tu editor). ## Integraciones ### Resumen de integraciones > Usa Pyzed como frontend de escritorio para FastAPI, Django, SQLAlchemy, SQLModel, httpx, Pydantic y aiosqlite, con demos que siguen el tutorial oficial de cada librería. Source: https://albakore.github.io/pyzed/es/integrations/overview/ :::note[Demos que puedes ejecutar] Cada integración tiene una demo completa en `examples/integrations` que se ejecuta con uv, pip o python. Con el tiempo se añadirán más bibliotecas. ::: Pyzed es solo la interfaz: el resto de tu app es Python normal. Por eso las librerías que ya usas para backends web, bases de datos y HTTP funcionan sin cambios en una app de escritorio. Esta sección muestra cómo conectar las más comunes. Cada demo parte del tutorial oficial de la propia librería (el tutorial de SQL de FastAPI, la app de encuestas de Django, el Quick Start de SQLAlchemy, los héroes de SQLModel) y le añade una ventana de Pyzed. Cuando un backend tiene que ejecutarse junto a la ventana, [Arquitecturas](https://albakore.github.io/pyzed/es/integrations/architectures/) explica las cuatro formas de combinarlos, con un diagrama para cada una: mismo proceso, cliente y servidor, servidor integrado y API en el proceso. | Demo | Librerías | Qué muestra | Tutorial | | --- | --- | --- | --- | | [`fastapi_heroes`](https://github.com/albakore/pyzed/tree/main/examples/integrations/fastapi_heroes) | FastAPI, SQLModel, httpx | La API del tutorial de SQL de FastAPI y un cliente de escritorio para ella, ejecutados de tres formas: servidor aparte, servidor integrado y en el proceso. | [FastAPI](https://albakore.github.io/pyzed/es/integrations/fastapi/) | | [`django_polls`](https://github.com/albakore/pyzed/tree/main/examples/integrations/django_polls) | Django, httpx | El tutorial de encuestas de Django, usado desde una ventana con el ORM en el mismo proceso o por HTTP mientras corre runserver. | [Django](https://albakore.github.io/pyzed/es/integrations/django/) | | [`sqlalchemy_quickstart`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlalchemy_quickstart) | SQLAlchemy 2.0, aiosqlite | Los usuarios y direcciones del ORM Quick Start, con una Session síncrona en hilos o una AsyncSession en el loop. | [SQLAlchemy](https://albakore.github.io/pyzed/es/integrations/sqlalchemy/) | | [`sqlalchemy_contacts`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlalchemy_contacts) | SQLAlchemy 2.0 | 20.000 filas en una tabla virtual, con búsqueda y orden en SQL desde un hilo de trabajo. | [SQLAlchemy](https://albakore.github.io/pyzed/es/integrations/sqlalchemy/#muchas-filas-la-demo-de-contactos) | | [`sqlmodel_heroes`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlmodel_heroes) | SQLModel | El tutorial de SQLModel: equipos, héroes, filtros con where(), relaciones y un formulario validado. | [SQLModel](https://albakore.github.io/pyzed/es/integrations/sqlmodel/) | | [`sqlmodel_inventory`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlmodel_inventory) | SQLModel, Pydantic | Un solo modelo para validar el formulario y definir la tabla. | [SQLModel](https://albakore.github.io/pyzed/es/integrations/sqlmodel/#un-modelo-para-el-formulario-y-la-tabla-la-demo-de-inventario) | | [`httpx_pypi`](https://github.com/albakore/pyzed/tree/main/examples/integrations/httpx_pypi) | httpx, Pydantic | Peticiones concurrentes que llegan a la ventana a medida que responden, convertidas en modelos. | [httpx](https://albakore.github.io/pyzed/es/integrations/httpx/) | | [`httpx_download`](https://github.com/albakore/pyzed/tree/main/examples/integrations/httpx_download) | httpx | Descargas en streaming con barras de progreso y cancelación. | [httpx](https://albakore.github.io/pyzed/es/integrations/httpx/#descargas-en-streaming-con-progreso) | | [`pydantic_signup`](https://github.com/albakore/pyzed/tree/main/examples/integrations/pydantic_signup) | Pydantic | Un formulario de registro validado por un modelo, guardado y cargado como JSON. | [Pydantic](https://albakore.github.io/pyzed/es/integrations/pydantic/) | | [`aiosqlite_journal`](https://github.com/albakore/pyzed/tree/main/examples/integrations/aiosqlite_journal) | aiosqlite | Una conexión SQLite async que vive en el loop de Pyzed. | [Otras librerías](https://albakore.github.io/pyzed/es/integrations/other-libraries/#aiosqlite) | #### Ejecutar una demo Cada demo es un pequeño proyecto de [uv](https://docs.astral.sh/uv/). Desde su carpeta: La primera vez, `uv run` crea el entorno virtual e instala las dependencias del `pyproject.toml` de la demo. Dentro del repositorio las demos usan tu copia de Pyzed (la tabla `[tool.uv.sources]`); para empezar tu propia app a partir de una, copia la carpeta y borra esas dos líneas para que `pyzed-ui` venga de PyPI. #### Dónde se ejecuta tu código Una app de Pyzed ejecuta código en tres sitios. Saber en cuál cae cada llamada a una librería es todo lo que hace falta para integrar cualquier cosa: ```mermaid flowchart LR UI["Hilo de la UI
view() · update() · callbacks"] Loop["Loop de asyncio (un hilo)
corrutinas · generadores async"] Threads["Hilos de trabajo
Command.perform(función)"] UI -- "devolver una corrutina" --> Loop UI -- "Command.perform(fn, then)" --> Threads Loop -- "resultado → update()" --> UI Threads -- "then(resultado)" --> UI ``` | La librería es… | Úsala con | Ejemplos | | --- | --- | --- | | **async** (`await ...`) | Devuelve una corrutina o un generador async desde `update`, un callback u `on_start`. Su resultado va a `update`. | `AsyncClient` de httpx, aiosqlite, ORM async de Django, `AsyncSession` de SQLAlchemy, websockets, redis.asyncio | | **bloqueante** | `Command.perform(función, then)`: `función` corre en un hilo y `then(resultado)` en el hilo de la UI. | Sesiones de SQLAlchemy y SQLModel, requests, pandas, archivos | | **un servidor** | Ejecútalo aparte (`fastapi dev`, `manage.py runserver`) o en un hilo daemon antes de `pz.run`. Mira [Arquitecturas](https://albakore.github.io/pyzed/es/integrations/architectures/). | FastAPI, Django, Starlette | | **un stream** | `pz.listen(generador_async)` en `subscriptions()`. | websockets, server-sent events, pub/sub de Redis | Tres reglas lo mantienen seguro: 1. **Modifica `self` en el hilo de la UI.** Las corrutinas y los hilos *devuelven* resultados; `update` o `then` los guardan. Leer `self` desde una corrutina está bien. 2. **No bloquees nunca el hilo de la UI.** Una consulta de 50 ms congela la ventana 50 ms. Usa `Command.perform` o una librería async. 3. **Descarta respuestas viejas.** Mientras el usuario escribe una búsqueda, los resultados pueden llegar desordenados. Envía la consulta junto con la petición y descarta las respuestas de una anterior (todas las demos lo hacen en su manejador `loaded`). Todas las corrutinas comparten **un** loop de asyncio (`pz.asyncio_loop()`), así que los objetos atados a un loop, como un `httpx.AsyncClient` o una conexión de `aiosqlite`, se crean una vez y se reutilizan. #### Errores de validación en cada campo La mayoría de los backends informan de una entrada inválida con una lista de errores por campo. `pz.validation_errors(error)` convierte cualquiera de ellos en `{campo: mensaje}`, y `ui.field(..., error=)` muestra el mensaje bajo el título del campo, con el color de error del tema: ```python from pydantic import ValidationError def save(self): try: item = ItemIn.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) return ... def view(self): return ui.field("Price", ui.input(self.draft["price"], self.set_price), error=self.errors.get("price")) ``` Entiende: - `ValidationError` de Pydantic y SQLModel (los mensajes de tus `ValueError` pierden el prefijo `Value error,`), - el JSON de una respuesta **422** de FastAPI, `{"detail": [...]}`, sin la parte `body`/`query` de cada ubicación, y un `{"detail": "Not found"}` simple, - el `ValidationError` de Django que lanza `full_clean()` (los errores `__all__` usan la clave `""`). Los campos anidados se unen con puntos (`"address.city"`) y los errores que no son de ningún campo usan la clave `""`, que las demos muestran en un `ui.callout`. #### Selects con objetos `ui.select` acepta cualquier objeto como opción y lo muestra con `str()`: un modelo de Django, un enum, una dataclass. `on_select` recibe el objeto, no su texto: ```python ui.select(self.authors, self.draft["author"], lambda author: self.draft.update(author=author)) ``` ### Arquitecturas > Las formas en que una app de escritorio de Pyzed puede convivir con FastAPI, Django, SQLAlchemy, SQLModel y otros backends, con un diagrama y una demo ejecutable para cada una. Source: https://albakore.github.io/pyzed/es/integrations/architectures/ Una ventana de Pyzed es un programa de Python, así que una librería de backend puede colocarse a su lado de cuatro formas distintas. La elección depende de una pregunta: **¿quién más necesita los datos?** | Arquitectura | Quién usa los datos | Red | Demos | | --- | --- | --- | --- | | [1. Mismo proceso](#1-mismo-proceso) | Solo esta app de escritorio | Ninguna | SQLAlchemy, SQLModel, ORM de Django, aiosqlite, Pydantic | | [2. Cliente y servidor](#2-cliente-y-servidor) | Una web, una app móvil, otros usuarios | HTTP a un servidor | FastAPI `client.py`, Django `client.py`, httpx | | [3. Servidor integrado](#3-servidor-integrado) | Esta app y otros programas del mismo ordenador | Puerto local | FastAPI `embedded.py` | | [4. API en el proceso](#4-api-en-el-proceso) | Solo esta app, pero el código es una API | Ninguna | FastAPI `in_process.py` | Las cuatro pueden compartir el mismo código: los modelos y la API no saben si los llama una ventana, un navegador o un test. Eso es lo que permite que una app empiece con la arquitectura 1 y pase a la 2 más adelante. #### 1. Mismo proceso La ventana importa la librería y la llama directamente. No hay servidor y nada escucha en la red. Es la opción por defecto para una app de escritorio con su propia base de datos. ```mermaid flowchart LR subgraph app["Tu programa (uv run main.py)"] W["Ventana de Pyzed
view · update"] T["Hilos de trabajo
Command.perform"] L["Loop de asyncio
corrutinas"] M["Modelos y consultas
SQLAlchemy · SQLModel · ORM de Django"] W --> T --> M W --> L --> M end M --> DB[("SQLite / PostgreSQL")] ``` - **Las librerías bloqueantes** (una `Session` de SQLAlchemy, SQLModel, el ORM de Django) corren en un hilo de trabajo con `Command.perform(función, then)`. - **Las librerías async** (`AsyncSession`, aiosqlite) corren como corrutinas en el loop de Pyzed. - La base de datos se puede seguir compartiendo: el `desktop.py` de la [demo de Django](https://albakore.github.io/pyzed/es/integrations/django/) y `manage.py runserver` usan el mismo `db.sqlite3` a la vez. Demos: [`sqlalchemy_quickstart`](https://albakore.github.io/pyzed/es/integrations/sqlalchemy/) (los dos backends), [`sqlmodel_heroes`](https://albakore.github.io/pyzed/es/integrations/sqlmodel/), [`django_polls`](https://albakore.github.io/pyzed/es/integrations/django/) `desktop.py`, [`aiosqlite_journal`](https://albakore.github.io/pyzed/es/integrations/other-libraries/#aiosqlite), [`pydantic_signup`](https://albakore.github.io/pyzed/es/integrations/pydantic/). #### 2. Cliente y servidor El backend es un programa aparte, que se arranca con su propia herramienta (`fastapi dev`, `manage.py runserver`, un despliegue en un servidor). La ventana es un cliente más y solo habla HTTP, normalmente con httpx. ```mermaid flowchart LR subgraph server["Proceso del servidor (fastapi dev main.py · manage.py runserver)"] API["FastAPI / Django
rutas y vistas"] --> DB[("Base de datos")] end subgraph desktop["Proceso de escritorio (uv run client.py)"] W["Ventana de Pyzed"] --> H["httpx.AsyncClient"] end H -- "HTTP / JSON" --> API B["Navegador · app móvil"] -- "HTTP" --> API ``` - Úsala cuando los datos se deben compartir entre usuarios o dispositivos, o cuando la API ya existe. - La ventana no necesita tener instalados ni el driver de la base de datos ni el framework del backend. - Los errores llegan como códigos HTTP: un 422 de FastAPI va directo a `pz.validation_errors`. Demos: [`fastapi_heroes`](https://albakore.github.io/pyzed/es/integrations/fastapi/) `client.py`, [`django_polls`](https://albakore.github.io/pyzed/es/integrations/django/) `client.py`, [`httpx_pypi`](https://albakore.github.io/pyzed/es/integrations/httpx/) (una API pública). #### 3. Servidor integrado Un solo programa arranca la API en un hilo en segundo plano con Uvicorn y luego abre la ventana. La API está disponible en un puerto local mientras la ventana está abierta, para un navegador, un script u otra app. ```mermaid flowchart LR subgraph app["Tu programa (uv run embedded.py)"] direction LR W["Ventana de Pyzed
hilo principal"] -- "httpx" --> U U["Uvicorn en un hilo daemon
app de FastAPI"] --> DB[("Base de datos")] end B["Navegador en /docs
otros programas locales"] -- "http://127.0.0.1:8000" --> U ``` - Úsala para una herramienta que también expone una API o una página web, como un panel local o un dispositivo que recibe webhooks. - El servidor se detiene con la ventana, porque su hilo es daemon. - Escucha en `127.0.0.1` salvo que otros ordenadores tengan que conectarse. Demo: [`fastapi_heroes`](https://albakore.github.io/pyzed/es/integrations/fastapi/#3-servidor-integrado) `embedded.py`. #### 4. API en el proceso httpx puede enviar peticiones directamente a una app ASGI con `httpx.ASGITransport`, sin servidor ni puerto. El código se escribe como una API y un cliente, pero se ejecuta como un único programa sin conexión. ```mermaid flowchart LR subgraph app["Tu programa (uv run in_process.py)"] W["Ventana de Pyzed"] --> H["httpx.AsyncClient
transport=ASGITransport(app)"] H -- "llamada a función, sin red" --> API["App de FastAPI"] API --> DB[("Base de datos")] end ``` - Úsala para distribuir la misma API como una app de escritorio que funciona sin conexión, o para probar el cliente. - `ASGITransport` no ejecuta los eventos de startup ni de lifespan: llama tú mismo a tu código de inicialización. Demo: [`fastapi_heroes`](https://albakore.github.io/pyzed/es/integrations/fastapi/#4-en-el-proceso) `in_process.py`. #### Cuál encaja con cada librería | Librería | Mismo proceso | Cliente y servidor | Servidor integrado | API en el proceso | | --- | --- | --- | --- | --- | | FastAPI | Solo importar sus modelos | ✅ `fastapi dev` / `fastapi run` | ✅ Uvicorn en un hilo | ✅ `ASGITransport` | | Django | ✅ ORM tras `django.setup()` | ✅ `manage.py runserver` | No recomendado: ejecútalo aparte | Posible con su app ASGI | | SQLAlchemy | ✅ `Session` o `AsyncSession` | Detrás de tu API | — | — | | SQLModel | ✅ `Session` | Detrás de FastAPI | — | — | | httpx | — | ✅ el cliente | ✅ el cliente | ✅ el transporte | | Pydantic | ✅ valida el formulario | ✅ convierte las respuestas | ✅ | ✅ | | aiosqlite | ✅ en el loop de Pyzed | — | — | — | #### Estructura de un proyecto Mantén el backend libre de Pyzed y la ventana libre de SQL. Todas las demos siguen esta estructura, que permite que el mismo backend sirva a una ventana, a una API y a los tests: ```text myapp/ ├── pyproject.toml ├── models.py # modelos de SQLModel / SQLAlchemy / Django, esquemas de Pydantic ├── db.py # las consultas, una función por acción, sin imports de Pyzed ├── main.py # app de FastAPI o proyecto de Django (cuando hay servidor) └── window.py # la App de Pyzed: llama a db.py o a la API ``` ### FastAPI > Crea la API del tutorial de SQL de FastAPI tal como está en su documentación y úsala desde una ventana de escritorio de Pyzed como servidor aparte, servidor integrado o en el proceso. Source: https://albakore.github.io/pyzed/es/integrations/fastapi/ La API de esta demo es el tutorial [SQL (Relational) Databases](https://fastapi.tiangolo.com/tutorial/sql-databases/) de FastAPI, copiado sin cambios: héroes guardados con SQLModel, con modelos separados para crear, leer y actualizar. La ventana de Pyzed es un cliente de escritorio para ella, y la misma ventana funciona en las tres [arquitecturas](https://albakore.github.io/pyzed/es/integrations/architectures/) en las que hay una API. (Image: Una ventana de escritorio con la lista de héroes de la API de FastAPI y un formulario de edición) ```text fastapi_heroes/ ├── main.py # la API, exactamente como en el tutorial de FastAPI ├── heroes_window.py # la ventana de Pyzed, que solo habla HTTP (httpx) ├── client.py # 2. cliente y servidor ├── embedded.py # 3. servidor integrado └── in_process.py # 4. en el proceso ``` #### 1. La API, como la enseña FastAPI 1. **Instala FastAPI con sus extras estándar**, que traen la CLI `fastapi` y Uvicorn. El `pyproject.toml` de la demo ya los incluye: 2. **Escribe los modelos.** `HeroBase` tiene los campos comunes, `Hero` es la tabla, `HeroPublic` es lo que devuelve la API (nunca el `secret_name`), y `HeroCreate` y `HeroUpdate` describen los cuerpos de las peticiones: ```python class HeroBase(SQLModel): name: str = Field(index=True) age: int | None = Field(default=None, index=True) class Hero(HeroBase, table=True): id: int | None = Field(default=None, primary_key=True) secret_name: str class HeroPublic(HeroBase): id: int class HeroCreate(HeroBase): secret_name: str class HeroUpdate(HeroBase): name: str | None = None age: int | None = None secret_name: str | None = None ``` 3. **Declara una dependencia de sesión** para que cada petición tenga su propia `Session`: ```python def get_session(): with Session(engine) as session: yield session SessionDep = Annotated[Session, Depends(get_session)] ``` 4. **Escribe las operaciones de ruta.** `response_model` filtra la salida y FastAPI valida la entrada, y responde **422** con una entrada por cada campo inválido: ```python @app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate, session: SessionDep): db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero @app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate, session: SessionDep): hero_db = session.get(Hero, hero_id) if not hero_db: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) hero_db.sqlmodel_update(hero_data) ... ``` 5. **Ejecútala con la CLI de FastAPI** y abre la documentación interactiva en [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs): `fastapi dev` recarga con cada cambio; `fastapi run main.py` es el comando de producción. #### El cliente de escritorio `heroes_window.py` es una `pz.App` que habla con la API con un `httpx.AsyncClient`. Recibe una función que crea el cliente, así que la misma ventana funciona con cualquiera de las arquitecturas de abajo. 1. **Envía las peticiones desde corrutinas.** Devolver una corrutina desde un callback la ejecuta en el loop de asyncio de Pyzed, y lo que devuelve llega a `update`. El cliente se crea la primera vez que se usa, para que pertenezca a ese loop: ```python async def api(self, method, url, **kwargs): if self.client is None: self.client = self.make_client() try: return await self.client.request(method, url, **kwargs) except httpx.HTTPError as error: return f"Can't reach the API: {error}" async def load(self): response = await self.api("GET", "/heroes/", params={"limit": 100}) if isinstance(response, str): return ("offline", response) return ("loaded", response.json()) ``` 2. **Crea con POST y edita con PATCH.** Solo se envían los campos que el usuario rellenó, que es lo que esperan `HeroUpdate` y `exclude_unset=True`: ```python async def save(self): body = {key: value for key, value in self.draft.items() if value != ""} if self.selected is None: response = await self.api("POST", "/heroes/", json=body) else: response = await self.api("PATCH", f"/heroes/{self.selected['id']}", json=body) if response.status_code == 422: return ("invalid", response.json()) ... ``` 3. **Muestra el 422 de FastAPI en cada campo.** `pz.validation_errors` lee `{"detail": [...]}` y devuelve `{"age": "Input should be a valid integer, ..."}`: ```python def update(self, message): kind, value = message if kind == "invalid": self.errors = pz.validation_errors(value) ``` #### 2. Cliente y servidor La API y la ventana son dos programas. Así se usa una API desplegada, y así trabajas en las dos a la vez con recarga: `client.py`: ```py """Architecture 1, client and server: the API runs on its own and the window talks to it over HTTP. uv run fastapi dev main.py # terminal 1: the API (FastAPI's own CLI) uv run client.py # terminal 2: the desktop client uv run client.py https://api.example.com # or any other deployment of main.py The usual setup when the API also serves a website or a mobile app, or runs on a server. """ import sys import httpx import pyzed as pz from heroes_window import HeroesWindow API = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8000" if __name__ == "__main__": pz.run(HeroesWindow(lambda: httpx.AsyncClient(base_url=API, timeout=5), mode=f"server at {API}", docs_url=f"{API}/docs")) ``` #### 3. Servidor integrado Un solo comando arranca la API en un hilo en segundo plano con Uvicorn, el servidor que usa `fastapi run`, y abre la ventana. Mientras la ventana está abierta, [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) también funciona en un navegador. `embedded.py`: ```py """Architecture 2, embedded server: one program starts the API and opens the window. uv run embedded.py The API runs with Uvicorn (the server `fastapi run` and `fastapi dev` use) in a background thread, so other programs on this computer, a browser at /docs for instance, can use it while the window is open. It stops when the window closes. """ import threading import httpx import uvicorn import pyzed as pz from heroes_window import HeroesWindow from main import app HOST, PORT = "127.0.0.1", 8000 def start_api() -> None: # What `fastapi run main.py` does, but in a thread of this process. server = uvicorn.Server(uvicorn.Config(app, host=HOST, port=PORT, log_level="warning")) threading.Thread(target=server.run, daemon=True).start() if __name__ == "__main__": start_api() url = f"http://{HOST}:{PORT}" pz.run(HeroesWindow(lambda: httpx.AsyncClient(base_url=url, timeout=5), mode=f"embedded at {url}", docs_url=f"{url}/docs")) ``` #### 4. En el proceso Sin servidor y sin puerto: `httpx.ASGITransport` entrega cada petición a la app de FastAPI como una llamada a función. El programa funciona sin conexión, y el código de la API no cambia. `in_process.py`: ```py """Architecture 3, in process: the window calls the FastAPI app directly, with no server or port. uv run in_process.py httpx can send requests straight to an ASGI app (httpx.ASGITransport), so the same API code and the same client code run inside one desktop program that works offline. Nothing listens on the network. ASGITransport doesn't send startup events, so the tables are created here. """ import httpx import pyzed as pz from heroes_window import HeroesWindow from main import app, create_db_and_tables if __name__ == "__main__": create_db_and_tables() transport = httpx.ASGITransport(app=app) pz.run(HeroesWindow(lambda: httpx.AsyncClient(transport=transport, base_url="http://heroes"), mode="in process")) ``` **Caution:** `ASGITransport` no envía los eventos de startup ni de lifespan, así que el código de `@app.on_event("startup")` o de una función `lifespan` no se ejecuta. `in_process.py` llama a `create_db_and_tables()` por su cuenta. #### Código completo **main.py (API):** ```py """The FastAPI app, exactly as FastAPI's "SQL (Relational) Databases" tutorial writes it: https://fastapi.tiangolo.com/tutorial/sql-databases/ uv run fastapi dev main.py # http://127.0.0.1:8000, interactive docs at /docs Nothing in this file knows about Pyzed. The three desktop entry points (client.py, embedded.py, in_process.py) reuse it unchanged. """ from typing import Annotated from fastapi import Depends, FastAPI, HTTPException, Query from sqlmodel import Field, Session, SQLModel, create_engine, select class HeroBase(SQLModel): name: str = Field(index=True) age: int | None = Field(default=None, index=True) class Hero(HeroBase, table=True): id: int | None = Field(default=None, primary_key=True) secret_name: str class HeroPublic(HeroBase): id: int class HeroCreate(HeroBase): secret_name: str class HeroUpdate(HeroBase): name: str | None = None age: int | None = None secret_name: str | None = None sqlite_file_name = "database.db" sqlite_url = f"sqlite:///{sqlite_file_name}" connect_args = {"check_same_thread": False} engine = create_engine(sqlite_url, connect_args=connect_args) def create_db_and_tables(): SQLModel.metadata.create_all(engine) def get_session(): with Session(engine) as session: yield session SessionDep = Annotated[Session, Depends(get_session)] app = FastAPI() @app.on_event("startup") def on_startup(): create_db_and_tables() @app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate, session: SessionDep): db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) return db_hero @app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( session: SessionDep, offset: int = 0, limit: Annotated[int, Query(le=100)] = 100, ): heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes @app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int, session: SessionDep): hero = session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") return hero @app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate, session: SessionDep): hero_db = session.get(Hero, hero_id) if not hero_db: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) hero_db.sqlmodel_update(hero_data) session.add(hero_db) session.commit() session.refresh(hero_db) return hero_db @app.delete("/heroes/{hero_id}") def delete_hero(hero_id: int, session: SessionDep): hero = session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") session.delete(hero) session.commit() return {"ok": True} ``` **heroes_window.py:** ```py """The Pyzed window: a desktop client for the heroes API in main.py, written with httpx. It only knows the API's URLs and JSON, so it works the same whether the API runs in another process, in a thread of this one or in-process without a network (see client.py, embedded.py and in_process.py). """ from __future__ import annotations import webbrowser from typing import Callable import httpx import pyzed as pz from pyzed import ui EMPTY = {"name": "", "secret_name": "", "age": ""} class HeroesWindow(pz.App): title = "Heroes · FastAPI" size = (980, 620) def __init__(self, make_client: Callable[[], httpx.AsyncClient], mode: str, docs_url: str | None = None): self.make_client = make_client # called once, inside a coroutine, so the client lives on Pyzed's loop self.client: httpx.AsyncClient | None = None self.mode, self.docs_url = mode, docs_url self.heroes: list[dict] = [] self.selected: dict | None = None # the hero being edited, or None for a new one self.draft = dict(EMPTY) self.errors: dict[str, str] = {} self.status = "Connecting…" # -- the API, with httpx -------------------------------------------------------------- async def api(self, method: str, url: str, **kwargs) -> httpx.Response | str: """Sends a request; returns the response, or the error text when the API can't be reached.""" if self.client is None: self.client = self.make_client() try: return await self.client.request(method, url, **kwargs) except httpx.HTTPError as error: return f"Can't reach the API: {error or type(error).__name__}" async def load(self): response = await self.api("GET", "/heroes/", params={"limit": 100}) # GET /heroes/?offset=0&limit=100 if isinstance(response, str): return ("offline", response) return ("loaded", response.json()) async def save(self): body = {key: value for key, value in self.draft.items() if value != ""} # leave empty fields out if self.selected is None: response = await self.api("POST", "/heroes/", json=body) else: response = await self.api("PATCH", f"/heroes/{self.selected['id']}", json=body) if isinstance(response, str): return ("offline", response) if response.status_code == 422: # FastAPI's validation error, one entry per field return ("invalid", response.json()) response.raise_for_status() return ("saved", response.json()) async def delete(self): response = await self.api("DELETE", f"/heroes/{self.selected['id']}") if isinstance(response, str): return ("offline", response) return ("deleted", self.selected["id"]) # -- state ----------------------------------------------------------------------------- def on_start(self): return self.load() def pick(self, hero: dict | None): self.selected, self.errors = hero, {} self.draft = dict(EMPTY) if hero is None else { "name": hero["name"], "secret_name": "", "age": "" if hero["age"] is None else str(hero["age"])} def edit(self, key: str): return lambda value: self.draft.__setitem__(key, value) def update(self, message): kind, value = message if kind == "loaded": self.heroes, self.status = value, f"{len(value)} heroes · {self.mode}" elif kind == "offline": self.status = value elif kind == "invalid": self.errors = pz.validation_errors(value) elif kind == "saved": self.pick(value) return self.load() elif kind == "deleted": self.pick(None) return self.load() # -- view -------------------------------------------------------------------------------- def form(self): e = self.errors.get new = self.selected is None return pz.column([ ui.heading("New hero" if new else f"Hero #{self.selected['id']}", level=3), ui.field("Name", ui.input(self.draft["name"], self.edit("name"), placeholder="Deadpond", width=220), error=e("name")), ui.field("Secret name", ui.input(self.draft["secret_name"], self.edit("secret_name"), placeholder="Dive Wilson" if new else "unchanged", width=220), description="Never sent back by the API", error=e("secret_name")), ui.field("Age", ui.input(self.draft["age"], self.edit("age"), placeholder="optional", width=220), error=e("age")), pz.row([ None if new else ui.button("Delete", self.delete, icon="trash-2", variant="ghost", tint="error"), pz.hspace(), ui.button("New", lambda: self.pick(None), variant="ghost") if not new else None, ui.button("Create" if new else "Save", self.save, icon="check", variant="solid"), ], spacing=8), ], spacing=14, padding=16, width=380) def view(self): t = self.resolved_theme() columns = [ui.Column("ID", "id", width=50), ui.Column("Name", "name"), ui.Column("Age", lambda h: "—" if h["age"] is None else h["age"], width=70, align="end")] table = ui.table(columns, self.heroes, on_row_press=self.pick, selected=self.selected, height="fill") offline = self.status.startswith("Can't") status = ui.status_bar( [ui.status_item(self.status, icon="cloud-off" if offline else "server", icon_color="error" if offline else "success")], [ui.status_item("API docs", lambda: webbrowser.open(self.docs_url), icon="book-open")] if self.docs_url else [], ) body = pz.row([pz.container(table, padding=16, width="fill", height="fill"), pz.container(self.form(), height="fill", bg=t.panel)]) return pz.container(pz.column([body, status]), width="fill", height="fill", bg=t.editor) ``` ### Django > Sigue el tutorial de encuestas de Django y usa el proyecto desde una ventana de escritorio de Pyzed, con el ORM en el mismo proceso o como cliente HTTP de runserver. Source: https://albakore.github.io/pyzed/es/integrations/django/ La demo es el proyecto del [tutorial de Django](https://docs.djangoproject.com/en/stable/intro/tutorial01/), `mysite` con su app `polls`, construido paso a paso como lo hace el tutorial: modelos, migraciones, el admin, vistas genéricas y plantillas. Después, una ventana de Pyzed vota en las encuestas de dos formas, que corresponden a dos [arquitecturas](https://albakore.github.io/pyzed/es/integrations/architectures/): - **`desktop.py`**: la ventana importa los modelos y usa el ORM directamente (mismo proceso). - **`client.py`**: el sitio corre con `runserver` y la ventana llama a sus vistas JSON (cliente y servidor). (Image: Una ventana de escritorio con encuestas de Django, resultados en barras y botones para votar) #### El proyecto de Django, como lo construye el tutorial 1. **Crea el proyecto y la app** (ya está hecho en la carpeta de la demo): y añade `"polls.apps.PollsConfig"` a `INSTALLED_APPS` en `mysite/settings.py`. 2. **Escribe los modelos** en `polls/models.py` (parte 2 del tutorial): ```python class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) ``` 3. **Crea las tablas y un usuario administrador:** 4. **Registra el admin** (parte 7), con las opciones en línea, para poder añadir encuestas en [http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/). 5. **Escribe las vistas y las URLs** (partes 3 y 4): `IndexView`, `DetailView`, `ResultsView` y `vote`, que usa una expresión `F()` para que los votos simultáneos no se pisen. La demo añade dos pequeñas vistas JSON para el cliente de escritorio, `api/questions/` y `api/questions//vote/`. 6. **Ejecuta el sitio:** Las encuestas están en [http://127.0.0.1:8000/polls/](http://127.0.0.1:8000/polls/). #### Forma 1: el ORM en el mismo proceso `desktop.py` configura Django igual que `manage.py` y luego importa los modelos. No hace falta ningún servidor web; la ventana lee y escribe `db.sqlite3` a través del ORM. ```mermaid flowchart LR subgraph p["uv run desktop.py"] W["PollsWindow"] -- "Command.perform" --> F["load · vote · create
hilo de trabajo"] F --> ORM["ORM de Django
polls.models"] end ORM --> DB[("db.sqlite3")] S["manage.py runserver
(opcional, a la vez)"] --> DB ``` 1. **Llama a `django.setup()` antes de importar los modelos:** ```python os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") django.setup() from polls.models import Question ``` 2. **Ejecuta las consultas del ORM en hilos de trabajo.** Son bloqueantes, así que `Command.perform` las ejecuta fuera del hilo de la UI. Django abre una conexión a la base de datos por hilo; cerrarlas al terminar evita que el pool de hilos acumule conexiones: ```python def in_thread(function): @functools.wraps(function) def wrapper(*args, **kwargs): try: return function(*args, **kwargs) finally: connections.close_all() return wrapper @in_thread def vote(question_id, choice_id): choice = Question.objects.get(pk=question_id).choice_set.get(pk=choice_id) choice.votes = F("votes") + 1 choice.save() ``` 3. **Valida con las reglas del propio modelo.** `full_clean()` comprueba `max_length`, los campos obligatorios y los validadores propios; `pz.validation_errors` convierte el `ValidationError` de Django en `{campo: mensaje}` para `ui.field`: ```python question = Question(question_text=draft["question_text"], pub_date=timezone.now()) try: question.full_clean() except ValidationError as error: return pz.validation_errors(error) with transaction.atomic(): question.save() for text in draft["choices"]: question.choice_set.create(choice_text=text, votes=0) ``` **Tip:** `runserver` puede correr a la vez sobre la misma base de datos. Un voto emitido en la ventana aparece en `/polls/` y en el admin, y una pregunta añadida en el admin aparece en la ventana al recargar. #### Forma 2: un cliente HTTP del sitio `client.py` no importa Django en absoluto. Habla con las vistas JSON del sitio con httpx, así que el sitio puede correr en este ordenador o en un servidor: ```mermaid flowchart LR W["PollsWindow
uv run client.py"] -- "httpx · JSON" --> V["polls/views.py
api_questions · api_vote"] B["Navegador
/polls/ · /admin/"] --> V2["Vistas del tutorial
y admin"] subgraph s["uv run manage.py runserver"] V --> DB[("db.sqlite3")] V2 --> DB end ``` ```python async def vote(question_id, choice_id): async with httpx.AsyncClient(base_url=SITE, timeout=5) as client: response = await client.post(f"/polls/api/questions/{question_id}/vote/", json={"choice": choice_id}) response.raise_for_status() return response.json() ``` **Caution:** La vista JSON de votar está marcada con `@csrf_exempt` porque una app de escritorio no tiene cookie CSRF. Para una API real, autentica al cliente con tokens, por ejemplo con [Django REST framework](https://www.django-rest-framework.org/). #### Una ventana, dos backends `PollsWindow` recibe `load`, `vote` y, opcionalmente, `create` como funciones normales. `desktop.py` le pasa funciones normales (que corren en hilos) y `client.py` le pasa corrutinas (que corren en el loop de asyncio); `Command.perform` acepta las dos, así que el código de la ventana es el mismo. #### Código completo **desktop.py:** ```py """Desktop app on Django's ORM: the models, validation and database of the site, with no web server. uv run manage.py migrate # once, as in Django's tutorial uv run desktop.py The window imports the polls app and queries it directly (architecture "same process"). `uv run manage.py runserver` can run at the same time on the same database: votes cast in the window show up on /polls/ and in /admin/, and the other way round. """ import functools import os import django os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") django.setup() # what manage.py does before anything touches the models from django.core.exceptions import ValidationError # noqa: E402 from django.db import connections, transaction # noqa: E402 from django.db.models import F # noqa: E402 from django.utils import timezone # noqa: E402 import pyzed as pz # noqa: E402 from polls.models import Question # noqa: E402 from polls.views import question_json # noqa: E402 from polls_window import PollsWindow # noqa: E402 def in_thread(function): """Pyzed runs these in a worker thread; Django opens one connection per thread, so close it.""" @functools.wraps(function) def wrapper(*args, **kwargs): try: return function(*args, **kwargs) finally: connections.close_all() return wrapper @in_thread def load(): questions = Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date").prefetch_related("choice_set") return [question_json(q) for q in questions] @in_thread def vote(question_id, choice_id): choice = Question.objects.get(pk=question_id).choice_set.get(pk=choice_id) choice.votes = F("votes") + 1 # the database adds one, so two voters never overwrite each other choice.save() @in_thread def create(draft): question = Question(question_text=draft["question_text"], pub_date=timezone.now()) try: question.full_clean() # the model's own rules: required, max_length=200 if len(draft["choices"]) < 2: raise ValidationError("A poll needs at least two choices.") except ValidationError as error: return pz.validation_errors(error) with transaction.atomic(): question.save() for text in draft["choices"]: question.choice_set.create(choice_text=text, votes=0) return question_json(question) if __name__ == "__main__": pz.run(PollsWindow(load, vote, create, mode="Django ORM, same process")) ``` **client.py:** ```py """Desktop client for the Django site over HTTP: the site runs on its own, the window uses its JSON API. uv run manage.py runserver # terminal 1: the site (http://127.0.0.1:8000/polls/) uv run client.py # terminal 2: the window uv run client.py https://polls.example.com Use this when the site is deployed on a server and the desktop app is one more client. The window needs no Django and no database, only httpx. """ import sys import httpx import pyzed as pz from polls_window import PollsWindow SITE = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8000" async def load(): try: async with httpx.AsyncClient(base_url=SITE, timeout=5) as client: response = await client.get("/polls/api/questions/") response.raise_for_status() except httpx.HTTPError as error: return f"Can't reach {SITE}: {error or type(error).__name__}" return response.json()["questions"] async def vote(question_id, choice_id): try: async with httpx.AsyncClient(base_url=SITE, timeout=5) as client: response = await client.post(f"/polls/api/questions/{question_id}/vote/", json={"choice": choice_id}) response.raise_for_status() except httpx.HTTPError as error: return f"The vote failed: {error or type(error).__name__}" return response.json() if __name__ == "__main__": pz.run(PollsWindow(load, vote, mode=f"HTTP client of {SITE}")) ``` **polls_window.py:** ```py """The Pyzed window shared by desktop.py (Django ORM in this process) and client.py (HTTP to the site). A backend is three functions returning plain dicts like ``polls.views.question_json``. They may be regular functions (Pyzed runs them in a thread) or ``async def`` (run on Pyzed's asyncio loop): ``Command.perform`` accepts both. """ from __future__ import annotations from datetime import datetime, timedelta, timezone from typing import Callable, Optional import pyzed as pz from pyzed import Command, ui class PollsWindow(pz.App): title = "Polls · Django" size = (960, 600) def __init__(self, load: Callable, vote: Callable, create: Optional[Callable] = None, mode: str = ""): self.backend_load, self.backend_vote, self.backend_create = load, vote, create self.mode = mode self.questions: list[dict] = [] self.selected_id: int | None = None self.message = "" self.creating = False self.draft = {"question_text": "", "choices": ["", "", ""]} self.errors: dict[str, str] = {} # -- backend calls ---------------------------------------------------------------------- def on_start(self): return self.reload() def reload(self): return Command.perform(self.backend_load, self.loaded) def loaded(self, result): if isinstance(result, str): # the backend couldn't answer self.message = result return self.questions, self.message = result, "" if self.questions and self.selected_id not in {q["id"] for q in self.questions}: self.selected_id = self.questions[0]["id"] def vote(self, question_id: int, choice_id: int): return Command.perform(lambda: self.backend_vote(question_id, choice_id), self.voted) def voted(self, result): if isinstance(result, str): self.message = result return None return self.reload() def create(self): draft = {"question_text": self.draft["question_text"], "choices": [c for c in self.draft["choices"] if c.strip()]} return Command.perform(lambda: self.backend_create(draft), self.created) def created(self, result): if "id" not in result: # {field: message} self.errors = result return None self.creating, self.selected_id = False, result["id"] self.draft = {"question_text": "", "choices": ["", "", ""]} return self.reload() # -- view -------------------------------------------------------------------------------- @staticmethod def recent(question: dict) -> bool: # Question.was_published_recently, on the JSON published = datetime.fromisoformat(question["pub_date"]) now = datetime.now(timezone.utc) return now - timedelta(days=1) <= published <= now def sidebar(self, t): rows = [ui.list_item(q["question_text"], lambda q=q: setattr(self, "selected_id", q["id"]), icon="vote", end=ui.badge("new", tint="success") if self.recent(q) else None, selected=q["id"] == self.selected_id, inset=True, height=30) for q in self.questions] new = ui.button("New question", lambda: setattr(self, "creating", True), icon="plus", full_width=True) \ if self.backend_create else ui.muted("Add questions in the Django admin.", size="sm") return pz.container(pz.column([ui.muted("QUESTIONS", size="xs", weight="semibold"), *rows, pz.vspace(), new], spacing=2), padding=8, width=300, height="fill", bg=t.panel) def results(self): question = next((q for q in self.questions if q["id"] == self.selected_id), None) if question is None: return ui.empty_state("No polls are available.", icon="vote") total = sum(c["votes"] for c in question["choices"]) or 1 rows = [pz.row([ pz.container(ui.label(c["choice_text"]), width=200), pz.container(ui.progress(c["votes"] / total, height=8), width="fill"), pz.container(ui.muted(f"{c['votes']} vote{'s' if c['votes'] != 1 else ''}", size="sm"), width=70, align_x="end"), ui.button("Vote", lambda c=c: self.vote(question["id"], c["id"]), size="sm"), ], spacing=12, align="center") for c in question["choices"]] return pz.column([ui.heading(question["question_text"], level=2), ui.muted(f"Published {question['pub_date'][:16].replace('T', ' ')}", size="sm"), *rows], spacing=14, padding=24) def dialog(self): if not self.creating: return None choices = self.draft["choices"] def set_choice(i): return lambda value: choices.__setitem__(i, value) form = pz.column([ ui.field("Question", ui.input(self.draft["question_text"], lambda v: self.draft.__setitem__("question_text", v), placeholder="What's new?", width=260), error=self.errors.get("question_text")), *[ui.field(f"Choice {i + 1}", ui.input(text, set_choice(i), width=260)) for i, text in enumerate(choices)], ui.callout(self.errors[""], tint="error") if self.errors.get("") else None, ], spacing=12) close = lambda: setattr(self, "creating", False) # noqa: E731 return ui.modal(form, title="New question", on_close=close, width=520, footer=[ui.button("Cancel", close, variant="ghost"), ui.button("Publish", self.create, icon="check", variant="solid")]) def view(self): t = self.resolved_theme() status = ui.status_bar([ui.status_item(self.message or self.mode, icon="circle-alert" if self.message else "database", icon_color="error" if self.message else "success")]) page = pz.column([pz.row([self.sidebar(t), pz.container(self.results(), width="fill", height="fill", bg=t.editor)]), status]) return ui.layer(page, self.dialog(), on_dismiss=lambda: setattr(self, "creating", False), backdrop="#00000066") ``` **polls/views.py:** ```py """The views from Django's tutorial (parts 3 and 4), plus a small JSON API for the desktop client. Tutorial: https://docs.djangoproject.com/en/stable/intro/tutorial04/ """ import json from django.db.models import F from django.http import HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.utils import timezone from django.views import generic from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_GET, require_POST from .models import Choice, Question class IndexView(generic.ListView): template_name = "polls/index.html" context_object_name = "latest_question_list" def get_queryset(self): """Return the last five published questions (not including those set to be published in the future).""" return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[:5] class DetailView(generic.DetailView): model = Question template_name = "polls/detail.html" def get_queryset(self): """Excludes any questions that aren't published yet.""" return Question.objects.filter(pub_date__lte=timezone.now()) class ResultsView(generic.DetailView): model = Question template_name = "polls/results.html" def vote(request, question_id): question = get_object_or_404(Question, pk=question_id) try: selected_choice = question.choice_set.get(pk=request.POST["choice"]) except (KeyError, Choice.DoesNotExist): # Redisplay the question voting form. return render(request, "polls/detail.html", {"question": question, "error_message": "You didn't select a choice."}) else: selected_choice.votes = F("votes") + 1 selected_choice.save() # Always return an HttpResponseRedirect after successfully dealing with POST data. return HttpResponseRedirect(reverse("polls:results", args=(question.id,))) # -- JSON API for desktop clients (client.py) --------------------------------------------------- def question_json(question): return { "id": question.id, "question_text": question.question_text, "pub_date": question.pub_date.isoformat(), "choices": [{"id": c.id, "choice_text": c.choice_text, "votes": c.votes} for c in question.choice_set.all()], } @require_GET def api_questions(request): questions = Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date").prefetch_related("choice_set") return JsonResponse({"questions": [question_json(q) for q in questions]}) @csrf_exempt # a desktop app has no CSRF cookie; protect a real API with tokens instead (e.g. Django REST framework) @require_POST def api_vote(request, question_id): question = get_object_or_404(Question, pk=question_id) try: choice_id = json.loads(request.body)["choice"] selected_choice = question.choice_set.get(pk=choice_id) except (KeyError, ValueError, Choice.DoesNotExist): return JsonResponse({"detail": "You didn't select a choice."}, status=400) selected_choice.votes = F("votes") + 1 selected_choice.save() return JsonResponse(question_json(question)) ``` ### SQLAlchemy > El ORM Quick Start de SQLAlchemy en una app de escritorio de Pyzed, con una Session síncrona en hilos de trabajo o una AsyncSession en el loop de asyncio de Pyzed, más una tabla de 20.000 filas. Source: https://albakore.github.io/pyzed/es/integrations/sqlalchemy/ SQLAlchemy corre en el mismo proceso que la ventana (la arquitectura 1 de [Arquitecturas](https://albakore.github.io/pyzed/es/integrations/architectures/)). Hay dos demos: - **`sqlalchemy_quickstart`**: los modelos `User` y `Address` del [ORM Quick Start](https://docs.sqlalchemy.org/en/20/orm/quickstart.html) de SQLAlchemy, con la misma ventana sobre una `Session` síncrona o sobre la [extensión asyncio](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html). - **`sqlalchemy_contacts`**: 20.000 filas en una tabla virtual, con búsqueda y orden en SQL. #### El Quick Start en una ventana (Image: Usuarios y sus direcciones de email del Quick Start de SQLAlchemy) 1. **Declara los modelos** igual que el Quick Start, con `DeclarativeBase`, `Mapped` y `relationship`. La cascada borra las direcciones de un usuario junto con el usuario: ```python class Base(DeclarativeBase): pass class User(Base): __tablename__ = "user_account" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(30)) fullname: Mapped[Optional[str]] addresses: Mapped[List["Address"]] = relationship(back_populates="user", cascade="all, delete-orphan") class Address(Base): __tablename__ = "address" id: Mapped[int] = mapped_column(primary_key=True) email_address: Mapped[str] user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) user: Mapped["User"] = relationship(back_populates="addresses") ``` 2. **Crea un Engine y las tablas:** ```python engine = create_engine("sqlite:///quickstart.db") Base.metadata.create_all(engine) ``` 3. **Abre una Session por unidad de trabajo.** Cada función del backend abre su propia `Session`, así que se puede ejecutar sin riesgo en cualquier hilo de trabajo. `selectinload` carga las direcciones antes de que se cierre la sesión, para que la ventana pueda leer `user.addresses` después: ```python def users() -> list[User]: stmt = select(User).options(selectinload(User.addresses)).order_by(User.name) with Session(engine) as session: return list(session.scalars(stmt)) def add_address(user_id: int, email_address: str) -> None: with Session(engine) as session: user = session.get(User, user_id) user.addresses.append(Address(email_address=email_address)) session.commit() ``` 4. **Llámalo desde la ventana con `Command.perform`.** La función corre en un hilo y `then` recibe el resultado en el hilo de la UI: ```python def on_start(self): return Command.perform(backend.setup, lambda _: self.reload()) def reload(self): return Command.perform(backend.users, self.loaded) def add_address(self, *_): user_id, email = self.selected_id, self.new_email.strip() return Command.perform(lambda: backend.add_address(user_id, email), lambda _: self.reload()) ``` ##### La versión asyncio `async_backend.py` tiene las mismas funciones escritas con `create_async_engine`, un `async_sessionmaker` y el driver aiosqlite. `Command.perform` ve que son funciones de corrutina y las ejecuta en el loop de asyncio de Pyzed en lugar de en un hilo, así que `main.py` no cambia: ```python engine = create_async_engine("sqlite+aiosqlite:///quickstart.db") async_session = async_sessionmaker(engine, expire_on_commit=False) async def setup() -> None: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async def users() -> list[User]: stmt = select(User).options(selectinload(User.addresses)).order_by(User.name) async with async_session() as session: return list(await session.scalars(stmt)) ``` **Note:** Con `AsyncSession`, la carga perezosa necesitaría un `await`, así que las relaciones se cargan de antemano con `selectinload`, y `expire_on_commit=False` mantiene los objetos legibles después de `commit()`. Instala `sqlalchemy[asyncio]` y `aiosqlite`; el extra trae `greenlet`, que la extensión necesita. | | `Session` | `AsyncSession` | | --- | --- | --- | | Dónde corre | Un hilo de trabajo por llamada | El loop de asyncio de Pyzed | | Llamada de Pyzed | `Command.perform(función_síncrona, then)` | `Command.perform(función_async, then)` o devolver la corrutina | | Úsala cuando | La mayoría de las apps de escritorio; cualquier driver | Ya usas drivers de asyncio (asyncpg, aiosqlite) | **main.py:** ```py """SQLAlchemy's ORM Quick Start in a Pyzed window: users and their email addresses. uv run main.py # sync Session, run in worker threads (sync_backend.py) uv run main.py --async # AsyncSession on Pyzed's asyncio loop (async_backend.py) Both backends have the same functions and the window doesn't care which one it gets: Command.perform runs a regular function in a thread and a coroutine on the loop. Walkthrough: https://albakore.github.io/pyzed/integrations/sqlalchemy/ """ from __future__ import annotations import sys import pyzed as pz from pyzed import Command, ui if "--async" in sys.argv: import async_backend as backend else: import sync_backend as backend class Quickstart(pz.App): title = "Users · SQLAlchemy" size = (900, 560) def __init__(self): self.users = [] self.selected_id: int | None = None self.new_user = {"name": "", "fullname": ""} self.new_email = "" self.editing: tuple[int, str] | None = None # (address id, text being typed) self.errors: dict[str, str] = {} # -- database ---------------------------------------------------------------------------- def on_start(self): return Command.perform(backend.setup, lambda _: self.reload()) def reload(self): return Command.perform(backend.users, self.loaded) def loaded(self, users): self.users = users if self.selected_id not in {u.id for u in users}: self.selected_id = users[0].id if users else None def run(self, work): """Runs a backend call, then reloads. ``work`` is a lambda, so its arguments are read now.""" return Command.perform(work, lambda _: self.reload()) # -- actions ------------------------------------------------------------------------------- def add_user(self, *_): name, fullname = self.new_user["name"].strip(), self.new_user["fullname"].strip() self.errors = {"name": "Required"} if not name else {"name": "30 characters at most"} if len(name) > 30 else {} if self.errors: return None self.new_user = {"name": "", "fullname": ""} return self.run(lambda: backend.add_user(name, fullname)) def add_address(self, *_): email, self.new_email = self.new_email.strip(), "" user_id = self.selected_id return self.run(lambda: backend.add_address(user_id, email)) if "@" in email else None def save_address(self, *_): address_id, email = self.editing self.editing = None return self.run(lambda: backend.rename_address(address_id, email.strip())) def delete_user(self): user_id = self.selected_id return self.run(lambda: backend.delete_user(user_id)) # -- view ---------------------------------------------------------------------------------- def sidebar(self, t): rows = [ui.list_item(u.name, lambda u=u: setattr(self, "selected_id", u.id), icon="user", end=ui.count_badge(len(u.addresses), tint="text_muted") if u.addresses else None, selected=u.id == self.selected_id, inset=True, height=30) for u in self.users] form = pz.column([ ui.input(self.new_user["name"], lambda v: self.new_user.__setitem__("name", v), placeholder="name", on_submit=self.add_user), ui.label(self.errors["name"], size="sm", color="error") if self.errors.get("name") else None, ui.input(self.new_user["fullname"], lambda v: self.new_user.__setitem__("fullname", v), placeholder="Full name (optional)", on_submit=self.add_user), ui.button("Add user", self.add_user, icon="user-plus", full_width=True), ], spacing=6) return pz.container(pz.column([ui.muted("user_account", size="xs", mono=True), *rows, pz.vspace(), form], spacing=2), padding=8, width=260, height="fill", bg=t.panel) def address_row(self, address): if self.editing and self.editing[0] == address.id: return pz.row([ ui.input(self.editing[1], lambda v: setattr(self, "editing", (address.id, v)), on_submit=self.save_address), ui.icon_button("check", self.save_address, tooltip="Save"), ], spacing=6, align="center") return ui.list_item(address.email_address, lambda: setattr(self, "editing", (address.id, address.email_address)), icon="mail", end=ui.muted("click to edit", size="sm"), inset=True, height=30) def detail(self): user = next((u for u in self.users if u.id == self.selected_id), None) if user is None: return ui.empty_state("No users", "Add one on the left.", icon="users") return pz.column([ pz.row([ui.heading(user.fullname or user.name, level=2), pz.hspace(), ui.button("Delete user", self.delete_user, icon="trash-2", variant="ghost", tint="error")], align="center"), ui.muted(repr(user), mono=True, size="sm"), ui.label("Addresses", weight="semibold"), *[self.address_row(a) for a in user.addresses], pz.row([ui.input(self.new_email, lambda v: setattr(self, "new_email", v), placeholder="new@sqlalchemy.org", icon="mail-plus", on_submit=self.add_address), ui.button("Add", self.add_address)], spacing=6), ], spacing=10, padding=24) def view(self): t = self.resolved_theme() mode = "AsyncSession · aiosqlite" if backend.__name__ == "async_backend" else "Session · worker threads" return pz.column([ pz.row([self.sidebar(t), pz.container(self.detail(), width="fill", height="fill", bg=t.editor)]), ui.status_bar([ui.status_item(mode, icon="database", icon_color="success")]), ]) if __name__ == "__main__": pz.run(Quickstart) ``` **models.py:** ```py """The models of SQLAlchemy's ORM Quick Start: https://docs.sqlalchemy.org/en/20/orm/quickstart.html""" from typing import List, Optional from sqlalchemy import ForeignKey, String from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship class Base(DeclarativeBase): pass class User(Base): __tablename__ = "user_account" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(30)) fullname: Mapped[Optional[str]] addresses: Mapped[List["Address"]] = relationship(back_populates="user", cascade="all, delete-orphan") def __repr__(self) -> str: return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})" class Address(Base): __tablename__ = "address" id: Mapped[int] = mapped_column(primary_key=True) email_address: Mapped[str] user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) user: Mapped["User"] = relationship(back_populates="addresses") def __repr__(self) -> str: return f"Address(id={self.id!r}, email_address={self.email_address!r})" def sample_users() -> list[User]: """The users the Quick Start creates.""" return [ User(name="spongebob", fullname="Spongebob Squarepants", addresses=[Address(email_address="spongebob@sqlalchemy.org")]), User(name="sandy", fullname="Sandy Cheeks", addresses=[Address(email_address="sandy@sqlalchemy.org"), Address(email_address="sandy@squirrelpower.org")]), User(name="patrick", fullname="Patrick Star"), ] ``` **sync_backend.py:** ```py """The database code, as the Quick Start writes it: an Engine, a Session per unit of work, select(). Each function opens its own Session, so Pyzed can run it in a worker thread (Command.perform). """ from sqlalchemy import create_engine, select from sqlalchemy.orm import Session, selectinload from models import Address, Base, User, sample_users engine = create_engine("sqlite:///quickstart.db") def setup() -> None: Base.metadata.create_all(engine) with Session(engine) as session: if session.scalars(select(User)).first() is None: session.add_all(sample_users()) session.commit() def users() -> list[User]: # selectinload fetches the addresses now, so the window can read them after the session closes. stmt = select(User).options(selectinload(User.addresses)).order_by(User.name) with Session(engine) as session: return list(session.scalars(stmt)) def add_user(name: str, fullname: str) -> None: with Session(engine) as session: session.add(User(name=name, fullname=fullname or None)) session.commit() def add_address(user_id: int, email_address: str) -> None: with Session(engine) as session: user = session.get(User, user_id) user.addresses.append(Address(email_address=email_address)) session.commit() def rename_address(address_id: int, email_address: str) -> None: with Session(engine) as session: session.get(Address, address_id).email_address = email_address session.commit() def delete_user(user_id: int) -> None: with Session(engine) as session: session.delete(session.get(User, user_id)) # cascade="all, delete-orphan" removes the addresses session.commit() ``` **async_backend.py:** ```py """The same database code with SQLAlchemy's asyncio extension and the aiosqlite driver. https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html The functions are coroutines: Pyzed runs them on its asyncio loop, where the engine lives. """ from sqlalchemy import select from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.orm import selectinload from models import Address, Base, User, sample_users engine = create_async_engine("sqlite+aiosqlite:///quickstart.db") async_session = async_sessionmaker(engine, expire_on_commit=False) async def setup() -> None: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async with async_session() as session: if (await session.scalars(select(User))).first() is None: session.add_all(sample_users()) await session.commit() async def users() -> list[User]: # Lazy loading needs an await, so load relationships eagerly with selectinload. stmt = select(User).options(selectinload(User.addresses)).order_by(User.name) async with async_session() as session: return list(await session.scalars(stmt)) async def add_user(name: str, fullname: str) -> None: async with async_session() as session: session.add(User(name=name, fullname=fullname or None)) await session.commit() async def add_address(user_id: int, email_address: str) -> None: async with async_session() as session: session.add(Address(user_id=user_id, email_address=email_address)) await session.commit() async def rename_address(address_id: int, email_address: str) -> None: async with async_session() as session: address = await session.get(Address, address_id) address.email_address = email_address await session.commit() async def delete_user(user_id: int) -> None: async with async_session() as session: user = await session.get(User, user_id, options=[selectinload(User.addresses)]) await session.delete(user) await session.commit() ``` #### Muchas filas: la demo de contactos Un gestor de contactos sobre SQLAlchemy 2.0 y SQLite. La tabla tiene 20.000 filas y se desplaza con fluidez porque `ui.table` solo construye las filas visibles; la búsqueda y el orden se hacen en SQL, en un hilo de trabajo, así que la ventana nunca espera a la base de datos. (Image: Una tabla de contactos con 20.000 filas, estadísticas y un panel de edición) La demo guarda el código de la base de datos en `db.py`, sin nada de Pyzed, y la ventana en `main.py`. Vale la pena copiar esa separación: el mismo `db.py` podría servir a una app de FastAPI o a una CLI. 1. **Los modelos son SQLAlchemy 2.0 normal.** ```python class Contact(Base): __tablename__ = "contacts" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(80), index=True) email: Mapped[str] = mapped_column(String(120), unique=True) company: Mapped[str] = mapped_column(String(80), default="") city: Mapped[str] = mapped_column(String(60), default="") ``` 2. **Cada consulta es una función con su propia sesión.** Las sesiones de SQLAlchemy no se comparten entre hilos, así que cada función abre una y la cierra. Los objetos cargados conservan sus valores después de cerrar la sesión, que es todo lo que necesita la vista: ```python def search(text, sort, descending): column = COLUMNS[sort] query = select(Contact).order_by(column.desc() if descending else column, Contact.id) if text: pattern = f"%{text}%" query = query.where(or_(Contact.name.ilike(pattern), Contact.email.ilike(pattern), Contact.company.ilike(pattern))) with Session(engine) as session: return list(session.scalars(query)) ``` 3. **Ejecútala con `Command.perform`.** La función corre en un hilo y `then` recibe el resultado en el hilo de la UI. Devolver una lista de comandos los ejecuta juntos: ```python def reload(self): query, sort, descending = self.query, self.sort, self.descending return [ Command.perform(lambda: db.search(query, sort, descending), lambda rows: self.loaded(query, rows)), Command.perform(db.stats, lambda stats: setattr(self, "stats", stats)), ] def loaded(self, query, rows): if query == self.query: # a newer search is on its way: keep waiting for it self.rows, self.loading = rows, False ``` Copia primero en variables locales el estado que necesitas (`query, sort, ...`): la lambda se ejecuta más tarde, en otro hilo, y no debería leer `self` mientras el usuario sigue escribiendo. 4. **Pasa todas las filas a `ui.table`.** Está virtualizada, así que 20.000 filas cuestan lo mismo que 20. Al pulsar una cabecera se llama a `on_sort`, que aquí vuelve a ejecutar la consulta con un nuevo `ORDER BY`: ```python ui.table(columns, self.rows, sort_by=self.sort, descending=self.descending, on_sort=self.sort_by, on_row_press=self.open, selected=self.editing, striped=True, height="fill") ``` 5. **Convierte los errores de la base de datos en errores de campo.** `save` devuelve el contacto o `{campo: mensaje}`. Una restricción de unicidad se convierte en un mensaje bajo el campo Email: ```python try: session.commit() except IntegrityError: return {"email": "Another contact already uses this email"} ``` ```python ui.field("Email", ui.input(self.draft["email"], ...), error=self.errors.get("email")) ``` **Tip:** ¿Prefieres `asyncio`? La `AsyncSession` de SQLAlchemy con `aiosqlite` o `asyncpg` también funciona: devuelve la corrutina en lugar de usar `Command.perform`. Mira [Otras librerías](https://albakore.github.io/pyzed/es/integrations/other-libraries/#sqlalchemy-asyncio). **main.py:** ```py """A contacts manager on SQLAlchemy 2.0: 20,000 rows in a virtual table, searched and sorted in SQL. uv run main.py The database (contacts.db) is created next to this file on the first run. Walkthrough: https://albakore.github.io/pyzed/integrations/sqlalchemy/ """ from __future__ import annotations import pyzed as pz from pyzed import Command, ui import db FIELDS = [("name", "Name", "Ada Lovelace"), ("email", "Email", "ada@example.com"), ("company", "Company", "Analytical Engines"), ("city", "City", "London")] class Contacts(pz.App): title = "Contacts · SQLAlchemy" size = (1180, 720) def __init__(self): self.rows: list[db.Contact] = [] self.stats = {"total": 0, "companies": 0, "cities": []} self.query = "" self.sort, self.descending = "Name", False self.loading = True self.editing = None # a Contact, "new" or None self.draft: dict[str, str] = {} self.errors: dict[str, str] = {} # -- database work runs in a thread with Command.perform ------------------------------ def on_start(self): return Command.perform(db.setup, lambda _: self.reload()) def reload(self): self.loading = True query, sort, descending = self.query, self.sort, self.descending return [ Command.perform(lambda: db.search(query, sort, descending), lambda rows: self.loaded(query, rows)), Command.perform(db.stats, lambda stats: setattr(self, "stats", stats)), ] def loaded(self, query: str, rows: list[db.Contact]): if query == self.query: # a newer search is on its way: keep waiting for it self.rows, self.loading = rows, False def search(self, text: str): self.query = text return self.reload() def sort_by(self, title: str): self.descending = not self.descending if title == self.sort else False self.sort = title return self.reload() # -- the edit panel ------------------------------------------------------------------ def open(self, contact): self.editing, self.errors = contact, {} self.draft = {key: "" if contact == "new" else getattr(contact, key) for key, _, _ in FIELDS} def save(self): contact_id = None if self.editing == "new" else self.editing.id values = dict(self.draft) return Command.perform(lambda: db.save(contact_id, values), self.saved) def saved(self, result): if isinstance(result, dict): self.errors = result return None self.editing = None return self.reload() def delete(self): contact_id = self.editing.id self.editing = None return Command.perform(lambda: db.delete(contact_id), lambda _: self.reload()) def on_key(self, combo): if combo == "escape": self.editing = None elif combo == "ctrl-n": self.open("new") # -- view ---------------------------------------------------------------------------- def header(self): cities = self.stats["cities"] return pz.row([ ui.stat("Contacts", f"{self.stats['total']:,}"), ui.stat("Companies", self.stats["companies"]), ui.card(pz.column([ui.muted("Top cities", size="sm"), ui.bar_chart([count for _, count in cities], labels=[c.split()[0] for c, _ in cities], width=360, height=90)], spacing=6)), ], spacing=12) def panel(self, t): if self.editing is None: return None rows = [ui.field(title, ui.input(self.draft[key], lambda v, k=key: self.draft.__setitem__(k, v), placeholder=hint, width=220), error=self.errors.get(key)) for key, title, hint in FIELDS] new = self.editing == "new" return pz.container( pz.column([ pz.row([ui.heading("New contact" if new else "Edit contact", level=3), pz.hspace(), ui.icon_button("x", lambda: setattr(self, "editing", None), tooltip="Close (Esc)")], align="center"), *rows, pz.row([None if new else ui.button("Delete", self.delete, icon="trash-2", variant="ghost", tint="error"), pz.hspace(), ui.button("Save", self.save, icon="check", variant="solid")], spacing=8), ], spacing=14), padding=16, width=420, height="fill", bg=t.panel, ) def view(self): t = self.resolved_theme() columns = [ ui.Column("Name", "name", width="fill:3"), ui.Column("Email", "email", width="fill:3"), ui.Column("Company", "company", width="fill:2"), ui.Column("City", "city", width="fill:2"), ] toolbar = pz.row([ ui.search_input(self.query, self.search, placeholder="Search by name, email or company…", width=360), ui.spinner() if self.loading else ui.muted(f"{len(self.rows):,} found", size="sm"), pz.hspace(), ui.button("New contact", lambda: self.open("new"), icon="user-plus", keys="ctrl-n"), ], spacing=10, align="center") # Rows come sorted from SQL; sort_by only draws the arrow (re-sorting a sorted list is cheap). table = ui.table(columns, self.rows, sort_by=self.sort, descending=self.descending, on_sort=self.sort_by, on_row_press=self.open, selected=self.editing, striped=True, height="fill") main = pz.column([self.header(), toolbar, table], spacing=14, padding=16) return pz.container(pz.row([main, self.panel(t)]), width="fill", height="fill", bg=t.editor) if __name__ == "__main__": pz.run(Contacts) ``` **db.py:** ```py """The database: SQLAlchemy 2.0 models on SQLite, plus the queries the window runs.""" from __future__ import annotations import random from datetime import datetime from pathlib import Path from sqlalchemy import String, create_engine, func, or_, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column DATABASE = Path(__file__).with_name("contacts.db") engine = create_engine(f"sqlite:///{DATABASE}") class Base(DeclarativeBase): pass class Contact(Base): __tablename__ = "contacts" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(80), index=True) email: Mapped[str] = mapped_column(String(120), unique=True) company: Mapped[str] = mapped_column(String(80), default="") city: Mapped[str] = mapped_column(String(60), default="") created: Mapped[datetime] = mapped_column(default=datetime.now) # Sorting by a column title of the table is an ORDER BY on this column. COLUMNS = {"Name": Contact.name, "Email": Contact.email, "Company": Contact.company, "City": Contact.city} def setup(rows: int = 20_000) -> None: """Creates the table and fills it with made-up contacts the first time.""" Base.metadata.create_all(engine) with Session(engine) as session: if session.scalar(select(func.count()).select_from(Contact)): return rng = random.Random(7) first = ["Ada", "Alan", "Grace", "Linus", "Guido", "Margaret", "Ken", "Barbara", "Dennis", "Radia", "Tim", "Frances", "Bjarne", "Anita", "John", "Hedy", "Yukihiro", "Sophie", "Brendan", "Katherine"] last = ["Lovelace", "Turing", "Hopper", "Torvalds", "van Rossum", "Hamilton", "Thompson", "Liskov", "Ritchie", "Perlman", "Berners-Lee", "Allen", "Stroustrup", "Borg", "McCarthy", "Lamarr", "Matsumoto", "Wilson", "Eich", "Johnson"] companies = ["Acme", "Globex", "Initech", "Umbrella", "Hooli", "Stark", "Wayne", "Tyrell", "Cyberdyne", "Aperture"] cities = ["Buenos Aires", "Madrid", "Mexico City", "Bogotá", "Lima", "Santiago", "Montevideo", "Barcelona", "Berlin", "Tokyo", "Toronto", "Lisbon"] weights = [12, 10, 9, 7, 6, 5, 4, 4, 3, 3, 2, 2] session.add_all( Contact(name=f"{rng.choice(first)} {rng.choice(last)}", email=f"user{i}@example.com", company=rng.choice(companies), city=rng.choices(cities, weights)[0]) for i in range(rows) ) session.commit() def search(text: str, sort: str, descending: bool) -> list[Contact]: """Contacts whose name, email or company contains ``text``, sorted in SQL.""" column = COLUMNS[sort] query = select(Contact).order_by(column.desc() if descending else column, Contact.id) if text: pattern = f"%{text}%" query = query.where(or_(Contact.name.ilike(pattern), Contact.email.ilike(pattern), Contact.company.ilike(pattern))) with Session(engine) as session: return list(session.scalars(query)) # plain objects keep their loaded values after the session closes def stats() -> dict: """Totals and the five cities with more contacts, for the header.""" with Session(engine) as session: total = session.scalar(select(func.count()).select_from(Contact)) companies = session.scalar(select(func.count(func.distinct(Contact.company)))) cities = session.execute(select(Contact.city, func.count()).group_by(Contact.city) .order_by(func.count().desc()).limit(5)).all() return {"total": total, "companies": companies, "cities": [(city, count) for city, count in cities]} def save(contact_id: int | None, values: dict) -> Contact | dict: """Inserts or updates a contact. Returns it, or ``{field: message}`` when it can't be saved.""" errors = {} if not values["name"].strip(): errors["name"] = "A contact needs a name" if "@" not in values["email"]: errors["email"] = "Not an email address" if errors: return errors with Session(engine, expire_on_commit=False) as session: contact = session.get(Contact, contact_id) if contact_id else Contact() for key, value in values.items(): setattr(contact, key, value.strip()) session.add(contact) try: session.commit() except IntegrityError: return {"email": "Another contact already uses this email"} return contact def delete(contact_id: int) -> None: with Session(engine) as session: contact = session.get(Contact, contact_id) if contact is not None: session.delete(contact) session.commit() ``` ### SQLModel > El tutorial de SQLModel (equipos, héroes, filtros where, relaciones) en una app de escritorio de Pyzed, más un inventario cuyo formulario y tabla comparten un solo modelo. Source: https://albakore.github.io/pyzed/es/integrations/sqlmodel/ SQLModel corre en el mismo proceso que la ventana (la arquitectura 1 de [Arquitecturas](https://albakore.github.io/pyzed/es/integrations/architectures/)); cuando los mismos modelos se tienen que compartir por HTTP, ponlos detrás de FastAPI como en la [demo de FastAPI](https://albakore.github.io/pyzed/es/integrations/fastapi/). Hay dos demos: - **`sqlmodel_heroes`**: las tablas `Team` y `Hero` del [tutorial de SQLModel](https://sqlmodel.tiangolo.com/tutorial/), con sus datos de ejemplo, filtros `where()`, relaciones y un modelo de datos para el formulario. - **`sqlmodel_inventory`**: un inventario cuyas reglas de validación y cuya tabla salen de una sola clase. #### Los héroes del tutorial en una ventana (Image: Héroes agrupados por equipo, filtrados por edad, con un panel de edición) 1. **Define las tablas** como en el capítulo [Relationship Attributes](https://sqlmodel.tiangolo.com/tutorial/relationship-attributes/) del tutorial: ```python class Team(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) headquarters: str heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str age: int | None = Field(default=None, index=True) team_id: int | None = Field(default=None, foreign_key="team.id") team: Team | None = Relationship(back_populates="heroes") ``` 2. **Crea el engine y las tablas**, y añade los héroes del tutorial la primera vez: ```python sqlite_file_name = "database.db" sqlite_url = f"sqlite:///{sqlite_file_name}" engine = create_engine(sqlite_url) def create_db_and_tables(): SQLModel.metadata.create_all(engine) ``` 3. **Consulta con `select()` y `where()`.** Los filtros se añaden uno a uno; `col()` le da al comprobador de tipos una columna para la comparación, como recomienda el tutorial: ```python def heroes(team_id, min_age): statement = select(Hero).options(selectinload(Hero.team)).order_by(Hero.name) if team_id != "all": statement = statement.where(Hero.team_id == team_id) if min_age is not None: statement = statement.where(col(Hero.age) >= min_age) with Session(engine) as session: return list(session.exec(statement)) ``` `selectinload` carga el equipo de cada héroe antes de que se cierre la sesión, para que la tabla pueda mostrar `hero.team.name`. 4. **Valida el formulario con un modelo de datos.** Los modelos con `table=True` no validan (los crea SQLAlchemy), así que se aplica el enfoque de [Multiple Models](https://sqlmodel.tiangolo.com/tutorial/fastapi/multiple-models/) del tutorial: un `HeroCreate` sin `table=True` comprueba lo que se escribió en el formulario. ```python class HeroCreate(SQLModel): name: str = Field(min_length=1) secret_name: str = Field(min_length=1) age: int | None = Field(default=None, ge=0) team_id: int | None = None def save(self): try: data = HeroCreate.model_validate({**self.draft, "age": self.draft["age"] or None, ...}) except ValidationError as error: self.errors = pz.validation_errors(error) return None return Command.perform(lambda: db.save(hero_id, data), lambda _: self.reload()) ``` 5. **Crea o actualiza con `sqlmodel_update`:** ```python def save(hero_id, data: HeroCreate): with Session(engine) as session: hero = session.get(Hero, hero_id) if hero_id else Hero.model_validate(data) hero.sqlmodel_update(data.model_dump()) session.add(hero) session.commit() ``` **main.py:** ```py """SQLModel's tutorial in a Pyzed window: teams, heroes, where() filters and a validated form. uv run main.py The database (database.db) is created and filled with the tutorial's heroes on the first run. Walkthrough: https://albakore.github.io/pyzed/integrations/sqlmodel/ """ from __future__ import annotations from pydantic import ValidationError import pyzed as pz from pyzed import Command, ui import db from models import Hero, HeroCreate NO_TEAM = "No team" class Heroes(pz.App): title = "Heroes · SQLModel" size = (1060, 620) def __init__(self): self.teams = [] self.heroes: list[Hero] = [] self.team_id: int | None | str = "all" # a team id, None (heroes without a team) or "all" self.older = False # only heroes aged 35 or more self.editing: Hero | None | str = None # a Hero, "new" or None self.draft: dict = {} self.errors: dict[str, str] = {} # -- database, in worker threads ------------------------------------------------------------ def on_start(self): return Command.perform(db.setup, lambda _: self.reload()) def reload(self): team_id, min_age = self.team_id, 35 if self.older else None return [Command.perform(db.teams, lambda teams: setattr(self, "teams", teams)), Command.perform(lambda: db.heroes(team_id, min_age), lambda heroes: setattr(self, "heroes", heroes))] def show_team(self, team_id): self.team_id = team_id return self.reload() def toggle_older(self, value: str): self.older = value != "All ages" return self.reload() # -- the form --------------------------------------------------------------------------------- def open(self, hero): self.editing, self.errors = hero, {} if hero == "new": self.draft = {"name": "", "secret_name": "", "age": "", "team": NO_TEAM} else: self.draft = {"name": hero.name, "secret_name": hero.secret_name, "age": "" if hero.age is None else str(hero.age), "team": hero.team.name if hero.team else NO_TEAM} def save(self): team = next((t for t in self.teams if t.name == self.draft["team"]), None) try: data = HeroCreate.model_validate({**self.draft, "age": self.draft["age"] or None, "team_id": team.id if team else None}) except ValidationError as error: self.errors = pz.validation_errors(error) return None hero_id = None if self.editing == "new" else self.editing.id self.editing, self.errors = None, {} return Command.perform(lambda: db.save(hero_id, data), lambda _: self.reload()) def delete(self): hero_id, self.editing = self.editing.id, None return Command.perform(lambda: db.delete(hero_id), lambda _: self.reload()) def edit(self, key): return lambda value: self.draft.__setitem__(key, value) # -- view --------------------------------------------------------------------------------------- def sidebar(self, t): def item(label, team_id, count, icon): return ui.list_item(label, lambda: self.show_team(team_id), icon=icon, end=ui.muted(str(count), size="sm"), selected=self.team_id == team_id, inset=True, height=30) teams = [item(team.name, team.id, len(team.heroes), "shield") for team in self.teams] return pz.container(pz.column([ ui.muted("TEAMS", size="xs", weight="semibold"), item("All heroes", "all", sum(len(t.heroes) for t in self.teams), "users"), *teams, item(NO_TEAM, None, "", "user-x"), ], spacing=2), padding=8, width=230, height="fill", bg=t.panel) def panel(self, t): if self.editing is None: return None e = self.errors.get new = self.editing == "new" return pz.container(pz.column([ ui.heading("New hero" if new else self.editing.name, level=3), ui.field("Name", ui.input(self.draft["name"], self.edit("name"), width=180), error=e("name")), ui.field("Secret name", ui.input(self.draft["secret_name"], self.edit("secret_name"), width=180), error=e("secret_name")), ui.field("Age", ui.input(self.draft["age"], self.edit("age"), placeholder="unknown", width=180), error=e("age")), ui.field("Team", ui.select([NO_TEAM, *[team.name for team in self.teams]], self.draft["team"], self.edit("team"), width=180)), pz.row([None if new else ui.button("Delete", self.delete, icon="trash-2", variant="ghost", tint="error"), pz.hspace(), ui.button("Cancel", lambda: setattr(self, "editing", None), variant="ghost"), ui.button("Save", self.save, icon="check", variant="solid")], spacing=6), ], spacing=12), padding=16, width=380, height="fill", bg=t.panel) def view(self): t = self.resolved_theme() columns = [ui.Column("Name", "name", width="fill:2"), ui.Column("Secret name", "secret_name", width="fill:2"), ui.Column("Age", lambda h: "—" if h.age is None else h.age, width=60, align="end"), ui.Column("Team", lambda h: h.team.name if h.team else "—")] main = pz.column([ pz.row([ui.segmented(["All ages", "Age ≥ 35"], "Age ≥ 35" if self.older else "All ages", self.toggle_older), pz.hspace(), ui.button("New hero", lambda: self.open("new"), icon="plus", variant="solid")], align="center"), ui.table(columns, self.heroes, on_row_press=self.open, selected=self.editing, height="fill"), ], spacing=12, padding=16) return pz.row([self.sidebar(t), pz.container(main, width="fill", height="fill", bg=t.editor), self.panel(t)]) if __name__ == "__main__": pz.run(Heroes) ``` **models.py:** ```py """The tables of SQLModel's tutorial ("Relationship Attributes"): https://sqlmodel.tiangolo.com/tutorial/""" from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select class Team(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) headquarters: str heroes: list["Hero"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str = Field(index=True) secret_name: str age: int | None = Field(default=None, index=True) team_id: int | None = Field(default=None, foreign_key="team.id") team: Team | None = Relationship(back_populates="heroes") class HeroCreate(SQLModel): """A data model (no table=True) that validates what the form typed, as in the "Multiple Models" chapter.""" name: str = Field(min_length=1) secret_name: str = Field(min_length=1) age: int | None = Field(default=None, ge=0) team_id: int | None = None sqlite_file_name = "database.db" sqlite_url = f"sqlite:///{sqlite_file_name}" engine = create_engine(sqlite_url) def create_db_and_tables(): SQLModel.metadata.create_all(engine) def create_heroes(): """The tutorial's heroes and teams, added the first time.""" with Session(engine) as session: if session.exec(select(Hero)).first(): return team_preventers = Team(name="Preventers", headquarters="Sharp Tower") team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add_all([ Hero(name="Deadpond", secret_name="Dive Wilson", team=team_z_force), Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers), Hero(name="Spider-Boy", secret_name="Pedro Parqueador", team=team_preventers), Team(name="Wakaland", headquarters="Wakaland Capital City", heroes=[ Hero(name="Black Lion", secret_name="Trevor Challa", age=35), Hero(name="Princess Sure-E", secret_name="Sure-E"), ]), Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32, team=team_preventers), Hero(name="Dr. Weird", secret_name="Steve Weird", age=36, team=team_preventers), Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93, team=team_preventers), ]) session.commit() ``` **db.py:** ```py """Queries the window runs (in worker threads): select(), where(), relationships, add, delete.""" from sqlalchemy.orm import selectinload from sqlmodel import Session, col, select from models import Hero, HeroCreate, Team, create_db_and_tables, create_heroes, engine def setup() -> None: create_db_and_tables() create_heroes() def teams() -> list[Team]: with Session(engine) as session: # Load each team's heroes now: relationships can't be read after the session closes. return list(session.exec(select(Team).options(selectinload(Team.heroes)).order_by(Team.name))) def heroes(team_id: int | None | str, min_age: int | None) -> list[Hero]: """team_id is a team id, None for heroes without a team, or "all".""" statement = select(Hero).options(selectinload(Hero.team)).order_by(Hero.name) if team_id != "all": statement = statement.where(Hero.team_id == team_id) if min_age is not None: statement = statement.where(col(Hero.age) >= min_age) # the tutorial's "where" chapter with Session(engine) as session: return list(session.exec(statement)) def save(hero_id: int | None, data: HeroCreate) -> None: with Session(engine) as session: hero = session.get(Hero, hero_id) if hero_id else Hero.model_validate(data) hero.sqlmodel_update(data.model_dump()) session.add(hero) session.commit() def delete(hero_id: int) -> None: with Session(engine) as session: session.delete(session.get(Hero, hero_id)) session.commit() ``` #### Un modelo para el formulario y la tabla: la demo de inventario SQLModel une Pydantic y SQLAlchemy: una sola clase describe los campos, sus reglas y la tabla. En una app de escritorio eso significa que el formulario y la base de datos coinciden por diseño. Esta demo es un inventario con niveles de stock, un gráfico de dona y un diálogo cuyos campos muestran los errores de Pydantic. (Image: Una tabla de inventario con badges de stock y un diálogo de edición con errores de validación) 1. **Pon las reglas en una clase base.** La tabla y el validador del formulario heredan de ella: ```python class ProductBase(SQLModel): name: str = Field(min_length=2, max_length=60) sku: str category: str = "Keyboards" price: float = Field(gt=0) stock: int = Field(ge=0) @field_validator("sku") @classmethod def sku_format(cls, sku): sku = sku.strip().upper() if not re.fullmatch(r"[A-Z]{3}-\d{4}", sku): raise ValueError("Expected something like KEY-0001") return sku class Product(ProductBase, table=True): id: int | None = Field(default=None, primary_key=True) sku: str = Field(unique=True, index=True) class ProductIn(ProductBase): """Validates what the form typed.""" ``` 2. **Valida con `ProductIn`, no con `Product`.** SQLModel no valida los modelos con `table=True` (los construye SQLAlchemy), así que lo hace una clase aparte sin `table=True`. El formulario guarda todo como texto y Pydantic lo convierte: `"12.5"` pasa a ser `12.5` y `"abc"`, un error. ```python def save(self): try: data = ProductIn.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) return None product_id = None if self.editing == "new" else self.editing.id return Command.perform(lambda: models.save(product_id, data), self.saved) ``` 3. **Guárdalo en un hilo.** `models.save` copia los datos validados en la fila con `sqlmodel_update` y devuelve `{campo: mensaje}` cuando algo que solo sabe la base de datos está mal, como un SKU repetido: ```python def save(product_id, data): with Session(engine, expire_on_commit=False) as session: clash = session.exec(select(Product).where(Product.sku == data.sku, Product.id != product_id)).first() if clash: return {"sku": f"Already used by “{clash.name}”"} product = session.get(Product, product_id) if product_id else Product.model_validate(data) product.sqlmodel_update(data.model_dump()) session.add(product) session.commit() return product ``` 4. **Muestra los errores en un diálogo.** `ui.layer` pone un `ui.modal` sobre la página; cada `ui.field` toma su mensaje de `self.errors`: ```python ui.field("Price", ui.input(self.draft["price"], self.edit("price")), error=self.errors.get("price")) ``` 5. **Dibuja las celdas con widgets.** Una `ui.Column` puede dibujar cualquier cosa; la columna de stock muestra botones y un badge que se vuelve ámbar o rojo cuando queda poco stock: ```python ui.Column("Stock", lambda p: p, width=180, align="center", render=self.stock_cell, sortable=False) ``` **Note:** El mismo `ProductIn` puede ser el cuerpo de un endpoint de FastAPI. Si tu app acaba teniendo un servidor, el cliente de escritorio y la API validan con un solo modelo. Mira [FastAPI](https://albakore.github.io/pyzed/es/integrations/fastapi/). **main.py:** ```py """An inventory app on SQLModel: the form is validated by the same model that defines the table. uv run main.py The database (inventory.db) is created next to this file on the first run. Walkthrough: https://albakore.github.io/pyzed/integrations/sqlmodel/ """ from __future__ import annotations from pydantic import ValidationError import pyzed as pz from pyzed import Command, ui import models from models import CATEGORIES, Product, ProductIn LOW_STOCK = 5 class Inventory(pz.App): title = "Inventory · SQLModel" size = (1100, 680) def __init__(self): self.products: list[Product] = [] self.category = "All" self.editing = None # a Product, "new" or None self.draft: dict[str, str] = {} self.errors: dict[str, str] = {} def on_start(self): return Command.perform(lambda: (models.setup(), models.products())[1], self.loaded) def loaded(self, products: list[Product]): self.products = products def reload(self): return Command.perform(models.products, self.loaded) # -- the form ---------------------------------------------------------------------- def open(self, product): self.editing, self.errors = product, {} if product == "new": self.draft = {"name": "", "sku": "", "category": CATEGORIES[0], "price": "", "stock": "0"} else: self.draft = {key: str(getattr(product, key)) for key in ("name", "sku", "category", "price", "stock")} def edit(self, key: str): return lambda value: self.draft.__setitem__(key, value) def save(self): try: data = ProductIn.model_validate(self.draft) # strings from the inputs are coerced or rejected here except ValidationError as error: self.errors = pz.validation_errors(error) return None product_id = None if self.editing == "new" else self.editing.id return Command.perform(lambda: models.save(product_id, data), self.saved) def saved(self, result): if isinstance(result, dict): self.errors = result return None self.editing = None return self.reload() def restock(self, product: Product, amount: int): return Command.perform(lambda: models.restock(product.id, amount), lambda _: self.reload()) def on_key(self, combo): if combo == "escape": self.editing = None elif combo == "ctrl-n": self.open("new") # -- view ---------------------------------------------------------------------------- def stock_cell(self, product: Product): if product.stock == 0: badge = ui.badge("out of stock", tint="error") elif product.stock < LOW_STOCK: badge = ui.badge(f"{product.stock} left", tint="warning") else: badge = ui.label(str(product.stock), mono=True) return pz.row([ ui.icon_button("minus", lambda: self.restock(product, -1), size="sm", tooltip="Sell one"), pz.container(badge, width=96, align_x="center"), ui.icon_button("plus", lambda: self.restock(product, 1), size="sm", tooltip="Receive one"), ], spacing=4, align="center") def summary(self, t): value = sum(p.price * p.stock for p in self.products) by_category = [sum(p.price * p.stock for p in self.products if p.category == c) for c in CATEGORIES] palette = [t.accent, t.success, t.warning, t.error, t.players[3]] legend = pz.column([pz.row([ui.indicator(color), ui.muted(c, size="sm")], spacing=6, align="center") for c, color in zip(CATEGORIES, palette)], spacing=2) return pz.row([ ui.stat("Stock value", f"${value:,.0f}"), ui.stat("Low stock", sum(0 < p.stock < LOW_STOCK for p in self.products)), ui.stat("Out of stock", sum(p.stock == 0 for p in self.products)), ui.card(pz.row([ui.donut(by_category, colors=palette, size=84, thickness=12), legend], spacing=14, align="center"), padding=10), ], spacing=12, align="center") def dialog(self): if self.editing is None: return None e = self.errors.get fields = pz.column([ ui.field("Name", ui.input(self.draft["name"], self.edit("name"), width=240), error=e("name")), ui.field("SKU", ui.input(self.draft["sku"], self.edit("sku"), placeholder="KEY-0003", mono=True, width=240), description="Three letters, a dash and four digits", error=e("sku")), ui.field("Category", ui.select(CATEGORIES, self.draft["category"], self.edit("category"), width=240)), ui.field("Price", ui.input(self.draft["price"], self.edit("price"), placeholder="0.00", width=240), error=e("price")), ui.field("Stock", ui.input(self.draft["stock"], self.edit("stock"), width=240), error=e("stock")), ], spacing=12) close = lambda: setattr(self, "editing", None) # noqa: E731 return ui.modal(fields, title="New product" if self.editing == "new" else "Edit product", on_close=close, width=520, footer=[ui.button("Cancel", close, variant="ghost"), ui.button("Save", self.save, icon="check", variant="solid")]) def view(self): t = self.resolved_theme() shown = [p for p in self.products if self.category in ("All", p.category)] columns = [ ui.Column("Product", "name", width="fill:3"), ui.Column("SKU", "sku", render=lambda sku: ui.label(sku, mono=True, color="text_muted")), ui.Column("Category", "category"), ui.Column("Price", "price", align="end", render=lambda price: ui.label(f"${price:,.2f}", mono=True)), ui.Column("Stock", lambda p: p, width=180, align="center", render=self.stock_cell, sortable=False), ] toolbar = pz.row([ ui.segmented(["All", *CATEGORIES], self.category, lambda c: setattr(self, "category", c)), pz.hspace(), ui.button("New product", lambda: self.open("new"), icon="package-plus", variant="solid", keys="ctrl-n"), ], align="center") page = pz.container( pz.column([self.summary(t), toolbar, ui.table(columns, shown, on_row_press=self.open, row_height=40, height="fill")], spacing=14), padding=16, width="fill", height="fill", bg=t.editor, ) return ui.layer(page, self.dialog(), on_dismiss=lambda: setattr(self, "editing", None), backdrop="#00000066") if __name__ == "__main__": pz.run(Inventory) ``` **models.py:** ```py """SQLModel models: one class is both the Pydantic schema and the database table.""" from __future__ import annotations import re from pathlib import Path from pydantic import field_validator from sqlmodel import Field, Session, SQLModel, create_engine, select CATEGORIES = ["Keyboards", "Mice", "Monitors", "Audio", "Cables"] engine = create_engine(f"sqlite:///{Path(__file__).with_name('inventory.db')}") class ProductBase(SQLModel): """The fields and their rules, shared by the form and the table.""" name: str = Field(min_length=2, max_length=60) sku: str category: str = "Keyboards" price: float = Field(gt=0) stock: int = Field(ge=0) @field_validator("sku") @classmethod def sku_format(cls, sku: str) -> str: sku = sku.strip().upper() if not re.fullmatch(r"[A-Z]{3}-\d{4}", sku): raise ValueError("Expected something like KEY-0001") return sku class Product(ProductBase, table=True): # Table models skip validation on purpose (SQLAlchemy builds them); validate with ProductIn first. id: int | None = Field(default=None, primary_key=True) sku: str = Field(unique=True, index=True) class ProductIn(ProductBase): """Validates what the form typed: "12.5" becomes 12.5, "abc" becomes an error on that field.""" def setup() -> None: SQLModel.metadata.create_all(engine) with Session(engine) as session: if session.exec(select(Product)).first(): return session.add_all([ Product(name="Mechanical keyboard", sku="KEY-0001", category="Keyboards", price=129.0, stock=14), Product(name="Low-profile keyboard", sku="KEY-0002", category="Keyboards", price=89.5, stock=3), Product(name="Wireless mouse", sku="MOU-0001", category="Mice", price=49.9, stock=40), Product(name="Vertical mouse", sku="MOU-0002", category="Mice", price=59.0, stock=0), Product(name='27" 4K monitor', sku="MON-0001", category="Monitors", price=399.0, stock=6), Product(name='34" ultrawide', sku="MON-0002", category="Monitors", price=649.0, stock=2), Product(name="Studio headphones", sku="AUD-0001", category="Audio", price=149.0, stock=11), Product(name="USB microphone", sku="AUD-0002", category="Audio", price=99.0, stock=7), Product(name="USB-C cable 2 m", sku="CAB-0001", category="Cables", price=12.0, stock=120), Product(name="HDMI 2.1 cable", sku="CAB-0002", category="Cables", price=18.0, stock=4), ]) session.commit() def products() -> list[Product]: with Session(engine) as session: return list(session.exec(select(Product).order_by(Product.category, Product.name))) def save(product_id: int | None, data: ProductIn) -> Product | dict: """Stores validated data; returns ``{field: message}`` when the SKU is taken.""" with Session(engine, expire_on_commit=False) as session: clash = session.exec(select(Product).where(Product.sku == data.sku, Product.id != product_id)).first() if clash: return {"sku": f"Already used by “{clash.name}”"} product = session.get(Product, product_id) if product_id else Product.model_validate(data) product.sqlmodel_update(data.model_dump()) session.add(product) session.commit() return product def restock(product_id: int, amount: int) -> None: with Session(engine) as session: product = session.get(Product, product_id) product.stock = max(0, product.stock + amount) session.add(product) session.commit() ``` ### httpx > Llama a APIs web desde una app de Pyzed con httpx, siguiendo su quickstart: peticiones, un AsyncClient, llamadas concurrentes, errores y descargas en streaming con barra de progreso. Source: https://albakore.github.io/pyzed/es/integrations/httpx/ [httpx](https://www.python-httpx.org/) es el cliente HTTP que usa una ventana de Pyzed para hablar con cualquier API: una pública, o tu propio servidor de [FastAPI](https://albakore.github.io/pyzed/es/integrations/fastapi/) o [Django](https://albakore.github.io/pyzed/es/integrations/django/) (la [arquitectura](https://albakore.github.io/pyzed/es/integrations/architectures/) de cliente y servidor). Dos demos: - **`httpx_pypi`**: peticiones concurrentes a la API JSON de PyPI, que se muestran a medida que llegan. - **`httpx_download`**: un gestor de descargas con respuestas en streaming y una barra de progreso. #### Lo básico, del quickstart de httpx httpx tiene la misma API en versión síncrona y async. En una app de Pyzed puedes usar cualquiera: **AsyncClient (recomendado):** ```python import httpx class App(pz.App): async def load(self): # A coroutine returned from a callback runs on Pyzed's asyncio loop. async with httpx.AsyncClient() as client: response = await client.get("https://pypi.org/pypi/httpx/json") response.raise_for_status() return ("loaded", response.json()) def update(self, message): kind, data = message self.info = data["info"] ``` **API síncrona en un hilo:** ```python import httpx def fetch(): response = httpx.get("https://pypi.org/pypi/httpx/json") response.raise_for_status() return response.json() class App(pz.App): def load(self): # A blocking call runs in a worker thread; then() receives the result. return Command.perform(fetch, lambda data: setattr(self, "info", data["info"])) ``` El resto del quickstart se aplica sin cambios: | Tarea | httpx | | --- | --- | | Parámetros de consulta | `client.get(url, params={"q": "pyzed", "page": 2})` | | Enviar JSON | `client.post(url, json={"name": "Deadpond"})` | | Enviar un formulario o archivos | `client.post(url, data={...}, files={"upload": open("a.png", "rb")})` | | Cabeceras y autenticación | `httpx.AsyncClient(headers={"Authorization": f"Bearer {token}"})`, `auth=("user", "pass")` | | Una URL base | `httpx.AsyncClient(base_url="https://api.example.com")` y luego `client.get("/items/")` | | Timeouts | `httpx.AsyncClient(timeout=10)` | | Errores | `response.raise_for_status()`; captura `httpx.HTTPStatusError` y `httpx.RequestError` | **Tip:** Crea un solo `AsyncClient` y reutilízalo, como [recomienda httpx](https://www.python-httpx.org/advanced/clients/), para mantener las conexiones abiertas. Todas las corrutinas de Pyzed comparten un loop de asyncio, así que un cliente creado dentro de una corrutina se puede guardar en `self` y usar en las siguientes (la demo de FastAPI lo hace). #### Peticiones concurrentes: el explorador de PyPI (Image: Un explorador de PyPI con la lista de paquetes y sus versiones, y los detalles de FastAPI) 1. **Devuelve un valor para cada resultado posible.** El modelo, o un texto corto para la lista: ```python async def fetch(client, name): try: response = await client.get(f"https://pypi.org/pypi/{name}/json") except httpx.HTTPError as error: return f"Network error: {error}" if response.status_code == 404: return "Not on PyPI" return Package.model_validate_json(response.content) # see the Pydantic page ``` 2. **Envía todas las peticiones a la vez y emite cada respuesta con `yield`.** Un generador async devuelto desde `on_start` o desde un callback envía cada `yield` a `update`, así que la lista se va llenando a medida que llegan las respuestas: ```python async def fetch_all(names): async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client: async def one(name): return name, await fetch(client, name) for done in asyncio.as_completed([one(n) for n in names]): yield await done class Explorer(pz.App): def on_start(self): return fetch_all(self.names) def update(self, message): name, result = message self.packages[name] = result ``` **main.py:** ```py """A PyPI explorer: concurrent httpx requests streamed into the window, responses validated by Pydantic. uv run main.py uv run main.py rich textual polars # start with other packages Walkthrough: https://albakore.github.io/pyzed/integrations/httpx/ """ from __future__ import annotations import sys import webbrowser from datetime import datetime, timezone import pyzed as pz from pyzed import ui from pypi import Package, fetch_all DEFAULT = ["pyzed-ui", "fastapi", "django", "sqlalchemy", "sqlmodel", "httpx", "pydantic", "requests"] def ago(when: datetime) -> str: days = (datetime.now(timezone.utc) - when).days if days < 1: return "today" if days < 60: return f"{days} days ago" return f"{days // 30} months ago" if days < 730 else f"{days // 365} years ago" class Explorer(pz.App): title = "PyPI explorer · httpx + Pydantic" size = (1100, 680) def __init__(self, names: list[str]): self.names = names self.packages: dict[str, Package | str | None] = {name: None for name in names} # None while loading self.selected = names[0] if names else None self.new_name = "" def on_start(self): return fetch_all(self.names) # an async generator: each (name, result) it yields reaches update() def update(self, message): name, result = message self.packages[name] = result def add(self, *_): name = self.new_name.strip().lower() self.new_name = "" if not name: return None self.selected = name if name in self.packages and isinstance(self.packages[name], Package): return None self.packages[name] = None return fetch_all([name]) # -- view ---------------------------------------------------------------------------- def row(self, name: str): package = self.packages[name] if package is None: end = ui.spinner(size=12) elif isinstance(package, str): end = pz.icon("circle-alert", size=14, color=self.resolved_theme().error) else: end = ui.muted(package.info.version, size="sm", mono=True) return ui.list_item(name, lambda: setattr(self, "selected", name), icon="package", end=end, selected=name == self.selected, inset=True, height=30) def details(self, t): package = self.packages.get(self.selected) if package is None: return pz.center(ui.spinner(size=24)) if isinstance(package, str): return pz.center(ui.empty_state(self.selected, package, icon="circle-alert")) info, history = package.info, package.history() years = package.per_year() links = [ui.button(label, lambda url=url: webbrowser.open(url), icon="external-link", size="sm", variant="ghost") for label, url in list((info.project_urls or {}).items())[:4]] versions = ui.virtual_list(history, lambda item: pz.row([ ui.label(item[0], mono=True, width="fill"), ui.muted(item[1].strftime("%Y-%m-%d"), size="sm")], padding=(0, 8), align="center", height=26), item_height=26) return pz.column([ pz.row([ui.heading(info.name, level=2), ui.badge(info.version, tint="success")], spacing=10, align="center"), ui.muted(info.summary or "No summary.", size="lg"), pz.row(links, spacing=4) if links else None, pz.row([ ui.stat("Releases", len(history)), ui.stat("Latest", ago(history[0][1]) if history else "—"), ui.stat("Python", info.requires_python or "any"), ui.stat("License", (info.license_expression or info.license or "—")[:18]), ], spacing=12), pz.row([ ui.card(ui.bar_chart([count for _, count in years], labels=[str(y)[2:] for y, _ in years], width=340, height=150), title="Releases per year", width=374), ui.card(pz.container(versions, height=150), title="Versions", width="fill"), ], spacing=12), ], spacing=14, padding=20) def view(self): t = self.resolved_theme() loading = sum(p is None for p in self.packages.values()) sidebar = pz.container(pz.column([ ui.input(self.new_name, lambda v: setattr(self, "new_name", v), placeholder="Add a package…", icon="plus", on_submit=self.add), ui.virtual_list(list(self.packages), self.row, item_height=30), ui.muted(f"Loading {loading}…" if loading else f"{len(self.packages)} packages", size="sm"), ], spacing=8), padding=8, width=260, height="fill", bg=t.panel) return pz.row([sidebar, pz.container(self.details(t), width="fill", height="fill", bg=t.editor)]) if __name__ == "__main__": pz.run(Explorer(sys.argv[1:] or DEFAULT)) ``` **pypi.py:** ```py """PyPI's JSON API, described with Pydantic models and fetched with httpx.""" from __future__ import annotations import asyncio from collections import Counter from datetime import datetime from typing import AsyncIterator import httpx from pydantic import BaseModel, ValidationError class Info(BaseModel): name: str version: str summary: str | None = None license: str | None = None license_expression: str | None = None # newer packages declare an SPDX expression instead requires_python: str | None = None project_urls: dict[str, str] | None = None class File(BaseModel): upload_time_iso_8601: datetime yanked: bool = False class Package(BaseModel): """Only the fields the app shows; Pydantic ignores the rest of the response.""" info: Info releases: dict[str, list[File]] def history(self) -> list[tuple[str, datetime]]: """(version, first upload) for every release with files, newest first.""" dated = [(version, min(f.upload_time_iso_8601 for f in files)) for version, files in self.releases.items() if files] return sorted(dated, key=lambda item: item[1], reverse=True) def per_year(self, years: int = 8) -> list[tuple[int, int]]: counts = Counter(date.year for _, date in self.history()) last = datetime.now().year return [(year, counts.get(year, 0)) for year in range(last - years + 1, last + 1)] async def fetch(client: httpx.AsyncClient, name: str) -> Package | str: """The package, or a short message saying why it couldn't be loaded.""" try: response = await client.get(f"https://pypi.org/pypi/{name}/json") except httpx.HTTPError as error: return f"Network error: {error or type(error).__name__}" if response.status_code == 404: return "Not on PyPI" if response.is_error: return f"PyPI answered {response.status_code}" try: return Package.model_validate_json(response.content) except ValidationError as error: return f"Unexpected response: {error.error_count()} problems" async def fetch_all(names: list[str]) -> AsyncIterator[tuple[str, Package | str]]: """Requests every package at once and yields each one as soon as it arrives.""" async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client: async def one(name): return name, await fetch(client, name) for done in asyncio.as_completed([one(n) for n in names]): yield await done ``` #### Descargas en streaming con progreso (Image: Un gestor de descargas con barras de progreso) Las [respuestas en streaming](https://www.python-httpx.org/quickstart/#streaming-responses) de httpx leen el cuerpo por partes en lugar de cargarlo entero en memoria, y su receta de [progreso de descarga](https://www.python-httpx.org/advanced/clients/#monitoring-download-progress) usa `response.num_bytes_downloaded`. En Pyzed el bucle se convierte en un generador async que hace `yield` tras cada parte, y la ventana vuelve a dibujar la barra cada vez: ```python async def download(item): async with httpx.AsyncClient(follow_redirects=True) as client: async with client.stream("GET", item.url) as response: response.raise_for_status() item.total = int(response.headers["Content-Length"]) with open(FOLDER / item.name, "wb") as file: async for chunk in response.aiter_bytes(): if item.cancel: # set by the Cancel button break file.write(chunk) item.done = response.num_bytes_downloaded yield item # reaches update(), which redraws class Downloads(pz.App): def start(self, *_): item = Download(self.url) self.items.insert(0, item) return download(item) def view(self): ... ui.progress(item.done / item.total) ``` Varias descargas pueden correr a la vez: cada una es su propio generador en el mismo loop. `examples/integrations/httpx_download/main.py`: ```py """A download manager built on httpx's streaming responses, with a live progress bar. uv run main.py It follows httpx's "Monitoring download progress" recipe (https://www.python-httpx.org/advanced/clients/#monitoring-download-progress), written with ``AsyncClient.stream`` and ``aiter_bytes``. The download is an async generator: every progress report it yields reaches ``update``, so the window redraws while the bytes arrive. Files are saved in a ``downloads`` folder next to this file. Walkthrough: https://albakore.github.io/pyzed/integrations/httpx/ """ from __future__ import annotations import time from pathlib import Path from urllib.parse import urlsplit import httpx import pyzed as pz from pyzed import ui FOLDER = Path(__file__).with_name("downloads") EXAMPLE = ("https://files.pythonhosted.org/packages/54/a4/f8188c4f3e07f7737683588210c073478abcb542048cf4ab6fedad0b458a/" "numpy-2.1.0.tar.gz") def size(num_bytes: float) -> str: for unit in ("B", "KB", "MB", "GB"): if num_bytes < 1024 or unit == "GB": return f"{num_bytes:.0f} {unit}" if unit == "B" else f"{num_bytes:.1f} {unit}" num_bytes /= 1024 class Download: """One row of the list. ``total`` is None when the server sends no Content-Length.""" def __init__(self, url: str): self.url = url self.name = Path(urlsplit(url).path).name or "download" self.done, self.total = 0, None self.speed = 0.0 self.state = "waiting" # waiting, downloading, finished, cancelled or the error text self.cancel = False # set by the Cancel button, read by the loop below async def download(item: Download): """Streams ``item.url`` to disk, yielding ``item`` after every chunk.""" FOLDER.mkdir(exist_ok=True) started = time.monotonic() try: async with httpx.AsyncClient(follow_redirects=True, timeout=30) as client: async with client.stream("GET", item.url) as response: response.raise_for_status() length = response.headers.get("Content-Length") item.total, item.state = (int(length) if length else None), "downloading" with open(FOLDER / item.name, "wb") as file: async for chunk in response.aiter_bytes(): if item.cancel: item.state = "cancelled" break file.write(chunk) item.done = response.num_bytes_downloaded item.speed = item.done / max(time.monotonic() - started, 1e-6) yield item if item.state == "downloading": item.state = "finished" except httpx.HTTPStatusError as error: item.state = f"The server answered {error.response.status_code}" except httpx.HTTPError as error: item.state = f"{type(error).__name__}: {error}" yield item class Downloads(pz.App): title = "Downloads · httpx" size = (760, 480) def __init__(self): self.url = EXAMPLE self.items: list[Download] = [] def start(self, *_): url = self.url.strip() if not url.startswith(("http://", "https://")): return None item = Download(url) self.items.insert(0, item) return download(item) # Pyzed runs the generator on its loop and sends each yield to update() def update(self, item: Download): pass # the Download object was changed in place; returning from update redraws the window def row(self, item: Download): t = self.resolved_theme() if item.state == "downloading": detail = f"{size(item.done)} of {size(item.total) if item.total else '?'} · {size(item.speed)}/s" action = ui.icon_button("x", lambda: setattr(item, "cancel", True), tooltip="Cancel") elif item.state == "finished": detail, action = f"{size(item.done)} · saved in downloads/", None else: detail, action = item.state.capitalize(), None failed = item.state not in ("waiting", "downloading", "finished") value = item.done / item.total if item.total else (1.0 if item.state == "finished" else 0.0) return ui.card(pz.column([ pz.row([pz.icon("file-down", size=16, color=t.icon_muted), ui.label(item.name, width="fill"), action], spacing=8, align="center"), ui.progress(value, tint="error" if failed else "success" if item.state == "finished" else "accent"), ui.muted(detail, size="sm"), ], spacing=8), padding=12) def view(self): t = self.resolved_theme() body = pz.scrollable(pz.column([self.row(i) for i in self.items], spacing=8), height="fill") if self.items \ else ui.empty_state("No downloads", "Paste a URL and press Download.", icon="download") return pz.container(pz.column([ pz.row([ui.input(self.url, lambda v: setattr(self, "url", v), icon="link", on_submit=self.start), ui.button("Download", self.start, icon="download", variant="solid")], spacing=8), body, ], spacing=14), padding=20, width="fill", height="fill", bg=t.editor) if __name__ == "__main__": pz.run(Downloads) ``` ### Pydantic > Valida formularios de Pyzed con modelos de Pydantic, muestra los errores en cada campo y lee o guarda JSON con model_validate_json y model_dump_json. Source: https://albakore.github.io/pyzed/es/integrations/pydantic/ [Pydantic](https://docs.pydantic.dev/) comprueba datos contra anotaciones de tipo. En una app de escritorio hace dos trabajos: valida lo que el usuario escribió en un formulario y convierte el JSON de archivos y APIs. Corre en el mismo proceso que la ventana, sin necesidad de hilos ni de loop, porque validar es rápido. (Image: Un formulario de registro con errores de Pydantic y las cuentas guardadas como JSON) 1. **Escribe las reglas como un modelo.** Los [campos](https://docs.pydantic.dev/latest/concepts/fields/) llevan las restricciones, tipos especiales como `EmailStr` (de `pydantic[email]`) y `SecretStr` hacen el resto, y los [validadores](https://docs.pydantic.dev/latest/concepts/validators/) añaden comprobaciones propias. Un validador de modelo `after` compara dos campos: ```python class SignUp(BaseModel): username: str = Field(min_length=3, max_length=20, pattern=r"^[a-z0-9_]+$") email: EmailStr birthday: date plan: Literal["free", "pro", "team"] = "free" password: SecretStr password_repeat: SecretStr newsletter: bool = False @field_validator("username", mode="before") @classmethod def lowercase(cls, value: str) -> str: return value.strip().lower() @model_validator(mode="after") def passwords_match(self) -> "SignUp": if self.password.get_secret_value() != self.password_repeat.get_secret_value(): raise ValueError("The passwords don't match") return self ``` 2. **Guarda el formulario como valores simples y valida al enviar.** Los inputs dan texto; Pydantic convierte `"1990-05-02"` en un `date` e informa de todos los problemas a la vez: ```python def submit(self, *_): try: signup = SignUp.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) return ... ``` 3. **Muestra cada error bajo su campo.** `pz.validation_errors` devuelve `{campo: mensaje}`; los errores de un validador de modelo usan la clave `""`: ```python ui.field("Email", ui.input(d["email"], self.edit("email")), error=self.errors.get("email")) ui.callout(self.errors[""], tint="error") if self.errors.get("") else None ``` 4. **Guarda y carga JSON.** [Serialización](https://docs.pydantic.dev/latest/concepts/serialization/) con `model_dump` y `model_dump_json`, y un `TypeAdapter` para una lista entera: ```python account = Account.model_validate(signup.model_dump(exclude={"password", "password_repeat"})) Accounts = TypeAdapter(list[Account]) FILE.write_bytes(Accounts.dump_json(self.accounts, indent=2)) accounts = Accounts.validate_json(FILE.read_bytes()) ``` #### Leer respuestas de APIs Los mismos modelos convierten el JSON que llega por la red. Declara solo los campos que usas; Pydantic ignora el resto, convierte las fechas y falla con un error claro si la API cambia de forma. Del [explorador de PyPI](https://albakore.github.io/pyzed/es/integrations/httpx/#peticiones-concurrentes-el-explorador-de-pypi): ```python class Info(BaseModel): name: str version: str summary: str | None = None class File(BaseModel): upload_time_iso_8601: datetime class Package(BaseModel): info: Info releases: dict[str, list[File]] package = Package.model_validate_json(response.content) ``` **Tip:** Los modelos de Pydantic son lo que FastAPI usa para los cuerpos de las peticiones y la base sobre la que se construye SQLModel. Un modelo escrito para el formulario de escritorio puede convertirse más adelante en el cuerpo de un endpoint de una API; mira [FastAPI](https://albakore.github.io/pyzed/es/integrations/fastapi/) y [SQLModel](https://albakore.github.io/pyzed/es/integrations/sqlmodel/). #### Código completo **main.py:** ```py """A sign-up form whose rules live in a Pydantic model; accounts are saved to accounts.json. uv run main.py The window keeps what the user types as plain strings and hands them to ``SignUp.model_validate``: Pydantic converts them (a date, an email, a bool), checks every rule and reports the errors per field, which ``pz.validation_errors`` turns into ``{field: message}``. Walkthrough: https://albakore.github.io/pyzed/integrations/pydantic/ """ from __future__ import annotations from pathlib import Path from pydantic import ValidationError import pyzed as pz from pyzed import ui from models import Account, Accounts, SignUp FILE = Path(__file__).with_name("accounts.json") EMPTY = {"username": "", "email": "", "birthday": "", "plan": "free", "password": "", "password_repeat": "", "newsletter": False} def load() -> list[Account]: return Accounts.validate_json(FILE.read_bytes()) if FILE.exists() else [] class SignUpForm(pz.App): title = "Sign up · Pydantic" size = (860, 600) def __init__(self): self.draft = dict(EMPTY) self.errors: dict[str, str] = {} self.accounts = load() self.preview = "" # the JSON Pydantic produced for the last account def edit(self, key): return lambda value: self.draft.__setitem__(key, value) def submit(self, *_): try: signup = SignUp.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) # the model validator's error has the key "" return account = Account.model_validate(signup.model_dump(exclude={"password", "password_repeat"})) if any(a.username == account.username for a in self.accounts): self.errors = {"username": "That username is taken"} return self.accounts.append(account) FILE.write_bytes(Accounts.dump_json(self.accounts, indent=2)) self.preview = account.model_dump_json(indent=2) self.draft, self.errors = dict(EMPTY), {} def form(self): e, d = self.errors.get, self.draft def text(key, title, placeholder="", **kwargs): return ui.field(title, ui.input(d[key], self.edit(key), placeholder=placeholder, on_submit=self.submit, **kwargs), error=e(key)) return pz.column([ ui.heading("Create an account", level=2), text("username", "Username", "lowercase letters, digits and _"), text("email", "Email", "you@example.com"), text("birthday", "Birthday", "YYYY-MM-DD"), ui.field("Plan", ui.segmented(["free", "pro", "team"], d["plan"], self.edit("plan"))), text("password", "Password", "8 characters or more", secure=True), text("password_repeat", "Repeat the password", secure=True), ui.checkbox(d["newsletter"], self.edit("newsletter"), label="Send me the newsletter"), ui.callout(e(""), tint="error") if e("") else None, ui.button("Sign up", self.submit, icon="user-plus", variant="solid", full_width=True), ], spacing=12, padding=24, width=420) def side(self, t): rows = [ui.list_item(a.username, None, icon="user", end=ui.badge(a.plan), inset=True, height=30) for a in reversed(self.accounts)] return pz.container(pz.column([ ui.muted("accounts.json", size="xs", mono=True), *rows, ui.card(ui.label(self.preview, mono=True, size="sm"), title="model_dump_json()") if self.preview else None, ], spacing=6), padding=16, width="fill", height="fill", bg=t.panel) def view(self): t = self.resolved_theme() return pz.row([pz.container(pz.scrollable(self.form()), height="fill", bg=t.editor), self.side(t)]) if __name__ == "__main__": pz.run(SignUpForm) ``` **models.py:** ```py """The form's rules, written as Pydantic models (https://docs.pydantic.dev/latest/concepts/models/).""" from __future__ import annotations from datetime import date from typing import Literal from pydantic import BaseModel, EmailStr, Field, SecretStr, TypeAdapter, field_validator, model_validator class SignUp(BaseModel): username: str = Field(min_length=3, max_length=20, pattern=r"^[a-z0-9_]+$") email: EmailStr birthday: date plan: Literal["free", "pro", "team"] = "free" password: SecretStr # repr() and str() show ******** password_repeat: SecretStr newsletter: bool = False @field_validator("username", mode="before") @classmethod def lowercase(cls, value: str) -> str: return value.strip().lower() @field_validator("birthday") @classmethod def old_enough(cls, value: date) -> date: today = date.today() age = today.year - value.year - ((today.month, today.day) < (value.month, value.day)) if age < 13: raise ValueError("You must be at least 13 years old") return value @field_validator("password") @classmethod def long_enough(cls, value: SecretStr) -> SecretStr: if len(value.get_secret_value()) < 8: raise ValueError("Use 8 characters or more") return value @model_validator(mode="after") def passwords_match(self) -> SignUp: if self.password.get_secret_value() != self.password_repeat.get_secret_value(): raise ValueError("The passwords don't match") return self class Account(BaseModel): """What gets saved: the sign-up without the passwords.""" username: str email: EmailStr birthday: date plan: Literal["free", "pro", "team"] newsletter: bool Accounts = TypeAdapter(list[Account]) # validates and serializes a whole list at once ``` ### Otras librerías > Recetas para aiosqlite, pandas, Polars, requests, Redis y Matplotlib en una app de escritorio de Pyzed. Source: https://albakore.github.io/pyzed/es/integrations/other-libraries/ Cada librería cae en uno de los casos del [resumen](https://albakore.github.io/pyzed/es/integrations/overview/#dónde-se-ejecuta-tu-código): async, bloqueante, un servidor o un stream. Estas recetas cubren las que más se preguntan. #### aiosqlite [aiosqlite](https://aiosqlite.omnilib.dev/) le da a SQLite una API async. Como Pyzed ejecuta todas las corrutinas en un mismo loop, una sola conexión abierta en `on_start` sirve a toda la app: (Image: Una app de diario que guarda las entradas con aiosqlite) ```python class Journal(pz.App): async def on_start(self): self.db = await aiosqlite.connect("journal.db") self.db.row_factory = aiosqlite.Row await self.db.execute("CREATE TABLE IF NOT EXISTS entries (...)") return await self.load() async def load(self): async with self.db.execute("SELECT * FROM entries ORDER BY id DESC") as cursor: return ("loaded", await cursor.fetchall()) ```
Código completo: examples/integrations/aiosqlite_journal/main.py ```py """A journal on aiosqlite: one async SQLite connection living on Pyzed's asyncio loop. uv run main.py The database (journal.db) is created next to this file on the first run. Walkthrough: https://albakore.github.io/pyzed/integrations/other-libraries/#aiosqlite """ from __future__ import annotations from datetime import datetime from pathlib import Path import aiosqlite import pyzed as pz from pyzed import ui DATABASE = Path(__file__).with_name("journal.db") MOODS = {"great": ("smile", "success"), "okay": ("meh", "warning"), "rough": ("frown", "error")} class Journal(pz.App): title = "Journal · aiosqlite" size = (720, 560) def __init__(self): self.db: aiosqlite.Connection | None = None # opened on Pyzed's loop, used only from coroutines self.entries: list[aiosqlite.Row] = [] self.text = "" self.mood = "great" async def on_start(self): self.db = await aiosqlite.connect(DATABASE) self.db.row_factory = aiosqlite.Row await self.db.execute("""CREATE TABLE IF NOT EXISTS entries ( id INTEGER PRIMARY KEY, written TEXT NOT NULL, mood TEXT NOT NULL, text TEXT NOT NULL)""") await self.db.commit() return await self.load() async def load(self): async with self.db.execute("SELECT * FROM entries ORDER BY id DESC LIMIT 200") as cursor: return ("loaded", await cursor.fetchall()) async def add(self, text: str, mood: str): await self.db.execute("INSERT INTO entries (written, mood, text) VALUES (?, ?, ?)", (datetime.now().isoformat(timespec="minutes"), mood, text)) await self.db.commit() return await self.load() async def remove(self, entry_id: int): await self.db.execute("DELETE FROM entries WHERE id = ?", (entry_id,)) await self.db.commit() return await self.load() def submit(self, *_): text, self.text = self.text.strip(), "" return self.add(text, self.mood) if text else None def update(self, message): kind, rows = message if kind == "loaded": self.entries = rows def entry(self, row): icon, tint = MOODS[row["mood"]] return ui.card(pz.row([ pz.icon(icon, size=18, color=self.resolved_theme().color(tint)), pz.column([ui.label(row["text"]), ui.muted(row["written"].replace("T", " · "), size="sm")], spacing=4, width="fill"), ui.icon_button("trash-2", lambda: self.remove(row["id"]), size="sm", tooltip="Delete"), ], spacing=12, align="center"), padding=12) def view(self): t = self.resolved_theme() composer = pz.row([ ui.toggle_group([(mood, icon) for mood, (icon, _) in MOODS.items()], self.mood, lambda m: setattr(self, "mood", m)), ui.input(self.text, lambda v: setattr(self, "text", v), placeholder="How was your day? (Enter to save)", on_submit=self.submit), ], spacing=8, align="center") entries = pz.scrollable(pz.column([self.entry(r) for r in self.entries], spacing=8), height="fill") \ if self.entries else ui.empty_state("Nothing written yet", "Your entries are stored in journal.db.", icon="notebook-pen") return pz.container(pz.column([ui.heading("Journal", level=2), composer, entries], spacing=14), padding=20, width="fill", height="fill", bg=t.editor) if __name__ == "__main__": pz.run(Journal) ```
#### SQLAlchemy asyncio Se explica en la [página de SQLAlchemy](https://albakore.github.io/pyzed/es/integrations/sqlalchemy/#la-versión-asyncio): la demo `sqlalchemy_quickstart` ejecuta la misma ventana sobre una `Session` síncrona o sobre una `AsyncSession` con aiosqlite (`uv run main.py --async`). #### pandas y Polars `ui.table` recibe una lista de dicts, que está a una llamada de un DataFrame. Lee los archivos grandes en un hilo para que la ventana siga respondiendo: ```python import pandas as pd class Sales(pz.App): def __init__(self): self.frame = pd.DataFrame() def open(self, path): return pz.Command.perform(lambda: pd.read_csv(path), self.loaded) def loaded(self, frame): self.frame = frame self.rows = frame.to_dict("records") # Polars: frame.to_dicts() def view(self): columns = [ui.Column(name, name) for name in self.frame.columns] return ui.table(columns, self.rows, height="fill") ``` La tabla está virtualizada, así que cientos de miles de filas se desplazan con fluidez. Para filtrar, haz la consulta sobre el DataFrame (`frame[frame.city == city]`) y reconstruye `self.rows`. #### requests y otros clientes bloqueantes Cualquier llamada bloqueante pasa por `Command.perform`. Conviene capturar los errores de `raise_for_status` dentro de la función para que `then` siempre reciba algo que mostrar: ```python import requests def fetch_weather(city): try: response = requests.get("https://wttr.in/" + city, params={"format": "j1"}, timeout=10) response.raise_for_status() return response.json() except requests.RequestException as error: return {"error": str(error)} class Weather(pz.App): def search(self, city): return pz.Command.perform(lambda: fetch_weather(city), self.show) ``` #### Pub/sub de Redis y otros streams Un flujo de eventos es una suscripción: `pz.listen` ejecuta un generador async mientras aparece en `subscriptions()` y lo detiene cuando deja de aparecer: ```python import redis.asyncio as redis async def events(): client = redis.Redis() async with client.pubsub() as pubsub: await pubsub.subscribe("orders") async for message in pubsub.listen(): if message["type"] == "message": yield ("order", message["data"].decode()) class Orders(pz.App): def subscriptions(self): return [pz.listen(events)] if self.live else [] ``` Los WebSockets funcionan igual; mira `examples/websocket_stream.py` y la [guía de async](https://albakore.github.io/pyzed/es/guides/async/#suscripciones-temporizadores-y-streams). #### Matplotlib Pyzed dibuja sus propios gráficos (`ui.bar_chart`, `ui.sparkline`, `ui.donut`, `pz.canvas`), pero una figura de Matplotlib se puede mostrar como imagen. `pz.image` acepta bytes PNG; crea la figura con `matplotlib.figure.Figure` (no con `pyplot`) para poder dibujarla en un hilo sin riesgo: ```python import io from matplotlib.figure import Figure def render(values) -> bytes: figure = Figure(figsize=(6, 3), dpi=144) figure.subplots().plot(values) buffer = io.BytesIO() figure.savefig(buffer, format="png", transparent=True) return buffer.getvalue() class Plot(pz.App): def refresh(self): return pz.Command.perform(lambda: render(self.values), lambda png: setattr(self, "png", png)) def view(self): return pz.image(self.png) if self.png else ui.spinner() ``` ## Agentes de IA ### Pyzed para agentes de IA > Pyzed es un framework de interfaces en Python pensado para la IA: servidor MCP, llms.txt, reglas para editores, un agente dentro de Pyzed Design y pyzed.agents para crear, comprobar y ver apps de escritorio. Source: https://albakore.github.io/pyzed/es/ai/ :::tip[Funciona con cualquier modelo] Todo lo de esta sección funciona con Claude, GPT, Gemini, modelos locales con Ollama y cualquier cliente MCP. Nada de esto es obligatorio para usar Pyzed. ::: :::caution[Provisional] `pyzed.agents` y el servidor MCP (`pyzed.mcp`) son provisionales en Pyzed 1.x: una versión menor todavía puede ajustar detalles, y cada cambio aparece en *Breaking Changes* de las [notas de versión](https://albakore.github.io/pyzed/es/changelog/). ::: Pyzed está hecho para escribirse **con** agentes de IA y para poner IA **dentro** de tus apps. Un agente conectado a Pyzed no adivina la API de memoria: consulta la firma exacta de cada componente, sigue las reglas de Pyzed, comprueba el código que escribe y renderiza una captura de la ventana para ver el resultado, todo sin abrir ninguna ventana. ```mermaid flowchart LR subgraph You["Tu editor o agente"] A["Agente de IA
(Cursor, VS Code, Zed,
Claude Desktop...)"] end subgraph Pyzed["Pyzed"] M["Servidor MCP
pyzed mcp"] K["Conocimiento
componentes · iconos · temas · docs"] G["Generadores
new_app · new_component · new_form"] V["Comprobaciones
validate_code · validate_form"] S["Render sin pantalla
screenshot"] end D["Docs como texto
llms.txt · página.md"] A -- "herramientas por stdio" --> M M --> K & G & V & S A -. "lee" .-> D S -- "PNG" --> A A -- "escribe" --> P["Tu app Pyzed"] ``` #### Tres formas de trabajar con IA | | Qué es | Empieza aquí | | --- | --- | --- | | **Agentes que escriben Pyzed** | Conecta cualquier agente que hable [MCP](https://modelcontextprotocol.io) y tendrá los componentes, la documentación, los generadores, las comprobaciones y las capturas de Pyzed. Los agentes sin MCP pueden leer la documentación como texto. | [Servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/), [llms.txt y reglas para editores](https://albakore.github.io/pyzed/es/ai/llms-txt/) | | **El agente de Pyzed Design** | El diseñador visual tiene un panel de agente que coloca controles, conecta eventos, escribe handlers y crea formularios, componentes y apps completas; ve imágenes, usa servidores MCP y ejecuta agentes externos, y todo se puede deshacer. | [Agente en Pyzed Design](https://albakore.github.io/pyzed/es/ai/agent-composer/), [Claves de API](https://albakore.github.io/pyzed/es/ai/api-keys/) | | **IA dentro de tu app** | `pyzed.agents` habla con muchos proveedores de modelos con herramientas y streaming, y el modelo async de Pyzed muestra las respuestas a medida que llegan sin congelar la interfaz. | [Crear apps con IA](https://albakore.github.io/pyzed/es/ai/ai-apps/) | #### Por qué los agentes escriben buen código Pyzed - **Una sola forma de hacer interfaces.** El estado está en la app, `view()` devuelve widgets y los eventos llaman a métodos. No hay un árbol de widgets que mutar, ni gestores de geometría que elegir, ni reglas de hilos del `mainloop` que recordar: hay menos maneras de que el código generado falle. - **Todo se consulta, nada se adivina.** `describe_component("ui.table")` devuelve la firma real y la documentación de los parámetros de la versión instalada, y `validate_code` rechaza parámetros que no existen (`heigth=8`), efectos secundarios en `view()` y lambdas en bucles antes de ejecutar nada. - **El agente puede ver.** La herramienta `screenshot` renderiza la app sin pantalla (sin display ni GPU) y devuelve un PNG, así el agente revisa el diseño y lo corrige, como lo haría una persona. - **Documentación hecha también para máquinas.** Cada página de este sitio existe en Markdown (`/pyzed/es/.md`), el sitio entero como [`llms.txt`](https://albakore.github.io/pyzed/es/llms.txt) y [`llms-full.txt`](https://albakore.github.io/pyzed/es/llms-full.txt), y el paquete incluye stubs tipados (`.pyi`) que leen editores y agentes. - **Bloques para empezar.** Los [Bloques](https://albakore.github.io/pyzed/es/blocks/) son pantallas completas y probadas que un agente puede leer con `read_example("blocks/dashboard")` y adaptar, en vez de partir de un archivo vacío. #### Qué puede hacer un agente con Pyzed | Pide | Qué pasa | | --- | --- | | *"Haz un temporizador Pomodoro con barra de progreso"* | Lee las buenas prácticas, consulta `ui.progress`, escribe la app, la valida y mira una captura. Mira el [ejemplo completo](https://albakore.github.io/pyzed/es/ai/prompting/#un-ejemplo-completo). | | *"Haz un componente de valoración reutilizable"* | Genera un `pz.Component` con props, estado y eventos que también aparece en la caja de herramientas de Pyzed Design. | | *"Crea un formulario de clientes con validación"* | Genera un `.pzform` y su módulo de handlers, y comprueba ambos con `validate_form`. | | *"Parte del bloque kanban y carga las tarjetas desde SQLite"* | Lee el bloque, mantiene la interfaz y cambia los datos de ejemplo por una consulta. | | *"¿Por qué no se desplaza mi lista?"* | Lee la guía de layout y la documentación del componente, y corrige el código. | | *"Añade un chat con un modelo local"* | Usa `pyzed.agents` o una llamada HTTP compatible con OpenAI, con streaming hacia la interfaz. | #### Inicio rápido 1. Instala Pyzed con el extra MCP: `pip install "pyzed-ui[mcp]"` (o deja que el agente lo ejecute con `uvx`). 2. Añade el servidor a tu agente, por ejemplo en `.cursor/mcp.json` o `.vscode/mcp.json`, como se explica en [Servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/#conecta-tu-agente). 3. Copia la [plantilla de `AGENTS.md`](https://albakore.github.io/pyzed/es/ai/llms-txt/#agentsmd-para-un-proyecto-pyzed) en tu proyecto y pide lo que necesites. La [guía de prompts](https://albakore.github.io/pyzed/es/ai/prompting/) tiene peticiones que funcionan bien. ### Servidor MCP > Conecta Cursor, VS Code, Zed, Claude Desktop o cualquier cliente MCP a Pyzed: componentes, documentación, generadores de código, validación y capturas sin pantalla para agentes de IA. Source: https://albakore.github.io/pyzed/es/ai/mcp/ `pyzed mcp` es un servidor del [Model Context Protocol](https://modelcontextprotocol.io) que viene con Pyzed. Le da a un agente de IA lo que sabe una persona experta en Pyzed, sacado de la **versión instalada** del paquete: la firma y la documentación de cada componente, los iconos, los temas, esta documentación, los ejemplos y bloques, generadores de apps, componentes y formularios, dos validadores, un renderizador sin pantalla y herramientas de proyecto para construir una app completa en una carpeta. Esta página es la versión corta. La [referencia completa de MCP](https://albakore.github.io/pyzed/es/guides/mcp/) lista cada herramienta, recurso y prompt. #### Conecta tu agente La mayoría de los clientes MCP (Claude Desktop, Cursor, Windsurf y otros) leen un JSON con esta forma, como `.cursor/mcp.json` en tu proyecto: ```json { "mcpServers": { "pyzed": { "command": "uvx", "args": ["--from", "pyzed-ui[mcp]", "pyzed", "mcp"] } } } ``` VS Code lee `.vscode/mcp.json`: ```json { "servers": { "pyzed": { "type": "stdio", "command": "uvx", "args": ["--from", "pyzed-ui[mcp]", "pyzed", "mcp"] } } } ``` Zed y otros editores aceptan el mismo comando y argumentos en sus propios ajustes (Zed los llama *context servers*). :::tip[Usa el entorno de tu proyecto] En un proyecto creado con `pyzed new`, apunta el agente al entorno del proyecto para que el servidor vea la misma versión de Pyzed y **tus propios componentes**: `"command": "uv", "args": ["run", "pyzed", "mcp"]`. Si Pyzed ya está instalado, `"command": "pyzed", "args": ["mcp"]` también sirve. ::: `uvx` ejecuta el servidor sin instalar nada antes; necesita [uv](https://docs.astral.sh/uv/). Sin uv, ejecuta `pip install "pyzed-ui[mcp]"` (Python 3.10 o superior) y usa `pyzed mcp` como comando. #### Qué puede hacer el agente | Grupo | Herramientas | Para | | --- | --- | --- | | Aprender | `get_guide`, `list_guides`, `list_docs`, `read_docs` | Leer cómo funciona Pyzed, sus reglas, el recetario de ejemplos ejecutables, temas como juegos o shaders y cualquier página de esta documentación (en inglés o español). | | Consultar | `list_components`, `describe_component`, `search_reference`, `read_reference`, `search_icons`, `list_themes`, `editor_keymap` | Obtener firmas exactas, parámetros, nombres de iconos, tokens de color y atajos en vez de adivinarlos. | | Reutilizar | `list_examples`, `read_example` | Leer los ejemplos, los [Bloques](https://albakore.github.io/pyzed/es/blocks/) (`read_example("blocks/settings")`), juegos, showcase e integraciones con backend. | | Generar | `new_app`, `new_component`, `new_form` | Partir de esqueletos correctos: una app, un `pz.Component`, un formulario del diseñador. | | Comprobar | `validate_code`, `validate_form` | Detectar nombres que no existen, argumentos mal escritos, efectos en `view()` y formularios rotos antes de ejecutar. | | Ver | `screenshot` | Renderizar la app o un componente sin pantalla y recibir un PNG. | | Planificar | `plan_app` | Encontrar los bloques, ejemplos, componentes y guías que necesita una app, y los pasos para construirla. | | Construir | `project_info`, `create_project`, `list_files`, `read_file`, `write_files`, `add_dependency`, `check_project`, `run_tests`, `screenshot_project`, `project_settings`, `set_project_settings` | Crear un proyecto, escribirlo, añadir librerías, revisarlo, probarlo, mirarlo, elegir su renderizador y sus ajustes compartidos, en la carpeta donde corre el servidor y en el entorno del propio proyecto. | | Ver en vivo | `start_live_preview`, `live_preview_status`, `stop_live_preview` | Abrir la app en una ventana con hot reload, para verla cambiar mientras el agente escribe cada archivo. | Los prompts `create_app`, `create_component`, `create_form` y `build_project` (una app completa, de principio a fin) empiezan una tarea con los pasos correctos, y los clientes que admiten recursos pueden adjuntar `pyzed://best-practices` o `pyzed://docs/{slug}`. #### Peticiones que aprovechan el servidor - *"Usa las herramientas MCP de pyzed. Lee primero las buenas prácticas y luego construye..."*: los agentes siguen mejor las instrucciones sobre sus herramientas cuando las nombras. - *"Valida con validate_code y mira una captura antes de responder."*: el agente cierra el ciclo. - *"Parte del bloque de dashboard."*: una pantalla probada es mejor base que un archivo vacío. - *"Consulta describe_component de ui.table antes de usarlo."*: para componentes con muchos parámetros. Más en la [guía de prompts](https://albakore.github.io/pyzed/es/ai/prompting/), con una sesión completa y las salidas reales de las herramientas. #### Seguridad - Por **stdio** (lo habitual) el servidor solo habla con el agente que lo inició. - `screenshot` **ejecuta el código que recibe** en otro proceso, con un límite de 60 segundos y sin abrir ventanas. Trátalo como ejecutar código tú mismo: los agentes suelen pedir permiso antes de usar herramientas; mantenlo así con prompts que no sean de confianza. - Las herramientas de proyecto solo leen y escriben dentro de la carpeta donde arrancó el servidor (o `--root RUTA`), nunca archivos que puedan guardar secretos (`.env`, claves...), y ejecutan uv, ruff, ty y pytest ahí. - `pyzed mcp --transport streamable-http` lo sirve en `127.0.0.1` sin autenticación; ahí `screenshot` y las herramientas de proyecto están desactivados salvo que añadas `--allow-code`. #### Problemas frecuentes | Problema | Solución | | --- | --- | | El agente no muestra herramientas `pyzed` | Ejecuta a mano el comando de la configuración (`uvx --from "pyzed-ui[mcp]" pyzed mcp`): debería quedarse esperando en silencio. Revisa los registros MCP del cliente. | | `ModuleNotFoundError: mcp` | Instala el extra: `pip install "pyzed-ui[mcp]"`. Hace falta Python 3.10+. | | El agente no conoce tus componentes | Ejecuta el servidor desde el entorno del proyecto (`uv run pyzed mcp`) para que los importe. | | La documentación no se puede leer sin conexión | Las páginas se leen de una copia del repositorio o se descargan de GitHub; sin conexión, usa una copia local o las [páginas en Markdown](https://albakore.github.io/pyzed/es/ai/llms-txt/). | | Las capturas tardan demasiado | El código debe definir una `pz.App` o una función `preview()`; un trabajo largo en `__init__` llega al límite de 60 segundos. | #### Sin MCP Todo el servidor es Python normal y se puede usar desde scripts, tests o tu propio agente: ```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")) ``` Los agentes que no pueden usar MCP pueden leer la documentación como texto: mira [llms.txt y reglas para editores](https://albakore.github.io/pyzed/es/ai/llms-txt/). ### El agente de Pyzed Design > El agente de IA de Pyzed Design de un vistazo: un compositor que crea formularios, componentes y apps completas, ve imágenes, usa servidores MCP y agentes externos e inicia sesión en muchos proveedores de modelos. Source: https://albakore.github.io/pyzed/es/ai/agent-composer/ Pyzed Design, el diseñador visual, tiene un agente de IA en su panel derecho. Describes una pantalla con palabras y el agente coloca los controles, conecta los eventos, escribe los handlers en Python, crea formularios, componentes o un proyecto entero, mira el resultado y comprueba su propio código. Cada cambio en el formulario abierto es un paso normal de deshacer, y **Rechazar** devuelve todo lo que tocó una ejecución. (Image: El agente añadió un interruptor y conectó su evento) Esta página es un resumen. El manual completo, con cada botón, comando y ajuste, está en [Pyzed Design → Agentes de IA](https://albakore.github.io/pyzed/es/editor/ai-agents/). #### De un vistazo | Función | Qué hace | Detalles | | --- | --- | --- | | **Compositor** | Un cuadro de mensaje: @ adjunta la selección, el formulario, su código, un control o un archivo; / ejecuta comandos como `/app`, `/fix`, `/tidy` y `/look`. | [El panel](https://albakore.github.io/pyzed/es/editor/ai-agents/#el-panel) | | **Perfiles** | **Escribir** (por defecto), **Preguntar** (solo lee) y **Mínimo** (sin herramientas), más **Preguntar antes de escribir archivos**. Los cambios en el formulario abierto nunca preguntan, porque se pueden deshacer. | [Perfiles](https://albakore.github.io/pyzed/es/editor/ai-agents/#el-panel) | | **Muchos proveedores** | Anthropic, OpenAI, Gemini, Vertex AI, GitHub Copilot, OpenRouter, xAI, Mistral, DeepSeek, Groq, Ollama, LM Studio y cualquier servidor compatible con OpenAI, con esfuerzo y modo rápido en Claude. | [Proveedores](https://albakore.github.io/pyzed/es/editor/ai-agents/#proveedores) | | **Inicio de sesión** | Iniciar sesión con el navegador (se recoge la clave copiada), OAuth de OpenRouter, **Iniciar sesión con GitHub** (flujo de dispositivo) e **Iniciar sesión con Google** para Vertex AI. | [Iniciar sesión](https://albakore.github.io/pyzed/es/editor/ai-agents/#iniciar-sesión) | | **Claude sin clave** | La federación de identidad de cargas de trabajo cambia el token de tu proveedor de identidad por un token de Claude de corta duración. | [Claude sin clave de API](https://albakore.github.io/pyzed/es/editor/ai-agents/#claude-sin-clave-de-api) | | **Imágenes** | Adjunta o pega imágenes, o una captura del formulario; el agente también mira el formulario por su cuenta con `screenshot_form`. | [Imágenes](https://albakore.github.io/pyzed/es/editor/ai-agents/#imágenes) | | **Apps completas** | `/app` crea un proyecto, añade paquetes, escribe formularios y componentes, los mira y ejecuta las comprobaciones. | [Crear una aplicación](https://albakore.github.io/pyzed/es/editor/ai-agents/#crear-una-aplicación) | | **Servidores MCP** | Suma las herramientas de cualquier servidor del Model Context Protocol (comando local o URL); sus secretos van al llavero. | [Servidores MCP](https://albakore.github.io/pyzed/es/editor/ai-agents/#servidores-mcp) | | **Agentes externos** | Ejecuta Claude Code, Gemini CLI, Codex o cualquier agente del Agent Client Protocol dentro del panel, compartiendo el servidor MCP de Pyzed. | [Agentes externos](https://albakore.github.io/pyzed/es/editor/ai-agents/#agentes-externos) | | **Asistente en línea** | Ctrl+Alt+I en el editor de código reescribe un handler o el archivo en su sitio. | [Asistente en línea](https://albakore.github.io/pyzed/es/editor/ai-agents/#asistente-en-línea) | | **Credenciales** | Las claves y tokens viven en el llavero del sistema, nunca en los ajustes ni en el proyecto. | [Claves de API y credenciales](https://albakore.github.io/pyzed/es/ai/api-keys/) | (Image: Añadir contexto con @) #### Una primera sesión 1. Abre el panel: la pestaña **Agente** del dock derecho, o Ctrl+Alt+A. 2. Conecta un modelo: pulsa un proveedor e inicia sesión, o elige un modelo local. Consulta [Claves de API y credenciales](https://albakore.github.io/pyzed/es/ai/api-keys/) para las claves, las variables de entorno y las buenas prácticas. 3. Pide una pantalla: *"Un formulario de cliente: nombre, email, un desplegable de país y Guardar / Cancelar; Guardar valida el email y muestra un aviso"*. Luego afínala: selecciona controles y pide, adjunta un boceto o ejecuta `/look` y `/fix`. 4. Revisa la ejecución con **Conservar** o **Rechazar** y pulsa F5 para ejecutar el formulario. Hay consejos para buenos prompts en [Cómo obtener buenos resultados](https://albakore.github.io/pyzed/es/editor/ai-agents/#cómo-obtener-buenos-resultados) y en la [guía de prompts](https://albakore.github.io/pyzed/es/ai/prompting/). #### El mismo agente en tu propia app El panel funciona sobre `pyzed.agents`, que cualquier app de Pyzed puede usar: los mismos proveedores, el llavero, imágenes, servidores MCP, agentes externos, herramientas escritas como funciones de Python y eventos en streaming. Consulta [Crear apps con IA](https://albakore.github.io/pyzed/es/ai/ai-apps/) y [Agentes de IA en tu app](https://albakore.github.io/pyzed/es/guides/agents/). ### Claves de API y credenciales > Cómo encuentran Pyzed Design y pyzed.agents las credenciales de los proveedores de modelos: el llavero del sistema, variables de entorno, inicio de sesión con el navegador, credenciales de Google Cloud y federación de identidades, y cómo mantener las claves seguras. Source: https://albakore.github.io/pyzed/es/ai/api-keys/ El agente de [Pyzed Design](https://albakore.github.io/pyzed/es/editor/ai-agents/) y la biblioteca [`pyzed.agents`](https://albakore.github.io/pyzed/es/guides/agents/) necesitan una credencial del proveedor de modelos que elijas: una clave de API, un token de inicio de sesión o, para Claude, la federación de identidad de cargas de trabajo. Esta página explica dónde las busca Pyzed, cómo dársela sin escribirla en un archivo y cómo mantenerla segura. Los modelos locales (Ollama, LM Studio) no necesitan nada. #### Dónde busca Pyzed En cada petición Pyzed usa la primera credencial que encuentra, en este orden: 1. **El llavero del sistema.** Una clave que pegaste o un token de inicio de sesión, guardados con el servicio `pyzed` en el almacén de credenciales del sistema operativo: Llavero en macOS, Administrador de credenciales en Windows, Secret Service (GNOME Keyring, KWallet) en Linux. Los tokens de inicio de sesión (OpenRouter, GitHub, Google) también se guardan ahí y se renuevan cuando están por caducar. 2. **La federación de identidad de cargas de trabajo** (solo Anthropic), cuando está activada en los ajustes y no hay ninguna clave guardada. Consulta [Claude sin clave de API](#claude-sin-clave-de-api). 3. **La variable de entorno del proveedor**, como `ANTHROPIC_API_KEY` u `OPENAI_API_KEY` (tabla más abajo). Se lee, nunca se copia a ningún sitio. 4. **Lo que el equipo ya tiene.** Google Vertex AI usa las credenciales de Google Cloud de `gcloud auth application-default login`. Anthropic también usa un perfil de la CLI de Anthropic (`ant auth login`) y las variables `ANTHROPIC_FEDERATION_RULE_ID` / `ANTHROPIC_ORGANIZATION_ID`. Una clave guardada siempre gana: si defines una variable de entorno y parece que se ignora, abre el proveedor en los ajustes y pulsa **Quitar la clave**. Cada proveedor tiene en los ajustes un distintivo que dice de dónde sale su credencial, y **Probar la conexión** comprueba que funciona: | Distintivo | Significado | | --- | --- | | **Clave guardada en el llavero del sistema** | Una clave que pegaste, guardada en el llavero. | | **Clave solo para esta sesión** | No se encontró un llavero: la clave vive en memoria hasta que cierres el diseñador. | | **Sesión iniciada** | Un token de inicio de sesión (OAuth o el flujo de dispositivo de GitHub) en el llavero. | | **Clave del entorno** | La variable de entorno del proveedor. | | **Federación de identidad de cargas de trabajo** | Claude mediante federación, sin clave. | | **Sesión con el perfil de la CLI de Anthropic** | Un perfil de `ant auth login` en este equipo. | | **Credenciales de Google Cloud de este equipo** | Vertex AI con las credenciales predeterminadas de `gcloud`. | | **No necesita clave** | Ollama, LM Studio o un servidor compatible con OpenAI sin clave. | #### Variables de entorno por proveedor | Proveedor | Prefijo del modelo | Variable de entorno | Otras formas | | --- | --- | --- | --- | | Anthropic | `anthropic:` | `ANTHROPIC_API_KEY` | Iniciar sesión con el navegador, federación, `ant auth login` | | OpenAI | `openai:` | `OPENAI_API_KEY` | Iniciar sesión con el navegador | | Google Gemini (AI Studio) | `google:` | `GEMINI_API_KEY` | Iniciar sesión con el navegador | | Google Vertex AI | `vertex:` | ninguna | Iniciar sesión con Google, `gcloud auth application-default login` o un archivo de cuenta de servicio en `GOOGLE_APPLICATION_CREDENTIALS`; el proyecto sale de los ajustes o de `GOOGLE_CLOUD_PROJECT` | | GitHub Copilot (GitHub Models) | `github:` | `GITHUB_TOKEN` | Iniciar sesión con GitHub (flujo de dispositivo), un token detallado con el permiso **Models** | | OpenRouter | `openrouter:` | `OPENROUTER_API_KEY` | Iniciar sesión con el navegador (OAuth con PKCE) | | xAI | `xai:` | `XAI_API_KEY` | Iniciar sesión con el navegador | | Mistral | `mistral:` | `MISTRAL_API_KEY` | Iniciar sesión con el navegador | | DeepSeek | `deepseek:` | `DEEPSEEK_API_KEY` | Iniciar sesión con el navegador | | Groq | `groq:` | `GROQ_API_KEY` | Iniciar sesión con el navegador | | Ollama, LM Studio | `ollama:`, `lmstudio:` | ninguna | Nada: corren en tu equipo | | Compatible con OpenAI | `custom:` | `PYZED_AGENT_API_KEY` | Una clave en los ajustes, solo si el servidor la necesita | Otras variables que lee Pyzed: `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_IDENTITY_TOKEN_FILE` y `ANTHROPIC_IDENTITY_TOKEN` (federación), `PYZED_GITHUB_CLIENT_ID`, `PYZED_GOOGLE_CLIENT_ID` y `PYZED_GOOGLE_CLIENT_SECRET` (tus propias apps OAuth para iniciar sesión), `GOOGLE_APPLICATION_CREDENTIALS` (un archivo de cuenta de servicio de Google Cloud para Vertex AI) y `PYZED_KEYRING=0` (no usar el llavero). #### La forma recomendada: el llavero No escribas las claves en archivos ni en la terminal: deja que Pyzed Design las guarde en el llavero. 1. Abre **Ajustes → Agentes de IA → Ajustes de los agentes de IA…** (también el engranaje del panel del agente, o `/settings`). 2. En **Proveedores**, pulsa el proveedor para abrirlo. 3. Pulsa **Iniciar sesión con el navegador** (Pyzed abre la página de claves del proveedor y recoge la clave cuando pulsas su botón **Copiar**; consulta [Iniciar sesión](https://albakore.github.io/pyzed/es/editor/ai-agents/#iniciar-sesión)), o pega la clave en **Clave de API** y pulsa **Guardar**. 4. Un aviso dice **Clave guardada en el llavero del sistema**, y el distintivo del proveedor lo mismo. La clave nunca va a `agents.json`, a las conversaciones ni a tu proyecto, y después el campo solo muestra una vista enmascarada (`sk-ant…a1b2`). **Quitar la clave** la borra del llavero. Desde código, el mismo almacén está a una llamada: ```python from pyzed import agents agents.set_key("anthropic", "sk-ant-…") # True: en el llavero; False: solo para esta sesión agents.get_key("anthropic") # la clave guardada o la variable de entorno agents.delete_key("anthropic") ``` Ejecútalo una vez desde una consola de Python, no desde un archivo que guardes, para que la clave nunca se escriba en disco. ##### "Clave solo para esta sesión" Si los ajustes muestran **No se encontró un llavero del sistema** y el distintivo dice **Clave solo para esta sesión**, Pyzed no pudo usar un llavero, así que una clave que pegues dura hasta que cierres el diseñador. Nunca se guarda en texto plano como alternativa. Para arreglarlo: - **Si usas Pyzed desde pip o uv:** instala el extra que trae `keyring`: - **Linux:** `keyring` necesita un Secret Service en marcha, que los escritorios GNOME y KDE arrancan por ti. En un gestor de ventanas mínimo o una máquina sin escritorio, instala y desbloquea `gnome-keyring` (o KWallet), o usa una variable de entorno. - **El Pyzed Design compilado** ([descarga](https://albakore.github.io/pyzed/es/download/)) ya incluye `keyring`; si aun así lo dice, el llavero está bloqueado o no existe. Pyzed ignora a propósito los backends "fail" y de texto plano de keyring, para que una clave nunca se guarde sin cifrar. #### Usar variables de entorno Una variable de entorno es práctica en un runner de CI, en un contenedor o si ya guardas las claves en un gestor de contraseñas que las exporta. Pyzed solo la lee. Desde una terminal, defínela y abre el diseñador desde esa misma terminal: ```bash export ANTHROPIC_API_KEY="sk-ant-…" # macOS y Linux (bash, zsh) pyzed design ``` ```powershell $env:ANTHROPIC_API_KEY = "sk-ant-…" # Windows PowerShell, solo esta ventana pyzed design ``` Las apps que abres desde el Finder, el menú Inicio o un lanzador del escritorio no leen el `~/.zshrc` ni el `~/.bashrc` de tu shell, así que el Pyzed Design compilado no ve una variable definida ahí. Defínela donde las apps gráficas toman su entorno y luego cierra y vuelve a abrir Pyzed Design: - **macOS:** `launchctl setenv ANTHROPIC_API_KEY "sk-ant-…"`. Dura hasta que reinicies el Mac; para conservarla, ejecútalo desde un elemento de inicio o un LaunchAgent. - **Windows:** **Inicio → Editar las variables de entorno de esta cuenta → Nueva…** (variables de usuario), o `setx ANTHROPIC_API_KEY "sk-ant-…"` en una terminal. Los programas que abras después la ven. - **Linux (escritorios con systemd):** añade `ANTHROPIC_API_KEY=sk-ant-…` a un archivo como `~/.config/environment.d/pyzed.conf` y cierra la sesión y vuelve a entrar. Ten en cuenta que una variable de entorno es texto plano: todos los programas que se abren desde ese entorno la heredan, también las apps que ejecutas con F5. Una clave del llavero solo se entrega a Pyzed. En tu propio equipo prefiere el llavero, y las variables de entorno donde no haya llavero. Pyzed Design no carga archivos `.env`. En tu propia app puedes cargar uno con una biblioteca como `python-dotenv`; déjalo fuera de git (añádelo a `.gitignore`). #### Claude sin clave de API La federación de identidad de cargas de trabajo cambia un token de corta duración de tu proveedor de identidad (GitHub Actions, Google Cloud, Azure, Okta, Kubernetes…) por un token de Claude de corta duración, así que no se guarda ninguna clave de Anthropic. Configúrala en la Claude Console y luego rellena **Anthropic → Federación de identidad de cargas de trabajo** en los ajustes, o define `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID` y `ANTHROPIC_IDENTITY_TOKEN_FILE`. El token de identidad puede salir de un archivo, de un comando que imprime uno nuevo (`gcloud auth print-identity-token`) o de una variable. Los campos se explican en [Claude sin clave de API](https://albakore.github.io/pyzed/es/editor/ai-agents/#claude-sin-clave-de-api), y la versión en código en [Agentes de IA en tu app](https://albakore.github.io/pyzed/es/guides/agents/#claude-sin-clave-de-api). La federación solo se usa si no hay una clave guardada: quita la clave de Anthropic guardada para pasar a ella. #### Tokens de inicio de sesión - **OpenRouter** inicia sesión con OAuth y PKCE; la clave que emite para Pyzed va al llavero. - **GitHub Copilot** inicia sesión con el flujo de dispositivo de GitHub (escribes un código corto en `github.com/login/device`). Necesita el client ID de una GitHub App en los ajustes del proveedor o `PYZED_GITHUB_CLIENT_ID`; sin él, pega un token de acceso personal detallado con el permiso **Models**. - **Google Vertex AI** inicia sesión con Google (OAuth con PKCE) o usa las credenciales de `gcloud`. Iniciar sesión con tu propio cliente OAuth necesita su secreto, que también va al llavero. Los tokens se guardan en el llavero como la entrada del proveedor, se renuevan poco antes de caducar y **Cerrar sesión** los borra. #### Servidores MCP y agentes externos Los [servidores MCP](https://albakore.github.io/pyzed/es/editor/ai-agents/#servidores-mcp) y los [agentes externos](https://albakore.github.io/pyzed/es/editor/ai-agents/#agentes-externos) suelen necesitar su propio token (`GITHUB_TOKEN` para el servidor MCP de GitHub, una clave para un agente externo). Ponlos en el campo **Secretos** como pares `NOMBRE=valor` separados por `;`. Se guardan en el llavero, nunca en `agents.json`, y se pasan al servidor como variables de entorno (o como cabeceras HTTP para un servidor remoto) cuando arranca. En código: ```python from pyzed.agents import mcp_client mcp_client.set_secrets("github", {"GITHUB_TOKEN": "github_pat_…"}) # en el llavero ``` #### Lo que el agente no puede leer Las herramientas de archivos del agente se niegan a listar, leer, buscar o escribir archivos que suelen contener credenciales, para que una inyección de prompt escondida en una página web o en un archivo no consiga que se las envíe al modelo: `.env`, `.env.*`, `*.pem`, `*.key`, `*.p12`, `*.pfx`, `id_rsa*`, `id_ecdsa*`, `id_ed25519*`, `.netrc`, `.pypirc`, `.npmrc` y `.git-credentials`. Tampoco se ofrecen en el menú de @, y los agentes externos siguen la misma regla cuando piden archivos a Pyzed. La lista es `pyzed.agents.tools.SECRET_FILES`; pasa la tuya a `file_tools(carpeta, hidden=...)` en tu app. Las claves tampoco se escriben nunca en las conversaciones ni en los ajustes, y el campo solo muestra una vista enmascarada. #### Buenas prácticas - **Usa una clave dedicada** para Pyzed, con un **límite de gasto** o presupuesto en la consola del proveedor, para que una filtración o un bucle descontrolado tengan techo. - **Nunca pongas una clave en el código**, en un `.pzform`, en `agents.json` ni en ningún lugar del repositorio. Si llega a un commit, borrar el commit no basta: revócala. - **Rota una clave que creas filtrada**: revócala en la consola del proveedor, crea una nueva y pégala otra vez (**Quitar la clave** y luego **Guardar**). - **Prefiere credenciales de corta duración** cuando puedas: tokens de inicio de sesión, credenciales de Google Cloud o la federación de Claude en lugar de una clave de API de larga duración, sobre todo en equipos compartidos y en CI. - **¿Distribuyes una app que usa `pyzed.agents`?** No incluyas tu clave en ella: todo lo que va dentro de una app se puede extraer. Deja que cada usuario inicie sesión o pegue su propia clave (va a su llavero), o pon un servidor tuyo entre la app y el proveedor. - **Usa modelos locales** (Ollama, LM Studio) cuando los datos no deban salir del equipo. ### llms.txt y reglas para editores > Da la documentación de Pyzed a cualquier LLM o IDE con IA: llms.txt, llms-full.txt, páginas en Markdown, y plantillas listas de AGENTS.md, reglas de Cursor e instrucciones de Copilot para proyectos Pyzed. Source: https://albakore.github.io/pyzed/es/ai/llms-txt/ No todos los agentes hablan MCP, y a veces solo quieres pegar la documentación en un chat. Este sitio se publica también como texto plano para modelos de lenguaje, siguiendo la convención [llms.txt](https://llmstxt.org), y esta página tiene archivos de instrucciones listos que enseñan a cualquier editor con IA a escribir Pyzed. #### La documentación como texto | URL | Qué contiene | Para | | --- | --- | --- | | [`/pyzed/es/llms.txt`](https://albakore.github.io/pyzed/es/llms.txt) | Un índice: cada página con su título, enlace y resumen de una línea. | Que un agente elija qué leer. | | [`/pyzed/es/llms-full.txt`](https://albakore.github.io/pyzed/es/llms-full.txt) | Toda la documentación en español en un archivo. | Indexarla en un IDE con IA o en modelos de contexto largo. | | `/pyzed/es/.md` | Cualquier página en Markdown: [`/pyzed/es/guides/layout.md`](https://albakore.github.io/pyzed/es/guides/layout.md). | Enlazar una página desde un prompt o una regla. | | [`/pyzed/llms.txt`](https://albakore.github.io/pyzed/llms.txt), [`/pyzed/llms-small.txt`](https://albakore.github.io/pyzed/llms-small.txt), [`/pyzed/llms-full.txt`](https://albakore.github.io/pyzed/llms-full.txt) | Lo mismo en inglés; `llms-small.txt` tiene lo esencial (primeros pasos, tutoriales, guías, IA y preguntas frecuentes). | Modelos que trabajan mejor en inglés o con poco contexto. | | `/pyzed/llms/.txt` | Una sección en inglés, como [`guides`](https://albakore.github.io/pyzed/llms/guides.txt), [`library`](https://albakore.github.io/pyzed/llms/library.txt) o [`blocks`](https://albakore.github.io/pyzed/llms/blocks.txt). | Solo lo que necesita una tarea. | La dirección completa empieza por `https://albakore.github.io`, por ejemplo `https://albakore.github.io/pyzed/es/llms-full.txt`. Todos se generan a partir de las mismas páginas que estás leyendo, así que siempre coinciden con la versión actual. #### En tu editor con IA - **Cursor**: *Settings → Indexing & Docs → Add doc* con `https://albakore.github.io/pyzed/llms-full.txt`, y menciónalo como `@Pyzed` en el chat. Añade la [regla de Cursor](#reglas-de-cursor) de abajo para todas las peticiones. - **VS Code con Copilot**: añade el [archivo de instrucciones](#github-copilot) de abajo; en el chat, usa `#fetch` con una página como `https://albakore.github.io/pyzed/es/guides/async.md` cuando necesites sus detalles. - **Zed**: `/fetch https://albakore.github.io/pyzed/llms-small.txt` en el panel del agente, y añade el [`AGENTS.md`](#agentsmd-para-un-proyecto-pyzed) al proyecto. - **Un chat en el navegador**: adjunta o pega `llms-small.txt`, o dale el enlace al modelo si puede navegar. - **Cualquier agente con MCP**: mejor el [servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/). Lee las mismas páginas cuando las necesita y además valida código y hace capturas. El paquete también incluye **stubs de tipos** (`pyzed/_core.pyi` y tipos en línea), así los editores y agentes que leen tipos ven los parámetros de cada widget incluso sin la documentación. #### AGENTS.md para un proyecto Pyzed Muchos agentes leen un `AGENTS.md` en la raíz del proyecto. Copia este; encaja con los proyectos de `pyzed new`. Está en inglés porque los modelos siguen las instrucciones igual de bien y así sirve a todo el equipo, pero puedes traducirlo. ```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. ``` #### Reglas de Cursor Guárdalo como `.cursor/rules/pyzed.mdc`. Se aplica a cada archivo Python y apunta Cursor a la documentación y al `AGENTS.md` de arriba: ```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 Guárdalo como `.github/copilot-instructions.md` (para todo el repositorio), o como `.github/instructions/pyzed.instructions.md` con una cabecera `applyTo` para acotarlo: ```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. ``` #### Un prompt de sistema para tu propio agente ¿Construyes un agente que escribe Pyzed (con `pyzed.agents` o cualquier SDK)? Dale la guía y las herramientas: ```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()` le da al modelo las mismas consultas, generadores y comprobaciones que el servidor MCP. ### Guía de prompts > Cómo pedir a agentes de IA que construyan apps de escritorio con Pyzed, con un ejemplo completo: un agente crea un temporizador Pomodoro usando las herramientas MCP de Pyzed, con sus salidas reales. Source: https://albakore.github.io/pyzed/es/ai/prompting/ Los agentes escriben mucho mejor código Pyzed cuando el prompt les da tres cosas: **para qué es la pantalla**, **qué debe hacer** y **cómo comprobar el resultado**. Esta página tiene patrones de prompts que funcionan y una sesión completa, con las salidas reales de las [herramientas MCP](https://albakore.github.io/pyzed/es/ai/mcp/), de un agente construyendo una app pequeña. #### Anatomía de un buen prompt > Construye un **temporizador Pomodoro** como app Pyzed. *(qué)* > Tiene modos Foco (25 min) y Descanso (5 min), una cuenta atrás grande, una barra de progreso, Empezar/Pausa y > Reiniciar, y cuenta las sesiones terminadas. *(comportamiento)* > Usa componentes de `pyzed.ui` y tokens del tema, y no pongas efectos secundarios en `view()`. *(restricciones)* > Usa las herramientas MCP de pyzed: consulta firmas con `describe_component`, ejecuta `validate_code` y mira un > `screenshot` antes de responder. *(cómo comprobar)* | Incluye | Por qué | | --- | --- | | El propósito y quién lo usa | *"Una pantalla de ajustes para una app de notas"* da mejores decisiones que *"un formulario"*. | | El comportamiento como lista | Cada punto se convierte en estado y un método; no se olvida nada. | | Restricciones | Tema, tamaño de ventana, componentes a usar o evitar, versión de Python. | | Un punto de partida | *"Parte del bloque de ajustes"* o *"sigue examples/todo.py"*. | | La comprobación | La validación y una captura cierran el ciclo. Sin ellas el agente se queda en "debería funcionar". | #### Prompts que funcionan **App nueva** > Crea una app Pyzed para explorar una carpeta de notas Markdown: una lista con búsqueda a la izquierda, la nota > renderizada a la derecha y Ctrl+N para una nota nueva. Lee primero las buenas prácticas con get_guide. Valídala > y haz una captura. **Desde un bloque** > Parte del bloque de dashboard (`read_example("blocks/dashboard")`). Cambia los datos de ejemplo por cifras leídas > de `ventas.csv` con el módulo csv, cargadas en una corrutina para que la ventana nunca se bloquee. **Componente** > Haz un `pz.Component` reutilizable llamado TagInput: un campo de texto que convierte palabras separadas por comas > en chips que se pueden quitar. Props: `tags: list[str]`; evento `change` con la lista nueva. Muéstralo en una > captura con `preview()`. **Formulario del diseñador** > Crea un formulario del diseñador "factura" con cliente, fecha, una tabla de líneas y un total; el total se > actualiza cuando cambia una línea. Usa new_form y validate_form. **Corregir** > Esta lista no se desplaza y la ventana se congela mientras carga. Lee guides/layout y guides/async con read_docs, > explica la causa y corrígela. **Estilo** > Haz que esta pantalla parezca el lanzador de un videojuego: tema oscuro con acento morado, un panel de vidrio > sobre un degradado y un botón Jugar que brille. Mira los efectos en animation/effects. #### Qué evitar - **"Usa código estilo tkinter"** o pegar código de otro toolkit: Pyzed no tiene `pack()`, `grid()` ni objetos de widget que mutar. Describe el comportamiento y deja que el agente lo escriba al estilo Pyzed. - **Pedirlo todo a la vez** en una app grande. Construye la pantalla, compruébala y luego añade funciones: cada paso se valida y se mira. - **Saltarse la comprobación.** Un agente que nunca ejecuta `validate_code` a veces inventa un parámetro; una llamada lo detecta. #### Un ejemplo completo Lo que sigue es una sesión real con el servidor MCP de Pyzed (`python -m pyzed.mcp`), manejada por un cliente MCP. Las salidas son exactamente lo que devolvieron las herramientas, acortadas donde se indica. **El prompt:** *Construye un temporizador Pomodoro con modos Foco y Descanso, una barra de progreso, Empezar/Pausa y Reiniciar. Usa las herramientas MCP de pyzed para comprobar lo que uses, valida el código y mira una captura.* ##### 1. Consultar los componentes El agente necesita una barra de progreso, así que la busca: ```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."} ] ``` Prefiere el componente `ui` y pide su firma exacta: ```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/" } ``` Y el nombre de un icono, porque los iconos son nombres de Lucide: ```text search_icons(query="timer", limit=8) ``` ```json {"total": 3, "icons": ["timer", "timer-off", "timer-reset"]} ``` ##### 2. Un primer borrador y lo que dice el validador El primer borrador tiene tres errores típicos del código generado: un atributo de clase mutable, un cambio de estado dentro de `view()` y un argumento mal escrito. ```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. La app corregida El agente corrige los tres problemas, añade los modos y el contador de sesiones, y vuelve a validar: ```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. Mirar el resultado ```text screenshot(code=…, width=420, height=380, scale=2) ``` La herramienta renderizó la app sin pantalla y devolvió este PNG, que el agente revisa antes de responder: (Image: El temporizador Pomodoro renderizado por la herramienta screenshot) Todo el ciclo (consultar, escribir, validar, mirar) llevó cinco llamadas a herramientas. La app está en el repositorio como [`examples/pomodoro.py`](https://github.com/albakore/pyzed/blob/main/examples/pomodoro.py) y se ejecuta con `python examples/pomodoro.py`. #### Reproducir la sesión La sesión se puede ejecutar con el SDK de MCP para Python, útil para probar tus propios prompts o herramientas: ```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()) ``` ### Crear apps con IA > Crea una app de escritorio de chat con IA en Python con Pyzed: una API compatible con OpenAI por httpx, generadores async que transmiten tokens a la interfaz, detener, errores y tests sin red. Source: https://albakore.github.io/pyzed/es/ai/ai-apps/ Pyzed encaja muy bien con las apps de IA: las respuestas llegan token a token mientras la ventana sigue respondiendo, el Markdown y el código se muestran sin hacer nada y los componentes (campos, tarjetas, spinners, badges) ya tienen el aspecto de una herramienta de IA moderna. Esta página construye un **chat con streaming** completo que habla con cualquier servidor compatible con OpenAI (OpenAI, Ollama, LM Studio, vLLM, llama.cpp, OpenRouter, Groq...) usando [httpx](https://www.python-httpx.org), y que funciona sin red en modo demo. | Conversación vacía | Respuesta en curso | | --- | --- | | (Image: El chat con sugerencias) | (Image: Una respuesta llegando) | (Image: La respuesta terminada con Markdown, código y acciones) #### Ejecutarlo } uv run --with httpx python examples/ai_chat.py `} pip={` python examples/ai_chat.py # demo: una respuesta fija, sin red ni clave pip install httpx # para un modelo real {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: una respuesta fija, sin red ni clave {py} -m pip install httpx # para un modelo real {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 `} /> El código es [`examples/ai_chat.py`](https://github.com/albakore/pyzed/blob/main/examples/ai_chat.py), unas 220 líneas con la interfaz. Estas son las partes importantes. #### Cómo funciona el streaming ```mermaid sequenceDiagram participant UI as Hilo de la UI participant App as AIChat participant Loop as bucle asyncio participant API as Servidor del modelo UI->>App: Enter → send() App-->>Loop: return reply() (async generator) Loop->>API: POST /chat/completions (stream=true) loop cada token API-->>Loop: data: {"choices":[{"delta":{"content":"…"}}]} Loop-->>App: yield ("delta", turn, text) → update() App->>UI: view() con la respuesta más larga end Loop-->>App: yield ("done", turn, None) ``` Un callback que **devuelve un generador async** hace que Pyzed lo ejecute en su bucle asyncio, y cada valor que produce llega a `update` en el hilo de la interfaz (mira [Async y suscripciones](https://albakore.github.io/pyzed/es/guides/async/)). Sin hilos, colas ni bloqueos en tu código. ##### 1. Leer el stream del servidor Los servidores compatibles con OpenAI envían [server-sent events](https://developer.mozilla.org/es/docs/Web/API/Server-sent_events): líneas que empiezan por `data:` con un trozo JSON, y terminan con `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 ``` El modo demo la cambia por una función con la misma forma, así el resto de la app no nota la diferencia: ```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. Enviar y convertir el stream en mensajes ```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` devuelve **una lista**: el generador y un comando que baja el hilo hasta el final. Pyzed ejecuta los dos. - Cada respuesta lleva un número de **turno**. **Detener** solo incrementa `self.turn`: el generador en marcha ve un turno más nuevo y termina, y `update` ignora los trozos de un turno anterior que ya venían en camino. - Los errores se convierten en un mensaje en vez de una excepción: una clave mal puesta o un servidor caído muestran un aviso rojo en el hilo y la app sigue funcionando. ##### 3. Actualizar el estado ```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. La vista La respuesta del asistente es un `ui.markdown`, con un cursor (`▍`) al final mientras llega y un `ui.spinner` antes del primer token. El hilo es un `pz.scrollable(..., anchor_bottom=True)` para que el texto nuevo quede a la vista, y el compositor cambia el botón de enviar por **Detener** mientras `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())) ``` Bajo las respuestas terminadas aparecen los botones de copiar, regenerar y valorar; `pz.Command.copy(text)` deja la respuesta en el portapapeles. #### Probarlo sin red El stream es un argumento del constructor, así que los tests manejan la app entera con un modelo falso, igual que los tests de Pyzed en `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 ``` El test del repositorio también levanta un pequeño servidor HTTP local que habla server-sent events y comprueba que `openai_stream` envía la petición correcta y une los trozos. #### Con `pyzed.agents`: proveedores y herramientas Para varios proveedores, claves en el llavero del sistema y **herramientas** (funciones que el modelo puede llamar), usa [`pyzed.agents`](https://albakore.github.io/pyzed/es/guides/agents/), el motor del agente de Pyzed Design. `Agent.run` bloquea, así que ejecútalo en un hilo con `pz.Command.perform` y pasa sus eventos a `update` con `pz.post`, que es seguro desde cualquier hilo: ```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 para seguir - **Varias conversaciones**: guarda una lista de hilos y muéstralos en una barra lateral con `ui.list_item`, como el [bloque de chat](https://albakore.github.io/pyzed/es/blocks/chat/). - **Adjuntos**: lee un archivo que elija el usuario y añade su texto al mensaje. - **Herramientas que tocan la interfaz**: una herramienta con `ui=True` se ejecuta en el hilo de la interfaz a través del `executor` del agente. - **Una app local**: Ollama o LM Studio en la misma máquina, sin clave y sin que los datos salgan del ordenador. - **Estilo**: dale un brillo al avatar del asistente (`.style(glow=("#8b5cf6", 18))`) o un panel de vidrio al compositor; mira [Animación y efectos](https://albakore.github.io/pyzed/es/animation/effects/). ## Componentes por categoría ### Visión general > Resumen de pyzed.ui, los componentes listos para usar para GUIs de escritorio en Python: workspaces, pestañas, árboles, editores, paletas y menús. Source: https://albakore.github.io/pyzed/es/components/overview/ :::tip[Biblioteca de componentes] Cada componente tiene ahora su propia página, con vista previa, ejemplos y su API completa, en la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/). Esta página se mantiene como resumen. ::: `pyzed.ui` es una colección de componentes listos para usar que reproducen el aspecto del editor [Zed](https://zed.dev). Son **funciones** que devuelven widgets, así que se combinan libremente con los [widgets base](https://albakore.github.io/pyzed/es/reference/widgets/) y entre sí. (Image: Galería de componentes (One Dark)) ```python import pyzed as pz from pyzed import ui ui.button("Guardar", self.guardar, icon="save", variant="solid", keys="ctrl-s") ``` #### Reglas comunes - **Colores del tema.** Cada componente lee el tema actual (`App.theme`, o el de la ventana en [multi-ventana](https://albakore.github.io/pyzed/es/guides/windows/)). Cambiar el tema restiliza toda la interfaz. Todos aceptan `theme=` para usar otro. - **Tokens o hex.** Los parámetros de color (`color`, `icon_color`, `tint`…) aceptan un token del tema como `"accent"`, `"text_muted"` o `"error"`, o un color `"#rrggbb"`. - **Mensajes o callables.** `on_press`, `on_select`, `on_toggle`… aceptan un mensaje para `update` o un callable. Los que producen un valor (texto, `bool`, id) llaman al callable con ese valor. - **Iconos.** El parámetro `icon=` acepta cualquier nombre de [Lucide](https://lucide.dev). - **Atajos.** `keys="ctrl-shift-p"` muestra el atajo (con símbolos en macOS). #### Grupos | Grupo | Componentes | | --- | --- | | [Básicos](https://albakore.github.io/pyzed/es/components/basics/) | `button`, `icon_button`, `label`, `muted`, `heading`, `badge`, `indicator`, `kbd`, `avatar`, `tooltip`, `divider`, `card` | | [Formularios](https://albakore.github.io/pyzed/es/components/forms/) | `input`, `search_input`, `switch`, `checkbox`, `slider`, `progress`, `segmented`, `field` | | [Listas y árbol](https://albakore.github.io/pyzed/es/components/lists/) | `list_item`, `list_header`, `tree` + `TreeNode`, `menu` + `MenuItem` | | [Navegación](https://albakore.github.io/pyzed/es/components/navigation/) | `tab_bar` + `Tab`, `breadcrumbs`, `toolbar` | | [Workspace](https://albakore.github.io/pyzed/es/components/workspace/) | `workspace`, `title_bar`, `title_button`, `status_bar`, `status_item`, `panel`, `panel_header` | | [Overlays](https://albakore.github.io/pyzed/es/components/overlays/) | `layer`, `modal`, `command_palette`, `picker`, `fuzzy_filter`, `toast`, `toasts` | | [Código](https://albakore.github.io/pyzed/es/components/code/) | `editor`, `terminal`, `ansi` | | [Más controles](https://albakore.github.io/pyzed/es/components/controls/) | `select`, `radio_group`, `number_field`, `split_button`, `toggle_group`, `chip`, `disclosure` | | [Datos y feedback](https://albakore.github.io/pyzed/es/components/data/) | `table` + `Column`, `callout`, `empty_state`, `settings_group`, `stat`, `sparkline`, `bar_chart`, `donut`, `spinner`, `count_badge`, `facepile`, `diff_stat`, `keybinding_hint` | | [Contenido y paneles](https://albakore.github.io/pyzed/es/components/content/) | `markdown`, `split`, `popover`; widgets base `pz.image`, `pz.image_viewer`, `pz.rich_text`, `pz.qr_code`, `pz.grid` | | [Diálogos y herramientas](https://albakore.github.io/pyzed/es/components/dialogs/) | `dialog_layer`, `alert`, `confirm`, `prompt`, `choice`, `unsaved_changes`, `accordion`, `Log` + `console`, `shortcuts`, `about`, `ColorPicker`, `color_swatch` | Para ver todos funcionando, ejecuta la galería: ```bash python examples/gallery.py ``` :::tip[Ilustraciones] Las imágenes de esta sección se generan con `python docs/scripts/component_shots.py`, que dibuja cada ejemplo fuera de pantalla con `App.screenshot`. ::: ### Básicos > Componentes básicos de Pyzed para GUIs de escritorio en Python: botones, textos, badges, atajos de teclado, avatares, tooltips y tarjetas que siguen el tema. Source: https://albakore.github.io/pyzed/es/components/basics/ :::tip[Biblioteca de componentes] Cada componente tiene ahora su propia página, con vista previa, ejemplos y su API completa, en la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/). Esta página se mantiene como resumen. ::: #### Botones (Image: Botones) ```python pz.row([ ui.button("Filled", self.accion), ui.button("Ghost", self.accion, variant="ghost"), ui.button("Outline", self.accion, variant="outline"), ui.button("Solid", self.accion, variant="solid"), ui.button("Disabled", self.accion, disabled=True), ], spacing=8) ui.button("Error", self.borrar, variant="error", icon="trash-2") ui.button("Guardar", self.guardar, icon="save", keys="ctrl-s") ui.button("lg", self.accion, size="lg") ui.icon_button("settings", self.ajustes, tooltip="Ajustes") ui.icon_button("bell", self.avisos, selected=True) ``` - `variant`: `"filled"` (por defecto), `"ghost"`, `"subtle"`, `"outline"`, `"solid"`, `"tinted"` o directamente un color semántico (`"accent"`, `"error"`, `"warning"`, `"success"`, `"info"`). - `size`: `"xs"`, `"sm"`, `"md"`, `"lg"`. - `icon` + `icon_position="start" | "end"`, `full_width=True`, `selected=True`. - Si `on_press` es `None`, el botón se dibuja deshabilitado. - Cualquier parámetro de estilo de [`pz.button`](https://albakore.github.io/pyzed/es/reference/widgets/#button) (`bg`, `hover_bg`, `radius`…) sobrescribe el del tema. #### Texto (Image: Textos) ```python ui.heading("Heading 1") ui.heading("Heading 2", level=2) ui.label("Label normal") ui.muted("Texto atenuado") ui.label("Texto de acento", color="accent") ui.label("def mono(): ...", mono=True, size="sm") ``` `size` acepta `"xs"`, `"sm"`, `"md"`, `"lg"`, `"xl"`, `"2xl"` o un número. #### Badges, indicadores, atajos y avatares (Image: Badges, indicadores, atajos y avatares) ```python ui.badge("success", tint="success") ui.badge("error", tint="error", icon="circle-x") ui.badge("solid", variant="solid") # "soft" (por defecto), "solid", "outline" ui.indicator("success") # punto de color ui.kbd("ctrl-shift-p") # ⌃⇧P en macOS, Ctrl Shift P en el resto ui.kbd("cmd-k cmd-s") # acordes ui.avatar("KV", index=1) # index elige un color de colaborador del tema ``` El estilo de los atajos se controla con `pyzed.ui.base.KEY_STYLE`: `"auto"` (por defecto), `"symbols"` o `"words"`. #### Tooltip ```python ui.tooltip(ui.icon_button("save", self.guardar), "Guardar", keys="ctrl-s", position="bottom") ``` `ui.icon_button(..., tooltip="Texto")` es un atajo para lo mismo. #### Tarjeta y divisor (Image: Tarjeta) ```python ui.card( pz.column([ ui.muted("Pyzed dibuja la interfaz con Iced en Rust."), pz.row([pz.hspace(), ui.button("Cancelar", self.cerrar, variant="ghost"), ui.button("Aceptar", self.aceptar, variant="solid")], spacing=8), ], spacing=12), title="Tarjeta", actions=[ui.icon_button("ellipsis", self.menu, size="sm")], width=380, ) ui.divider() # horizontal ui.divider(vertical=True) ``` Referencia completa: [Componentes (pyzed.ui)](https://albakore.github.io/pyzed/es/reference/ui/#básicos). ### Formularios > Campos de texto, interruptores, casillas, deslizadores, barras de progreso y grupos segmentados: formularios para GUIs nativas en Python con Pyzed. Source: https://albakore.github.io/pyzed/es/components/forms/ :::tip[Biblioteca de componentes] Cada componente tiene ahora su propia página, con vista previa, ejemplos y su API completa, en la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/). Esta página se mantiene como resumen. ::: (Image: Controles de formulario) ```python class Ajustes(pz.App): def __init__(self): self.nombre = "" self.query = "" self.autosave = True self.formatear = False self.fuente = 0.6 self.modo = "Editor" def view(self): return pz.column([ ui.input(self.nombre, lambda t: setattr(self, "nombre", t), placeholder="Nombre del proyecto"), ui.search_input(self.query, lambda t: setattr(self, "query", t)), ui.field("Autoguardado", ui.switch(self.autosave, lambda v: setattr(self, "autosave", v)), description="Guarda al cambiar de pestaña"), ui.field("Formato al guardar", ui.checkbox(self.formatear, lambda v: setattr(self, "formatear", v))), ui.field("Tamaño de fuente", pz.container(ui.slider(self.fuente, lambda v: setattr(self, "fuente", v)), width=160)), ui.segmented(["Editor", "Terminal", "Ambos"], self.modo, lambda m: setattr(self, "modo", m)), ui.progress(0.65), ], spacing=12, padding=20, width=420) ``` #### Campos de texto Los campos son **controlados**: muestran `value` y te avisan de cada cambio con `on_input`. Guarda el texto en tu estado y vuelve a pasarlo en `view()`. ```python ui.input(self.email, self.set_email, placeholder="tu@correo.com", icon="mail", on_submit=self.enviar, id="email") ui.input(self.clave, self.set_clave, secure=True) # contraseña ui.input(self.query, self.set_query, borderless=True, size="lg") ``` Para enfocar un campo desde el código, dale un `id` y devuelve `Command.focus("email")`. #### Interruptores y casillas `on_toggle` recibe el nuevo `bool`: ```python ui.switch(self.wifi, lambda on: setattr(self, "wifi", on), label="Wi-Fi") ui.checkbox(self.acepto, lambda v: setattr(self, "acepto", v), label="Acepto los términos") ``` #### Deslizador y progreso ```python ui.slider(self.volumen, self.set_volumen, min=0, max=100, step=1) ui.progress(self.descargado, max=self.total, tint="success", height=6) ``` #### Grupo segmentado Botones mutuamente excluyentes, con uno seleccionado. `on_select` recibe la opción elegida: ```python ui.segmented(["Día", "Semana", "Mes"], self.vista, lambda v: setattr(self, "vista", v)) ``` #### Filas de ajustes `ui.field(título, control, description=...)` alinea un título a la izquierda y el control a la derecha, como una pantalla de ajustes. Referencia completa: [Formularios](https://albakore.github.io/pyzed/es/reference/ui/#formularios). ### Listas, árbol y menús > Filas de lista, el árbol de archivos del proyecto y menús contextuales: componentes de listas para apps de escritorio en Python. Source: https://albakore.github.io/pyzed/es/components/lists/ :::tip[Biblioteca de componentes] Cada componente tiene ahora su propia página, con vista previa, ejemplos y su API completa, en la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/). Esta página se mantiene como resumen. ::: #### Elementos de lista (Image: Elementos de lista) ```python pz.column([ ui.list_header("RECIENTES", end=ui.icon_button("plus", self.nuevo, size="xs")), ui.list_item("main.py", self.abrir, icon="file-code", icon_color="#3572a5", selected=True, inset=True), ui.list_item("README.md", self.abrir, icon="file-text", inset=True, end=ui.badge("M", tint="warning")), ui.list_item("Cargo.toml", self.abrir, icon="file-cog", inset=True, end=ui.kbd("ctrl-1", size="xs")), ui.list_item("antiguo.txt", self.abrir, icon="file", inset=True, disabled=True), ], width=300) ``` `list_item` tiene huecos `start` y `end` para cualquier widget, `indent` para anidar, `focused` para el borde de foco y `on_right_press` para abrir un menú contextual. Para pasar el elemento al callback, usa un argumento por defecto en la lambda: ```python [ui.list_item(f, lambda f=f: self.abrir(f), selected=f == self.actual) for f in self.archivos] ``` #### Árbol de archivos Un panel de proyecto como el de los editores de código: carpetas plegables, iconos por tipo de archivo y colores de git. (Image: Árbol de archivos) ```python class Explorador(pz.App): def __init__(self): self.nodes = ui.TreeNode.from_paths( ["src/main.rs", "src/app.rs", "src/ui/button.rs", "src/ui/mod.rs", "tests/app.rs", "Cargo.toml", "README.md"], status={"src/app.rs": "modified", "src/ui/button.rs": "created", "README.md": "modified"}, ) self.expanded = {"src", "src/ui"} self.selected = "src/app.rs" def toggle(self, folder): self.expanded ^= {folder} def select(self, path): self.selected = path def view(self): return ui.tree(self.nodes, expanded=self.expanded, selected=self.selected, on_toggle=self.toggle, on_select=self.select) ``` - `TreeNode.from_paths(rutas, status=...)` crea el árbol, pone las carpetas primero y propaga el estado de git a las carpetas que contienen cambios. - El id de cada nodo es su ruta (`"src/ui/button.rs"`). - `status`: `"modified"`, `"created"`, `"deleted"`, `"conflict"` o `"ignored"`. - Puedes crear los nodos a mano: `TreeNode("src", children=[TreeNode("main.rs", id="src/main.rs")])`. Para leer una carpeta real: ```python from pathlib import Path root = Path(".") paths = [p.relative_to(root).as_posix() for p in root.rglob("*") if p.is_file() and ".git" not in p.parts] nodes = ui.TreeNode.from_paths(paths) ``` #### Menú contextual (Image: Menú contextual) ```python ui.menu([ ui.MenuItem("Nuevo archivo", self.nuevo, icon="file-plus", keys="ctrl-n"), ui.MenuItem("Nueva carpeta", self.nueva_carpeta, icon="folder-plus"), ui.SEPARATOR, "EDITAR", # un texto suelto es un encabezado ui.MenuItem("Copiar ruta", self.copiar, keys="ctrl-alt-c"), ui.MenuItem("Ajuste de línea", self.toggle_wrap, checked=self.wrap), ui.SEPARATOR, ui.MenuItem("Eliminar", self.eliminar, icon="trash-2", danger=True), ]) ``` Para mostrarlo flotando, úsalo como overlay de [`ui.layer`](https://albakore.github.io/pyzed/es/components/overlays/#capas): ```python layout = ui.layer(layout, self.menu_abierto and ui.menu(...) or None, on_dismiss=self.cerrar_menu, align_x="start", offset=120) ``` Referencia completa: [Listas, árbol y menús](https://albakore.github.io/pyzed/es/reference/ui/#listas-árbol-y-menús). ### Navegación > Pestañas de editor, breadcrumbs y barra de herramientas: componentes de navegación para crear GUIs tipo IDE en Python con Pyzed. Source: https://albakore.github.io/pyzed/es/components/navigation/ :::tip[Biblioteca de componentes] Cada componente tiene ahora su propia página, con vista previa, ejemplos y su API completa, en la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/). Esta página se mantiene como resumen. ::: (Image: Pestañas, breadcrumbs y barra de herramientas) ```python pz.column([ ui.tab_bar( [ ui.Tab.for_file("src/main.rs"), ui.Tab.for_file("src/app.rs", dirty=True, status="modified"), ui.Tab.for_file("README.md", preview=True), ], active="src/main.rs", on_select=self.activar, on_close=self.cerrar, start=[ui.icon_button("arrow-left", self.atras, size="sm"), ui.icon_button("arrow-right", self.adelante, size="sm")], end=[ui.icon_button("plus", self.nuevo, size="sm"), ui.icon_button("columns-2", self.dividir, size="sm")], ), ui.toolbar( [ui.breadcrumbs([("file-code", "src"), "main.rs", "fn main"])], [ui.icon_button("search", self.buscar, size="sm")], ), ]) ``` #### Pestañas - `Tab(id, label, icon=None, icon_color=None, dirty=False, preview=False, status=None)`. - `Tab.for_file(ruta)` usa el nombre del archivo como etiqueta, la ruta como id y el icono de su tipo. - `dirty=True` muestra un punto; al pasar el ratón se convierte en el botón de cerrar. - `preview=True` dibuja el nombre en cursiva (pestaña de vista previa). - `status` tiñe el nombre con el color de git. - `on_select(id)` y `on_close(id)` reciben el id de la pestaña. Un patrón habitual para gestionar pestañas: ```python def abrir(self, path): if path not in self.abiertas: self.abiertas.append(path) self.activa = path def cerrar(self, path): i = self.abiertas.index(path) self.abiertas.remove(path) if self.activa == path: self.activa = self.abiertas[min(i, len(self.abiertas) - 1)] if self.abiertas else None ``` #### Breadcrumbs ```python ui.breadcrumbs(["src", "ui", "button.rs"]) ui.breadcrumbs([("folder", "src"), ("file-code", "main.rs")], on_press=self.ir_a_nivel) ``` Las partes pueden ser textos o tuplas `(icono, texto)`. `on_press` recibe el índice. #### Barra de herramientas `ui.toolbar(start, end)` es la barra bajo las pestañas de un editor de código: normalmente breadcrumbs a la izquierda y acciones del buffer a la derecha. Referencia completa: [Navegación](https://albakore.github.io/pyzed/es/reference/ui/#navegación). ### Workspace > Crea una ventana estilo editor en Python: barra de título, docks redimensionables, paneles y barra de estado con los componentes de workspace de Pyzed. Source: https://albakore.github.io/pyzed/es/components/workspace/ :::tip[Biblioteca de componentes] Cada componente tiene ahora su propia página, con vista previa, ejemplos y su API completa, en la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/). Esta página se mantiene como resumen. ::: `ui.workspace` monta la estructura de una ventana estilo editor: barra de título arriba, docks a la izquierda, derecha y abajo, el área central y la barra de estado. (Image: Workspace) ```python def view(self): return ui.workspace( pz.column([ ui.tab_bar([ui.Tab.for_file("src/main.py")], "src/main.py", on_close=self.cerrar), ui.editor("src/main.py", self.codigo, path="main.py"), ]), title_bar=ui.title_bar( [ui.title_button("pyzed", chevron=True), ui.title_button("main", icon="git-branch", muted=True)], [ui.icon_button("bell", self.avisos, size="sm"), ui.avatar("KV")], ), left=pz.column([ ui.panel_header("Proyecto", [ui.icon_button("plus", self.nuevo, size="xs")]), ui.tree(self.nodes, expanded=self.expanded, selected="src/main.py"), ]) if self.show_left else None, bottom=pz.column([ ui.panel_header("Terminal", icon="terminal"), ui.terminal("$ python main.py\n\x1b[32m✓\x1b[0m listo"), ]) if self.show_bottom else None, status_bar=ui.status_bar( [ui.status_item("0", icon="circle-x"), ui.status_item("1", icon="triangle-alert")], [ui.status_item("Ln 4, Col 8"), ui.status_item("Python")], ), left_width=200, bottom_height=110, ) ``` #### Docks - Pasa `None` en `left`, `right` o `bottom` para ocultar un dock. Así se implementan atajos como Ctrl+B (`self.show_left = not self.show_left`). - `left_width`, `right_width` y `bottom_height` fijan su tamaño. - Dentro de un dock, `ui.panel_header(título, acciones, icon=...)` hace de cabecera. - `ui.panel(contenido, header=..., side=...)` sirve si montas tu propio layout. #### Barra de título ```python ui.title_bar( start=[ui.title_button("mi-proyecto", self.proyectos, chevron=True)], end=[ui.icon_button("settings", self.ajustes, size="sm")], center=ui.muted("main.py"), ) ``` #### Barra de estado ```python 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}"), ui.status_item(pz.icons.language_for(self.activa))], ) ``` Referencia completa: [Workspace](https://albakore.github.io/pyzed/es/reference/ui/#workspace). ### Overlays > Capas, modales, paleta de comandos, pickers con búsqueda difusa y notificaciones: componentes superpuestos para apps de escritorio con Pyzed. Source: https://albakore.github.io/pyzed/es/components/overlays/ :::tip[Biblioteca de componentes] Cada componente tiene ahora su propia página, con vista previa, ejemplos y su API completa, en la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/). Esta página se mantiene como resumen. ::: #### Capas `ui.layer(base, overlay, on_dismiss=...)` dibuja `overlay` encima de la interfaz. Hacer clic fuera envía `on_dismiss`. Si `overlay` es `None`, devuelve `base` sin cambios, así que puedes escribir: ```python def view(self): layout = ui.workspace(...) return ui.layer(layout, self.dialogo(), on_dismiss=self.cerrar, backdrop="#00000066") def dialogo(self): if not self.confirmando: return None return ui.modal(...) ``` `align_x`, `align_y` y `offset` controlan dónde aparece el overlay (por defecto, centrado arriba como una paleta de comandos). #### Modal (Image: Modal) ```python ui.modal( ui.muted("Hay cambios sin guardar en main.py. ¿Quieres guardarlos antes de cerrar?"), title="¿Guardar cambios?", on_close=self.cerrar, footer=[ ui.button("No guardar", self.descartar, variant="ghost"), ui.button("Cancelar", self.cerrar), ui.button("Guardar", self.guardar, variant="solid"), ], width=420, ) ``` #### Paleta de comandos (Image: Paleta de comandos) ```python class App(pz.App): def __init__(self): self.palette_open = False self.query = "" self.selected = 0 def commands(self): return [ ui.PaletteItem("editor: toggle soft wrap", self.toggle_wrap, keys="alt-z"), ui.PaletteItem("theme selector: toggle", self.toggle_theme, keys="ctrl-k ctrl-t"), ui.PaletteItem("workspace: toggle left dock", self.toggle_left, keys="ctrl-b"), ] def on_key(self, combo): if combo == "ctrl-shift-p": self.palette_open, self.query, self.selected = True, "", 0 return Command.focus("command-palette") if self.palette_open and combo == "escape": self.palette_open = False elif self.palette_open and combo in ("up", "down"): self.selected += 1 if combo == "down" else -1 def run_selected(self): matches = ui.fuzzy_filter(self.query, self.commands(), key=lambda c: c.label) if matches: self.palette_open = False command = matches[self.selected % len(matches)][0] return command.on_press() def view(self): layout = ... palette = None if self.palette_open: palette = ui.command_palette(self.query, lambda q: setattr(self, "query", q), self.commands(), selected=self.selected, on_submit=self.run_selected) return ui.layer(layout, palette, on_dismiss=lambda: setattr(self, "palette_open", False)) ``` Enter (`on_submit`) ejecuta el comando resaltado; las flechas cambian `selected`. #### Picker y búsqueda difusa (Image: Picker con coincidencias difusas) `command_palette` está construida sobre `picker` y `fuzzy_filter`. Úsalos para tus propios buscadores (archivos, símbolos, ramas…): ```python matches = ui.fuzzy_filter(self.query, self.archivos) # [(archivo, posiciones), ...] items = [(ui.PaletteItem(Path(f).name, lambda f=f: self.abrir(f), icon="file", detail=f), pos) for f, pos in matches[:50]] ui.picker(self.query, self.set_query, items, selected=self.sel, placeholder="Buscar archivos…") ``` `fuzzy_match(query, texto)` devuelve `(puntuación, posiciones)` o `None`, y `highlighted()` dibuja un texto con las coincidencias resaltadas. #### Notificaciones (Image: Notificaciones) ```python ui.toast("Guardado", "main.py se guardó correctamente.", kind="success", on_dismiss=self.cerrar_aviso) ui.toast("Actualización disponible", "Pyzed 0.2 ya está en PyPI.", kind="info", actions=[ui.button("Ver novedades", self.novedades, size="sm")]) ``` `ui.toasts(base, items)` las apila en la esquina inferior derecha. Para que desaparezcan solas, usa un hilo y [`pz.post`](https://albakore.github.io/pyzed/es/guides/commands/#mensajes-desde-otros-hilos): ```python def avisar(self, titulo): self.avisos.append(titulo) threading.Timer(3, pz.post, args=(("quitar_aviso", titulo),)).start() ``` Referencia completa: [Overlays](https://albakore.github.io/pyzed/es/reference/ui/#overlays). ### Código > Un editor de código con resaltado de sintaxis y marcadores de git, y una vista de terminal: componentes listos para apps de escritorio en Python. Source: https://albakore.github.io/pyzed/es/components/code/ :::tip[Biblioteca de componentes] Cada componente tiene ahora su propia página, con vista previa, ejemplos y su API completa, en la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/). Esta página se mantiene como resumen. ::: #### Editor (Image: Editor de código) ```python ui.editor( "counter.py", # id del buffer codigo, # texto inicial path="counter.py", # deduce el lenguaje markers={6: "modified", 7: "created", 10: "created"}, on_change=self.cambiado, # recibe el texto completo on_cursor=self.cursor, # recibe (línea, columna), desde 1 ) ``` - **No controlado.** El texto, el cursor, la selección y el historial de deshacer viven en Rust, asociados al `id`. `text` solo se usa la primera vez que aparece ese `id`. Para reemplazar el contenido devuelve `Command.set_editor_text(id, texto)`. - **Un buffer por archivo.** Usa la ruta como `id`: al cambiar de pestaña cada archivo conserva su cursor y su historial. - **Resaltado.** Más de 100 lenguajes (gramáticas de syntect / two-face) con los colores de sintaxis del tema. El lenguaje se deduce de `path` o se pasa con `language="rs"`. - **Marcadores de git.** `markers` asigna a cada línea `"created"`, `"modified"` o `"deleted"`. - **Cursor.** `Command.set_editor_cursor(id, línea, columna)` mueve el cursor, incluso antes del primer dibujado. ```python def guardar(self): Path(self.activa).write_text(self.textos[self.activa]) self.sucios.discard(self.activa) def cambiado(self, texto): self.textos[self.activa] = texto self.sucios.add(self.activa) ``` - **Líneas largas.** Con `wrap=False` (por defecto) el editor se desplaza en horizontal y los números de línea quedan fijos a la izquierda. Con `wrap=True` las líneas se ajustan y el número aparece en la primera fila de cada línea. - **Línea activa.** La línea del cursor se resalta y el scroll sigue al cursor (también con RePág/AvPág). ##### Editar como en un editor de código moderno (Image: Bloques plegados, code lens y el minimapa) ```python ui.editor( "app.py", codigo, path="app.py", minimap=True, # o un ancho en píxeles cursor_shape="block", # "bar" (por defecto), "block", "underline", "hollow" on_zoom=self.zoom, # 1, -1 o 0 con Ctrl+= / Ctrl+- / Ctrl+0 y Ctrl+rueda font_size=self.tamano, ) def zoom(self, paso): self.tamano = 14 if paso == 0 else self.tamano + paso ``` - **Cierre automático** (`auto_close=True`, por defecto). Al escribir `(`, `[`, `{`, `"`, `'` o `` ` `` también se inserta el de cierre y el cursor queda en medio. Con una selección, el par la envuelve. Escribir un cierre que ya está a continuación lo salta, Retroceso dentro de un par vacío borra los dos y Enter entre llaves abre una línea con sangría. Las comillas no se cierran dentro de palabras (`don't`), pero sí tras un prefijo de cadena (`f"`, `rb'`). - **Plegado** (`folding=True`, por defecto). Los bloques se pliegan según la sangría: pasa el ratón por el margen para ver las flechas y haz clic en una para plegar su bloque. Una línea plegada termina en `⋯` (haz clic para desplegarla). Ctrl+Shift+[ pliega el bloque más interno alrededor del cursor y Ctrl+Shift+] lo despliega. `Command.fold_line(id, línea)` y `Command.fold_all(id)` lo hacen desde código. Los números de línea, `markers`, `annotations`, `on_change`, `on_cursor` y `set_editor_cursor` siguen usando las líneas reales; editar una línea plegada o mover el cursor dentro de un pliegue lo despliega. - **Minimapa.** `minimap=True` dibuja el código en miniatura a la derecha con el área visible resaltada; haz clic para saltar, arrastra para desplazarte o usa la rueda encima. - **Zoom de fuente.** El editor pasa Ctrl+=, Ctrl+-, Ctrl+0 y Ctrl+rueda a `on_zoom`; cambia `font_size` en respuesta. - **Cursor.** `cursor_shape`, `cursor_blink`, `cursor_width` (de la barra) y `cursor_color` (por defecto, el color de jugador del tema), como la opción `cursor_shape` de Zed. ##### Varios cursores y los atajos de Zed (Image: Tres cursores y los resultados de buscar prices) El editor usa los atajos por defecto de Zed y VS Code (Linux y Windows; en macOS, Cmd en lugar de Ctrl): | Teclas | Acción | | --- | --- | | Alt+clic | Añade un cursor (o quita el que hay bajo el clic) | | Ctrl+Alt+↑ / ↓ | Añade un cursor arriba / abajo | | Ctrl+D | Selecciona la palabra bajo el cursor y luego añade la siguiente coincidencia como cursor | | Ctrl+Shift+L | Selecciona todas las coincidencias | | Alt+Shift+I | Un cursor en cada línea de la selección | | Ctrl+U | Deshace el último cambio de cursores | | Esc | Vuelve a un cursor y luego quita la selección | | Ctrl+Z, Ctrl+Y o Ctrl+Shift+Z | Deshacer y rehacer (lo escrito seguido es un solo paso) | | Alt+↑ / ↓ | Sube / baja las líneas | | Alt+Shift+↑ / ↓ o Ctrl+Alt+Shift+↑ / ↓ | Duplica las líneas de cada cursor arriba / abajo | | Ctrl+Shift+D | Duplica la selección de cada cursor justo después (la línea si no hay) | | Ctrl+Shift+K | Borra las líneas | | Ctrl+L | Selecciona la línea (otra vez: también la siguiente) | | Ctrl+Shift+J | Une líneas | | Ctrl+Enter / Ctrl+Shift+Enter | Nueva línea debajo / encima, con sangría | | Ctrl+] / Ctrl+[, Shift+Tab | Sangra / quita sangría (Tab sangra una selección de varias líneas) | | Ctrl+/ | Comenta o descomenta las líneas (`#`, `//` o `--`, según `language`) | | Ctrl+M | Salta al corchete pareja (o al que lo encierra) | | Ctrl+Retroceso / Ctrl+Supr | Borra la palabra anterior / siguiente | | Inicio | Primer carácter que no es espacio y luego el principio de la línea | | Ctrl+↑ / ↓ | Desplaza una línea sin mover el cursor | | Ctrl+C / Ctrl+X | Sin selección, copia / corta la línea entera; con varios cursores, el texto de todos | | Ctrl+K Ctrl+0 / Ctrl+J / Ctrl+L | Pliega todo / despliega todo / pliega o despliega en el cursor | Escribir, borrar y moverse se aplica a todos los cursores. Al pegar un texto con una línea por cursor, cada cursor recibe su línea, como en Zed. El corchete junto al cursor y su pareja se remarcan (`match_brackets`), y al seleccionar un texto se resaltan sus demás apariciones (`selection_highlight`). Cada acción tiene un nombre (el de Zed, en snake case). `on_action(nombre)` se llama después de cada una, `Command.editor_action(id, nombre)` ejecuta una desde código (un menú, una paleta de comandos) y `Command.editor_actions()` las lista: ```python ui.editor("main.py", code, path="main.py", on_action=self.ran, highlights=self.found) # [(línea, columna, línea_final, columna_final)], desde 1 def comment(self): return pz.Command.editor_action("main.py", "toggle_comments") def select_matches(self): # Un cursor por rango; el primero es el cursor principal. return pz.Command.select_editor_ranges("main.py", self.found) def replace_all(self): # Un solo paso de deshacer; on_change recibe el texto nuevo. return pz.Command.replace_editor_ranges("main.py", [(r, "total") for r in self.found]) ``` Entre los nombres están `undo`, `redo`, `add_selection_above`, `add_selection_below`, `select_next`, `select_previous`, `select_all_matches`, `split_selection_into_lines`, `undo_selection`, `cancel`, `move_line_up`, `move_line_down`, `duplicate_line_up`, `duplicate_line_down`, `delete_line`, `newline_above`, `newline_below`, `indent`, `outdent`, `select_line`, `join_lines`, `toggle_comments`, `move_to_enclosing_bracket`, `convert_to_upper_case`, `convert_to_lower_case` y `sort_lines_case_sensitive`. ##### Métodos de entrada El chino, el japonés, el coreano y otros idiomas que se escriben con un método de entrada (IME) funcionan en el editor. Mientras compones, el texto del IME se dibuja en un recuadro en el cursor y el cursor se oculta; el texto confirmado se inserta en cada cursor y varias confirmaciones seguidas se deshacen en un solo paso, como al escribir. El IME sigue al cursor cuando lo mueves. (Image: Componiendo 你好 con un método de entrada pinyin) #### Terminal (Image: Terminal) ```python salida = "$ pytest -q\n\x1b[32m24 passed\x1b[0m in 1.21s\n$ " ui.terminal(salida, id="term") ``` Es una vista de **solo lectura** que entiende los colores ANSI y se mantiene desplazada al final. Pásale la salida de un proceso que ejecutes tú (por ejemplo con `subprocess` en un hilo y `pz.post`). También acepta una lista de líneas de segmentos `(texto, color)`, donde el color es un nombre ANSI (`"green"`, `"red"`, `"dim"`…) o un token del tema. `ui.ansi(texto)` hace la conversión a segmentos por si quieres dibujarlos a tu manera. Referencia completa: [Código](https://albakore.github.io/pyzed/es/reference/ui/#código). ### Más controles > Desplegables, radios, campos numéricos, botones divididos, chips y secciones plegables: controles con tema para apps de escritorio en Python. Source: https://albakore.github.io/pyzed/es/components/controls/ :::tip[Biblioteca de componentes] Cada componente tiene ahora su propia página, con vista previa, ejemplos y su API completa, en la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/). Esta página se mantiene como resumen. ::: (Image: Más controles) ```python class Ajustes(pz.App): def __init__(self): self.lenguaje = "Rust" self.fuente = 14 self.indentacion = "a" self.alineado = "c" self.avanzado = True def view(self): return pz.column([ pz.row([ ui.select(["Python", "Rust", "TypeScript"], self.lenguaje, lambda v: setattr(self, "lenguaje", v), width=160), ui.number_field(self.fuente, lambda v: setattr(self, "fuente", v), min=8, max=32, suffix="px"), ui.split_button("Ejecutar", self.ejecutar, self.abrir_menu, icon="play"), ], spacing=10, align="center"), ui.radio_group([("a", "Espacios"), ("b", "Tabs")], self.indentacion, lambda v: setattr(self, "indentacion", v), horizontal=True), ui.toggle_group([("l", "align-left"), ("c", "align-center"), ("r", "align-right")], self.alineado, lambda v: setattr(self, "alineado", v)), pz.row([ui.chip("python", icon="tag", selected=True), ui.chip("rust", on_remove=self.quitar_etiqueta), ui.chip("iced", on_press=self.filtrar)], spacing=6), ui.disclosure("Opciones avanzadas", ui.muted("Contenido plegable."), open=self.avanzado, on_toggle=lambda: setattr(self, "avanzado", not self.avanzado)), ], spacing=14) ``` | Componente | Para qué | | --- | --- | | `ui.select(options, selected, on_select)` | Desplegable con el estilo del tema. Llama a `on_select` con la opción elegida. | | `ui.radio_group(options, selected, on_select)` | Opciones excluyentes. `options` son textos o pares `(valor, etiqueta)`. `horizontal=True` las pone en fila. | | `ui.number_field(value, on_change)` | Número con botones `−`/`+`, límites `min`/`max`, `step` y `suffix`. | | `ui.split_button(content, on_press, on_menu)` | Acción principal y un chevron para más opciones (normalmente abre un [`ui.popover`](https://albakore.github.io/pyzed/es/components/content/#popover)). | | `ui.toggle_group(options, selected, on_select)` | Grupo segmentado solo con iconos: `options` son pares `(valor, icono)`. | | `ui.chip(content)` | Etiqueta compacta. `on_press` la hace pulsable y `on_remove` añade una ✕. | | `ui.disclosure(title, content, open=)` | Sección plegable. El estado abierto/cerrado es tuyo: cámbialo en `on_toggle`. | Para los controles básicos (`input`, `switch`, `slider`…) mira [Formularios](https://albakore.github.io/pyzed/es/components/forms/). Referencia completa: [Controles](https://albakore.github.io/pyzed/es/reference/ui/#controles). ### Datos y feedback > Tablas, avisos, estados vacíos, ajustes agrupados, estadísticas, gráficos e indicadores: componentes de datos para GUIs de escritorio con Pyzed. Source: https://albakore.github.io/pyzed/es/components/data/ :::tip[Biblioteca de componentes] Cada componente tiene ahora su propia página, con vista previa, ejemplos y su API completa, en la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/). Esta página se mantiene como resumen. ::: #### Avisos e indicadores (Image: Avisos e indicadores) ```python ui.callout("Hay una versión nueva", "Pyzed 0.2 añade ventanas, async y canvas.", tint="info", actions=[ui.button("Ver novedades", self.novedades, size="sm")], on_dismiss=self.cerrar) ui.callout("No se pudo guardar", "El archivo es de solo lectura.", tint="error") pz.row([ ui.spinner(), # gira solo mientras está en pantalla ui.count_badge(7), ui.count_badge(120, tint="error"), # muestra "99+" ui.facepile(["Ana", "Luis", "Kev", "Marta"]), # avatares solapados ui.diff_stat(42, 9), # +42 −9 y bloques de color ui.keybinding_hint("Abrir paleta", "ctrl-shift-p"), ], spacing=14, align="center") ``` `tint` acepta `"info"`, `"success"`, `"warning"`, `"error"` o cualquier token o color del tema. :::note `ui.spinner()` se anima con el reloj de la app: necesita redibujarse. Mientras se muestre, devuelve una suscripción como `pz.every(1 / 30, lambda: None)` (ver [Async](https://albakore.github.io/pyzed/es/guides/async/)). ::: #### Tablas (Image: Tabla) ```python ui.table( [ui.Column("Archivo", "name"), ui.Column("Lenguaje", "lang", width=120), ui.Column("Líneas", "lines", width=80, align="end")], self.filas, # dicts u objetos sort_by=self.orden, descending=self.desc, on_sort=self.ordenar, on_row_press=self.seleccionar, selected=self.seleccion, striped=True, ) def ordenar(self, titulo): self.desc = not self.desc if self.orden == titulo else False self.orden = titulo ``` `ui.Column(title, key, width="fill", align="start", render=None, sortable=True)`: `key` es una clave del dict, un atributo o una función `fila -> valor`; `render(valor)` devuelve un widget para la celda. La tabla ordena por la columna cuyo **título** es `sort_by`; tú solo guardas el título y la dirección. #### Estadísticas y gráficos (Image: Estadísticas y gráficos) ```python ui.stat("Usuarios", "1.284", delta=12.5, trend=[3, 5, 4, 6, 8, 7, 9]) ui.stat("Errores", "37", delta=-8.0) ui.bar_chart([12, 19, 8, 15, 22, 17], labels=["L", "M", "X", "J", "V", "S"], width=260, height=150) ui.donut([60, 25, 15], label="60%") ui.sparkline([1, 4, 2, 6, 3, 7, 5, 8]) ``` Se dibujan con [`pz.canvas`](https://albakore.github.io/pyzed/es/guides/canvas/) y usan los colores del tema (`tint`, `colors`). #### Estados vacíos y ajustes (Image: Estado vacío y grupo de ajustes) ```python ui.empty_state("Sin resultados", "Prueba con otra búsqueda.", icon="search", actions=[ui.button("Limpiar filtros", self.limpiar, size="sm")]) ui.settings_group("Editor", [ ui.field("Tamaño de fuente", ui.number_field(self.fuente, self.set_fuente, suffix="px")), ui.field("Ajuste de línea", ui.switch(self.wrap, self.set_wrap)), ], description="Preferencias del editor.") ``` Referencia completa: [Datos y feedback](https://albakore.github.io/pyzed/es/reference/ui/#datos-y-feedback). ### Contenido y paneles > Markdown, texto enriquecido, imágenes, códigos QR, paneles redimensionables, rejillas y popovers: componentes de contenido para GUIs en Python. Source: https://albakore.github.io/pyzed/es/components/content/ :::tip[Biblioteca de componentes] Cada componente tiene ahora su propia página, con vista previa, ejemplos y su API completa, en la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/). Esta página se mantiene como resumen. ::: #### Markdown, texto enriquecido y QR (Image: Markdown, rich_text y QR) ```python pz.markdown("## Markdown\n\nTexto con **negrita**, `código` y [enlaces](https://iced.rs).", on_link=webbrowser.open) # import webbrowser pz.rich_text([ pz.span("rich_text "), pz.span("mezcla ", color="#e5c07b"), pz.span("estilos", weight="bold", underline=True), pz.span(" en una línea", italic=True), ]) pz.qr_code("https://albakore.github.io/pyzed/", cell_size=4) ``` - `pz.markdown(source)` renderiza Markdown con los colores del tema. `ui.markdown` es la misma idea con el tamaño y los colores de los componentes. `on_link(url)` recibe los enlaces pulsados. - `pz.rich_text(spans)` combina fragmentos `pz.span(texto, color=, size=, font=, weight=, italic=, bg=, underline=, strikethrough=, link=)` en un solo bloque de texto. - `pz.qr_code(data)` dibuja un código QR; `color` y `bg` cambian sus colores. Ejemplo completo: [`markdown_viewer.py`](https://github.com/albakore/pyzed/blob/main/examples/markdown_viewer.py). #### Imágenes (Image: Imágenes) ```python pz.image("captura.png", width=240, height=140, content_fit="contain") pz.image("captura.png", width=160, height=140, opacity=0.5) pz.image("pixel-art.png", width=120, height=140, filter="nearest") pz.image_viewer("mapa.png", width="fill", height="fill") # zoom con la rueda, arrastrar para mover ``` `source` puede ser una ruta, `bytes` con el archivo (PNG, JPEG, GIF, WebP, BMP…) o píxeles RGBA con `size=(ancho, alto)`. Opciones: `content_fit` (`"contain"`, `"cover"`, `"fill"`, `"none"`, `"scale_down"`), `filter` (`"linear"` o `"nearest"`), `opacity`, `radius`, `rotation` y `scale`. Para iconos vectoriales usa `pz.svg`. Ejemplo completo: [`image_viewer.py`](https://github.com/albakore/pyzed/blob/main/examples/image_viewer.py). #### Paneles redimensionables (Image: Paneles divididos) ```python ui.split( barra_lateral, ui.split(editor, terminal, vertical=True, ratio=self.alto, on_resize=self.set_alto), ratio=self.ancho, on_resize=self.set_ancho, ) ``` `ui.split(first, second)` separa dos widgets con un divisor arrastrable. La proporción es tuya: guárdala en `on_resize(ratio)`. `vertical=True` los apila. `pz.split` es la versión base, con `color`, `hover_color`, `thickness` y `handle` configurables. #### Rejillas ```python pz.grid([tarjeta(p) for p in proyectos], columns=3, spacing=12) pz.grid(miniaturas, max_width=180, spacing=8, aspect_ratio=1) # columnas según el ancho ``` `columns` fija el número de columnas; `max_width` las calcula según el espacio disponible. #### Popover (Image: Popover con un menú contextual) ```python def view(self): base = pz.mouse_area(contenido, on_move=self.recordar_puntero, on_right_press=self.abrir_menu) menu = ui.menu([ui.MenuItem("Copiar", self.copiar), ui.MenuItem("Pegar", self.pegar)]) if self.menu else None return ui.popover(base, menu, x=self.x, y=self.y, on_dismiss=self.cerrar_menu) ``` `ui.popover(base, content, x=, y=)` muestra `content` encima de `base` en esa posición; un clic fuera llama a `on_dismiss`. Con `content=None` solo se ve `base`. Ejemplo completo: [`context_menu.py`](https://github.com/albakore/pyzed/blob/main/examples/context_menu.py). Referencia: [Widgets base](https://albakore.github.io/pyzed/es/reference/widgets/) y [Overlays](https://albakore.github.io/pyzed/es/reference/ui/#overlays). ### Diálogos y herramientas > Diálogos listos para usar, acordeones, consola de logs, lista de atajos, ventana Acerca de y selector de color para apps de escritorio con Pyzed. Source: https://albakore.github.io/pyzed/es/components/dialogs/ :::tip[Biblioteca de componentes] Cada componente tiene ahora su propia página, con vista previa, ejemplos y su API completa, en la [biblioteca de componentes](https://albakore.github.io/pyzed/es/library/). Esta página se mantiene como resumen. ::: #### Diálogos (Image: Un diálogo de confirmación y uno de texto) Guarda en tu estado qué diálogo está abierto y envuelve la vista con `ui.dialog_layer`, que oscurece la ventana, centra el diálogo y llama a `on_dismiss` al pulsar el fondo. Con `dialog=None` devuelve la vista sin cambios, así que puede envolverla siempre. ```python class Archivos(pz.App): def __init__(self): self.preguntando = False self.renombrando = None def view(self): dialogo = None if self.preguntando: dialogo = ui.confirm("¿Eliminar main.py? No se puede deshacer.", self.eliminar, self.cerrar_dialogo, title="Eliminar archivo", danger=True) elif self.renombrando is not None: dialogo = ui.prompt("Renombrar", self.renombrando, self.set_nombre, self.renombrar, self.cerrar_dialogo) return ui.dialog_layer(self.vista_principal(), dialogo, on_dismiss=self.cerrar_dialogo) ``` | Diálogo | Qué hace | | --- | --- | | `ui.alert(message, on_close, kind=)` | Un mensaje con un botón; `kind` (`"info"`, `"success"`, `"warning"`, `"error"`) añade un icono. | | `ui.confirm(message, on_confirm, on_cancel, danger=)` | Una pregunta de sí o no; `danger=True` pinta de rojo el botón de confirmar. | | `ui.prompt(title, value, on_input, on_submit, on_cancel)` | Pide una línea de texto; Enter o el botón la envían. | | `ui.choice(message, [(label, on_press), ...], on_cancel)` | Tus propios botones; el último es el principal. | | `ui.unsaved_changes(name, on_save, on_discard, on_cancel)` | "¿Guardar los cambios antes de cerrar?" | Los textos de los botones se traducen con [`pyzed.i18n`](https://albakore.github.io/pyzed/es/guides/i18n/). Para cualquier otra cosa, `ui.modal(content, title=, footer=[...])` es la misma superficie con tu contenido. #### Acordeón (Image: Un acordeón con dos secciones abiertas) ```python ui.accordion( [("editor", "Editor", ajustes_editor), ("files", "Archivos", ajustes_archivos), ("git", "Git", ajustes_git)], open=self.secciones_abiertas, # un set de ids, un dict {id: bool} o True on_toggle=lambda clave: self.secciones_abiertas.symmetric_difference_update({clave}), icons={"editor": "file-code", "files": "folder", "git": "git-branch"}, ) ``` Las secciones también pueden ser `ui.AccordionSection(key, title, content, icon=, end=)`, donde `end` añade widgets a la derecha de la cabecera. Para un único bloque plegable usa [`ui.disclosure`](https://albakore.github.io/pyzed/es/components/controls/). #### Consola de logs (Image: Una consola de logs con cinco niveles) `ui.Log` reúne las entradas (seguro entre hilos, con límite y agrupando líneas repetidas) y `ui.console` las muestra como la consola de las herramientas de desarrollo de un navegador: un filtro, un interruptor por nivel con su recuento, copiar y limpiar. ```python self.log = ui.Log(on_write=lambda: pz.post("log")) # redibuja cuando llega una línea self.log.write("warning", "no se encontró settings.json, se usan los valores por defecto") self.log.capture() # recoge también print() y stderr ui.console(self.log.entries, query=self.filtro, on_query=self.set_filtro, hidden=self.ocultos, on_toggle_level=self.alternar_nivel, on_clear=self.log.clear, on_copy=lambda: pz.Command.copy(self.log.text())) ``` Los niveles son `info`, `event`, `stdout`, `stderr`, `warning` y `error` (`ui.LOG_LEVELS`). #### Atajos y Acerca de | Atajos de teclado | Acerca de | | --- | --- | | (Image: Una lista de atajos con búsqueda) | (Image: Una ventana Acerca de) | ```python ui.shortcuts([("Archivo", [("Guardar", "ctrl-s"), ("Abrir archivo", "ctrl-p")]), ("Ver", [("Paleta de comandos", "ctrl-shift-p")])], query=self.filtro, on_query=self.set_filtro) ui.about("Notas", "1.2.0", "Una pequeña app de notas hecha con Pyzed.", icon="notebook-pen", details=[("Python", "3.12")], links=[("Web", lambda: webbrowser.open(URL))]) ``` Los dos encajan bien dentro de un `ui.modal`. #### Selector de color (Image: El selector de color y muestras de color) ```python ui.ColorPicker(value=self.relleno, on_change=lambda e: setattr(self, "relleno", e.value)) ui.color_swatch(self.relleno, size=24, on_press=self.abrir_selector) ``` `ui.ColorPicker` es un componente: elige un color con transparencia, un degradado lineal o un relleno con máscara de opacidad, y `on_change` recibe un texto `"#rrggbb[aa]"`, un `pz.Gradient` o un `pz.Paint`, todos válidos como `background`. `ui.color_swatch` muestra cualquiera de ellos sobre un tablero de cuadros. Ver [Estilos](https://albakore.github.io/pyzed/es/theming/styles/#selector-de-color). Referencia completa: [Diálogos](https://albakore.github.io/pyzed/es/reference/ui/#diálogos), [Consola, errores y ayuda](https://albakore.github.io/pyzed/es/reference/ui/#consola-errores-y-ayuda) y [Selector de color](https://albakore.github.io/pyzed/es/reference/ui/#selector-de-color). ## Referencia de la API ### Referencia > La referencia de la API de Pyzed: todas las clases, funciones y parámetros de pyzed, pyzed.ui, temas, dibujo, iconos, juegos, grafos de shaders, el diseñador y la línea de comandos. Source: https://albakore.github.io/pyzed/es/reference/ Usa estas páginas cuando sabes lo que buscas y necesitas el nombre exacto, los parámetros o los valores por defecto. Para aprender cómo encajan las piezas, empieza por los [tutoriales](https://albakore.github.io/pyzed/es/tutorials/) y las [guías](https://albakore.github.io/pyzed/es/guides/architecture/). pz.App, pz.run, ventanas y capturas.', href: '/pyzed/es/reference/app/', icon: 'laptop' }, { title: 'Command', text: 'Efectos que devuelven update y los callbacks.', href: '/pyzed/es/reference/command/', icon: 'right-arrow' }, { title: 'Widgets base', text: 'Disposición, texto, controles, el editor de código, imágenes y más.', href: '/pyzed/es/reference/widgets/', icon: 'puzzle' }, { title: 'Componentes (pyzed.ui)', text: 'Cada componente, sus parámetros, valores por defecto y eventos.', href: '/pyzed/es/reference/ui/', icon: 'puzzle' }, { title: 'Temas', text: 'Temas incluidos, tokens de color y temas propios.', href: '/pyzed/es/reference/theme/', icon: 'moon' }, { title: 'Dibujo (pyzed.draw)', text: 'Formas, trazados, texto y transformaciones para el canvas.', href: '/pyzed/es/reference/draw/', icon: 'pencil' }, { title: 'Iconos', text: 'El conjunto de iconos incluido e iconos por tipo de archivo.', href: '/pyzed/es/reference/icons/', icon: 'star' }, { title: 'CLI', text: 'Crea, ejecuta, compila e inspecciona proyectos desde la terminal.', href: '/pyzed/es/reference/cli/', icon: 'seti:shell' }, { title: 'Diseñador visual (pyzed.designer)', text: 'Abre, ejecuta y construye formularios desde código Python.', href: '/pyzed/es/reference/designer/', icon: 'pencil' }, { title: 'Servicios (pyzed.services)', text: 'Temporizadores, vigilantes de archivos, procesos, tareas, peticiones web y más.', href: '/pyzed/es/reference/services/', icon: 'setting' }, { title: 'Juegos (pyzed.game)', text: 'Teclas pulsadas, movimiento, colisiones, sprites y partículas.', href: '/pyzed/es/reference/game/', icon: 'rocket' }, { title: 'Grafos de shaders (pyzed.shading)', text: 'Carga, crea y edita grafos .pzshad en código.', href: '/pyzed/es/reference/shading/', icon: 'star' }, ]} /> **También para agentes de IA:** La misma referencia la sirve el [servidor MCP](https://albakore.github.io/pyzed/es/ai/mcp/) (`search_reference` y `read_reference`), así un agente de código lee la firma exacta en vez de adivinarla. ### App y ejecución > Referencia de la API de pyzed.App: la clase de aplicación, run, screenshot, post, load_font y las opciones de ventana de una app de escritorio. Source: https://albakore.github.io/pyzed/es/reference/app/ Todo lo que necesitas para arrancar una aplicación. Ver también la guía de [arquitectura](https://albakore.github.io/pyzed/es/guides/architecture/). Ver también [varias ventanas](https://albakore.github.io/pyzed/es/guides/windows/) y [async y suscripciones](https://albakore.github.io/pyzed/es/guides/async/). #### Aplicación ##### `App` Clase base de las aplicaciones (arquitectura Elm, como Iced). Es también la **ventana principal**, así que hereda todo lo de [`Window`](#window). Sobrescribe `view()` y, si usas mensajes, `update(message)`. El estado vive en atributos de la instancia. | Atributo de clase | Por defecto | Descripción | | --- | --- | --- | | `title` | `"Pyzed"` | Título de la ventana. Puede ser un callable (o propiedad) que devuelva el texto. | | `size` | `(1280, 800)` | Tamaño inicial de la ventana. | | `font_size` | `14` | Tamaño de texto por defecto. | | `theme` | `themes.ONE_DARK` | Un `Theme`, el nombre de un tema registrado o `themes.auto(claro, oscuro)`. Cámbialo en cualquier momento. | | `style` | `None` | El estilo global: un `pz.Style` para toda la ventana. Su color de texto, tamaño, fondo, color de borde y radio se vuelven tokens del tema. Ver [Estilos](https://albakore.github.io/pyzed/es/theming/styles/#el-estilo-global). | | `system_dark` | `True` | Si el sistema está en modo oscuro (lo actualiza el runtime). | | `tab_focus` | `True` | Tab y Shift+Tab mueven el foco entre los campos de la ventana. Ver [Accesibilidad](https://albakore.github.io/pyzed/es/guides/accessibility/). | | `splash` | `None` | Una ventana [`Splash`](#splash) que se muestra antes de abrir la ventana principal. | | `rendering` | `None` | Un [`Rendering`](#rendering): GPU o CPU, API gráfica, GPU y vsync. `None` lee `[tool.pyzed.rendering]` del `pyproject.toml` del proyecto. | El resto de opciones de ventana (`min_size`, `resizable`, `decorations`, `icon`, `scale`…) están en `Window`. | Método | Descripción | | --- | --- | | `view()` | Devuelve el árbol de widgets. Se llama después de cada mensaje. | | `update(message)` | Recibe los mensajes. Puede devolver un `Command`, una lista de ellos, una corrutina, un generador (normal o async) u otro mensaje. | | `on_start()` | Opcional. Se ejecuta una vez al arrancar; puede devolver un `Command` o una corrutina. | | `on_key(combo)` | Opcional. Recibe cada tecla como `"ctrl-shift-p"`, `"escape"`, `"up"`… | | `on_key_up(key)` | Opcional. Recibe las teclas al soltarlas, por nombre (`"left"`, `"space"`). | | `subscriptions()` | Opcional. Devuelve las suscripciones activas (`pz.every`, `pz.listen`). Se reevalúa tras cada `update`. | | `resolved_theme()` | El `Theme` concreto en uso (resuelve nombres y `themes.auto`). | | `run()` | Abre la ventana y bloquea hasta que la app termina. | | `screenshot(path, *, size=None, scale=2.0, wait=0.1)` | Renderiza el estado actual a un PNG sin ventana. | Si `view()` lanza una excepción, la ventana muestra el traceback en lugar de cerrarse. **Métodos y propiedades** ###### `App.subscriptions` ```python App.subscriptions() ``` Temporizadores y streams que deben estar activos ahora mismo (se reevalúa tras cada update). ###### `App.run` ```python App.run(*, reload=None, watch=()) ``` Abre la ventana y bloquea hasta que la app termina. ###### `App.screenshot` ```python App.screenshot(path, *, size=None, scale=2.0, wait=0.1) ``` Renderiza el estado actual fuera de pantalla (sin GPU ni display) en un PNG. ##### `run` ```python run(app, *, reload=None, watch=()) ``` Ejecuta una app: abre la ventana y bloquea hasta que se cierra. Acepta la clase o una instancia. | Parámetro | Descripción | | --- | --- | | `app` | Una instancia de `App` (o la clase, que se instancia sin argumentos). | | `reload` | Hot reload: al guardar un archivo del proyecto se actualiza la ventana sin perder el estado. `None` sigue a `PYZED_RELOAD` (lo define `pyzed dev`). | | `watch` | Carpetas o archivos extra a vigilar para el hot reload. | ##### `screenshot` ```python screenshot(app, path, *, size=None, scale=2.0, wait=0.1) ``` Renderiza `app` fuera de pantalla (sin ventana ni GPU) y guarda un PNG. Útil para tests y documentación. | Parámetro | Descripción | | --- | --- | | `app` | Una instancia de `App` (o la clase, que se instancia sin argumentos). | | `path` | Ruta del PNG de salida. | | `size` | Tamaño lógico `(ancho, alto)`. Por defecto, `app.size`. | | `scale` | Factor de escala del PNG (2.0 = retina). | | `wait` | Segundos que se esperan antes de capturar (para tareas de `on_start`). | ```python pz.screenshot(MiApp, "docs/ui.png", size=(800, 600)) ``` ##### `post` ```python post(message) ``` Envía un mensaje a la app en ejecución desde **cualquier hilo**. Lanza `RuntimeError` si no hay ninguna app corriendo. | Parámetro | Descripción | | --- | --- | | `message` | Cualquier valor de Python, o un callable. | ```python def worker(): for i in range(100): time.sleep(0.05) pz.post(("progress", i / 100)) threading.Thread(target=worker, daemon=True).start() ``` ##### `UNCHANGED` ```python UNCHANGED(...) ``` Devuélvelo desde un callback que no cambió nada en pantalla (un movimiento del puntero que solo guardas, un cambio de foco): la ventana no se reconstruye. Cualquier otro valor redibuja como siempre. Ver la [guía de rendimiento](https://albakore.github.io/pyzed/es/guides/performance/). ```python def pointer_moved(self, x, y): self.pointer = (x, y) return pz.UNCHANGED # no cambió nada visible: no se reconstruye ``` ##### `load_font` ```python load_font(source) ``` Carga una fuente extra (ruta a un `.ttf`/`.otf` o sus bytes) para usarla por su nombre de familia: `text("hola", font="JetBrains Mono")`. | Parámetro | Descripción | | --- | --- | | `source` | Ruta del archivo de fuente o `bytes`. | #### Ventanas ##### `Window` Una ventana extra con su propio `view()`, título, tamaño y tema. Se abre devolviendo `ventana.open()` (o `Command.open_window(ventana)`). Dentro, `self.app` es la app, así que las ventanas comparten estado; los mensajes que produce van a su `update()`, que por defecto los reenvía a `app.update()`. | Atributo | Por defecto | Descripción | | --- | --- | --- | | `title` | `"Pyzed"` | Texto o callable. | | `size` | `(800, 600)` | Tamaño inicial. | | `min_size`, `max_size` | `None` | Límites de tamaño. | | `position` | `None` | `"center"` o `(x, y)`. | | `resizable` | `True` | Si se puede redimensionar. | | `decorations` | `True` | `False` quita el marco del sistema (barra de título propia con `drag()`). | | `transparent`, `maximized`, `fullscreen` | `False` | Estado inicial. | | `scale` | `1.0` | Escala de la interfaz. | | `theme` | `None` | Tema propio; `None` usa el de la app. | | `style` | `None` | Estilo global propio; `None` usa el de la app. | | `icon` | `None` | El icono de la ventana (barra de título, barra de tareas): una ruta a una imagen o sus bytes. Fíjalo en `pz.Window` para dar el mismo a todas las ventanas. Ver [Iconos y metadatos](https://albakore.github.io/pyzed/es/packaging/icons-metadata/#el-icono-de-la-ventana). | | `app_id` | `None` | Linux: el id de la app que el escritorio empareja con un archivo `.desktop` (`com.acme.shop`). | | `app` | — | La app dueña de la ventana (se asigna al abrirla). | | `window_size` | — | Tamaño actual en píxeles lógicos (lo actualiza el runtime). | | `window_position` | — | Dónde está la ventana en la pantalla, su esquina superior izquierda exterior (lo actualiza el runtime; `None` en Wayland). | | Método | Descripción | | --- | --- | | `view()` | Obligatorio. Devuelve el árbol de widgets de la ventana. | | `update(message)` | Recibe los mensajes de la ventana (por defecto los reenvía a la app). | | `open()` / `close()` / `drag()` | Comandos para abrirla, cerrarla o empezar a moverla. | | `is_open` | Si está abierta. | | `resolved_theme()` | El `Theme` concreto que usa. | | `on_open()` | Al abrirse. | | `on_close()` | Cuando el usuario pide cerrarla; devuelve `False` para impedirlo. | | `on_resize(width, height)` | Al cambiar de tamaño. | | `on_key(combo)`, `subscriptions()` | Como en `App`. | | `on_file_hover(path)`, `on_file_drop(path)`, `on_file_leave()` | Archivos arrastrados desde el sistema sobre la ventana, soltados en ella (una vez por archivo) o retirados. Por defecto van a la `ui.DropZone` donde caen. | **Métodos y propiedades** ###### `Window.view` ```python Window.view() ``` Devuelve el widget raíz de la ventana; las subclases deben sobrescribirlo. ###### `Window.update` ```python Window.update(message) ``` Gestiona un mensaje producido en esta ventana (por defecto lo reenvía a la app). ###### `Window.on_close` ```python Window.on_close() ``` Se llama cuando el usuario pide cerrar la ventana. Devuelve `False` para mantenerla abierta. ###### `Window.on_file_hover` ```python Window.on_file_hover(path) ``` Un archivo arrastrado desde el sistema está sobre la ventana: ilumina la `ui.DropZone` donde caería. ###### `Window.on_file_drop` ```python Window.on_file_drop(path) ``` Se soltó un archivo del sistema en la ventana (una vez por archivo): va a una `ui.DropZone`. ###### `Window.on_file_leave` ```python Window.on_file_leave() ``` Los archivos arrastrados sobre la ventana salieron sin soltarse. ###### `Window.is_open` ```python Window.is_open # property ``` Si la ventana está abierta ahora. ###### `Window.open` ```python Window.open() ``` Un comando que abre esta ventana: `return settings.open()`. ###### `Window.close` ```python Window.close() ``` Un comando que cierra esta ventana: `on_press=self.close`. ###### `Window.drag` ```python Window.drag() ``` Un comando que empieza a mover la ventana (para barras de título propias). ###### `Window.minimize` ```python Window.minimize() ``` Un comando que minimiza la ventana. ###### `Window.toggle_maximize` ```python Window.toggle_maximize() ``` Un comando que maximiza la ventana, o la restaura si está maximizada. ###### `Window.frame_menus` ```python Window.frame_menus() ``` Los menús a la izquierda de la barra de título de un marco propio (`frame = "custom"`): una lista de `ui.Menu`. ###### `Window.resolved_theme` ```python Window.resolved_theme() ``` El tema concreto que usa ahora esta ventana (con los colores y tamaños de su estilo global). ###### `Window.resolved_style` ```python Window.resolved_style() ``` El estilo global de esta ventana: `pz.set_style` con su propio `style` (o el de la app) encima. ##### `Splash` ```python class Splash ``` Una pantalla de inicio: una ventana sin marco con una imagen, centrada en la pantalla, que se muestra mientras arranca la app. Dásela a `App.splash`; se queda al menos `duration` segundos, luego se abre la ventana principal y la pantalla se cierra. Ver [Pantalla de inicio](https://albakore.github.io/pyzed/es/packaging/build/#pantalla-de-inicio). | Parámetro | Descripción | | --- | --- | | `image` | Ruta a la imagen (PNG, JPEG...) o sus bytes. | | `duration` | Segundos que se queda, como mínimo. | | `size` | Tamaño de la ventana; por defecto, el de la imagen, reducido para caber en 900 × 600. | | `text` | Texto escrito sobre la parte inferior de la imagen (por ejemplo `"Cargando…"`). | | `color` | Color del texto. | | `background` | Color detrás de la imagen (para imágenes con transparencia). | | `title` | Título de la ventana (lo muestra la barra de tareas). | | `icon` | Icono de la ventana. | ```python class Shop(pz.App): splash = pz.Splash("assets/splash.png", duration=1.5, text="Cargando…") ``` ##### `Rendering` ```python @dataclass class Rendering(renderer='auto', gpu_api='auto', power='auto', vsync='auto') ``` Cómo dibuja la app sus ventanas: en la GPU o en la CPU, con qué API gráfica, qué GPU y vsync. Cada campo es `"auto"` por defecto (decide Pyzed). Dásela a `App.rendering`, o pon las mismas claves en `[tool.pyzed.rendering]` del `pyproject.toml` del proyecto, que leen `pz.run`, el diseñador y `pyzed build`. Ver [Renderizado](https://albakore.github.io/pyzed/es/editor/settings/#renderizado). | Parámetro | Descripción | | --- | --- | | `renderer` | `"auto"`, `"gpu"` (la necesitan los shaders) o `"cpu"` (tiny-skia: arranca antes, menos memoria, funciona en todas partes). | | `gpu_api` | `"auto"`, `"vulkan"`, `"metal"`, `"dx12"` o `"gl"`. | | `power` | `"auto"`, `"high"` (la GPU dedicada) o `"low"` (la integrada, ahorra batería). | | `vsync` | `"auto"`, `"on"` (sin cortes) u `"off"` (dibuja lo más rápido posible). | ```python class Game(pz.App): rendering = pz.Rendering(renderer="gpu", vsync="off") ``` **Métodos y propiedades** ###### `Rendering.from_table` ```python @classmethod Rendering.from_table(table) ``` Crea los ajustes a partir de una tabla `[tool.pyzed.rendering]`, ignorando claves y valores desconocidos. ###### `Rendering.to_table` ```python Rendering.to_table() ``` Los ajustes que no son `"auto"`, tal como se escriben en `pyproject.toml`. ###### `Rendering.custom` ```python Rendering.custom # property ``` Si algún ajuste no es `"auto"`. ###### `Rendering.env` ```python Rendering.env() ``` Las variables de entorno que llevan estos ajustes a Iced y wgpu. ###### `Rendering.apply` ```python Rendering.apply(environ=None) ``` Define las variables de `env()` que aún no están definidas (las tuyas mandan); devuelve las que definió. #### Async y suscripciones ##### `every` ```python every(seconds, message, *, key=None) ``` Una suscripción que envía `message` a `update` cada `seconds` segundos (como `time::every` de Iced). Si `message` es un callable, se llama. Devuélvela desde `subscriptions()`. | Parámetro | Descripción | | --- | --- | | `seconds` | Intervalo en segundos (mayor que 0). | | `message` | Cualquier valor de Python, o un callable. | | `key` | Clave que identifica la suscripción (por defecto, el mensaje o la función). | ```python def subscriptions(self): return [pz.every(1, "tick")] if self.running else [] ``` ##### `listen` ```python listen(function, *, key=None) ``` Una suscripción que ejecuta `function()` (un generador o generador async) mientras esté activa y envía cada valor que produce a `update`. Ideal para websockets y streams. | Parámetro | Descripción | | --- | --- | | `function` | Función generadora (normal o `async def`). | | `key` | Clave que identifica la suscripción (por defecto, el mensaje o la función). | ```python async def feed(): async for message in websocket: yield message def subscriptions(self): return [pz.listen(feed)] ``` ##### `frames` ```python frames(callback, *, key=None, max_fps=240.0) ``` Una suscripción que llama a `callback(dt)` una vez por fotograma (sincronizada con la pantalla, normalmente 60 veces por segundo), con `dt` los segundos desde el fotograma anterior (como mucho 0,25). Pensada para juegos y simulaciones: mueve las cosas `velocidad * dt`. Ver [Crear videojuegos](https://albakore.github.io/pyzed/es/games/). | Parámetro | Descripción | | --- | --- | | `callback` | Callable que recibe `dt` (un `float`). | | `key` | Clave que identifica la suscripción (por defecto, el mensaje o la función). | | `max_fps` | Tasa máxima donde la pantalla no espera a la sincronización vertical (renderizado por software). 240 por defecto. | ```python def subscriptions(self): return [] if self.paused else [pz.frames(self.tick)] def tick(self, dt): self.x += game.axis("left", "right") * 300 * dt ``` ##### `pressed` ```python pressed() ``` Las teclas pulsadas ahora mismo, como un `frozenset` de nombres sin modificadores (`"left"`, `"space"`, `"a"`…). Vacío si no hay ventana abierta. Consúltalo en cada fotograma para movimiento continuo; `on_key` es para pulsaciones puntuales. ##### `is_pressed` ```python is_pressed(*keys, held=None) ``` Si alguna de `keys` está pulsada. `held` sustituye a `pressed()` (útil en tests). | Parámetro | Descripción | | --- | --- | | `keys` | Nombres de teclas, como `"left"`, `"a"`. | | `held` | Conjunto opcional de teclas en lugar de `pressed()`. | ```python if pz.is_pressed("space") and self.cooldown <= 0: self.fire() ``` ##### `Subscription` Lo que devuelven `every` y `listen`. Se identifica por su `key`: mientras `subscriptions()` siga devolviendo una con la misma clave, sigue en marcha; cuando desaparece, se detiene. ##### `spawn` ```python spawn(awaitable) ``` Programa una corrutina en el loop de asyncio de Pyzed sin esperar su resultado y devuelve un `concurrent.futures.Future`. Para recibir el resultado en la interfaz, devuelve la corrutina desde `update` o usa `Command.perform`. | Parámetro | Descripción | | --- | --- | | `awaitable` | La corrutina. | ##### `asyncio_loop` ```python asyncio_loop() ``` El loop de asyncio (en un hilo en segundo plano) donde Pyzed ejecuta todas las corrutinas. Se arranca al usarlo por primera vez. #### Eventos y componentes ##### `on` ```python on(widget, *, cursor=None, **handlers) ``` Añade handlers de eventos comunes a un widget; igual que `widget.on(...)`. Eventos: `click`, `double_click`, `right_click`, `middle_click`, `mouse_down`, `mouse_up`, `mouse_enter`, `mouse_leave`, `mouse_move`, `scroll`, `drag_start`, `drag`, `drag_end`, `focus`, `blur`, `key_down`, `key_up`, `resize`. Ver [Eventos](https://albakore.github.io/pyzed/es/guides/events/). | Parámetro | Descripción | | --- | --- | | `widget` | El widget (un texto se envuelve en un container). | | `cursor` | Cursor del ratón sobre el widget, como `"pointer"` o `"grab"`. | | `handlers` | Pares `evento=handler`. Un handler con un argumento recibe un `pz.Event`; un valor no invocable es un mensaje para `update`; `None` lo quita. | ```python pz.container(tarjeta, padding=12).on(click=self.seleccionar, drag=lambda e: self.mover(e.dx, e.dy)) ``` ##### `Event` ```python class Event ``` Lo que pasó, se le pasa a los handlers: `type`, `x`, `y` (relativos al widget), `dx`, `dy` (scroll o movimiento del arrastre), `width`, `height`, `button`, `key`, `shift`, `ctrl`, `alt`, `logo`, `window_x`, `window_y`, `screen_x`, `screen_y` (el puntero en la ventana y en la pantalla), y `value`/`source` en eventos propios. | Parámetro | Descripción | | --- | --- | | `type` | Nombre del evento. | | `x` | X del puntero, relativa al widget. | | `y` | Y del puntero, relativa al widget. | | `dx` | Scroll horizontal (líneas) o movimiento del arrastre. | | `dy` | Scroll vertical (líneas) o movimiento del arrastre. | | `width` | Ancho del widget. | | `height` | Alto del widget. | | `button` | `"left"`, `"right"`, `"middle"`... | | `key` | Combinación de teclas, como `"ctrl-s"`. | | `shift` | Shift pulsado. | | `ctrl` | Control pulsado. | | `alt` | Alt pulsado. | | `logo` | Tecla Cmd/Windows pulsada. | | `value` | Valor de un evento propio. | | `source` | Componente que lo emitió. | | `window_x` | X del puntero en la ventana (fuera de ella si un arrastre se suelta fuera). | | `window_y` | Y del puntero en la ventana. | | `screen_x` | X del puntero en la pantalla, o `None` donde la plataforma no dice dónde están las ventanas (Wayland). `event.screen` da `(screen_x, screen_y)`. | | `screen_y` | Y del puntero en la pantalla, o `None`. | **Métodos y propiedades** ###### `Event.position` ```python Event.position # property ``` `(x, y)`. ###### `Event.size` ```python Event.size # property ``` `(width, height)`. ###### `Event.screen` ```python Event.screen # property ``` `(screen_x, screen_y)`, o `None` cuando la plataforma no informa de dónde están las ventanas. ##### `Dock` ```python class Dock ``` Un espacio de trabajo estilo editor: un `center` con `pz.DockPanel` acoplados a la izquierda, derecha, abajo o arriba. El usuario cambia el tamaño de un lado arrastrando su borde interior y mueve un panel arrastrando su pestaña a una zona de destino (o desde el menú del clic derecho de la pestaña). `on_layout` recibe la disposición como un dict para guardarla; pásala de vuelta en `layout=`. Ver [Layout](https://albakore.github.io/pyzed/es/guides/layout/#paneles-acoplables-y-redimensionables). | Parámetro | Descripción | | --- | --- | | `args` | Props por posición: `center`, `panels`, `layout`. | | `key` | Clave que identifica la suscripción (por defecto, el mensaje o la función). | | `kwargs` | Props y handlers por nombre, además de `key=`. | ```python pz.Dock(center=editor, panels=[ pz.DockPanel("project", "Proyecto", arbol, icon="folder", side="left"), pz.DockPanel("terminal", "Terminal", terminal, icon="terminal", side="bottom"), ], layout=self.disposicion, on_layout=lambda e: setattr(self, "disposicion", e.value)) ``` **Métodos y propiedades** ###### `Dock.side_of` ```python Dock.side_of(panel) ``` Devuelve el lado en que está acoplado un panel: su posición guardada si es válida, si no `panel.side`. ###### `Dock.groups` ```python Dock.groups() ``` Devuelve los paneles agrupados por lado (están todos los lados, quizá con una lista vacía). ###### `Dock.snapshot` ```python Dock.snapshot() ``` Devuelve la disposición como un dict simple (`positions`, `sizes`, `active`, `hidden`). ###### `Dock.changed` ```python Dock.changed() ``` Emite `layout` con la instantánea actual. ###### `Dock.move` ```python Dock.move(panel_id, side) ``` Acopla un panel en otro lado y lo hace la pestaña activa allí. ###### `Dock.select` ```python Dock.select(side, panel_id) ``` Hace de `panel_id` la pestaña activa en `side` (un clic en la pestaña) y emite `layout`. ###### `Dock.toggle` ```python Dock.toggle(side) ``` Muestra u oculta un lado entero. ###### `Dock.resized` ```python Dock.resized(side, size) ``` Guarda el nuevo `size` (en píxeles) de `side` tras arrastrar su borde y emite `layout`. ###### `Dock.start_drag` ```python Dock.start_drag(panel_id) ``` Empieza a arrastrar la pestaña de un panel: aparecen las zonas de soltar y se cierra el menú de la pestaña. ###### `Dock.end_drag` ```python Dock.end_drag() ``` Termina el arrastre de una pestaña y oculta las zonas de soltar, recordando el panel para un `drop` tardío. ###### `Dock.drop` ```python Dock.drop(side) ``` Mueve el panel arrastrado a `side` al soltarlo en esa zona; no hace nada si no hay ninguno. ###### `Dock.tab` ```python Dock.tab(panel, side, active, t) ``` Construye la pestaña de un panel en la cabecera de un lado. ###### `Dock.move_menu` ```python Dock.move_menu(panel, side, t) ``` Construye el menú contextual que mueve `panel` a cualquier lado distinto de `side`. ###### `Dock.side_view` ```python Dock.side_view(side, panels, t) ``` Construye un lado: una cabecera con las pestañas de los paneles sobre el contenido del panel activo. ###### `Dock.drop_zones` ```python Dock.drop_zones(t) ``` Construye las cuatro zonas de soltar (una por lado) que se muestran sobre el dock mientras se arrastra una pestaña. ##### `DockPanel` ```python class DockPanel ``` Un panel para `pz.Dock`. | Parámetro | Descripción | | --- | --- | | `id` | Id estable (se usa en la disposición guardada). | | `title` | Título de la pestaña. | | `content` | El widget del panel. | | `icon` | Icono de Lucide para la pestaña. | | `side` | Lado inicial: `"left"`, `"right"`, `"bottom"` o `"top"`. | ##### `Component` ```python class Component ``` Clase base de los componentes propios, como en React: los atributos de clase anotados son props, los demás atributos son estado que se conserva entre renders, `render()` devuelve widgets y `events` declara eventos propios que se lanzan con `self.emit(nombre, valor)`. Llamar a la clase devuelve un widget. Aparecen en la biblioteca del diseñador. Ver [Componentes propios](https://albakore.github.io/pyzed/es/guides/components/). | Parámetro | Descripción | | --- | --- | | `args` | Props, en el orden en que se declaran (o los hijos, si la clase tiene una prop `children`). | | `key` | Clave que identifica la suscripción (por defecto, el mensaje o la función). | | `kwargs` | Props por nombre, handlers `on_=` (propios, de ciclo de vida o comunes), `key=` para conservar el estado cuando los elementos se mueven, y `style=`, `class_=`, `id=` y `animate=`. | ```python class Rating(pz.Component): value: int = 0 events = {"change": "Se eligió una estrella."} def render(self): return pz.row([pz.text("★").on(click=lambda i=i: self.emit("change", i)) for i in range(1, 6)]) Rating(value=3, on_change=lambda e: print(e.value)) ``` **Métodos y propiedades** ###### `Component.refresh` ```python Component.refresh() ``` Vuelve a dibujar el componente en el próximo `view()`, aunque sus props y su estado sean los mismos. ###### `Component.create` ```python @classmethod Component.create(*args, **kwargs) ``` Una instancia suelta (sin renderizar ni ciclo de vida), útil en tests: `c.render()`. ###### `Component.setup` ```python Component.setup() ``` Se llama una vez antes del primer render (las props ya están puestas): inicializa aquí el estado. ###### `Component.render` ```python Component.render() ``` Devuelve los widgets de este componente (se llama en cada render). ###### `Component.provide` ```python Component.provide() ``` Valores que comparte con todos los componentes que dibuja (se leen con `pz.use_context`). ###### `Component.context` ```python Component.context(name, default=None) ``` El valor compartido más cercano llamado `name` (ver `pz.provide`). ###### `Component.on_mount` ```python Component.on_mount() ``` Se llama tras el primer render; puede devolver un `Command` o un mensaje. ###### `Component.on_unmount` ```python Component.on_unmount() ``` Se llama cuando el componente deja de renderizarse. ###### `Component.emit` ```python Component.emit(name, value=None) ``` Lanza el evento personalizado `name`: llama al handler que el padre pasó como `on_`. ###### `Component.handles` ```python Component.handles(name) ``` Si el padre pasó un handler para el evento personalizado `name`. ###### `Component.describe` ```python @classmethod Component.describe() ``` El primer párrafo del docstring (se muestra en el diseñador). ###### `Component.designer_spec` ```python @classmethod Component.designer_spec() ``` Este componente como entrada de la caja de herramientas del diseñador (`pyzed.designer.Component`). ##### `Color` ```python class Color ``` Anotación para un prop de componente que guarda un color (un token del tema o `"#rrggbb"`): el diseñador muestra un selector de color. En ejecución es un `str` normal. ```python tint: pz.Color = "accent" ``` ##### `Icon` ```python class Icon ``` Anotación para un prop de componente que guarda el nombre de un icono de Lucide: el diseñador muestra un selector de icono. En ejecución es un `str` normal. ```python mark: pz.Icon = "star" ``` ##### `Multiline` ```python class Multiline ``` Anotación para un prop de componente con texto largo: el diseñador muestra un área de texto. En ejecución es un `str` normal. ```python notes: pz.Multiline = "" ``` ##### `FilePath` ```python class FilePath ``` Anotación para un prop de componente con la ruta de un archivo: el diseñador muestra la ruta con un botón que abre el diálogo de archivos del sistema (una anotación `pathlib.Path` hace lo mismo). En ejecución es un `str` normal. ```python logo: pz.FilePath = "" ``` ##### `FolderPath` ```python class FolderPath ``` Anotación para un prop de componente con una carpeta: el diseñador muestra la ruta con un botón que abre el diálogo de carpetas del sistema. En ejecución es un `str` normal. ```python output: pz.FolderPath = "" ``` ##### `Range` ```python @dataclass class Range(min, max, step=1) ``` Límites de un prop numérico, con `typing.Annotated`: el diseñador muestra un campo numérico limitado a ellos. Ver [Componentes propios](https://albakore.github.io/pyzed/es/guides/components/#en-el-diseñador). | Parámetro | Descripción | | --- | --- | | `min` | Valor mínimo. | | `max` | Valor máximo. | | `step` | Paso de los botones + y -. | ```python level: Annotated[int, pz.Range(0, 10)] = 3 ``` ##### `provide` ```python provide(**values) ``` Un gestor de contexto que comparte valores con todos los componentes que se dibujan dentro, como el contexto de React. Se leen con `pz.use_context(nombre)` o `self.context(nombre)`. El método `provide()` de un componente hace lo mismo con lo que dibuja. Ver [Hijos, contexto y estilos](https://albakore.github.io/pyzed/es/guides/components/#hijos-contexto-y-estilos). | Parámetro | Descripción | | --- | --- | | `values` | Los valores, por nombre. | ```python with pz.provide(user=self.user): page = Dashboard() ``` ##### `use_context` ```python use_context(name, default=None) ``` El valor más cercano compartido como `name` con `pz.provide` o el `provide()` de un componente. | Parámetro | Descripción | | --- | --- | | `name` | El nombre del valor. | | `default` | Lo que devuelve si nadie lo comparte. | ##### `state` ```python state(default=None, *, doc='') ``` Declara una variable de estado privada en la clase de un componente: el padre no puede pasarla y cada instancia empieza con su propia copia del valor (listas y diccionarios son seguros). El diseñador la muestra junto a las props. Ver [Componentes propios](https://albakore.github.io/pyzed/es/guides/components/). | Parámetro | Descripción | | --- | --- | | `default` | Valor inicial, copiado para cada instancia. | | `doc` | Descripción que muestra el diseñador. | ```python class Counter(pz.Component): step: int = 1 count: int = pz.state(0) ``` ##### `State` ```python class State ``` El valor por defecto de una variable de estado declarada en la clase (ver `state`). ##### `StateInfo` ```python class StateInfo ``` Una variable de estado declarada con `pz.state`: su nombre, anotación, valor por defecto y descripción. ##### `PropInfo` ```python class PropInfo ``` Una prop: su nombre, anotación de tipo (puede ser un texto) y valor por defecto. **Métodos y propiedades** ###### `PropInfo.required` ```python PropInfo.required # property ``` Si la prop no tiene valor por defecto y hay que pasarla. ##### `find` ```python find(type_name) ``` La clase de componente registrada como `type_name` (ver `Component.type_name`). ##### `registered` ```python registered() ``` Todas las subclases de `Component` definidas hasta ahora que deben aparecer en el diseñador. ##### `render_pass` ```python render_pass(owner) ``` Registra los componentes renderizados mientras `owner` (una ventana) ejecuta su `view()`. Los componentes que no se vuelven a renderizar se desmontan al final. El runtime lo hace en cada ventana; úsalo tú solo para renderizar componentes fuera de una ventana. ##### `EventInfo` ```python class EventInfo ``` La documentación de un evento: su grupo y qué informa. ##### `accepts_argument` ```python accepts_argument(handler) ``` Si `handler` recibe el evento: tiene un parámetro posicional obligatorio o `*args`. Los parámetros con valor por defecto no cuentan, así que `lambda i=i: ...` se llama sin nada. La respuesta se guarda en caché por función. ##### `LIFECYCLE_EVENTS` Los nombres de los eventos de ciclo de vida que lanza todo componente (`mount`, `unmount`...). ##### `CATALOG` Todos los nombres de evento que conoce Pyzed, con su `EventInfo` (qué lo lanza y qué contiene `event.value`). ##### `COMMON` Los eventos que acepta todo widget con `widget.on(...)` (clic, hover, foco, teclas...). #### Hot reload ##### `enable` ```python enable(app, paths=(), *, interval=0.25) ``` Inicia el hot reload para `app` (una instancia de `App` en marcha) y devuelve el reloader. ##### `current` ```python current() ``` El `Reloader` activo, o `None` cuando el hot reload está apagado. ##### `is_reloading` ```python is_reloading() ``` `True` mientras una recarga vuelve a ejecutar los módulos. ##### `enabled_by_env` ```python enabled_by_env() ``` Si `PYZED_RELOAD` pide hot reload (`1`, `true`, `yes` u `on`). ##### `Reloader` ```python class Reloader ``` Vigila los archivos del proyecto y recarga el código modificado en una app en marcha. `app` es la instancia de `App` en marcha. `paths` son carpetas o archivos extra que vigilar; por defecto se vigila la carpeta del módulo que define la clase de la app. `interval` es cada cuántos segundos se comprueba si hay cambios. La mayoría de los programas nunca crean uno a mano: lo hace `pz.run(app, reload=True)`. **Métodos y propiedades** ###### `Reloader.watch` ```python Reloader.watch(path, callback) ``` Llama a `callback(path)` en el hilo de la UI cada vez que cambia `path`. ###### `Reloader.project_modules` ```python Reloader.project_modules() ``` Los módulos cargados cuyos archivos pertenecen al proyecto, por ruta de archivo. ###### `Reloader.scan` ```python Reloader.scan() ``` Revisa una vez cada archivo vigilado y devuelve los que cambiaron. ###### `Reloader.start` ```python Reloader.start() ``` Arranca el hilo vigilante y hace de este el reloader actual. ###### `Reloader.stop` ```python Reloader.stop() ``` Deja de vigilar archivos. ###### `Reloader.reload` ```python Reloader.reload(changed) ``` Recarga ahora el código de los archivos de `changed` (llámalo en el hilo de la UI). ##### `ReloadError` ```python class ReloadError ``` Lo lanza `view()` mientras la última recarga haya fallado; la ventana muestra su mensaje. ##### `Change` ```python @dataclass class Change(name, changed=[], added=[], removed=[], new_state=[], updated_state=[]) ``` Lo que una recarga cambió en una clase (ver `Reloader.changes`): `name`, los métodos cambiados (`changed`), nuevos (`added`) y quitados (`removed`), los atributos que los objetos vivos recibieron del nuevo `__init__` (`new_state`) y los que tomaron su nuevo valor inicial (`updated_state`). ##### `remember` ```python remember(obj) ``` Guarda (una vez) los valores iniciales simples de `obj`, para que una recarga sepa qué cambió la app. El runtime lo llama para la app, cada ventana y cada componente; llámalo para otros objetos creados después de iniciar el hot reload solo si quieres que también se actualicen sus valores iniciales. #### Idiomas ##### `t` ```python t(key, *, language=None, default=None, **values) ``` El texto de `key` en el idioma actual, formateado con `values`. Si falta, prueba de `"es_AR"` a `"es"`, luego el idioma de respaldo y por último devuelve la clave. Ver [Idiomas](https://albakore.github.io/pyzed/es/guides/i18n/). | Parámetro | Descripción | | --- | --- | | `key` | Clave con puntos, como `"menu.open"`. | | `language` | Busca en otro idioma. | | `default` | Lo que devuelve si falta la clave (por defecto, la clave). | | `values` | Valores para los `{nombre}`; `count=` además elige el plural. | ```python i18n.add("es", {"greeting": "¡Hola, {name}!", "files": {"one": "{count} archivo", "other": "{count} archivos"}}) i18n.set_language("es") pz.t("greeting", name="Kev"), pz.t("files", count=3) ``` ##### `add` ```python add(language, catalog) ``` Añade o reemplaza textos de un idioma. Las tablas anidadas se vuelven claves con puntos; las tablas `{"one": ..., "other": ...}` son plurales. | Parámetro | Descripción | | --- | --- | | `language` | Código de idioma, como `"es"` o `"pt_BR"`. | | `catalog` | Dict de textos. | ##### `load` ```python load(folder) ``` Carga todos los `.json` y `.toml` de `folder`; devuelve los idiomas encontrados. | Parámetro | Descripción | | --- | --- | | `folder` | Carpeta con los catálogos. | ##### `set_language` ```python set_language(language) ``` Cambia el idioma. La app lo muestra en el siguiente render, así que llámalo desde un handler. | Parámetro | Descripción | | --- | --- | | `language` | Código de idioma, como `"es"`. | ##### `language` ```python language() ``` El idioma actual, normalizado (`"es_ar"`). ##### `languages` ```python languages() ``` Los idiomas con catálogo. ##### `set_fallback` ```python set_fallback(language) ``` El idioma que se usa cuando falta un texto (por defecto `"en"`). | Parámetro | Descripción | | --- | --- | | `language` | Código de idioma. | ##### `on_change` ```python on_change(listener) ``` Llama a `listener(idioma)` tras cada cambio; devuelve una función que lo quita. | Parámetro | Descripción | | --- | --- | | `listener` | Función que recibe el nuevo idioma. | ##### `system_language` ```python system_language(default='en') ``` El idioma del sistema operativo (`LANG`, `LC_ALL` o el locale). | Parámetro | Descripción | | --- | --- | | `default` | Lo que devuelve si el sistema no informa ninguno. | ##### `has` ```python has(key, *, language=None) ``` Si `key` tiene texto en el idioma o sus respaldos. | Parámetro | Descripción | | --- | --- | | `key` | Clave con puntos. | | `language` | Idioma a consultar (por defecto, el actual). | ##### `normalize` ```python normalize(language) ``` `"es-AR"`, `"es_AR.UTF-8"` -> `"es_ar"`. #### Referencia integrada ##### `Browser` ```python class Browser ``` Lo que muestra un visor de la referencia: el texto buscado, la página abierta y el historial atrás/adelante. `ui.reference_browser(browser)` lo dibuja; sus botones y enlaces llaman a los métodos de abajo. **Métodos y propiedades** ###### `Browser.open` ```python Browser.open(id) ``` Muestra la página `id` (devuelve `False` si no existe). ###### `Browser.open_symbol` ```python Browser.open_symbol(symbol) ``` Muestra la página sobre un nombre escrito en código; devuelve si encontró una. ###### `Browser.back` ```python Browser.back() ``` Vuelve a la página anterior. ###### `Browser.forward` ```python Browser.forward() ``` Va a la página siguiente después de volver atrás. ###### `Browser.set_query` ```python Browser.set_query(query) ``` Cambia el texto buscado. ###### `Browser.toggle` ```python Browser.toggle(section) ``` Pliega o despliega una sección de la lista de clases. ###### `Browser.results` ```python Browser.results() ``` Las entradas que coinciden con el texto buscado. ###### `Browser.page` ```python Browser.page() ``` La página mostrada (`None` antes de abrir una). ###### `Browser.follow` ```python Browser.follow(url) ``` Gestiona un enlace de una página: los internos abren su página; si no, devuelve la URL que hay que abrir. ##### `Entry` ```python @dataclass class Entry(id, title, section, group='', summary='', icon='book-open', kind='function') ``` Una página de la referencia, tal como la muestran los resultados de búsqueda y la lista de clases. ##### `Page` ```python @dataclass class Page(entry, body, url=None) ``` Una página completa: la entrada a la que pertenece y su cuerpo en Markdown. **Métodos y propiedades** ###### `Page.id` ```python Page.id # property ``` El id de la entrada. ###### `Page.title` ```python Page.title # property ``` El título de la entrada. ##### `Provider` ```python @dataclass class Provider(name, entries, page, lookup=None) ``` Una sección de la referencia que viene de otro sitio que no son las páginas de la API de la librería. `entries(lang)` lista sus páginas, `page(id, lang)` construye una (`None` si el id no es suyo) y `lookup(symbol)` convierte un nombre escrito en código en uno de sus ids. ##### `register` ```python register(provider) ``` Añade una sección a la referencia (reemplaza la que tenga el mismo `name`). ##### `entries` ```python entries(lang=None) ``` Todas las páginas de la referencia, en el orden de la lista de clases (primero los controles del diseñador, luego la librería). ##### `sections` ```python sections(lang=None) ``` La lista de clases: `[(sección, [(grupo, [entradas]), ...]), ...]` en orden. ##### `search` ```python search(query, lang=None, limit=60) ``` Las entradas que coinciden con `query`: primero los nombres exactos, luego los que empiezan por él y después el resto. Cada palabra debe aparecer en el título, el grupo o el resumen (`"tab bar"` encuentra `ui.tab_bar`). ##### `page` ```python page(id, lang=None) ``` La página con `id` (`KeyError` si no existe). ##### `lookup` ```python lookup(symbol) ``` El id de la página sobre un nombre escrito en código (`"pz.text"`, `"ui.button"`, `"Theme"`...). ##### `languages` ```python languages() ``` Los idiomas en los que están las descripciones de la API. ##### `tables_as_lists` ```python tables_as_lists(markdown) ``` Tablas de Markdown convertidas en listas (`- primera celda: las otras celdas`). Para visores cuyas tablas no parten las celdas largas, como `ui.reference_browser` en una pestaña estrecha. Los bloques de código no se tocan. ##### `display_name` ```python display_name(name) ``` `"pyzed.text"` -> `"pz.text"`, `"pyzed.ui.button"` -> `"ui.button"`. ##### `summary` ```python summary(markdown) ``` La primera frase de un texto Markdown, sin formato (para listas y resultados de búsqueda). #### Formularios y validación ##### `validation_errors` ```python validation_errors(error) ``` Convierte un error de validación en `{campo: mensaje}`, listo para `ui.field(..., error=)`. Acepta un `ValidationError` de Pydantic o SQLModel, un `ValidationError` de Django, el JSON de una respuesta 422 de FastAPI (`{"detail": [...]}`) o su lista `detail`. Los campos anidados se unen con puntos y los errores que no son de ningún campo usan la clave `""`. Se guarda solo el primer mensaje de cada campo. Mira [Integraciones](https://albakore.github.io/pyzed/es/integrations/overview/#errores-de-validación-en-cada-campo). | Parámetro | Descripción | | --- | --- | | `error` | El error, o el JSON de la respuesta. | ```python try: item = ItemIn.model_validate(self.draft) except ValidationError as error: self.errors = pz.validation_errors(error) ui.field("Precio", ui.input(self.draft["price"], self.set_price), error=self.errors.get("price")) ``` ### Command > Referencia de la API de los comandos de Pyzed: los efectos que devuelven update y los callbacks, como enfocar campos, cerrar y tareas en segundo plano. Source: https://albakore.github.io/pyzed/es/reference/command/ Un `Command` es un efecto que el runtime ejecuta por ti. Devuélvelo desde `update`, `on_start`, `on_key` o cualquier callback. También puedes devolver una lista o una tupla de comandos. Ver la guía de [comandos y tareas](https://albakore.github.io/pyzed/es/guides/commands/). ```python from pyzed import Command def on_key(self, combo): if combo == "ctrl-p": self.finder_open = True return Command.focus("finder") ``` #### Métodos estáticos ##### `Command` La clase de los comandos. No se instancia directamente: usa sus métodos estáticos. ##### `exit` ```python exit() ``` Cierra la aplicación. ##### `focus` ```python focus(id) ``` Enfoca el `text_input` (o editor) con ese `id`. | Parámetro | Descripción | | --- | --- | | `id` | El `id` que pasaste al widget. | ##### `focus_next` ```python focus_next() ``` Mueve el foco del teclado al siguiente campo de la ventana, volviendo al primero tras el último (lo que hace Tab). ##### `focus_previous` ```python focus_previous() ``` Mueve el foco del teclado al campo anterior de la ventana (lo que hace Shift+Tab). ##### `select_all` ```python select_all(id) ``` Da el foco al `text_input` con ese `id` y selecciona todo su texto (lo que se escriba lo reemplaza). | Parámetro | Descripción | | --- | --- | | `id` | El `id` del campo. | ##### `set_editor_text` ```python set_editor_text(id, text) ``` Reemplaza el contenido del `code_editor` con ese `id`. Los editores no son controlados: `text` solo es el valor inicial. | Parámetro | Descripción | | --- | --- | | `id` | El `id` del editor. | | `text` | El texto nuevo. | ##### `set_editor_cursor` ```python set_editor_cursor(id, line, column) ``` Mueve el cursor del editor (línea y columna empiezan en 1). Funciona aunque el editor aún no se haya dibujado. | Parámetro | Descripción | | --- | --- | | `id` | El `id` del editor. | | `line` | Línea (desde 1). | | `column` | Columna (desde 1). | ##### `fold_all` ```python fold_all(id, fold=True) ``` Pliega todos los bloques del editor (`fold=False` los despliega). Funciona aunque el editor aún no se haya dibujado. | Parámetro | Descripción | | --- | --- | | `id` | El `id` del editor. | | `fold` | `True` para plegar, `False` para desplegar. | ##### `fold_line` ```python fold_line(id, line, fold=True) ``` Pliega (o despliega) el bloque que empieza en `line`, según la sangría. Funciona aunque el editor aún no se haya dibujado. | Parámetro | Descripción | | --- | --- | | `id` | El `id` del editor. | | `line` | Línea (desde 1) donde empieza el bloque. | | `fold` | `True` para plegar, `False` para desplegar. | ##### `editor_action` ```python editor_action(id, name) ``` Ejecuta una acción del editor con ese `id`, como si se pulsara su atajo: el nombre de la acción de Zed en snake case (`"undo"`, `"select_next"`, `"move_line_up"`, `"toggle_comments"`…). Lanza `ValueError` si el nombre no existe. | Parámetro | Descripción | | --- | --- | | `id` | El `id` del editor. | | `name` | El nombre de la acción (`Command.editor_actions()` las lista; también vale `"editor::SelectNext"`). | ##### `editor_actions` ```python editor_actions() ``` Los nombres que acepta `Command.editor_action`. ##### `select_editor_ranges` ```python select_editor_ranges(id, ranges) ``` Selecciona rangos del editor, un cursor por rango; el primero es el cursor principal. Despliega lo que los oculta y funciona aunque el editor aún no se haya dibujado. | Parámetro | Descripción | | --- | --- | | `id` | El `id` del editor. | | `ranges` | `[(línea, columna, línea_final, columna_final)]`, desde 1, con el final excluido (un rango vacío es un cursor sin selección). | ##### `replace_editor_ranges` ```python replace_editor_ranges(id, edits) ``` Reemplaza rangos del editor por textos en un solo paso de deshacer y llama al `on_change` del editor (buscar y reemplazar). | Parámetro | Descripción | | --- | --- | | `id` | El `id` del editor. | | `edits` | `[((línea, columna, línea_final, columna_final), texto)]`, desde 1, con el final excluido. | ##### `scroll_to_end` ```python scroll_to_end(id) ``` Desplaza el `scrollable` con ese `id` hasta el final. | Parámetro | Descripción | | --- | --- | | `id` | El `id` del scrollable. | ##### `batch` ```python batch(commands) ``` Ejecuta varios comandos. | Parámetro | Descripción | | --- | --- | | `commands` | Lista de `Command`. | ##### `message` ```python message(message) ``` Vuelve a enviar `message` a `update` en el siguiente ciclo. | Parámetro | Descripción | | --- | --- | | `message` | Cualquier valor de Python, o un callable. | ##### `perform` ```python perform(function, then=None) ``` Ejecuta `function()` en segundo plano y entrega el resultado a `then(resultado)` en el hilo de la interfaz. `function` puede ser normal (corre en un hilo) o `async def` (corre en el loop de asyncio de Pyzed). Si devuelve un generador (normal o async), `then` se llama con cada valor. También acepta una corrutina o un generador ya creados (`fetch(url)` en lugar de `fetch`). Sin `then`, los resultados van a `update`. Si `function` lanza, se imprime el error. | Parámetro | Descripción | | --- | --- | | `function` | Función sin argumentos que hace el trabajo lento. | | `then` | Callable que recibe el resultado (o `None` para enviarlo a `update`). | ```python def cargar(self): self.cargando = True return Command.perform(lambda: requests.get(URL).json(), self.recibido) def recibido(self, datos): self.cargando, self.datos = False, datos ``` ##### `scroll_to` ```python scroll_to(id, x=0.0, y=0.0) ``` Desplaza el `scrollable` con ese `id` a una posición absoluta en píxeles. | Parámetro | Descripción | | --- | --- | | `id` | El `id` del scrollable. | | `x` | Desplazamiento horizontal. | | `y` | Desplazamiento vertical. | ##### `copy` ```python copy(text) ``` Copia `text` al portapapeles del sistema. | Parámetro | Descripción | | --- | --- | | `text` | El texto. | ##### `paste` ```python paste(then) ``` Lee el portapapeles y llama a `then(texto)` (cadena vacía si está vacío). | Parámetro | Descripción | | --- | --- | | `then` | Callable que recibe el texto. | ##### `paste_image` ```python paste_image(then) ``` Lee una imagen del portapapeles (una captura, una imagen copiada) y llama a `then(png)` con sus bytes PNG, o a `then(None)` si el portapapeles no tiene una imagen. | Parámetro | Descripción | | --- | --- | | `then` | Callable que recibe los bytes PNG o `None`. | ##### `pick_files` ```python pick_files( then, *, title=None, filters=None, directory=None, multiple=True, folder=False, ) ``` Abre el diálogo de archivos del sistema y llama a `then(rutas)` con las rutas elegidas (una lista vacía si se cancela). | Parámetro | Descripción | | --- | --- | | `then` | Callable que recibe la lista de rutas. | | `title` | El título del diálogo. | | `filters` | Una lista de pares `(nombre, extensiones)`, como `[("Imágenes", ["png", "jpg"])]`. | | `directory` | La carpeta en la que se abre. | | `multiple` | `False` elige un solo archivo. | | `folder` | `True` elige carpetas en vez de archivos. | ##### `save_file` ```python save_file(then, *, title=None, filters=None, directory=None, file_name=Ellipsis) ``` Abre el diálogo «Guardar como» del sistema y llama a `then(rutas)` con la ruta elegida en una lista (una lista vacía si se cancela). | Parámetro | Descripción | | --- | --- | | `then` | Callable que recibe la lista con la ruta. | | `title` | El título del diálogo. | | `filters` | Una lista de pares `(nombre, extensiones)`, como `[("Imágenes", ["png", "jpg"])]`. | | `directory` | La carpeta en la que se abre. | | `file_name` | El nombre que sugiere. | #### Ventanas y portapapeles ##### `open_window` ```python open_window(window) ``` Abre una ventana nueva para esa instancia de `pz.Window`. | Parámetro | Descripción | | --- | --- | | `window` | La ventana. | ##### `close_window` ```python close_window(window=None) ``` Cierra la ventana. | Parámetro | Descripción | | --- | --- | | `window` | La ventana (`pz.Window`). Sin ella, la que produjo el mensaje. | ##### `drag_window` ```python drag_window(window=None) ``` Empieza a mover la ventana con el ratón (úsalo al pulsar una barra de título propia). | Parámetro | Descripción | | --- | --- | | `window` | La ventana (`pz.Window`). Sin ella, la que produjo el mensaje. | ##### `minimize_window` ```python minimize_window(window=None) ``` Minimiza la ventana. | Parámetro | Descripción | | --- | --- | | `window` | La ventana (`pz.Window`). Sin ella, la que produjo el mensaje. | ##### `toggle_maximize_window` ```python toggle_maximize_window(window=None) ``` Maximiza la ventana o la restaura si ya lo está. | Parámetro | Descripción | | --- | --- | | `window` | La ventana (`pz.Window`). Sin ella, la que produjo el mensaje. | ##### `maximize_window` ```python maximize_window(maximized=True, window=None) ``` Maximiza la ventana, o la restaura con `maximized=False`. | Parámetro | Descripción | | --- | --- | | `maximized` | `False` la restaura. | | `window` | La ventana (`pz.Window`). Sin ella, la que produjo el mensaje. | ##### `drag_resize_window` ```python drag_resize_window(direction, window=None) ``` Empieza a cambiar el tamaño de la ventana con el ratón desde un borde o una esquina (para ventanas sin decoraciones del sistema; `ui.window_frame` y `pz.window_edges` ya lo hacen). | Parámetro | Descripción | | --- | --- | | `direction` | `"n"`, `"s"`, `"e"`, `"w"`, `"ne"`, `"nw"`, `"se"` o `"sw"`. | | `window` | La ventana (`pz.Window`). Sin ella, la que produjo el mensaje. | ##### `show_window_menu` ```python show_window_menu(window=None) ``` Muestra el menú de ventana del sistema (mover, tamaño, cerrar…) bajo el puntero. Solo Windows tiene uno. | Parámetro | Descripción | | --- | --- | | `window` | La ventana (`pz.Window`). Sin ella, la que produjo el mensaje. | ##### `set_fullscreen` ```python set_fullscreen(fullscreen=True, window=None) ``` Pone o quita la pantalla completa. | Parámetro | Descripción | | --- | --- | | `fullscreen` | `True` para pantalla completa. | | `window` | La ventana (`pz.Window`). Sin ella, la que produjo el mensaje. | ##### `resize_window` ```python resize_window(width, height, window=None) ``` Cambia el tamaño de la ventana (píxeles lógicos). | Parámetro | Descripción | | --- | --- | | `width` | Ancho. | | `height` | Alto. | | `window` | La ventana (`pz.Window`). Sin ella, la que produjo el mensaje. | ##### `move_window` ```python move_window(x, y, window=None) ``` Mueve la ventana a una posición de la pantalla. | Parámetro | Descripción | | --- | --- | | `x` | Posición horizontal. | | `y` | Posición vertical. | | `window` | La ventana (`pz.Window`). Sin ella, la que produjo el mensaje. | ##### `focus_window` ```python focus_window(window=None) ``` Trae la ventana al frente y le da el foco. | Parámetro | Descripción | | --- | --- | | `window` | La ventana (`pz.Window`). Sin ella, la que produjo el mensaje. | ### Widgets base > Referencia de la API de los widgets nativos de Pyzed: layout, texto, controles, el editor de código, SVG e iconos, renderizados en Rust por Iced. Source: https://albakore.github.io/pyzed/es/reference/widgets/ Los widgets base están implementados en Rust y se importan desde `pyzed`. Son los ladrillos con los que están hechos los [componentes](https://albakore.github.io/pyzed/es/components/overview/). Ver la guía de [layout](https://albakore.github.io/pyzed/es/guides/layout/) para los tamaños, el relleno y los colores. #### Widget ##### `Widget` Un nodo inmutable del árbol de la interfaz, implementado en Rust. Lo devuelven todas las funciones de widgets y componentes. Donde se espera un hijo también puedes pasar un texto o un número (se convierte en `text`) o `None` (se ignora). Todo widget tiene `widget.on(...)` (ver [Eventos](https://albakore.github.io/pyzed/es/guides/events/)) y `widget.style(*estilos, **props)`, que le da estilo como CSS: fondo, bordes, radio, sombra, padding, margen, tamaño y la tipografía de todos los textos de dentro. `widget.style_props()` devuelve lo que se fijó. `widget.classes(*nombres, id=None)` aplica las reglas de `pz.stylesheet` que coinciden, como `class` e `id` en HTML. Ver [Estilos](https://albakore.github.io/pyzed/es/theming/styles/). `widget.animate(*animaciones, **opciones)` reproduce presets o animaciones por keyframes (al aparecer, en hover, al pulsar, con foco, al hacer clic o mientras `active`) y `widget.transition(**props)` anima los cambios de propiedades como una transición CSS. Ver [Animación](https://albakore.github.io/pyzed/es/animation/). #### Layout ##### `column` ```python column( children=None, *, spacing=0.0, padding=None, width=None, height=None, align=None, max_width=None, clip=False, ) ``` Apila los hijos en vertical. | Parámetro | Descripción | | --- | --- | | `children` | Lista de hijos. Se aplanan listas anidadas y se ignoran `None` y `False`, así que puedes escribir `cond and widget`. | | `spacing` | Separación entre hijos, en píxeles. | | `padding` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `align` | Alineación de los hijos en el eje cruzado: `"start"`, `"center"` o `"end"`. | | `max_width` | Ancho máximo en píxeles. | | `clip` | Recorta el contenido que se sale de los límites. | ```python pz.column([pz.text("Uno"), pz.text("Dos")], spacing=8, padding=16) ``` ##### `row` ```python row( children=None, *, spacing=0.0, padding=None, width=None, height=None, align=None, clip=False, wrap=False, line_spacing=None, ) ``` Coloca los hijos en horizontal. | Parámetro | Descripción | | --- | --- | | `children` | Lista de hijos. Se aplanan listas anidadas y se ignoran `None` y `False`, así que puedes escribir `cond and widget`. | | `spacing` | Separación entre hijos, en píxeles. | | `padding` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `align` | Alineación de los hijos en el eje cruzado: `"start"`, `"center"` o `"end"`. | | `clip` | Recorta el contenido que se sale de los límites. | | `wrap` | Ajuste de línea / pasa los hijos a una nueva fila cuando no caben. | | `line_spacing` | Separación entre filas cuando `wrap=True`. | ```python pz.row([ui.label("Nombre"), pz.hspace(), ui.button("OK", "ok")], align="center") ``` ##### `container` ```python container( content=None, *, padding=None, width=None, height=None, max_width=None, max_height=None, align_x=None, align_y=None, center=False, bg=None, color=None, border_color=None, border_width=0.0, radius=None, shadow=None, clip=False, gradient=None, ) ``` Envuelve un hijo para darle tamaño, alineación, fondo, borde, radio y sombra. | Parámetro | Descripción | | --- | --- | | `content` | El hijo (o `None` para una caja vacía, útil como rectángulo de color). | | `padding` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `max_width` | Ancho máximo en píxeles. | | `max_height` | Alto máximo en píxeles. | | `align_x` | Alineación horizontal: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `align_y` | Alineación vertical: `"start"`/`"top"`, `"center"`, `"end"`/`"bottom"`. | | `center` | Centra el contenido en ambos ejes y ocupa todo el espacio. | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `color` | Color del texto o del icono. | | `border_color` | Color del borde. | | `border_width` | Grosor del borde en píxeles. | | `radius` | Radio de las esquinas: un número o `(sup_izq, sup_der, inf_der, inf_izq)`. | | `shadow` | Sombra: `(color, (dx, dy), desenfoque)`. | | `clip` | Recorta el contenido que se sale de los límites. | | `gradient` | Degradado lineal de fondo: `(ángulo, [colores])` o `(ángulo, [(posición, color), ...])`. | ##### `center` ```python center(content, **kwargs) ``` Centra `content` en todo el espacio disponible. Equivale a `container(content, center=True)`. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `kwargs` | Cualquier parámetro de `container`. | ##### `scrollable` ```python scrollable( content, *, direction='vertical', width=None, height=None, padding=None, id=None, anchor_bottom=False, rail=None, scroller=None, scroller_hover=None, scrollbar_width=8.0, scroller_width=None, margin=0.0, on_scroll=None, ) ``` Hace desplazable su contenido. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `direction` | `"vertical"`, `"horizontal"` o `"both"`. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `padding` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `anchor_bottom` | Mantiene el scroll pegado al final (logs, terminales, chats). | | `rail` | Color del carril de la barra. | | `scroller` | Color del tirador. | | `scroller_hover` | Color del tirador al pasar el ratón. | | `scrollbar_width` | Ancho de la barra. | | `scroller_width` | Ancho del tirador (por defecto, el de la barra). | | `margin` | Margen entre la barra y el contenido. | | `on_scroll` | Callable que recibe un dict con `x`, `y`, `relative_x`, `relative_y`, `width`, `height`, `content_width` y `content_height`. | ##### `stack` ```python stack(children=None, *, width=None, height=None) ``` Superpone los hijos, el último encima. Base de capas, modales y notificaciones. | Parámetro | Descripción | | --- | --- | | `children` | Lista de hijos. Se aplanan listas anidadas y se ignoran `None` y `False`, así que puedes escribir `cond and widget`. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | ##### `space` ```python space(width=None, height=None) ``` Espacio vacío de un tamaño dado. | Parámetro | Descripción | | --- | --- | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | ##### `hspace` ```python hspace(width='fill') ``` Hueco horizontal flexible: empuja a los hermanos de una fila hacia los extremos. | Parámetro | Descripción | | --- | --- | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | ##### `vspace` ```python vspace(height='fill') ``` Hueco vertical flexible: empuja a los hermanos de una columna hacia los extremos. | Parámetro | Descripción | | --- | --- | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | ##### `rule` ```python rule(*, vertical=False, thickness=1.0, color=None) ``` Una línea divisoria horizontal (o vertical). | Parámetro | Descripción | | --- | --- | | `vertical` | Línea vertical en lugar de horizontal. | | `thickness` | Grosor en píxeles. | | `color` | Color del texto o del icono. | Una regla horizontal ocupa todo el ancho. Dentro de una columna que debe encoger, pon `width="shrink"` en la columna. ##### `grid` ```python grid( children=None, *, columns=None, rows=None, max_width=None, spacing=0.0, row_spacing=None, height=None, aspect_ratio=None, width=None, padding=None, align_x=None, align_y=None, ) ``` Coloca los hijos en una rejilla. Con `columns=3` las columnas son iguales (o `max_width` mete tantas como quepan). Con listas de pistas (`columns=[200, "1fr", "auto"]`, `rows=...`) funciona como un grid de CSS: los números son píxeles, `"auto"` se ajusta al contenido y `"fr"` reparte el espacio que sobra. Envuelve los hijos en `pz.cell` para colocarlos y hacer que ocupen varias celdas; el resto llena las celdas libres fila a fila. Ver [Layout](https://albakore.github.io/pyzed/es/guides/layout/#rejillas). | Parámetro | Descripción | | --- | --- | | `children` | Lista de hijos. Se aplanan listas anidadas y se ignoran `None` y `False`, así que puedes escribir `cond and widget`. | | `columns` | Número de columnas iguales, o pistas: una lista (`[200, "1fr", "auto"]`) o un texto (`"200 1fr auto"`). | | `rows` | Pistas de las filas, como `columns`. Las filas que no están en la lista son `"auto"`. | | `max_width` | Mete tantas columnas de como mucho este ancho como quepan (solo con `columns=`). | | `spacing` | Espacio entre celdas. | | `row_spacing` | Espacio entre filas, si es distinto. | | `height` | Con `columns=`: alto de cada fila. Con pistas: alto de la rejilla. | | `aspect_ratio` | Mantiene las celdas en esta proporción ancho/alto (solo con `columns=`). | | `width` | Con `columns=`: píxeles. Con pistas: ancho de la rejilla (por defecto `"fill"`). | | `padding` | Espacio alrededor de las celdas. | | `align_x` | Alineación horizontal de cada elemento en su celda: `"start"`, `"center"`, `"end"`. | | `align_y` | Alineación vertical de cada elemento en su celda. | ```python pz.grid([pz.cell(header, column=0, row=0, column_span=2), sidebar, content], columns=[200, "1fr"], rows=[48, "1fr"], spacing=8, height="fill") ``` ##### `cell` ```python cell( content, *, column=None, row=None, column_span=1, row_span=1, align_x=None, align_y=None, ) ``` Coloca un hijo en una rejilla con pistas: su columna y fila (desde 0) y cuántas ocupa. Los hijos sin celda llenan las celdas libres fila a fila. Fuera de una rejilla es solo el hijo. | Parámetro | Descripción | | --- | --- | | `content` | El widget. | | `column` | Índice de columna (desde 0). | | `row` | Índice de fila (desde 0). | | `column_span` | Columnas que ocupa. | | `row_span` | Filas que ocupa. | | `align_x` | Alineación horizontal en la celda. | | `align_y` | Alineación vertical en la celda. | ```python pz.cell(ui.button("Guardar"), column=2, row=3, align_x="end") ``` ##### `box` ```python box( *children, display='block', direction='row', gap=0, wrap=False, justify='start', align=None, columns=None, rows=None, overflow='visible', class_=None, id=None, style=None, **props, ) ``` Un contenedor para cualquier cosa, como un `div` de HTML: coloca sus hijos (`display` `"block"`, `"flex"`, `"grid"` o `"stack"`) y acepta todas las propiedades de estilo, clases de la hoja de estilo y un id. Los hijos con `position="absolute"` se colocan con `top`/`right`/`bottom`/`left` por encima del resto. Ver [Cajas](https://albakore.github.io/pyzed/es/guides/layout/#cajas). | Parámetro | Descripción | | --- | --- | | `children` | Los widgets, uno a uno o en listas. `None` y `False` se ignoran. | | `display` | `"block"` (uno debajo de otro, tan anchos como la caja), `"flex"`, `"grid"` o `"stack"`. | | `direction` | Con `display="flex"`: `"row"` o `"column"`. | | `gap` | Espacio entre hijos. | | `wrap` | En filas `"flex"`: pasa a nuevas líneas. | | `justify` | Con `"flex"`: `"start"`, `"center"`, `"end"`, `"between"`, `"around"` o `"evenly"`. | | `align` | `"start"`, `"center"`, `"end"` o `"stretch"` (por defecto en `"block"`). | | `columns` | Con `"grid"`: un número de columnas iguales o pistas (`"200 1fr auto"`). | | `rows` | Con `"grid"`: pistas de filas. | | `overflow` | `"visible"`, `"hidden"`, `"scroll"`, `"scroll_x"` o `"scroll_y"`. | | `class_` | Clases de la hoja de estilo (`"card selected"` o una lista). | | `id` | El id para las reglas `#id`. | | `style` | Un `pz.Style`, dict o nombre de estilo. | | `props` | Propiedades de estilo, encima del resto. | ```python pz.box(title, pz.box(cancel, save, display="flex", justify="end", gap=8), class_="card", gap=12) ``` ##### `flex` ```python flex( children=(), *, direction='row', justify='start', align='start', gap=0, wrap=False, padding=None, width=None, height=None, ) ``` Coloca los hijos a lo largo de un eje, como flexbox de CSS, con `justify` en el eje principal y `align` en el transversal. Usa `pz.flex_item` para que un hijo crezca o tenga un tamaño fijo. | Parámetro | Descripción | | --- | --- | | `children` | Los widgets, o `pz.flex_item(...)` para hijos con su propio tamaño. | | `direction` | `"row"` o `"column"`. | | `justify` | `"start"`, `"center"`, `"end"`, `"between"`, `"around"` o `"evenly"`. No tiene efecto si algún hijo crece. | | `align` | Alineación transversal: `"start"`, `"center"` o `"end"`. | | `gap` | Espacio entre hijos. | | `wrap` | Pasa a nuevas líneas (solo filas con `justify="start"`). | | `padding` | Espacio alrededor de los hijos. | | `width` | Ancho. | | `height` | Alto. | ```python pz.flex([logo, titulo, pz.flex_item(buscador, grow=1), avatar], justify="between", align="center", gap=8) ``` ##### `flex_item` ```python flex_item(content, *, grow=1, basis=None, align=None) ``` Da tamaño a un hijo de `pz.flex`. | Parámetro | Descripción | | --- | --- | | `content` | El widget. | | `grow` | Parte del espacio libre en el eje principal (`0` mantiene su tamaño natural). | | `basis` | Un tamaño fijo en píxeles en el eje principal, en lugar de crecer. | | `align` | Alineación transversal de este hijo. | ##### `anchor` ```python anchor( content, *, x=0, y=0, width=100, height=28, anchor='top_left', min_width=0, min_height=0, max_width=0, max_height=0, pivot=(0.5, 0.5), rotate=0, ) ``` Coloca un hijo de `pz.anchor_layout` en `x`, `y` con `width` y `height` (en el tamaño de diseño de la disposición) y dice cómo sigue a un cambio de tamaño. | Parámetro | Descripción | | --- | --- | | `content` | El widget. | | `x` | Borde izquierdo en el tamaño de diseño. | | `y` | Borde superior en el tamaño de diseño. | | `width` | Ancho en el tamaño de diseño. | | `height` | Alto en el tamaño de diseño. | | `anchor` | Un preset de `pyzed.anchors.PRESETS` (`"top_right"`, `"bottom_wide"`, `"center"`, `"full"`...), palabras (`"left right bottom"`) o una tupla de bordes. | | `min_width` | Ancho mínimo al estirarse (`0`: sin límite). | | `min_height` | Alto mínimo al estirarse. | | `max_width` | Ancho máximo al estirarse (`0`: sin límite). | | `max_height` | Alto máximo al estirarse. | | `pivot` | `(x, y)` de `0` a `1`: el punto que queda fijo cuando un límite lo frena, y alrededor del que gira `rotate`. | | `rotate` | Grados en sentido horario. | ```python pz.anchor(ui.button("Cerrar", on_close), x=284, y=252, width=100, height=32, anchor="bottom_right") ``` ##### `anchor_layout` ```python anchor_layout(children, *, design=None, width='fill', height='fill') ``` Coloca libremente hijos hechos con `pz.anchor`, como un formulario diseñado: al cambiar de tamaño, cada hijo mantiene su distancia a los bordes a los que está anclado, se estira entre bordes opuestos y se queda centrado en los demás casos. Los últimos hijos se dibujan encima. Ver [Disposiciones ancladas](https://albakore.github.io/pyzed/es/guides/layout/#disposiciones-ancladas). | Parámetro | Descripción | | --- | --- | | `children` | Hijos hechos con `pz.anchor(...)` (`None` se ignora). | | `design` | `(ancho, alto)` en el que se colocaron los hijos; por defecto, el más pequeño que los contiene. | | `width` | Ancho. | | `height` | Alto. | ```python pz.anchor_layout([ pz.anchor(ui.heading("Informe"), x=16, y=12, width=368, height=28, anchor="top_wide"), pz.anchor(ui.button("Cerrar", on_close), x=284, y=252, width=100, height=32, anchor="bottom_right"), ], design=(400, 300)) ``` ##### `place` ```python place( rect, anchor, design, actual, *, min_size=(0, 0), max_size=(0, 0), pivot=(0.5, 0.5), ) ``` Dónde va una caja diseñada en un padre de tamaño `design` dentro de un padre de tamaño `actual`, siguiendo su anclaje, con límites de tamaño y un pivote. La cuenta detrás de `pz.anchor_layout` y de los formularios diseñados. | Parámetro | Descripción | | --- | --- | | `rect` | `(x, y, ancho, alto)` en el tamaño de diseño. | | `anchor` | Un preset, palabras o bordes. | | `design` | El tamaño de diseño del padre. | | `actual` | El tamaño actual del padre. | | `min_size` | Límites inferiores `(ancho, alto)` (`0`: ninguno). | | `max_size` | Límites superiores `(ancho, alto)` (`0`: ninguno). | | `pivot` | El punto que queda fijo cuando un límite frena el estiramiento. | ```python anchors.place((20, 20, 100, 30), "top_right", design=(400, 300), actual=(600, 300)) # (220, 20, 100, 30) ``` ##### `PRESETS` Los 16 presets de anclaje por nombre (`top_left`, `top_center`, `top_right`, `top_wide`, `center_left`, `center`, `center_right`, `center_wide`, `bottom_left`, `bottom_center`, `bottom_right`, `bottom_wide`, `left_tall`, `center_tall`, `right_tall`, `full`), cada uno con la tupla de bordes a los que ancla. ##### `edges` ```python edges(anchor) ``` Los bordes de un anclaje dado como nombre de preset, palabras o bordes. Lanza `ValueError` con cualquier otra cosa. | Parámetro | Descripción | | --- | --- | | `anchor` | Lo que se lee. | ##### `preset_of` ```python preset_of(edges) ``` El nombre del preset que corresponde a un conjunto de bordes (todo conjunto es uno de los 16). | Parámetro | Descripción | | --- | --- | | `edges` | Bordes. | ##### `preset` ```python preset(horizontal, vertical) ``` El preset colocado `horizontal` (`left`, `center`, `right`, `wide`) y `vertical` (`top`, `center`, `bottom`, `tall`). | Parámetro | Descripción | | --- | --- | | `horizontal` | Colocación en el eje x. | | `vertical` | Colocación en el eje y. | ##### `axes` ```python axes(edges) ``` La colocación `(horizontal, vertical)` de un conjunto de bordes, como `("right", "top")`. | Parámetro | Descripción | | --- | --- | | `edges` | Bordes. | ##### `snap_to` ```python snap_to(anchor, rect, parent, margin=0.0) ``` Una caja movida (y redimensionada en los ejes estirados) adonde apunta un anclaje: contra los bordes anclados, centrada o llenando el padre, a `margin` píxeles. | Parámetro | Descripción | | --- | --- | | `anchor` | Un preset, palabras o bordes. | | `rect` | La caja. | | `parent` | El tamaño del padre. | | `margin` | Distancia a los bordes. | ##### `offsets` ```python offsets(rect, parent) ``` Las distancias `(izquierda, arriba, derecha, abajo)` de cada lado del padre a una caja. | Parámetro | Descripción | | --- | --- | | `rect` | La caja. | | `parent` | El tamaño del padre. | ##### `pivot_offset` ```python pivot_offset(size, pivot, rotate=0.0, scale=1.0) ``` La traslación que hace que una rotación y una escala (que giran alrededor del centro de la caja) giren alrededor de `pivot`. | Parámetro | Descripción | | --- | --- | | `size` | El tamaño de la caja. | | `pivot` | `(x, y)` de `0` a `1`. | | `rotate` | Grados en sentido horario. | | `scale` | Factor de escala. | ##### `focusable` ```python focusable( content, on_key=None, *, keys=None, id=None, ring_color=None, ring_radius=4.0, ring_offset=2.0, ) ``` Deja que cualquier widget tome el foco del teclado como un campo de texto: un clic o Tab lo enfocan (`Command.focus(id)` también) y, mientras lo tiene, las combinaciones de `keys` llaman a `on_key(combo)` en vez de llegar al `on_key` de la app. Al usar el teclado sobre él se dibuja un anillo alrededor. Los interruptores, casillas, deslizadores, segmentados y grupos de radios de la librería lo usan. | Parámetro | Descripción | | --- | --- | | `content` | El control. | | `on_key` | Se llama con la combinación (`"space"`, `"shift-right"`…) de cada tecla de `keys` pulsada mientras tiene el foco. `None` deja `content` sin foco. | | `keys` | Las combinaciones que toma (por defecto Espacio, Enter, las flechas, Inicio y Fin); `"*"` toma todas. Tab queda libre para mover el foco. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `ring_color` | Color del anillo de foco (ninguno por defecto). | | `ring_radius` | Radio de las esquinas del anillo. | | `ring_offset` | Píxeles entre el control y el anillo. | ```python pz.focusable(estrellas, lambda combo: self.puntuar(+1 if combo == "right" else -1), keys=["left", "right"], ring_color="#528bff") ``` ##### `window_edges` ```python window_edges(content, *, border=6.0, enabled=True) ``` Envuelve la vista entera de una ventana sin decoraciones del sistema para que pulsar cerca de sus bordes cambie el tamaño de la ventana (las esquinas ocupan el doble). `ui.window_frame` ya lo usa. | Parámetro | Descripción | | --- | --- | | `content` | La vista entera de la ventana. | | `border` | Ancho en píxeles de la zona que redimensiona. | | `enabled` | `False` lo desactiva (para una ventana maximizada). | ```python pz.window_edges(view, enabled=not self.window_maximized) ``` ##### `resizable` ```python resizable( content, *, edge='right', size=240.0, min_size=80.0, max_size=None, on_resize=None, color=None, hover_color=None, thickness=1.0, handle=8.0, width=None, height=None, ) ``` Un panel cuyo borde `edge` se arrastra para cambiar su tamaño. Conserva el tamaño al que lo arrastra el usuario; `on_resize(size)` se llama al terminar de arrastrar para que puedas guardarlo. Si `size` cambia después, vuelve a ese valor. | Parámetro | Descripción | | --- | --- | | `content` | El contenido del panel. | | `edge` | El borde con el asa: `"right"` (una barra lateral izquierda), `"left"`, `"top"` (un panel inferior) o `"bottom"`. | | `size` | Ancho inicial (alto con los bordes top/bottom). | | `min_size` | Tamaño mínimo. | | `max_size` | Tamaño máximo. | | `on_resize` | Se llama con el nuevo tamaño al terminar de arrastrar. | | `color` | Color de la línea. | | `hover_color` | Color de la línea al pasar el ratón o arrastrar. | | `thickness` | Grosor de la línea. | | `handle` | Ancho de la zona que se puede agarrar. | | `width` | Ancho con los bordes top/bottom. | | `height` | Alto con los bordes left/right. | ```python pz.row([pz.resizable(barra, edge="right", size=240, on_resize=self.guardar_ancho), editor]) ``` ##### `split` ```python split( first, second, *, ratio=0.5, on_resize=None, vertical=False, min_size=80.0, min_size_second=None, color=None, hover_color=None, thickness=1.0, handle=8.0, width=None, height=None, ) ``` Dos paneles con un divisor que se arrastra. Para el estilo del tema usa `ui.split`. | Parámetro | Descripción | | --- | --- | | `first` | Primer panel. | | `second` | Segundo panel. | | `ratio` | Proporción del primer panel (0–1). El divisor conserva la proporción a la que lo arrastra el usuario hasta que esto cambia. | | `on_resize` | Callable que recibe la nueva proporción mientras se arrastra. | | `vertical` | Apila los paneles arriba/abajo. | | `min_size` | Tamaño mínimo del primer panel. | | `min_size_second` | Tamaño mínimo del segundo panel. | | `color` | Color del divisor. | | `hover_color` | Color del divisor al pasar el ratón o arrastrar. | | `thickness` | Grosor de la línea. | | `handle` | Ancho de la zona que se puede arrastrar. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | ##### `pin` ```python pin(content, *, x=0.0, y=0.0, width=None, height=None) ``` Coloca `content` en `(x, y)` dentro de una caja. Úsalo en un `stack` para posicionar popovers y menús contextuales. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `x` | Posición horizontal. | | `y` | Posición vertical. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | ##### `responsive` ```python responsive(function) ``` Construye su contenido con `function(ancho, alto)` usando el espacio realmente disponible, para que el layout se adapte (por ejemplo, ocultar una barra lateral si la ventana es estrecha). | Parámetro | Descripción | | --- | --- | | `function` | Función que recibe `(ancho, alto)` y devuelve un widget. | ##### `FlexItem` ```python class FlexItem ``` Un hijo de `pz.flex` con su propio tamaño; se crea con `pz.flex_item`. #### Texto e iconos ##### `text` ```python text( content, *, size=None, color=None, font=None, weight=None, italic=False, line_height=None, align_x=None, align_y=None, width=None, height=None, wrap=True, selectable=None, selection_color=None, ) ``` Un texto. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `size` | Tamaño de la fuente. | | `color` | Color del texto o del icono. | | `font` | `"sans"` (IBM Plex Sans), `"mono"`, `"icons"`, `"serif"` o el nombre de una familia cargada con `load_font`. | | `weight` | `"thin"`, `"light"`, `"normal"`, `"medium"`, `"semibold"`, `"bold"`, `"black"` o un número (100–900). | | `italic` | Cursiva. | | `line_height` | Alto de línea relativo al tamaño (1.0, 1.5…). | | `align_x` | Alineación horizontal: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `align_y` | Alineación vertical: `"start"`/`"top"`, `"center"`, `"end"`/`"bottom"`. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `wrap` | Ajuste de línea. Con `False` el texto no se parte. | | `selectable` | `True` deja seleccionar el texto con el ratón (arrastrar, doble clic en una palabra, triple clic en un bloque) y copiarlo con Ctrl+C. Una cadena nombra un grupo de selección: los textos y el Markdown de un mismo grupo forman una sola selección, en el orden en que se dibujan (las líneas de un log, los mensajes de un chat). | | `selection_color` | Color detrás del texto seleccionado. | ##### `icon` ```python icon(name, *, size=16, color=None) ``` Un icono de [Lucide](https://lucide.dev). `color` por defecto es el color del texto que lo rodea. | Parámetro | Descripción | | --- | --- | | `name` | Nombre del icono (`pyzed icons` los lista). | | `size` | Tamaño en píxeles. | | `color` | Color. | ```python pz.icon("folder", size=16, color="#74ade8") ``` ##### `svg` ```python svg( source, *, width=None, height=None, color=None, content_fit=None, opacity=1.0, rotation=0.0, ) ``` Una imagen SVG. | Parámetro | Descripción | | --- | --- | | `source` | Ruta a un `.svg`, el código SVG como texto (empieza por `<`) o `bytes`. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `color` | Si se indica, tiñe todo el SVG de ese color (útil para iconos monocromos). | | `content_fit` | `"contain"`, `"cover"`, `"fill"`, `"none"` o `"scale-down"`. | | `opacity` | Opacidad de 0 a 1. | | `rotation` | Rotación en grados. | #### Imágenes y contenido ##### `image` ```python image( source, *, width=None, height=None, content_fit=None, filter='linear', opacity=1.0, radius=0.0, rotation=0.0, scale=1.0, size=None, ) ``` Una imagen de mapa de bits (PNG, JPEG, GIF, BMP, WebP…) desde una ruta o `bytes`, o píxeles RGBA crudos. | Parámetro | Descripción | | --- | --- | | `source` | Ruta del archivo, `bytes` del archivo codificado, o píxeles RGBA crudos con `size=(ancho, alto)`. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `content_fit` | `"contain"`, `"cover"`, `"fill"`, `"none"` o `"scale-down"`. | | `filter` | `"linear"` (suave) o `"nearest"` (píxeles nítidos, para pixel art). | | `opacity` | Opacidad de 0 a 1. | | `radius` | Radio de las esquinas. | | `rotation` | Rotación en grados. | | `scale` | Escala. | | `size` | `(ancho, alto)` cuando `source` son píxeles RGBA crudos. | ```python pz.image("foto.jpg", width=320, content_fit="cover", radius=8) ``` ##### `image_viewer` ```python image_viewer( source, *, width=None, height=None, min_scale=0.25, max_scale=10.0, scale_step=0.1, filter='linear', size=None, ) ``` Una imagen que se puede desplazar (arrastrando) y ampliar (rueda del ratón), como el visor de Iced. | Parámetro | Descripción | | --- | --- | | `source` | Ruta del archivo, `bytes` del archivo codificado, o píxeles RGBA crudos con `size=(ancho, alto)`. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `min_scale` | Zoom mínimo. | | `max_scale` | Zoom máximo. | | `scale_step` | Cuánto cambia el zoom por paso de rueda. | | `filter` | `"linear"` (suave) o `"nearest"` (píxeles nítidos, para pixel art). | | `size` | `(ancho, alto)` para píxeles RGBA crudos. | ##### `markdown` ```python markdown( source, *, size=14.0, color=None, link_color=None, code_color=None, code_bg=None, on_link=None, width=None, selectable=None, selection_color=None, ) ``` Renderiza Markdown (títulos, listas, bloques de código, citas, tablas, enlaces…). Para los colores del tema usa `ui.markdown`. | Parámetro | Descripción | | --- | --- | | `source` | El texto Markdown. | | `size` | Tamaño. | | `color` | Color del texto o del icono. | | `link_color` | Color de los enlaces. | | `code_color` | Color del código. | | `code_bg` | Fondo del código. | | `on_link` | Callable que recibe la URL al pulsar un enlace. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `selectable` | `True` deja seleccionar el texto con el ratón (arrastrar, doble clic en una palabra, triple clic en un bloque) y copiarlo con Ctrl+C. Una cadena nombra un grupo de selección: los textos y el Markdown de un mismo grupo forman una sola selección, en el orden en que se dibujan (las líneas de un log, los mensajes de un chat). | | `selection_color` | Color detrás del texto seleccionado. | ##### `rich_text` ```python rich_text( spans, *, size=None, color=None, font=None, line_height=None, width=None, align_x=None, wrap=True, ) ``` Texto hecho de fragmentos con estilos distintos. Los textos sueltos son fragmentos sin estilo. | Parámetro | Descripción | | --- | --- | | `spans` | Lista de `span(...)` o textos. | | `size` | Tamaño. | | `color` | Color del texto o del icono. | | `font` | Fuente del fragmento o del texto (nombre de familia, p. ej. `"IBM Plex Mono"`). | | `line_height` | Alto de línea. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `align_x` | Alineación horizontal: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `wrap` | Ajuste de línea / pasa los hijos a una nueva fila cuando no caben. | ```python pz.rich_text(["Pulsa ", pz.span("aquí", color="#74ade8", underline=True, link=self.abrir), " para abrir."]) ``` ##### `span` ```python span( text, *, color=None, size=None, font=None, weight=None, italic=False, bg=None, radius=0.0, padding=None, underline=False, strikethrough=False, link=None, ) ``` Un fragmento con estilo para `rich_text`. Con `link`, al pulsarlo se envía (o se llama) ese valor. | Parámetro | Descripción | | --- | --- | | `text` | El texto. | | `color` | Color del texto o del icono. | | `size` | Tamaño. | | `font` | Fuente del fragmento o del texto (nombre de familia, p. ej. `"IBM Plex Mono"`). | | `weight` | Grosor de la fuente: `"normal"`, `"medium"`, `"semibold"` o `"bold"`. | | `italic` | Si es `True`, el fragmento se dibuja en cursiva. | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `radius` | Radio del fondo. | | `padding` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `underline` | Subrayado. | | `strikethrough` | Tachado. | | `link` | Mensaje o callable al pulsarlo. | ##### `Span` El tipo de los fragmentos que crea `pz.span`. ##### `qr_code` ```python qr_code(data, *, cell_size=4.0, color=None, bg=None) ``` Un código QR que codifica `data`. | Parámetro | Descripción | | --- | --- | | `data` | El texto o URL a codificar. | | `cell_size` | Tamaño de cada módulo en píxeles. | | `color` | Color del texto o del icono. | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | ##### `shader` ```python shader( code, *, uniforms=None, textures=None, width=None, height=None, animate=True, time=None, fallback=None, ) ``` Un widget que pinta la GPU con tu función WGSL `fn shade(uv, px, size, time) -> vec4`, que devuelve el color de cada píxel, o con un grafo de shader hecho de nodos (una ruta `.pzshad` o un `Graph` o `Shader` de [`pyzed.shading`](https://albakore.github.io/pyzed/es/reference/shading/)). Cada entrada de `uniforms` se convierte en una función con su nombre (`{"speed": 2}` -> `speed()`); `mouse()` da la posición del puntero. Sin GPU el widget muestra `fallback`. Lanza `ValueError` con el mensaje del compilador si el código no es válido. Ver la guía de [shaders](https://albakore.github.io/pyzed/es/shading/shaders/). | Parámetro | Descripción | | --- | --- | | `code` | Código WGSL que define `shade` (y las funciones auxiliares que quieras), o una ruta `.pzshad`, un `Graph` o un `Shader`. | | `uniforms` | Hasta 32 valores con nombre: un número, de 2 a 4 números o un color. Con un `.pzshad`, las variables mutables del shader por nombre. | | `textures` | Hasta 8 imágenes, `{nombre: ruta}`: cada una se convierte en `nombre(uv)`, el color de la imagen en `uv`. | | `width` | Ancho (`"fill"` por defecto). | | `height` | Alto (`"fill"` por defecto). | | `animate` | Redibuja en cada frame para que avance `time` (`True`). | | `time` | Un `time` fijo en segundos, para un fotograma quieto. | | `fallback` | Color o degradado que se muestra sin GPU. | ##### `canvas` ```python canvas( shapes=None, *, width=None, height=None, id=None, on_press=None, on_release=None, on_right_press=None, on_move=None, on_scroll=None, cursor=None, ) ``` Una superficie de dibujo 2D. `shapes` es una lista creada con [`pyzed.draw`](https://albakore.github.io/pyzed/es/reference/draw/). Los callbacks del ratón reciben coordenadas relativas al canvas. Con un `id`, la geometría se cachea entre frames y solo se recalcula cuando cambian las formas. Ver la guía de [dibujo con canvas](https://albakore.github.io/pyzed/es/guides/canvas/). | Parámetro | Descripción | | --- | --- | | `shapes` | Lista de formas de `pyzed.draw` (se aceptan listas anidadas). | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `id` | Activa la caché de geometría. | | `on_press` | Callable que recibe `(x, y)` al pulsar. | | `on_release` | Callable que recibe `(x, y)` al soltar. | | `on_right_press` | Callable que recibe `(x, y)` con clic derecho. | | `on_move` | Se llama con `(x, y)` al mover el ratón. | | `on_scroll` | Callable que recibe `(dx, dy)` en líneas al usar la rueda. | | `cursor` | Cursor del ratón (`"crosshair"`, `"pointer"`…). | #### Interacción ##### `button` ```python button( content, on_press=None, *, padding=None, width=None, height=None, bg=None, color=None, border_color=None, border_width=0.0, radius=None, shadow=None, hover_bg=None, hover_color=None, hover_border_color=None, pressed_bg=None, disabled_color=None, clip=False, ) ``` Un botón sin estilo predefinido. Para el botón con tema usa `ui.button`. | Parámetro | Descripción | | --- | --- | | `content` | Contenido del botón. | | `on_press` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | | `padding` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `color` | Color del texto o del icono. | | `border_color` | Color del borde. | | `border_width` | Grosor del borde en píxeles. | | `radius` | Radio de las esquinas: un número o `(sup_izq, sup_der, inf_der, inf_izq)`. | | `shadow` | Sombra: `(color, (dx, dy), desenfoque)`. | | `hover_bg` | Fondo al pasar el ratón. | | `hover_color` | Color del texto al pasar el ratón. | | `hover_border_color` | Color del borde al pasar el ratón. | | `pressed_bg` | Fondo mientras se pulsa. | | `disabled_color` | Color del texto cuando está deshabilitado. | | `clip` | Recorta el contenido que se sale de los límites. | ##### `text_input` ```python text_input( value, *, placeholder='', on_input=None, on_submit=None, id=None, size=None, font=None, padding=None, width=None, secure=False, icon=None, icon_size=None, bg=None, border_color=None, hover_border_color=None, focus_border_color=None, border_width=None, radius=None, color=None, placeholder_color=None, selection_color=None, icon_color=None, align_x=None, ) ``` Un campo de texto controlado: muestra `value` y te avisa de los cambios con `on_input`. Para el campo con tema usa `ui.input`. | Parámetro | Descripción | | --- | --- | | `value` | Texto actual. | | `placeholder` | Texto de ayuda cuando está vacío. | | `on_input` | Callable que recibe el texto nuevo en cada pulsación. | | `on_submit` | Mensaje o callable que se envía al pulsar Enter. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `size` | Tamaño de la fuente. | | `font` | Familia de la fuente. | | `padding` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `secure` | Oculta el texto (contraseñas). | | `icon` | Carácter del icono (usa `pyzed.icons.glyph(nombre)`). | | `icon_size` | Tamaño del icono. | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `border_color` | Color del borde. | | `hover_border_color` | Color del borde al pasar el ratón. | | `focus_border_color` | Color del borde con el foco. | | `border_width` | Grosor del borde en píxeles. | | `radius` | Radio de las esquinas: un número o `(sup_izq, sup_der, inf_der, inf_izq)`. | | `color` | Color del texto o del icono. | | `placeholder_color` | Color del placeholder. | | `selection_color` | Color de la selección. | | `icon_color` | Color del icono. | | `align_x` | Alineación horizontal: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | ##### `mouse_area` ```python mouse_area( content, *, on_press=None, on_release=None, on_right_press=None, on_double_click=None, on_enter=None, on_exit=None, on_move=None, on_scroll=None, on_middle_press=None, on_middle_release=None, cursor=None, ) ``` Detecta eventos del ratón sobre su contenido. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `on_press` | Clic izquierdo (al pulsar). | | `on_release` | Al soltar el botón izquierdo. | | `on_right_press` | Clic derecho (menús contextuales). | | `on_double_click` | Doble clic. | | `on_enter` | El ratón entra. | | `on_exit` | El ratón sale. | | `on_move` | Se llama con `(x, y)` al mover el ratón. | | `on_scroll` | Callable que recibe `(dx, dy)` en líneas al usar la rueda. | | `on_middle_press` | Clic con el botón central. | | `on_middle_release` | Se suelta el botón central (para desplazar la vista, por ejemplo). | | `cursor` | `"pointer"`, `"text"`, `"grab"`, `"grabbing"`, `"crosshair"`, `"not-allowed"`, `"resize-horizontal"`, `"resize-vertical"`, `"move"` o `"default"`. | ##### `hover` ```python hover(base, top) ``` Muestra `top` encima de `base` solo mientras el ratón está encima (por ejemplo, el botón de cerrar de una pestaña). | Parámetro | Descripción | | --- | --- | | `base` | Widget de abajo. | | `top` | Widget que aparece al pasar el ratón. | ##### `opaque` ```python opaque(content) ``` Hace que un widget bloquee el ratón para lo que está debajo en un `stack`. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | ##### `popup` ```python popup(base, content=None, *, on_dismiss=None, side='bottom', align='start', gap=4.0) ``` Muestra `content` encima de todo junto a `base`, como el menú de un desplegable: no lo recortan los paneles donde está `base`, se pasa al otro lado cuando no cabe y se queda dentro de la ventana. Un clic fuera (y fuera de `base`) o Escape llama a `on_dismiss`. Con `content=None` solo se muestra `base`. Los selectores de fecha abren su calendario con él. | Parámetro | Descripción | | --- | --- | | `base` | El widget al que pertenece (un campo, un botón). | | `content` | Lo que se abre (un panel, un menú), o `None` mientras está cerrado. | | `on_dismiss` | Mensaje o función que se envía con un clic fuera o Escape. | | `side` | `"bottom"`, `"top"`, `"right"` o `"left"` de `base`. | | `align` | `"start"`, `"center"` o `"end"`: cómo se alinea con `base`. | | `gap` | Espacio entre `base` y el contenido. | ```python pz.popup(field, panel if self.open else None, on_dismiss=self.close) ``` ##### `tooltip` ```python tooltip( content, tip, *, position='bottom', gap=4.0, bg=None, color=None, border_color=None, border_width=0.0, radius=None, shadow=None, ) ``` Muestra `tip` junto a `content` al pasar el ratón. `ui.tooltip` añade el estilo del tema. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `tip` | Contenido del tooltip. | | `position` | `"top"`, `"bottom"`, `"left"`, `"right"` o `"cursor"`. | | `gap` | Distancia al contenido. | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `color` | Color del texto o del icono. | | `border_color` | Color del borde. | | `border_width` | Grosor del borde en píxeles. | | `radius` | Radio de las esquinas: un número o `(sup_izq, sup_der, inf_der, inf_izq)`. | | `shadow` | Sombra: `(color, (dx, dy), desenfoque)`. | #### Controles ##### `checkbox` ```python checkbox( checked, label=None, *, on_toggle=None, size=None, text_size=None, accent=None, bg=None, border_color=None, check_color=None, color=None, ) ``` Una casilla de verificación. | Parámetro | Descripción | | --- | --- | | `checked` | Estado actual. | | `label` | Texto de la etiqueta. | | `on_toggle` | Callable que recibe el nuevo `bool` (o mensaje que se envía). | | `size` | Tamaño de la casilla. | | `text_size` | Tamaño del texto. | | `accent` | Color de acento (parte activa). | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `border_color` | Color del borde. | | `check_color` | Color de la marca. | | `color` | Color del texto o del icono. | ##### `toggler` ```python toggler( checked, label=None, *, on_toggle=None, size=None, text_size=None, accent=None, bg=None, border_color=None, knob_color=None, color=None, ) ``` Un interruptor encendido/apagado. | Parámetro | Descripción | | --- | --- | | `checked` | Estado actual. | | `label` | Texto de la etiqueta. | | `on_toggle` | Callable que recibe el nuevo `bool` (o mensaje que se envía). | | `size` | Alto del interruptor. | | `text_size` | Tamaño del texto. | | `accent` | Color de acento (parte activa). | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `border_color` | Color del borde. | | `knob_color` | Color del tirador. | | `color` | Color del texto o del icono. | ##### `slider` ```python slider( min, max, value, *, step=1.0, on_change=None, on_release=None, width=None, height=None, vertical=False, accent=None, bg=None, knob_color=None, border_color=None, ) ``` Un deslizador entre `min` y `max`. | Parámetro | Descripción | | --- | --- | | `min` | Mínimo. | | `max` | Máximo. | | `value` | Valor actual. | | `step` | Paso. | | `on_change` | Callable que recibe el nuevo valor. | | `on_release` | Se envía al soltar el tirador. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `vertical` | Orientación vertical. | | `accent` | Color de acento (parte activa). | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `knob_color` | Color del tirador. | | `border_color` | Color del borde. | ##### `progress_bar` ```python progress_bar( value, *, min=0.0, max=1.0, width=None, height=None, accent=None, bg=None, radius=2.0, ) ``` Una barra de progreso. | Parámetro | Descripción | | --- | --- | | `value` | Valor actual. | | `min` | Mínimo. | | `max` | Máximo. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `accent` | Color de acento (parte activa). | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `radius` | Radio de las esquinas. | ##### `vertical_slider` ```python vertical_slider(min, max, value, **kwargs) ``` Un deslizador vertical (`slider(..., vertical=True)`). | Parámetro | Descripción | | --- | --- | | `min` | Mínimo. | | `max` | Máximo. | | `value` | Valor actual. | | `kwargs` | Los de `slider`. | ##### `pick_list` ```python pick_list( options, selected=None, on_select=None, *, on_open=None, on_close=None, placeholder=None, width=None, padding=None, text_size=None, font=None, handle=None, handle_size=12.0, menu_height=None, bg=None, color=None, placeholder_color=None, handle_color=None, border_color=None, hover_border_color=None, radius=None, menu_bg=None, menu_color=None, menu_border_color=None, selected_bg=None, selected_color=None, focus_border_color=None, id=None, ) ``` Un desplegable para elegir una de `options`. `on_select(opción)` recibe la elegida. Para el desplegable con tema usa `ui.select`. Toma el foco del teclado como un campo de texto (un clic, Tab o `Command.focus(id)`) y lo mantiene después de elegir: ↑/↓ eligen la opción anterior o la siguiente sin abrirlo, Inicio/Fin la primera y la última y al escribir salta a una opción; Enter, Espacio, Alt+↓ o F4 abren el menú, donde ↑/↓ se mueven, Enter elige y Esc cierra. | Parámetro | Descripción | | --- | --- | | `options` | Lista de opciones. | | `selected` | Opción actual. | | `on_select` | Callable que recibe el elemento elegido. | | `on_open` | Se envía al abrir el menú. | | `on_close` | Se envía al cerrar el menú. | | `placeholder` | Texto de ayuda cuando está vacío. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `padding` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `text_size` | Tamaño del texto. | | `font` | Familia de la fuente. | | `handle` | Carácter del icono de la flecha. | | `handle_size` | Tamaño de la flecha. | | `menu_height` | Alto máximo del menú. | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `color` | Color del texto o del icono. | | `placeholder_color` | Color del placeholder. | | `handle_color` | Color de la flecha. | | `border_color` | Color del borde. | | `hover_border_color` | Color del borde al pasar el ratón. | | `radius` | Radio de las esquinas: un número o `(sup_izq, sup_der, inf_der, inf_izq)`. | | `menu_bg` | Fondo del menú. | | `menu_color` | Texto del menú. | | `menu_border_color` | Borde del menú. | | `selected_bg` | Fondo de la opción resaltada. | | `selected_color` | Texto de la opción resaltada. | | `focus_border_color` | Color del borde mientras tiene el foco del teclado. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | ##### `radio` ```python radio( label, selected, on_select=None, *, size=None, text_size=None, spacing=None, accent=None, bg=None, border_color=None, color=None, ) ``` Un botón de radio. Agrupa varios tú mismo (o usa `ui.radio_group`); `on_select` se envía al pulsarlo. | Parámetro | Descripción | | --- | --- | | `label` | Texto de la etiqueta. | | `selected` | Si está marcado. | | `on_select` | Mensaje o callable al pulsarlo. | | `size` | Tamaño. | | `text_size` | Tamaño del texto. | | `spacing` | Separación entre el círculo y el texto. | | `accent` | Color de acento (parte activa). | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `border_color` | Color del borde. | | `color` | Color del texto o del icono. | #### Editor de código ##### `code_editor` ```python code_editor( id, text='', *, language='txt', size=14.0, line_height=None, font=None, width=None, height=None, padding=None, syntax=None, bg=None, color=None, selection_color=None, placeholder=None, placeholder_color=None, gutter=True, gutter_color=None, gutter_active_color=None, gutter_width=None, markers=None, wrap=False, on_change=None, on_cursor=None, active_line=None, scrollbar_color=None, annotations=None, whitespace=None, tab_size=None, capture_keys=None, on_scroll=None, auto_close=False, folding=False, minimap=None, minimap_thumb=None, cursor_shape='bar', cursor_width=2.0, cursor_blink=True, cursor_color=None, on_zoom=None, on_action=None, highlights=None, highlight_color=None, match_brackets=True, selection_highlight=True, keymap=None, scrollbar=True, readonly=False, hard_tabs=False, backspace_unindent=True, repeat_delay=None, repeat_rate=None, on_hover=None, hover_delay=500.0, sticky_scroll=0, ) ``` Un editor de código con resaltado de sintaxis, números de línea y marcadores de git, basado en `text_editor` de Iced. Es **no controlado**: `text` solo es el valor inicial para ese `id`; el texto, el cursor y el historial viven en Rust. Usa `Command.set_editor_text` para reemplazarlo. Para el editor con tema usa [`ui.editor`](https://albakore.github.io/pyzed/es/components/code/). | Parámetro | Descripción | | --- | --- | | `id` | Identifica el buffer. Reutilízalo para conservar el estado entre renders. | | `text` | Texto inicial. | | `language` | Extensión o nombre del lenguaje (`"py"`, `"rs"`, `"json"`…). | | `size` | Tamaño de la fuente. | | `line_height` | Alto de línea en píxeles. | | `font` | Familia (por defecto, monoespaciada). | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `padding` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `syntax` | Mapa de scopes TextMate a colores (lo da `Theme.syntax_scopes()`). | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `color` | Color del texto o del icono. | | `selection_color` | Color de la selección. | | `placeholder` | Texto de ayuda cuando está vacío. | | `placeholder_color` | Color del placeholder. | | `gutter` | Muestra los números de línea. | | `gutter_color` | Color de los números de línea. | | `gutter_active_color` | Color del número de la línea del cursor. | | `gutter_width` | Ancho del margen (automático por defecto). | | `markers` | `{línea: color}` para las marcas de git del margen (líneas desde 1). | | `wrap` | Ajuste de línea. | | `on_change` | Callable que recibe el texto completo tras cada edición. | | `on_cursor` | Callable que recibe `(línea, columna)`, desde 1. | | `active_line` | Color de fondo de la línea del cursor. | | `scrollbar_color` | Color de la barra de desplazamiento. | | `annotations` | `{línea: "texto"}` o `{línea: ("texto", color)}`: texto virtual al final de la línea (code lens, diagnósticos). | | `whitespace` | Dibuja los espacios como `·` y los tabuladores como `→` en este color. | | `tab_size` | Tab inserta espacios hasta el siguiente múltiplo; Enter mantiene la sangría; Backspace quita un nivel. | | `capture_keys` | Teclas que el editor deja al `on_key` de la app (`"up"`, `"enter"`…), por ejemplo con una lista de autocompletado abierta. | | `on_scroll` | Se llama al desplazar. | | `auto_close` | Al escribir un paréntesis, corchete, llave o comilla también inserta el de cierre. | | `folding` | Flechas de plegado en el margen y Ctrl+Shift+[ / ]. | | `minimap` | Ancho del minimapa en píxeles (`None`: sin minimapa). | | `minimap_thumb` | Color del resaltado del área visible en el minimapa. | | `cursor_shape` | `"bar"`, `"block"`, `"underline"` o `"hollow"`. | | `cursor_width` | Ancho del cursor de barra. | | `cursor_blink` | Si el cursor parpadea. | | `cursor_color` | Color del cursor. | | `on_zoom` | Callable que recibe `1`, `-1` o `0` con Ctrl+= / Ctrl+- / Ctrl+0 y Ctrl+rueda. | | `on_action` | Callable que recibe el nombre de cada acción del editor que se ejecuta (`"undo"`, `"select_next"`…). | | `highlights` | Rangos pintados detrás del texto (resultados de búsqueda): `[(línea, columna, línea_final, columna_final)]`, desde 1, con el final excluido. | | `highlight_color` | Color de `highlights`. | | `match_brackets` | Remarca el corchete pareja del que está junto al cursor. | | `selection_highlight` | Resalta las demás apariciones del texto seleccionado. | | `keymap` | Diccionario `{atajo: acción}` al estilo de Zed; `None` como acción quita una tecla por defecto (`pyzed.ui.editor.keymap_for` crea uno a partir de cambios por acción). | | `scrollbar` | `False` oculta las barras de desplazamiento. | | `readonly` | `True` deja seleccionar y copiar el texto, pero no cambiarlo (un log, un traceback). | | `hard_tabs` | Con `tab_size`: Tab, Enter y sangrar (Ctrl+]) usan tabulaciones en vez de espacios. | | `backspace_unindent` | Con `tab_size`: Backspace en los espacios iniciales quita un nivel entero (`False`: un espacio). | | `repeat_delay` | Milisegundos que esperan Backspace y Suprimir, mantenidos, antes de repetirse (`None`: los del sistema). | | `repeat_rate` | Borrados por segundo como máximo mientras se mantiene Backspace o Suprimir (`None`: el ritmo del sistema). | | `on_hover` | Función que recibe `{line, column, start, end, x, y, height}` (la palabra bajo el ratón, desde 1, y su lugar en el editor) cuando el ratón se detiene sobre ella, y `None` cuando se va (tarjetas de información). | | `hover_delay` | Milisegundos que el ratón debe quedarse sobre una palabra antes de llamar a `on_hover`. | | `sticky_scroll` | Cuántas líneas de los bloques que rodean al código visible (`class`, `def`, `if`…) quedan fijas arriba al desplazarse (`0`: ninguna). | #### Estilos ##### `Style` ```python class Style ``` Un conjunto reutilizable de propiedades de estilo para `widget.style(...)`, el `style=` de un componente o `App.style`. Combina estilos con `|` (gana el de la derecha). Ver [Estilos](https://albakore.github.io/pyzed/es/theming/styles/). | Parámetro | Descripción | | --- | --- | | `styles` | Estilos, dicts o nombres de estilo de partida. | | `props` | Propiedades: `background`, `color`, `font`, `weight`, `italic`, `size`, `line_height`, `border_color`, `border_width`, `border` (`(ancho, color)`), `radius`, `shadow`, `padding`, `margin`, `width`, `height`, `max_width`, `max_height`, `min_width`, `min_height`, `align_x`, `align_y`, `clip`. Efectos: `opacity`, `translate` (`(x, y)`, píxeles o `"50%"`), `scale`, `glow` (un color, un tamaño o `(color, tamaño)`), `bloom` (un color o `(color, intensidad)`) y `glass` (`True`, un tinte o `{"tint", "strength"}`). Estados: `hover`, `pressed` (o `active`) y `focus`, cada uno con las propiedades que se muestran entonces. `transition`: milisegundos, `True` o `{"duration", "easing", "delay"}`. `media`: `{consulta: estilo}` que se aplica cuando la ventana la cumple. Los colores pueden ser tokens del tema. | ```python tarjeta = pz.Style(background="panel", border=(1, "border"), radius=8, padding=12) pz.column([...]).style(tarjeta | pz.Style(shadow=("#0006", (0, 4), 12))) ``` **Métodos y propiedades** ###### `Style.get` ```python Style.get(key, default=None) ``` Devuelve el valor de una propiedad (se aceptan alias), o `default` si no está fijada. ###### `Style.merge` ```python Style.merge(*styles, **props) ``` Un estilo nuevo con `styles` y `props` encima de este. ###### `Style.without` ```python Style.without(*keys) ``` Un estilo nuevo sin estas propiedades. ###### `Style.text` ```python Style.text() ``` Solo las propiedades de texto (lo que un estilo pasa a los textos de dentro). ###### `Style.to_dict` ```python Style.to_dict() ``` Las propiedades en formato apto para JSON (los gradientes y pinturas pasan a ser dicts). ###### `Style.from_dict` ```python @classmethod Style.from_dict(data) ``` Crea un estilo a partir de datos hechos con `to_dict` (se aceptan alias). ##### `define_style` ```python define_style(name, *styles, **props) ``` Pone nombre a un estilo para usarlo así: `widget.style("card")`. Lo devuelve. | Parámetro | Descripción | | --- | --- | | `name` | Nombre del estilo. | | `styles` | Estilos a combinar. | | `props` | Propiedades encima. | ##### `stylesheet` ```python stylesheet(rules=None, **classes) ``` Añade reglas como en CSS, `{selector: estilo}`, que los widgets reciben con `widget.classes(...)` y los componentes con `class_=`/`id=`. Selectores: `.clase`, `.a.b`, `#id`, el nombre de la clase de un componente (`"Rating"`), un `:hover`, `:pressed`/`:active` o `:focus` opcional y listas con comas. Ganan los selectores más específicos (ids, luego clases, luego nombres) y después las reglas posteriores. Un selector repetido reemplaza su regla. Devuelve todas las reglas. Ver [Hojas de estilo](https://albakore.github.io/pyzed/es/theming/styles/#hojas-de-estilo). | Parámetro | Descripción | | --- | --- | | `rules` | Un dict `{selector: estilo}`. | | `classes` | Clases por palabra clave: `card={...}` es `".card"`. | ```python pz.stylesheet({ ".card": {"background": "panel", "padding": 16, "radius": 8}, ".card:hover": {"border_color": "accent"}, }) pz.column([...]).classes("card") ``` ##### `clear_stylesheet` ```python clear_stylesheet() ``` Quita todas las reglas añadidas con `pz.stylesheet`. ##### `media` ```python media(query=None, **conditions) ``` Si la ventana que se dibuja cumple una media query, como `@media` en CSS. Ver [Estilos responsive](https://albakore.github.io/pyzed/es/theming/styles/#estilos-responsive). | Parámetro | Descripción | | --- | --- | | `query` | `"max_width: 600"`, `"(min_width: 768px) and (orientation: landscape)"` o un breakpoint (`"md"`). | | `conditions` | Lo mismo por palabra clave: `min_width`, `max_width`, `min_height`, `max_height`, `orientation`, `theme`. | ```python sidebar = None if pz.media("max_width: 600") else self.sidebar() ``` ##### `viewport` ```python viewport() ``` El tamaño de la ventana que se dibuja, `(ancho, alto)` en píxeles lógicos, como `window.innerWidth` e `innerHeight`. ##### `current_breakpoint` ```python current_breakpoint() ``` El breakpoint más ancho que alcanza la ventana: `"xs"` (menos de 640 px), `"sm"`, `"md"`, `"lg"`, `"xl"` o `"xxl"`. ##### `set_style` ```python set_style(*styles, **props) ``` Fija el estilo global de todas las ventanas, por debajo del `style` de cada una. Sin argumentos lo quita. | Parámetro | Descripción | | --- | --- | | `styles` | Estilos a combinar. | | `props` | Propiedades encima. | ##### `Gradient` ```python class Gradient ``` Un gradiente lineal para fondos. Los ángulos siguen CSS: 0 arriba, 90 derecha, 180 (por defecto) abajo. Hasta 8 paradas. | Parámetro | Descripción | | --- | --- | | `stops` | Colores, repartidos por igual, o pares `(posición, color)` con posiciones de 0 a 1. | | `angle` | Dirección en grados. | ```python pz.container(None, height=80).style(background=pz.Gradient(["#f97316", "#7c3aed"], angle=90)) ``` **Métodos y propiedades** ###### `Gradient.to_value` ```python Gradient.to_value() ``` `(angle, [(offset, "#rrggbbaa"), ...])`, la forma que los widgets aceptan como `gradient=`. ###### `Gradient.to_dict` ```python Gradient.to_dict() ``` Devuelve el gradiente como datos listos para JSON: `{"type": "linear", "angle", "stops"}`. ##### `Mask` ```python class Mask ``` Una máscara de opacidad para un `Paint`: cuánto se ve el relleno a lo largo de una dirección. Sobre un gradiente sigue su dirección. | Parámetro | Descripción | | --- | --- | | `stops` | Opacidades (0 a 1), repartidas por igual, o pares `(posición, opacidad)`. | | `angle` | Dirección cuando el relleno es un color liso. | **Métodos y propiedades** ###### `Mask.at` ```python Mask.at(offset) ``` Devuelve la opacidad (0 a 1) en `offset` (0 a 1), interpolando entre paradas. ###### `Mask.to_dict` ```python Mask.to_dict() ``` Devuelve la máscara como datos listos para JSON: `{"angle", "stops"}`. ##### `Paint` ```python class Paint ``` Un relleno: un color o un `Gradient` con opacidad general y una `Mask` opcional. Sirve en cualquier fondo. | Parámetro | Descripción | | --- | --- | | `fill` | Un color o un `Gradient`. | | `opacity` | De 0 a 1; multiplica el alfa del relleno. | | `mask` | Una `pz.Mask`. | ```python pz.Paint("#3b82f6", opacity=0.8, mask=pz.Mask([1, 0])) # se desvanece hacia abajo ``` **Métodos y propiedades** ###### `Paint.resolve` ```python Paint.resolve() ``` `(color, gradient)`: lo que dibuja un contenedor (uno de los dos es `None`). ###### `Paint.to_dict` ```python Paint.to_dict() ``` Devuelve la pintura como datos listos para JSON: `fill`, `opacity` y `mask` cuando la hay. ##### `global_style` ```python global_style() ``` El estilo fijado con `set_style` (`None` si no hay). ##### `named_styles` ```python named_styles() ``` Los estilos definidos con `define_style`, por nombre. ##### `apply_global` ```python apply_global(view, style=None) ``` `view` con el estilo global de una ventana: las propiedades de texto completan lo que los textos no fijan. ##### `themed` ```python themed(theme, style) ``` `theme` con el color de texto, el fondo, el color de borde, el tamaño y el radio de un estilo global. ##### `to_hex` ```python to_hex(color) ``` Cualquier color como `"#rrggbb"` o `"#rrggbbaa"` (con los tokens del tema resueltos). ##### `paint_from_dict` ```python paint_from_dict(data) ``` Reconstruye un `Gradient` o un `Paint` guardado con `to_dict()`. #### Animaciones con línea de tiempo ##### `Clip` ```python @dataclass class Clip(name, length=None, tracks=(), calls=(), loop='none', description='') ``` Una línea de tiempo con nombre de `length` segundos, con pistas (`tracks`) y llamadas (`calls`). `loop` es `"none"` (se reproduce una vez y se queda en el último fotograma), `"loop"` o `"pingpong"`. Por defecto `length` es el tiempo de la última key o llamada. Ver la guía [Animaciones con línea de tiempo](https://albakore.github.io/pyzed/es/animation/timeline/). **Métodos y propiedades** ###### `Clip.targets` ```python Clip.targets # property ``` Los objetivos de widget que animan sus pistas, en orden. ###### `Clip.attributes` ```python Clip.attributes # property ``` Los atributos de la app que animan sus pistas (`":score"` -> `"score"`). ###### `Clip.track` ```python Clip.track(path) ``` La pista con `path` (`None` si no hay ninguna). ###### `Clip.values` ```python Clip.values(time, target=None) ``` `{ruta: valor}` de las pistas activas en `time` (solo las de `target`, como `{propiedad: valor}`). ###### `Clip.with_track` ```python Clip.with_track(track) ``` Una copia con `track` añadida, o reemplazando la pista con la misma ruta. ###### `Clip.without_track` ```python Clip.without_track(path) ``` Una copia sin la pista `path`. ###### `Clip.with_key` ```python Clip.with_key(path, time, value, easing=None) ``` Una copia con una key en `time` sobre `path` (reemplaza la que haya en ese tiempo; añade la pista). ###### `Clip.without_key` ```python Clip.without_key(path, time) ``` Una copia sin la key en `time` de `path`. ###### `Clip.retarget` ```python Clip.retarget(mapping, name=None) ``` Una copia cuyas pistas animan otros widgets: `clip.retarget({"card": "panel"})`. ###### `Clip.animation` ```python Clip.animation(target, *, on='mount', **options) ``` Las pistas de `target` como una `pz.Animation` (para reproducirla sin reproductor, o con `define_animation`). ###### `Clip.to_dict` ```python Clip.to_dict() ``` Datos listos para JSON (`Clip.from_dict` los vuelve a leer). ###### `Clip.from_dict` ```python @classmethod Clip.from_dict(data) ``` Reconstruye un clip guardado con `to_dict`. ##### `Track` ```python @dataclass class Track(path, keys=(), easing='linear', interpolation='linear', enabled=True) ``` Una propiedad que cambia con el tiempo: `Track("card:opacity", {0: 0, 0.5: 1})`. La ruta es `"objetivo:propiedad"` para una propiedad de widget o `":atributo"` para un atributo de la app. `keys` es `{tiempo: valor}`, una lista de tuplas `(tiempo, valor)` o `(tiempo, valor, easing)`, u objetos `Key`. `easing` es la curva por defecto entre keys (`"linear"`, como en Godot) e `interpolation="discrete"` mantiene cada valor hasta la siguiente key. Con `enabled=False` la pista queda en el clip sin reproducirse. **Métodos y propiedades** ###### `Track.target` ```python Track.target # property ``` El objetivo de widget (`""` para un atributo de la app). ###### `Track.property` ```python Track.property # property ``` La propiedad o la ruta del atributo que cambia. ###### `Track.value_at` ```python Track.value_at(time) ``` El valor de la pista a los `time` segundos (`None` si no tiene keys). ###### `Track.with_keys` ```python Track.with_keys(keys) ``` Una copia con otras keys. ###### `Track.to_dict` ```python Track.to_dict() ``` Datos listos para JSON (`Track.from_dict` los vuelve a leer). ###### `Track.from_dict` ```python @classmethod Track.from_dict(data) ``` Reconstruye una pista guardada con `to_dict`. ##### `Key` ```python @dataclass class Key(time, value, easing=None) ``` Un valor en un tiempo de una pista; `easing` es la curva hasta la siguiente key (la de la pista si es `None`). **Métodos y propiedades** ###### `Key.to_dict` ```python Key.to_dict() ``` Datos listos para JSON (`Key.from_dict` los vuelve a leer). ###### `Key.from_dict` ```python @classmethod Key.from_dict(data) ``` Reconstruye una key guardada con `to_dict`. ##### `Call` ```python @dataclass class Call(time, method, args=()) ``` Un método que se llama a los `time` segundos: `Call(1.2, "play_sound", "ding")`. `method` es el nombre de un método de la app (el `owner` del reproductor) o una función; `args` se le pasan. Lo que devuelve (un `Command`, un mensaje...) se trata como lo que devuelve un callback. **Métodos y propiedades** ###### `Call.to_dict` ```python Call.to_dict() ``` Datos listos para JSON (solo se pueden guardar llamadas a métodos por nombre). ###### `Call.from_dict` ```python @classmethod Call.from_dict(data) ``` Reconstruye una llamada guardada con `to_dict`. ##### `AnimationPlayer` ```python class AnimationPlayer ``` Reproduce `Clip`s sobre widgets y el estado de la app, con reproducir, pausar, buscar, velocidad y cola. `sources` son clips, objetos `AnimationLibrary` y rutas `.pzanim`. Dale a cada widget su objetivo con `widget.animate(player["card"])`. Las pistas de atributos (`":score"`) y las `Call` usan el `owner` del reproductor: la app (o ventana) que lo guarda en un atributo, que se encuentra sola, o el que le pases. - `autoplay`: el clip que se reproduce al crear el reproductor. - `speed`: la velocidad de todos los clips (`play` la multiplica). - `on_started(name)`/`on_finished(name)`: se llaman cuando un clip empieza y cuando termina (nunca en clips en bucle); pueden no recibir argumentos. - `fps`: cada cuánto se actualizan los atributos mientras suena un clip con pistas de atributos. **Métodos y propiedades** ###### `AnimationPlayer.add` ```python AnimationPlayer.add(source, *, prefix=None) ``` Añade un clip, una biblioteca (sus clips como `"nombre/clip"` si tiene nombre) o una ruta `.pzanim`. ###### `AnimationPlayer.remove` ```python AnimationPlayer.remove(name) ``` Quita el clip `name` (y lo detiene si es el actual). ###### `AnimationPlayer.names` ```python AnimationPlayer.names # property ``` Los nombres de los clips que puede reproducir. ###### `AnimationPlayer.has` ```python AnimationPlayer.has(name) ``` Si tiene un clip llamado `name`. ###### `AnimationPlayer.clip` ```python AnimationPlayer.clip(name=None) ``` El clip llamado `name` (por defecto el actual). ###### `AnimationPlayer.target` ```python AnimationPlayer.target(target, *, only=None) ``` Igual que `player[target]`; `only` deja solo algunas propiedades (`("opacity", "x", "y")`), por ejemplo para mover con él lo que está dentro de un contenedor animado. ###### `AnimationPlayer.current` ```python AnimationPlayer.current # property ``` El clip que se reproduce o está en pausa (`None` si está detenido). ###### `AnimationPlayer.playing` ```python AnimationPlayer.playing # property ``` Si el tiempo avanza. ###### `AnimationPlayer.paused` ```python AnimationPlayer.paused # property ``` Si un clip está en pausa (los detenidos y los terminados no). ###### `AnimationPlayer.finished` ```python AnimationPlayer.finished # property ``` Si el clip actual llegó al final. ###### `AnimationPlayer.length` ```python AnimationPlayer.length # property ``` La duración del clip actual en segundos (0 si no hay ninguno). ###### `AnimationPlayer.position` ```python AnimationPlayer.position # property ``` Segundos transcurridos del clip actual. ###### `AnimationPlayer.queued` ```python AnimationPlayer.queued # property ``` Los clips que se reproducen después del actual, en orden. ###### `AnimationPlayer.value` ```python AnimationPlayer.value(path) ``` El valor que el clip actual da a `path` (`"card:opacity"`, `":score"`) ahora mismo. ###### `AnimationPlayer.play` ```python AnimationPlayer.play(name=None, *, speed=1.0, backwards=False, from_end=False, restart=False) ``` Reproduce `name` (por defecto el clip actual o en pausa) desde donde está. Un clip que ya se reproduce sigue salvo con `restart`; uno terminado vuelve a empezar. `backwards` lo reproduce al revés (`from_end` empieza en el final). ###### `AnimationPlayer.play_backwards` ```python AnimationPlayer.play_backwards(name=None, *, speed=1.0) ``` Reproduce `name` al revés desde el final (o el clip actual desde donde está). ###### `AnimationPlayer.pause` ```python AnimationPlayer.pause() ``` Detiene el tiempo donde está (`play()` o `resume()` siguen). ###### `AnimationPlayer.resume` ```python AnimationPlayer.resume() ``` Sigue desde donde se pausó el clip. ###### `AnimationPlayer.toggle` ```python AnimationPlayer.toggle() ``` Pausa si se está reproduciendo y reproduce si no. ###### `AnimationPlayer.stop` ```python AnimationPlayer.stop(*, reset=False) ``` Vuelve al principio del clip (mostrando su primer fotograma); con `reset` los widgets recuperan sus propios valores y se olvida el clip. Vacía la cola. ###### `AnimationPlayer.seek` ```python AnimationPlayer.seek(seconds) ``` Salta a `seconds` dentro del clip actual (las llamadas del camino no se ejecutan). ###### `AnimationPlayer.advance` ```python AnimationPlayer.advance(seconds) ``` Avanza el clip actual `seconds` (se ejecutan las llamadas y se actualiza el estado), como `advance` de Godot. ###### `AnimationPlayer.queue` ```python AnimationPlayer.queue(name) ``` Reproduce `name` cuando termine el clip actual (o ahora si no suena nada). ###### `AnimationPlayer.clear_queue` ```python AnimationPlayer.clear_queue() ``` Olvida los clips en cola. ###### `AnimationPlayer.subscriptions` ```python AnimationPlayer.subscriptions() ``` Lo que mantiene en marcha las llamadas, las pistas de atributos y la cola del reproductor (se añade solo cuando el reproductor es un atributo de la app o de una ventana). ##### `AnimationLibrary` ```python class AnimationLibrary ``` Clips con nombre que se guardan juntos en un archivo `.pzanim` y se reutilizan. `AnimationLibrary([intro, idle], name="ui")`; `lib["intro"]`, `lib.add(clip)`, `lib.remove("idle")`, `lib.save("ui.pzanim")` y `AnimationLibrary.load(ruta)`. Un reproductor que recibe una biblioteca con `name` llama a sus clips `"ui/intro"`, como Godot. **Métodos y propiedades** ###### `AnimationLibrary.add` ```python AnimationLibrary.add(clip) ``` Añade `clip` (reemplazando el que tenga su nombre) y lo devuelve. ###### `AnimationLibrary.remove` ```python AnimationLibrary.remove(name) ``` Quita el clip `name` (no pasa nada si no existe). ###### `AnimationLibrary.rename` ```python AnimationLibrary.rename(old, new) ``` Renombra el clip `old` a `new` y lo devuelve. ###### `AnimationLibrary.names` ```python AnimationLibrary.names # property ``` Los nombres de sus clips, en orden. ###### `AnimationLibrary.to_dict` ```python AnimationLibrary.to_dict() ``` Los datos del `.pzanim` (`AnimationLibrary.from_dict` los vuelve a leer). ###### `AnimationLibrary.from_dict` ```python @classmethod AnimationLibrary.from_dict(data) ``` Reconstruye una biblioteca a partir de datos `.pzanim`. ###### `AnimationLibrary.save` ```python AnimationLibrary.save(path) ``` Escribe la biblioteca como JSON (un archivo `.pzanim`) y devuelve la ruta. ###### `AnimationLibrary.load` ```python @classmethod AnimationLibrary.load(path) ``` Lee un archivo `.pzanim` (lanza `ValueError` si no lo es). ##### `load_animations` ```python load_animations(path) ``` La biblioteca guardada en un archivo `.pzanim`. #### Animación ##### `Animation` ```python @dataclass class Animation(keyframes=(), duration=300, delay=0, easing='ease', repeat=1, direction='normal', fill='both', on='mount', category='custom', description='', _frames=()) ``` Una animación por keyframes, como una regla CSS `@keyframes` con su timing. `keyframes` es una lista de frames `{propiedad: valor}` (repartidos por igual; un frame puede fijar `offset` y `easing`) o un dict de offsets (`0` a `1`, `"50%"`, `"from"`, `"to"`) a frames. Una propiedad que falte al principio o al final sale del valor propio del widget o vuelve a él, y `None` nombra ese valor explícitamente: ```python pz.Animation([{"opacity": 0, "y": 12}, {"opacity": 1, "y": 0}], duration=250) pz.Animation({0: {"scale": 1}, "50%": {"scale": 1.1}, 1: {"scale": 1}}, repeat="infinite") ``` Los demás campos son los valores por defecto que sobrescriben las opciones de `animate()`. `category` y `description` dicen a los editores dónde y cómo mostrar un preset. **Métodos y propiedades** ###### `Animation.properties` ```python Animation.properties # property ``` Las propiedades que cambian los keyframes, en orden de aparición. ###### `Animation.total` ```python Animation.total # property ``` Milisegundos del principio al final (`inf` si se repite para siempre). ###### `Animation.with_options` ```python Animation.with_options(**options) ``` Una copia con otro timing o disparador (`duration`, `easing`, `on`...). ###### `Animation.compile` ```python Animation.compile(**options) ``` Los datos simples que reproduce la parte nativa (los usa `animate()`). ###### `Animation.to_dict` ```python Animation.to_dict() ``` Datos listos para JSON (`Animation.from_dict` los vuelve a leer). ###### `Animation.from_dict` ```python @classmethod Animation.from_dict(data) ``` Reconstruye una animación guardada con `to_dict`. ##### `Trigger` ```python class Trigger ``` Inicia una animación desde tu código, como un botón que pulsas en un manejador. Guarda uno en la app, pásalo a `animate(on=...)` en `view()` y llama a `fire()` cada vez que la animación deba reproducirse (desde el principio, aunque siga en marcha): ```python class Login(pz.App): def __init__(self): self.error = pz.Trigger() def view(self): return ui.input(self.clave, self.poner_clave).animate("shake", on=self.error) def enviar(self): if not self.comprobar(): self.error.fire() # el campo tiembla ahora ``` No se reproduce nada cuando el widget aparece: solo `fire()` la inicia. `count` dice cuántas veces se disparó. `fire` acepta e ignora cualquier argumento, así que puede ser un callback: `ui.button("Temblar", self.error.fire)`. **Métodos y propiedades** ###### `Trigger.fire` ```python Trigger.fire(*_args) ``` Reproduce desde el principio las animaciones que usan este disparador. ##### `define_animation` ```python define_animation(name, animation=None, **options) ``` Da nombre a una animación para que `animate(nombre)` la reproduzca; la devuelve. `define_animation("wiggle", [{"x": 0}, {"x": -4}, {"x": 4}, {"x": 0}], duration=300)` ##### `presence` ```python presence( content, key, *, enter='fade_in', exit='fade_out', id=None, duration=None, easing=None, ) ``` Muestra `content` y, cuando cambia `key`, anima la salida del contenido anterior (`exit`) mientras entra el nuevo (`enter`), como transiciones entre páginas: ```python pz.presence(self.vista_pagina(), key=self.pagina, enter="slide_in_right", exit="fade_out") ``` `id` distingue unas presences de otras (por defecto es el lugar desde donde se llama, así que da uno cuando la misma línea crea varias). `duration` y `easing` sobrescriben los de ambas animaciones. ##### `stagger` ```python stagger(items, animation='fade_in_up', *, step=40, delay=0, **options) ``` `items` con la misma animación, cada uno empezando `step` milisegundos después del anterior (una lista que entra en cascada): ```python pz.column(pz.stagger([fila(i) for i in items], "fade_in_up", step=30)) ``` ##### `set_reduced_motion` ```python set_reduced_motion(on=True) ``` Apaga el movimiento (`True`) o lo vuelve a encender: las animaciones muestran al instante su estado de reposo. Por defecto Pyzed sigue el ajuste de accesibilidad "reducir movimiento" del sistema (se lee al arrancar la app, ver `pyzed.system_reduced_motion`). `True` o `False` lo reemplazan (por ejemplo desde los ajustes de tu app, o en tests) y `None` vuelve a seguir al sistema. La variable de entorno `PYZED_REDUCED_MOTION` (`1` o `0`) también lo reemplaza. ##### `system_reduced_motion` ```python system_reduced_motion() ``` Si el sistema pide a las apps reducir el movimiento (`None` si no se sabe). ##### `reduced_motion` ```python reduced_motion() ``` Si el movimiento reducido está encendido. ##### `animation` ```python animation(name) ``` La animación llamada `name` (un preset o una definida con `define_animation`). ##### `presets` ```python presets(category=None) ``` Las animaciones con nombre (todas, o una `category`: entrance, exit, attention, interaction, loop). ##### `ease` ```python ease(easing, t) ``` `easing` en `t` (0 a 1), la curva que usan las animaciones: `ease("ease_out", 0.5)`. Útil para animar tus propios dibujos (`pz.canvas`) con las mismas curvas. ##### `window_animations` ```python window_animations(window, closing=False) ``` La animación que reproduce una ventana al abrirse (o cerrarse), según su `transition` y `exit_transition` (una lista vacía si no tiene). ##### `PRESETS` Todas las animaciones con nombre (los presets incluidos y los añadidos con `define_animation`), asociadas a su `Animation`. Ver [la lista de presets](https://albakore.github.io/pyzed/es/animation/presets/). ##### `CATEGORIES` Categorías de presets: `entrance` (entrada), `exit` (salida), `attention` (atención), `interaction` (interacción) y `loop` (bucle). ##### `EASINGS` Nombres de easing (`linear`, `ease`, `ease_out_cubic`, `ease_out_back`, `spring`...). `animate()` también acepta Béziers cúbicas `(x1, y1, x2, y2)` y `"steps(n)"`. ##### `TRIGGERS` Qué reproduce una animación: `mount`, `hover`, `press`, `focus`, `click`, `active` y `manual` (cada vez que cambia `key`; un `pz.Trigger` pasado a `on=` lo usa). ##### `PROPERTIES` Las propiedades que cambian las animaciones y transiciones: `opacity`, `x`, `y`, `scale`, `background`, `border_color`, `color`, `radius`, `border_width`, `glow` y `glow_color`. ##### `WINDOW_TRANSITIONS` Nombres para `Window.transition` (`fade`, `zoom`, `pop`, `slide_up`, `slide_down`, `slide_left`, `slide_right`) asociados a sus presets `(apertura, cierre)`. #### Muchos datos ##### `virtual_column` ```python virtual_column(count, item, *, item_height, spacing=0.0, overscan=4, width=None) ``` Una columna de `count` filas de la misma altura que solo construye las filas visibles: `item(índice)` se llama cuando la fila entra en la vista, y las que salen se descartan. Una columna de un millón de filas cuesta lo mismo que una de cincuenta. Ponla dentro de un `scrollable` (se virtualiza contra el área visible del más cercano, o de la ventana). | Parámetro | Descripción | | --- | --- | | `count` | Número de filas. | | `item` | Función que recibe el índice de una fila y devuelve su widget. | | `item_height` | Altura de cada fila, en píxeles. | | `spacing` | Espacio entre filas, en píxeles. | | `overscan` | Filas extra construidas por encima y por debajo de las visibles, para que el scroll rápido no deje huecos. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | ```python rows = load_logs() # una lista con 500 000 entradas pz.scrollable( pz.virtual_column(len(rows), lambda i: pz.text(rows[i]), item_height=20), id="logs", height="fill", ) ``` ##### `memo` ```python memo(function=None, *, maxsize=64) ``` Decorador que guarda lo que devuelve una función de la vista según sus argumentos, para que el siguiente `view()` reutilice el mismo árbol de widgets mientras no cambien. Pasa como argumentos todo lo que usa el subárbol. Las listas, dicts y sets se comparan por contenido y otros objetos no hashables por identidad; el tema y el idioma actuales siempre forman parte de la clave. `funcion.cache_clear()` vacía su caché. | Parámetro | Descripción | | --- | --- | | `function` | La función a memoizar (al usarlo como `@pz.memo`). | | `maxsize` | Cuántos resultados guardar por función (se descartan los usados hace más tiempo). | ```python @pz.memo def sidebar(files: tuple, selected: str): return pz.column([ui.list_item(f, selected=f == selected) for f in files]) ``` ##### `clear_memos` ```python clear_memos() ``` Vacía la caché de todas las funciones `@pz.memo`. ##### `memo_stats` ```python memo_stats() ``` Devuelve `{"hits": n, "misses": m}`: cuántas llamadas a funciones memoizadas se respondieron desde la caché y cuántas construyeron su resultado. ### Componentes (pyzed.ui) > Referencia de la API de pyzed.ui: cada componente listo para usar con sus parámetros, valores por defecto y eventos, para apps de escritorio en Python. Source: https://albakore.github.io/pyzed/es/reference/ui/ Firmas y parámetros de todos los componentes. Para verlos con imágenes y ejemplos, ve a la sección [Componentes](https://albakore.github.io/pyzed/es/components/overview/). Todos son funciones que devuelven un `Widget` y leen sus colores del tema actual. Los parámetros de color aceptan un **token** del tema (`"accent"`, `"text_muted"`, `"error"`…) o un color hex. #### Básicos ##### `label` ```python label( content, *, color='text', size='md', weight=None, mono=False, italic=False, wrap=True, theme=None, **kwargs, ) ``` Texto con un color del tema. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `color` | Token del tema o color hex. Por defecto `"text"`. | | `size` | `"xs"` (11), `"sm"` (12), `"md"` (13.5), `"lg"` (16), `"xl"` (20), `"2xl"` (26) o un número. | | `weight` | Peso: `"medium"`, `"semibold"`, `"bold"`… | | `mono` | Usa la fuente monoespaciada (IBM Plex Mono). | | `italic` | Cursiva. | | `wrap` | Ajuste de línea. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | | `kwargs` | Se pasan a `pyzed.text`. | ##### `muted` ```python muted(content, **kwargs) ``` Un `label` con color `"text_muted"`. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `kwargs` | Los de `label`. | ##### `heading` ```python heading(content, *, level=1, theme=None, **kwargs) ``` Un título en semibold. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `level` | 1 (26 px), 2 (20 px), 3 (16 px) o 4 (13.5 px). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | | `kwargs` | Los de `label`. | ##### `button` ```python button( content=None, on_press=None, *, icon=None, icon_position='start', icon_color=None, variant='filled', tint=None, size='md', selected=False, disabled=False, full_width=False, keys=None, loading=False, tooltip=None, theme=None, **style, ) ``` Un botón con tema, con variantes, tamaños, un icono y un atajo. | Parámetro | Descripción | | --- | --- | | `content` | Texto o widget. | | `on_press` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `icon_position` | `"start"` o `"end"`. | | `icon_color` | Color del icono: un token del tema o un color hex. | | `variant` | `"filled"`, `"ghost"`, `"subtle"`, `"outline"`, `"solid"`, `"tinted"`, o un color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"`. | | `tint` | Color de las variantes `solid` y `tinted`. | | `size` | `"xs"`, `"sm"`, `"md"` o `"lg"`. | | `selected` | Lo marca como seleccionado. | | `disabled` | Lo deshabilita (ignora los clics). | | `full_width` | Ocupa todo el ancho. | | `keys` | Atajo que se muestra al final, escrito como texto: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `loading` | Muestra un spinner en lugar del icono e ignora las pulsaciones (mantén una suscripción `pz.every(1/30, ...)` mientras gira). | | `tooltip` | Texto que aparece al pasar el ratón. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | | `style` | Cualquier parámetro de estilo de `pyzed.button` (`bg`, `hover_bg`, `color`…). | ##### `icon_button` ```python icon_button( name, on_press=None, *, size='md', variant='ghost', color=None, selected=False, disabled=False, tooltip=None, keys=None, tint=None, loading=False, indicator=None, theme=None, ) ``` Un botón cuadrado con solo un icono. | Parámetro | Descripción | | --- | --- | | `name` | Nombre del icono. | | `on_press` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | | `size` | `"xs"`, `"sm"`, `"md"` o `"lg"`. | | `variant` | Como en `ui.button`. Por defecto `"ghost"`. | | `color` | Color del icono. | | `selected` | Lo marca como seleccionado. | | `disabled` | Lo deshabilita (ignora los clics). | | `tooltip` | Texto del tooltip. | | `keys` | Atajo que se muestra en el tooltip. | | `tint` | Lo colorea (`"error"`, `"accent"`…). | | `loading` | Cambia el icono por un spinner e ignora las pulsaciones. | | `indicator` | Añade un punto de este color arriba a la derecha (sin leer, cambios…). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `badge` ```python badge( content, *, tint='accent', variant='soft', icon=None, size='md', on_press=None, theme=None, ) ``` Una píldora pequeña para estados y contadores. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `tint` | Color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` o un token. | | `variant` | `"soft"`, `"solid"` o `"outline"`. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `size` | `"sm"`, `"md"` o `"lg"`. | | `on_press` | La hace pulsable. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `indicator` ```python indicator(color='accent', *, size=6, theme=None) ``` Un punto de color (estado, cambios sin guardar, presencia). | Parámetro | Descripción | | --- | --- | | `color` | Token o color. | | `size` | Diámetro. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `kbd` ```python kbd(keys, *, size='sm', theme=None) ``` Dibuja un atajo como teclas: `"ctrl-shift-p"`, `"cmd-k cmd-s"`. En macOS usa símbolos (⌘⇧P) y en el resto palabras. | Parámetro | Descripción | | --- | --- | | `keys` | El atajo. | | `size` | `"xs"` o `"sm"`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `format_keys` ```python format_keys(keys) ``` Separa un atajo en etiquetas por acorde: `format_keys("ctrl-shift-p") == [["Ctrl", "Shift", "P"]]`. El estilo se controla con `pyzed.ui.base.KEY_STYLE` (`"auto"`, `"symbols"` o `"words"`). | Parámetro | Descripción | | --- | --- | | `keys` | El atajo. | ##### `keys_text` ```python keys_text(keys) ``` Un atajo como texto plano para una etiqueta o una línea de estado: `keys_text("ctrl-shift-p") == "Ctrl+Shift+P"` (`"⌃⇧P"` con los símbolos de macOS), los acordes separados por un espacio. Los tooltips, botones y menús toman `keys=` y dibujan las teclas, así que no escribas las teclas en su texto. | Parámetro | Descripción | | --- | --- | | `keys` | El atajo. | ##### `avatar` ```python avatar( initials, *, color=None, size=22, index=0, image=None, status=None, tooltip=None, theme=None, ) ``` Un avatar redondo con iniciales. | Parámetro | Descripción | | --- | --- | | `initials` | Iniciales (se usan las dos primeras). | | `color` | Color de fondo. | | `size` | Diámetro. | | `index` | Elige un color de colaborador del tema (`theme.players`). | | `image` | Una imagen (ruta o bytes) en lugar de las iniciales. | | `status` | Punto de presencia: `"online"`, `"away"`, `"busy"`, `"offline"` o un color. | | `tooltip` | Texto al pasar el ratón (el nombre de la persona). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `tooltip` ```python tooltip(content, text, *, keys=None, position='bottom', theme=None) ``` Envuelve `content` con un tooltip con el estilo del tema, opcionalmente con su atajo. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `text` | Texto del tooltip. | | `keys` | Atajo que se muestra al final, escrito como texto: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `position` | `"top"`, `"bottom"`, `"left"`, `"right"` o `"cursor"`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `divider` ```python divider(*, vertical=False, color=None, label=None, spacing=0, theme=None) ``` Una línea divisoria con el color del tema. | Parámetro | Descripción | | --- | --- | | `vertical` | Vertical en lugar de horizontal. | | `color` | Color del texto o del icono. | | `label` | Texto atenuado en el centro de una línea horizontal (`"o"`, una fecha…). | | `spacing` | Espacio a ambos lados de la línea. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `card` ```python card( content, *, title=None, actions=(), padding=16, width=None, footer=(), on_press=None, selected=False, elevated=False, theme=None, ) ``` Una superficie con borde para agrupar contenido, con título y acciones opcionales. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `title` | Título. | | `actions` | Widgets extra (normalmente botones) que se muestran como acciones. | | `padding` | Relleno interior. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `footer` | Widgets abajo a la derecha (botones). | | `on_press` | Hace pulsable toda la tarjeta. | | `selected` | La resalta con el color de acento. | | `elevated` | Añade una sombra. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | #### Formularios ##### `input` ```python input( value, on_input=None, *, placeholder='', icon=None, on_submit=None, on_clear=None, id=None, size='md', secure=False, width='fill', borderless=False, mono=False, disabled=False, readonly=False, invalid=False, max_length=None, align='start', theme=None, ) ``` Un campo de texto. El foco, la pérdida de foco y las teclas vienen de los eventos comunes: `.on(focus=..., blur=..., key_down=...)`. | Parámetro | Descripción | | --- | --- | | `value` | Texto actual. | | `on_input` | Callable que recibe el texto nuevo en cada pulsación. | | `placeholder` | Texto de ayuda cuando está vacío. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `on_submit` | Mensaje o callable que se envía al pulsar Enter. | | `on_clear` | Muestra una ✕ mientras hay texto; se envía al pulsarla. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `size` | `"sm"`, `"md"` o `"lg"`. | | `secure` | Oculta el texto. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `borderless` | Sin borde ni fondo (como en la paleta de comandos). | | `mono` | Usa la fuente monoespaciada (IBM Plex Mono). | | `disabled` | Lo atenúa e ignora lo que se escribe. | | `readonly` | Se puede seleccionar y copiar, pero no cambiar. | | `invalid` | Pinta el borde con el color de error. | | `max_length` | Ignora los cambios más largos que esto. | | `align` | Alineación del texto: `"start"`, `"center"` o `"end"`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `search_input` ```python search_input(value, on_input=None, *, placeholder='Search…', **kwargs) ``` Un `input` con icono de lupa. | Parámetro | Descripción | | --- | --- | | `value` | Texto actual. | | `on_input` | Callable que recibe el texto nuevo en cada pulsación. | | `placeholder` | Texto de ayuda cuando está vacío. | | `kwargs` | Los de `input`. | ##### `path_input` ```python path_input( value, on_change=None, *, folder=False, save=False, filters=None, kind=None, directory=None, relative_to=None, title=None, placeholder=None, size='md', mono=True, disabled=False, invalid=False, theme=None, ) ``` Un campo de texto para la ruta de un archivo o carpeta con un botón que abre el diálogo de archivos del sistema, para no tener que escribirla (aunque se puede). | Parámetro | Descripción | | --- | --- | | `value` | Ruta actual. | | `on_change` | Función que recibe la ruta escrita o elegida. | | `folder` | Elige una carpeta en vez de un archivo. | | `save` | Muestra el diálogo "Guardar como". | | `filters` | Pares `(nombre, extensiones)`: `[("Imágenes", ["png", "jpg"])]`. | | `kind` | Filtros comunes por nombre: `"image"`, `"audio"`, `"video"`, `"font"`, `"shader"` o `"data"`. | | `directory` | Carpeta donde se abre el diálogo (por defecto, la de la ruta actual). | | `relative_to` | Una ruta elegida dentro de esta carpeta se da relativa a ella. | | `title` | Título del diálogo. | | `placeholder` | Texto que se ve mientras está vacío. | | `size` | `"sm"`, `"md"` o `"lg"`. | | `mono` | Texto monoespaciado. | | `disabled` | Lo atenúa. | | `invalid` | Pinta el borde con el color de error. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ```python ui.path_input(self.logo, self.set_logo, kind="image", relative_to=".") ``` ##### `switch` ```python switch( checked, on_toggle=None, *, label=None, size='md', disabled=False, id=None, theme=None, ) ``` Un interruptor encendido/apagado. | Parámetro | Descripción | | --- | --- | | `checked` | Estado actual. | | `on_toggle` | Callable que recibe el nuevo `bool` (o mensaje que se envía). | | `label` | Texto de la etiqueta. | | `size` | `"sm"`, `"md"` o `"lg"`. | | `disabled` | Lo atenúa e ignora los clics. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `checkbox` ```python checkbox( checked, on_toggle=None, *, label=None, size='md', disabled=False, id=None, theme=None, ) ``` Una casilla de verificación. `checked=None` muestra el estado mixto (parte de un grupo marcada); al pulsarla se marca. | Parámetro | Descripción | | --- | --- | | `checked` | Estado actual. | | `on_toggle` | Callable que recibe el nuevo `bool` (o mensaje que se envía). | | `label` | Texto de la etiqueta. | | `size` | `"sm"`, `"md"` o `"lg"`. | | `disabled` | La atenúa e ignora los clics. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `slider` ```python slider( value, on_change=None, *, min=0.0, max=1.0, step=0.01, on_release=None, vertical=False, width=None, height=None, disabled=False, show_value=False, id=None, theme=None, ) ``` Un deslizador. | Parámetro | Descripción | | --- | --- | | `value` | Valor actual. | | `on_change` | Callable que recibe el nuevo valor. | | `min` | Mínimo. | | `max` | Máximo. | | `step` | Paso. | | `on_release` | Se envía al terminar de arrastrar (guarda o aplica ahí). | | `vertical` | Lo pone de pie. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `disabled` | Lo atenúa. | | `show_value` | Muestra el número al lado: `True`, o un formato como `"{:.0%}"`. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `progress` ```python progress( value, *, max=1.0, tint='accent', height=4, width='fill', label=None, show_value=False, theme=None, ) ``` Una barra de progreso fina. | Parámetro | Descripción | | --- | --- | | `value` | Progreso, o `None` para una barra indeterminada (mantén una suscripción `pz.every(1/30, ...)` mientras se ve). | | `max` | Valor máximo. | | `tint` | Color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` o un token. | | `height` | Grosor. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `label` | Texto sobre la barra. | | `show_value` | Añade el porcentaje a su derecha. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `segmented` ```python segmented( options, selected, on_select=None, *, size='md', width=None, disabled=False, id=None, theme=None, ) ``` Un grupo de botones mutuamente excluyentes. | Parámetro | Descripción | | --- | --- | | `options` | Textos, `(valor, etiqueta)` o `(valor, etiqueta, icono)`. | | `selected` | El valor activo. | | `on_select` | Callable que recibe el elemento elegido. | | `size` | `"sm"`, `"md"` o `"lg"`. | | `width` | `"fill"` reparte el ancho entre los botones. | | `disabled` | `True`, o los valores a desactivar. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `field` ```python field( title, control, *, description=None, error=None, required=False, vertical=False, theme=None, ) ``` Una fila de formulario: título y descripción a la izquierda, control a la derecha (como una pantalla de ajustes). | Parámetro | Descripción | | --- | --- | | `title` | Título. | | `control` | El control (switch, input…). | | `description` | Texto de ayuda. | | `error` | Un mensaje de validación que se muestra bajo el título con el color de error (ver `pz.validation_errors`). | | `required` | Añade un `*` rojo tras el título. | | `vertical` | Pone el control bajo el título. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | #### Diálogos ##### `dialog_layer` ```python dialog_layer(base, dialog=None, *, on_dismiss=None, theme=None) ``` Muestra un diálogo centrado sobre `base` con un fondo oscurecido. Con `dialog=None` devuelve `base` sin cambios, así que puede envolver siempre la vista. Ver [Diálogos](https://albakore.github.io/pyzed/es/guides/i18n/#diálogos). | Parámetro | Descripción | | --- | --- | | `base` | La interfaz de debajo. | | `dialog` | Un diálogo de las funciones de abajo, o `ui.modal`. | | `on_dismiss` | Se envía al hacer clic en el fondo. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ```python ui.dialog_layer(self.vista_principal(), ui.confirm("¿Eliminar 3 archivos?", self.eliminar, self.cancelar, danger=True) if self.preguntando else None, on_dismiss=self.cancelar) ``` ##### `alert` ```python alert(message, on_close, *, title=None, kind=None, ok_label=None, width=400, theme=None) ``` Un mensaje con un botón. | Parámetro | Descripción | | --- | --- | | `message` | Texto o widget. | | `on_close` | El botón y el icono de cerrar. | | `title` | Título. | | `kind` | `"info"`, `"success"`, `"warning"` o `"error"` añade un icono. | | `ok_label` | Texto del botón (por defecto: Aceptar, traducido). | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `confirm` ```python confirm( message, on_confirm, on_cancel, *, title=None, confirm_label=None, cancel_label=None, danger=False, kind=None, width=420, theme=None, ) ``` Hace una pregunta de sí o no. | Parámetro | Descripción | | --- | --- | | `message` | Texto o widget. | | `on_confirm` | El botón de confirmar. | | `on_cancel` | Cancelar, el icono de cerrar. | | `title` | Título. | | `confirm_label` | Texto del botón de confirmar. | | `cancel_label` | Texto del botón de cancelar. | | `danger` | Pinta el botón de rojo y lo llama Eliminar. | | `kind` | Añade un icono, como en `alert`. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `prompt` ```python prompt( title, value, on_input, on_submit, on_cancel, *, message=None, placeholder='', submit_label=None, cancel_label=None, id=None, width=440, theme=None, ) ``` Pide una línea de texto. | Parámetro | Descripción | | --- | --- | | `title` | Título. | | `value` | Texto actual. | | `on_input` | Recibe cada edición. | | `on_submit` | Enter o el botón de aceptar. | | `on_cancel` | Cancelar, el icono de cerrar. | | `message` | Texto sobre el campo. | | `placeholder` | Texto de ayuda cuando está vacío. | | `submit_label` | Texto del botón de aceptar. | | `cancel_label` | Texto del botón de cancelar. | | `id` | Id del campo (para `Command.focus`). | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `choice` ```python choice(message, options, on_cancel, *, title=None, width=440, theme=None) ``` Un mensaje con tus propios botones. | Parámetro | Descripción | | --- | --- | | `message` | Cualquier valor de Python, o un callable. | | `options` | Pares `(texto, on_press)`; el último es el botón principal. | | `on_cancel` | El icono de cerrar. | | `title` | Título. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `unsaved_changes` ```python unsaved_changes(name, on_save, on_discard, on_cancel, *, width=440, theme=None) ``` «¿Guardar los cambios de *nombre* antes de cerrar?» con No guardar, Cancelar y Guardar. | Parámetro | Descripción | | --- | --- | | `name` | Nombre del archivo o documento. | | `on_save` | Guardar. | | `on_discard` | No guardar. | | `on_cancel` | Cancelar. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | #### Controles ##### `select` ```python select( options, selected=None, on_select=None, *, placeholder='Select…', width=None, size='md', disabled=False, invalid=False, on_open=None, on_close=None, id=None, theme=None, ) ``` Un desplegable con el estilo del tema. Las opciones se muestran con `str()`, así que sirven textos, enums, dataclasses o modelos de base de datos; `on_select` recibe el objeto elegido, no su texto. | Parámetro | Descripción | | --- | --- | | `options` | Lista de opciones. | | `selected` | Opción actual. | | `on_select` | Callable que recibe el elemento elegido. | | `placeholder` | Texto de ayuda cuando está vacío. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `size` | `"sm"`, `"md"` o `"lg"`. | | `disabled` | Lo atenúa. | | `invalid` | Pinta el borde con el color de error. | | `on_open` | Se envía al abrir el menú. | | `on_close` | Se envía al cerrar el menú. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `radio_group` ```python radio_group( options, selected, on_select=None, *, horizontal=False, spacing=8, disabled=False, descriptions=None, id=None, theme=None, ) ``` Opciones mutuamente excluyentes como botones de radio. | Parámetro | Descripción | | --- | --- | | `options` | Textos o pares `(valor, etiqueta)`. | | `selected` | Valor actual. | | `on_select` | Callable que recibe el valor elegido. | | `horizontal` | En fila en lugar de columna. | | `spacing` | Separación entre hijos, en píxeles. | | `disabled` | `True`, o los valores a desactivar. | | `descriptions` | Asocia valores a una línea atenuada bajo su etiqueta. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `number_field` ```python number_field( value, on_change=None, *, min=None, max=None, step=1, width=120, suffix=None, prefix=None, align='center', decimals=None, disabled=False, invalid=False, id=None, on_submit=None, steppers=True, drag=True, drag_step=None, theme=None, ) ``` Un campo numérico con botones −/+ que también se puede arrastrar, como en Figma. `on_change(valor)` recibe el número ya limitado a `min`/`max`; lo que no se puede leer como número se ignora. Mantén pulsado y mueve el ratón de lado a lado sobre el número para cambiarlo (sigue aunque salgas de la ventana); haz doble clic para escribir un valor, y Enter, Escape o un clic fuera para terminar. Tras pulsarlo, ↑/↓ lo mueven un paso (con Shift, diez) e Inicio/Fin saltan a `min`/`max`. | Parámetro | Descripción | | --- | --- | | `value` | Valor actual. | | `on_change` | Callable que recibe el nuevo valor. | | `min` | Mínimo. | | `max` | Máximo. | | `step` | Paso de los botones. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `suffix` | Unidad que se muestra al final (`"px"`, `"%"`…). | | `prefix` | Texto antes del número (`"$"`…). | | `align` | Dónde va el número entre los botones: `"center"` (por defecto), `"start"` o `"end"`. | | `decimals` | Decimales que muestra (por defecto, los de `step`). | | `disabled` | Lo atenúa. | | `invalid` | Pinta el borde con el color de error. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `on_submit` | Se envía con Enter. | | `steppers` | Muestra los botones −/+ (`False`: un campo compacto, como en una rejilla de propiedades). | | `drag` | Deja cambiarlo arrastrando el ratón (`False`: un campo de texto normal). | | `drag_step` | Cuánto suma cada píxel arrastrado (por defecto `step`, hasta 1; los pasos menores de 1 piden tres píxeles cada uno). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `split_button` ```python split_button( content, on_press=None, on_menu=None, *, icon=None, variant='filled', tint=None, size='md', disabled=False, tooltip=None, theme=None, ) ``` Un botón con una flecha aparte para más opciones. `on_press` ejecuta la acción principal y `on_menu` normalmente abre un `ui.menu` en un `ui.popover`. | Parámetro | Descripción | | --- | --- | | `content` | Texto del botón. | | `on_press` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | | `on_menu` | Mensaje o callable del botón del menú. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `variant` | Como en `ui.button`. | | `tint` | Como en `ui.button`. | | `size` | `"xs"`, `"sm"`, `"md"` o `"lg"`. | | `disabled` | Desactiva las dos mitades. | | `tooltip` | Tooltip de la mitad principal. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `toggle_group` ```python toggle_group( options, selected, on_select=None, *, disabled=False, multiple=False, theme=None, ) ``` Un control segmentado solo con iconos. | Parámetro | Descripción | | --- | --- | | `options` | `(valor, nombre_de_icono)` o `(valor, nombre_de_icono, tooltip)`. | | `selected` | Valor actual (una colección con `multiple`). | | `on_select` | Callable que recibe el elemento elegido. | | `disabled` | `True`, o los valores a desactivar. | | `multiple` | Cada botón activa o desactiva su valor. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `chip` ```python chip( content, *, on_press=None, on_remove=None, selected=False, icon=None, tint='accent', disabled=False, theme=None, ) ``` Una etiqueta compacta, opcionalmente pulsable (filtros) y con botón para quitarla. | Parámetro | Descripción | | --- | --- | | `content` | Texto. | | `on_press` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | | `on_remove` | Muestra una ✕ que envía esto al pulsarla. | | `selected` | Lo marca como seleccionado. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `tint` | Color cuando está seleccionada (un token del tema). | | `disabled` | La atenúa e ignora las pulsaciones. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `disclosure` ```python disclosure(title, content, *, open, on_toggle=None, end=None, theme=None) ``` Una sección plegable: cabecera con flecha que muestra u oculta `content`. El estado abierto es tuyo: cámbialo en `on_toggle`. | Parámetro | Descripción | | --- | --- | | `title` | Título (texto o widget). | | `content` | Contenido. | | `open` | Si está abierta. | | `on_toggle` | Mensaje o callable al pulsar la cabecera. | | `end` | Widgets al final (derecha). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `AccordionSection` ```python class AccordionSection ``` Una sección de `ui.accordion`: `key`, `title`, `content` y, opcionales, `icon` y un widget `end` en la cabecera. | Parámetro | Descripción | | --- | --- | | `key` | Clave que identifica la suscripción (por defecto, el mensaje o la función). | | `title` | Título. | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `end` | Widgets al final (derecha). | ##### `accordion` ```python accordion( sections, *, open=(), on_toggle=None, icons=None, spacing=8, padding=(6, 12, 10, 12), theme=None, ) ``` Secciones plegables apiladas y separadas por divisores, como los paneles de una pantalla de ajustes. Cada cabecera ocupa todo el ancho, así que el contenido queda alineado con el resto del panel. El estado abierto es tuyo: cámbialo en `on_toggle`. | Parámetro | Descripción | | --- | --- | | `sections` | Elementos `ui.AccordionSection(key, title, content, icon=None, end=None)` (o tuplas `(key, title, content)`). | | `open` | El conjunto de ids abiertos, un dict `{id: bool}` o `True` para todos. | | `on_toggle` | Se llama con la clave de la cabecera pulsada. | | `icons` | `{id: icono}`: un icono de Lucide para cada cabecera. | | `spacing` | Espacio alrededor de los divisores entre secciones. | | `padding` | Relleno alrededor del contenido de una sección abierta. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ```python ui.accordion([ ui.AccordionSection("layout", "Layout", layout_rows, icon="ruler"), ui.AccordionSection("events", "Events", event_rows, icon="zap"), ], open=self.open_sections, on_toggle=lambda key: self.open_sections.symmetric_difference_update({key})) ``` #### Consola, errores y ayuda ##### `Log` ```python class Log ``` Las entradas de un `ui.console`: seguro entre hilos, limitado a `limit` y con las líneas repetidas agrupadas. `write(level, text)` añade una línea; `capture()` (o `with log:`) copia la salida de `print` y los tracebacks; `pipe(stream)` lee la salida de un subproceso en segundo plano; `text()` devuelve todo para copiarlo (`text(ids)` solo esas líneas). `write` también acepta el `source` de la línea, su `detail` (un traceback) y su `origin` (`(ruta, línea)`) y devuelve la entrada; `find(id)` busca una. | Parámetro | Descripción | | --- | --- | | `limit` | Máximo de entradas guardadas. | | `on_write` | Se llama tras cada escritura (desde cualquier hilo). | ```python log = ui.Log() with log: print("hola") # sigue llegando a la terminal ui.console(log.entries, on_clear=log.clear) ``` **Métodos y propiedades** ###### `Log.write` ```python Log.write(level, text, *, source='', detail='', origin=None) ``` Añade una línea con nivel `level` (de `source`, con `detail` y `origin`), llama a `on_write` y devuelve la entrada. ###### `Log.find` ```python Log.find(entry_id) ``` La entrada con número `entry_id`, o `None` si ya no está. ###### `Log.clear` ```python Log.clear() ``` Quita todas las entradas. ###### `Log.counts` ```python Log.counts() ``` Devuelve cuántas líneas hay por nivel, contando las repeticiones plegadas. ###### `Log.text` ```python Log.text(ids=None) ``` Todo (o las entradas con número en `ids`) como texto plano, para copiar. ###### `Log.capturing` ```python Log.capturing # property ``` Si `sys.stdout`/`sys.stderr` se están copiando al log. ###### `Log.capture` ```python Log.capture() ``` Copia `sys.stdout`/`sys.stderr` al log (siguen llegando a la terminal). ###### `Log.release` ```python Log.release() ``` Deja de capturar y restaura los streams originales. ###### `Log.pipe` ```python Log.pipe(stream, level='stdout', on_end=None) ``` Lee una tubería de un subproceso (bytes o texto) línea a línea hacia el log, en un hilo en segundo plano. ##### `LogEntry` ```python @dataclass class LogEntry(level, text, time=time.time(), count=1, source='', detail='', origin=None, id=0) ``` Una línea de un `ui.Log`: `level`, `text`, `time`, `count` (líneas idénticas seguidas), `source`, `detail`, `origin`, su número `id`, `clock` (`HH:MM:SS`) y `line()` (la entrada como texto). | Parámetro | Descripción | | --- | --- | | `level` | Nivel. | | `text` | La línea. | | `time` | Cuándo se escribió (`time.time()`). | | `count` | Líneas idénticas seguidas. | | `source` | De dónde viene (`"project"`, `"agent"`...); `ui.console` puede mostrarlo y filtrarlo. | | `detail` | Más texto que se ve bajo la línea mientras está seleccionada (un traceback). | | `origin` | `(ruta, línea)` del código al que se refiere. | | `id` | Su número en el log (lo que guarda un `ui.LogSelection`). | **Métodos y propiedades** ###### `LogEntry.clock` ```python LogEntry.clock # property ``` La hora local de la entrada como `HH:MM:SS`. ###### `LogEntry.line` ```python LogEntry.line(*, details=True) ``` La entrada como una línea de texto (hora, nivel y origen, y `detail` debajo). ##### `LogSelection` ```python class LogSelection ``` Las líneas seleccionadas de un `ui.console`, como en una lista de archivos: un clic selecciona una línea, Shift+clic un rango desde la última pulsada y Ctrl+clic (Cmd en macOS) añade o quita una. Guarda una en tu app y pásala a `ui.console(selection=...)`; `select(ids)` selecciona justo esas (Ctrl+A), `clear()` ninguna, `keep(entries)` olvida las líneas que ya no están e `ids` son los `id` de las seleccionadas para `log.text(ids)`. ```python self.selection = ui.LogSelection() ... ui.console(self.log.entries, selection=self.selection, on_context=self.abrir_menu) # Ctrl+C en on_key: return pz.Command.copy(self.log.text(self.selection.ids)) ``` **Métodos y propiedades** ###### `LogSelection.click` ```python LogSelection.click(entry_id, shown, *, shift=False, ctrl=False) ``` Selecciona tras un clic en la línea `entry_id`; `shown` son los ids de las líneas en pantalla, en orden. ###### `LogSelection.select` ```python LogSelection.select(ids) ``` Selecciona exactamente `ids` (todas las líneas visibles, para Ctrl+A). ###### `LogSelection.clear` ```python LogSelection.clear() ``` No selecciona nada. ###### `LogSelection.keep` ```python LogSelection.keep(entries) ``` Olvida las líneas seleccionadas que ya no están en `entries` (limpiadas o descartadas). ##### `LOG_LEVELS` ```python LOG_LEVELS(...) ``` Los niveles que colorea `ui.console`, en el orden de sus filtros: debug, info, event, stdout, stderr, warning, error. ##### `console` ```python console( entries, *, query='', on_query=None, hidden=(), on_toggle_level=None, on_clear=None, on_copy=None, actions=(), selection=None, on_context=None, on_open=None, sources=(), source=None, on_source=None, show_source=False, placeholder=None, empty=None, font_size=12.5, id=None, theme=None, ) ``` Una vista de registro como la consola de las herramientas de desarrollo del navegador: filtro, un botón por nivel con su cuenta, copiar y limpiar, y las entradas en monoespaciada, siempre desplazada a la más nueva. | Parámetro | Descripción | | --- | --- | | `entries` | Elementos `ui.LogEntry` (`log.entries`). | | `query` | Texto del filtro. | | `on_query` | Recibe cada edición del filtro. | | `hidden` | Niveles ocultos. | | `on_toggle_level` | Se llama con el nivel del botón pulsado. | | `on_clear` | Añade el botón Limpiar. | | `on_copy` | Añade el botón Copiar. | | `actions` | Widgets extra al final de la barra. | | `selection` | Un `ui.LogSelection`: las líneas se pueden seleccionar con el ratón y una línea seleccionada con `detail` lo muestra debajo. | | `on_context` | Se llama con la línea del clic derecho (`None` bajo las líneas): abre un menú ahí (copiar, ir al origen, limpiar...). | | `on_open` | Se llama con la línea del doble clic. | | `sources` | Pares `(valor, etiqueta)`: añade un desplegable que muestra las líneas de un `source`. | | `source` | El origen que se muestra (`None` para todos). | | `on_source` | Recibe el origen elegido en el desplegable (`None` para todos). | | `show_source` | Etiqueta cada línea con su origen. | | `placeholder` | Texto del filtro vacío. | | `empty` | Lo que se ve mientras no hay líneas. | | `font_size` | Tamaño de las entradas. | | `id` | Id del scrollable. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `ErrorInfo` ```python @dataclass class ErrorInfo(kind, message, traceback='', origin=None, where='', time=0.0) ``` Un error tal como lo muestran `ui.error_card` y `ui.error_dialog`: `kind` (el tipo de la excepción), `message`, el `traceback` completo, el `origin` `(ruta, línea)` del que viene (el último frame fuera de la librería estándar, los paquetes instalados y Pyzed), `where` pasó y `time`. `ErrorInfo.from_exception(err, where=...)` crea uno (`prefer=` elige qué frames pueden ser el origen); `ErrorInfo.current()` describe la excepción que se está manejando. `summary` es `Tipo: mensaje`, `location` es `archivo.py:12` y `report()` es todo como texto para un informe de error. | Parámetro | Descripción | | --- | --- | | `kind` | El nombre del tipo de la excepción. | | `message` | Su mensaje. | | `traceback` | El traceback completo, como lo imprime Python. | | `origin` | `(ruta, línea)` del código del que viene, o `None`. | | `where` | Qué se estaba ejecutando. | | `time` | Cuándo pasó (`time.time()`). | ```python try: guardar(datos) except Exception as err: self.error = ui.ErrorInfo.from_exception(err, where="Guardar") ``` **Métodos y propiedades** ###### `ErrorInfo.from_exception` ```python @classmethod ErrorInfo.from_exception(err, *, where='', prefer=None) ``` Describe `err`. `prefer(ruta)` elige los frames que pueden ser su origen (gana el último); por defecto, los que están fuera de la librería estándar, los paquetes instalados y Pyzed. ###### `ErrorInfo.current` ```python @classmethod ErrorInfo.current(*, where='') ``` La excepción que se está manejando (dentro de un `except`), o `None`. ###### `ErrorInfo.summary` ```python ErrorInfo.summary # property ``` `Tipo: mensaje` (solo el tipo si el mensaje está vacío). ###### `ErrorInfo.location` ```python ErrorInfo.location # property ``` `archivo.py:12` del origen, o `""`. ###### `ErrorInfo.report` ```python ErrorInfo.report() ``` Todo como texto plano, para pegarlo en un informe de error. ##### `error_boundary` ```python error_boundary( build, *, on_error=None, title=None, where='', on_details=None, fallback=None, theme=None, ) ``` Dibuja `build()`, o una tarjeta de error si falla, para que una parte de la vista que falla nunca tumbe la ventana entera (como los error boundaries de React). `on_error(info)` se entera del error cada vez que la parte se dibuja y falla, así que regístralo una sola vez (compara `info.traceback`). | Parámetro | Descripción | | --- | --- | | `build` | Una función que devuelve el widget de la parte. | | `on_error` | Recibe el `ui.ErrorInfo`. | | `title` | Título de la tarjeta (y el `where` del error). | | `where` | Qué es la parte, para el informe. | | `on_details` | Añade el botón Detalles; recibe el `ui.ErrorInfo` (abre un `ui.error_dialog`). | | `fallback` | Tu propio widget para el error: `fallback(info)`. | | `theme` | Tema a usar. | ```python lateral = ui.error_boundary(lambda: self.lateral(), on_error=self.registrar, title="Lateral", on_details=self.ver_error) ``` ##### `error_card` ```python error_card(info, *, title=None, on_details=None, actions=(), theme=None) ``` La tarjeta que dibuja `ui.error_boundary` en lugar de lo que falló: el error, de dónde viene y el botón Copiar el error (y Detalles con `on_details`). | Parámetro | Descripción | | --- | --- | | `info` | Un `ui.ErrorInfo`. | | `title` | Título. | | `on_details` | Añade el botón Detalles. | | `actions` | Más botones tras ellos. | | `theme` | Tema a usar. | ##### `error_dialog` ```python error_dialog( info, on_close, *, title=None, on_go_to=None, go_to_label=None, on_logs=None, more=0, width=680, theme=None, ) ``` Un diálogo con un error: su mensaje, dónde pasó, el archivo y la línea de los que viene y el traceback en un editor de solo lectura (selecciona y copia cualquier parte), con botones para copiarlo todo, ir al origen, verlo en los logs y cerrar. Muéstralo con `ui.dialog_layer` o `ui.layer`. | Parámetro | Descripción | | --- | --- | | `info` | Un `ui.ErrorInfo`. | | `on_close` | Botón Cerrar y Escape. | | `title` | Título ("Algo salió mal"). | | `on_go_to` | Añade el botón de ir al origen (abre el archivo en la línea). | | `go_to_label` | Su texto ("Ir al origen"). | | `on_logs` | Añade el botón Ver en Logs. | | `more` | Cuántos errores más llegaron después (una línea lo dice). | | `width` | Ancho en píxeles o un tamaño de modal. | | `theme` | Tema a usar. | ```python dialogo = ui.error_dialog(self.error, self.cerrar_error, on_go_to=self.abrir_origen) if self.error else None return ui.dialog_layer(self.vista_principal(), dialogo, on_dismiss=self.cerrar_error) ``` ##### `shortcuts` ```python shortcuts(groups, *, query='', on_query=None, height=380, theme=None) ``` Una lista de atajos de teclado agrupada y con búsqueda (para un diálogo de atajos). | Parámetro | Descripción | | --- | --- | | `groups` | `[(título, [(etiqueta, teclas), ...]), ...]`, teclas como `"ctrl-s"`. | | `query` | Texto del filtro. | | `on_query` | Añade un buscador. | | `height` | Alto de la lista. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `about` ```python about( name, version='', description='', *, icon='app-window', details=(), links=(), theme=None, ) ``` Un cuadro Acerca de: icono, nombre, versión, una descripción, algunos detalles y botones de enlace. | Parámetro | Descripción | | --- | --- | | `name` | Nombre de la app. | | `version` | Versión. | | `description` | Una o dos frases. | | `icon` | Icono de Lucide. | | `details` | Filas `(etiqueta, valor)`. | | `links` | Botones `(etiqueta, on_press)`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `reference_browser` ```python reference_browser( browser, *, on_open_url=None, sidebar_width=270, width='fill', height='fill', theme=None, ) ``` La referencia integrada, como la de Godot: una lista de clases con búsqueda junto a la página abierta. `browser` es un `pyzed.reference.Browser` (guárdalo en el estado de tu app); al hacer clic en entradas, enlaces y en los botones atrás/adelante cambia. `on_open_url(url)` recibe los enlaces que salen de la referencia (la web, GitHub...): ```python self.help = reference.Browser("pyzed.ui.button") ... ui.reference_browser(self.help, on_open_url=abrir_en_navegador) ``` #### Datos y feedback ##### `callout` ```python callout( title, description=None, *, tint='info', icon=None, actions=(), on_dismiss=None, theme=None, ) ``` Un aviso en línea (un callout o banner). | Parámetro | Descripción | | --- | --- | | `title` | Título (texto o widget). | | `description` | Texto secundario. | | `tint` | `"info"`, `"success"`, `"warning"`, `"error"` o `"hint"`: elige el color y el icono. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `actions` | Widgets extra (normalmente botones) que se muestran como acciones. | | `on_dismiss` | Añade un botón de cerrar. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `empty_state` ```python empty_state( title, description=None, *, icon='inbox', actions=(), size='md', height='fill', theme=None, ) ``` Un marcador centrado para listas vacías, búsquedas sin resultados, etc. | Parámetro | Descripción | | --- | --- | | `title` | Título. | | `description` | Texto secundario. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `actions` | Widgets extra (normalmente botones) que se muestran como acciones. | | `size` | `"sm"`, `"md"` o `"lg"`. | | `height` | `"fill"` (por defecto), píxeles o `"shrink"`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `settings_group` ```python settings_group(title, rows, *, description=None, end=None, theme=None) ``` Un bloque con título de filas `ui.field` separadas por líneas, como una pantalla de ajustes. | Parámetro | Descripción | | --- | --- | | `title` | Título del grupo. | | `rows` | Las filas (normalmente `ui.field`). | | `description` | Texto secundario. | | `end` | Widget a la derecha del título (un botón Restablecer…). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `Column` ```python @dataclass class Column(title, key, width='fill', align='start', render=None, sortable=True) ``` Una columna de `ui.table`. `key` es una clave de dict, un nombre de atributo o una función de la fila; `render` (opcional) convierte el valor en un widget. `width` es `"fill"`, un número o una porción como `"fill:2"`. | Parámetro | Descripción | | --- | --- | | `title` | Encabezado (y nombre para ordenar). | | `key` | Cómo obtener el valor de cada fila. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `align` | `"start"`, `"center"` o `"end"`. | | `render` | Función valor → widget. | | `sortable` | Si el encabezado se puede pulsar para ordenar. | **Métodos y propiedades** ###### `Column.value` ```python Column.value(row) ``` Devuelve el valor de esta columna para `row`: `key(row)`, un elemento de dict o un atributo. ##### `table` ```python table( columns, rows, *, sort_by=None, descending=False, on_sort=None, on_row_press=None, selected=None, striped=False, row_height=30, height=None, on_row_double_press=None, on_row_context=None, empty=None, theme=None, ) ``` Una tabla de datos con encabezado fijo, orden opcional y selección de filas. El orden se hace aquí cuando `sort_by` nombra una columna: pulsar un encabezado llama a `on_sort(título)` y tú cambias `sort_by`/`descending` en tu estado. Las filas están virtualizadas: solo se construyen las visibles, así que cientos de miles de filas se desplazan con fluidez. | Parámetro | Descripción | | --- | --- | | `columns` | `ui.Column` o claves simples. | | `rows` | Dicts u objetos. | | `sort_by` | Título de la columna por la que se ordena. | | `descending` | Orden descendente. | | `on_sort` | Callable que recibe el título pulsado. | | `on_row_press` | Callable que recibe la fila pulsada. | | `selected` | La fila a resaltar, o una lista o conjunto de filas (selección múltiple). | | `striped` | Filas alternas sombreadas. | | `row_height` | Alto de cada fila. | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `on_row_double_press` | Recibe la fila con un doble clic (abrir, editar…). | | `on_row_context` | Recibe la fila con un clic derecho (un menú contextual). | | `empty` | Texto o widget que se muestra en lugar de las filas cuando no hay ninguna. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `stat` ```python stat( title, value, *, delta=None, trend=None, width=None, icon=None, inverse=False, on_press=None, theme=None, ) ``` Un número de panel: título, valor grande, variación opcional (`+4.2%`) y sparkline. | Parámetro | Descripción | | --- | --- | | `title` | Título. | | `value` | Valor actual. | | `delta` | Variación en %; verde si es positiva, roja si no. | | `trend` | Valores para la sparkline. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `icon` | Icono antes del título. | | `inverse` | Pinta de verde una bajada y de rojo una subida (errores, latencia…). | | `on_press` | Hace pulsable la tarjeta. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `sparkline` ```python sparkline(values, *, width=120, height=32, tint='accent', fill=True, theme=None) ``` Un gráfico de línea diminuto para tendencias. | Parámetro | Descripción | | --- | --- | | `values` | Valores. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `tint` | Color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` o un token. | | `fill` | Rellena el área bajo la línea. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `bar_chart` ```python bar_chart(values, *, labels=None, width=320, height=160, tint='accent', theme=None) ``` Un gráfico de barras vertical con etiquetas opcionales. | Parámetro | Descripción | | --- | --- | | `values` | Valores. | | `labels` | Etiquetas bajo cada barra. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `tint` | Color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` o un token. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `donut` ```python donut(values, *, colors=None, size=96, thickness=14, label=None, theme=None) ``` Un gráfico de anillo; `label` se dibuja en el centro. | Parámetro | Descripción | | --- | --- | | `values` | Valores. | | `colors` | Colores (por defecto, los del tema). | | `size` | Tamaño. | | `thickness` | Grosor del anillo. | | `label` | Texto central. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `spinner` ```python spinner(*, size=16, tint='accent', speed=1.0, theme=None) ``` Un arco que gira para estados de carga. Su ángulo sale del reloj, así que mantén una suscripción `pz.every(1/30, ...)` activa mientras se muestra. | Parámetro | Descripción | | --- | --- | | `size` | Tamaño. | | `tint` | Color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` o un token. | | `speed` | Vueltas por segundo. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `count_badge` ```python count_badge(count, *, tint='accent', max=99, show_zero=True, theme=None) ``` Un contador redondo (notificaciones, problemas…). Muestra `99+` por encima de `max`. | Parámetro | Descripción | | --- | --- | | `count` | El número. | | `tint` | Color semántico: `"accent"`, `"error"`, `"warning"`, `"success"`, `"info"` o un token. | | `max` | Máximo antes de mostrar `N+`. | | `show_zero` | Con `False`, un 0 no dibuja nada. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `facepile` ```python facepile(people, *, size=22, overlap=6, max_shown=5, theme=None) ``` Avatares superpuestos de colaboradores, como en la barra de título de un editor colaborativo. Los que pasan de `max_shown` se agrupan en `+N`. | Parámetro | Descripción | | --- | --- | | `people` | Iniciales o nombres. | | `size` | Tamaño. | | `overlap` | Solapamiento en píxeles. | | `max_shown` | Cuántos mostrar. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `diff_stat` ```python diff_stat(added, removed, *, blocks=5, theme=None) ``` `+12 −3` con bloques de colores al estilo de GitHub. | Parámetro | Descripción | | --- | --- | | `added` | Líneas añadidas. | | `removed` | Líneas eliminadas. | | `blocks` | Número de bloques. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `keybinding_hint` ```python keybinding_hint(text, keys, *, theme=None) ``` Una pista atenuada como «Abrir archivo Ctrl P» para pies y estados vacíos. | Parámetro | Descripción | | --- | --- | | `text` | Texto. | | `keys` | Atajo que se muestra al final, escrito como texto: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `markdown` ```python markdown(source, *, size=None, on_link=None, width=None, selectable=None, theme=None) ``` `pz.markdown` con los colores de texto, enlaces y código del tema. | Parámetro | Descripción | | --- | --- | | `source` | El texto Markdown. | | `size` | Tamaño. | | `on_link` | Callable que recibe la URL al pulsar un enlace. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `selectable` | Igual que en `pz.markdown`; la selección usa el color `selection` del tema. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `split` ```python split( first, second, *, ratio=0.5, on_resize=None, vertical=False, min_size=80, theme=None, **kwargs, ) ``` `pz.split` con el divisor del color de borde del tema (acento al arrastrarlo). | Parámetro | Descripción | | --- | --- | | `first` | Primer panel. | | `second` | Segundo panel. | | `ratio` | Proporción del primero (0–1). | | `on_resize` | Callable que recibe la nueva proporción. | | `vertical` | Orientación vertical. | | `min_size` | Tamaño mínimo de cada panel. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | | `kwargs` | Los de `pz.split`. | #### Listas, árbol y menús ##### `list_item` ```python list_item( content, on_press=None, *, icon=None, icon_color=None, start=None, end=None, selected=False, focused=False, indent=0, height=26, color=None, inset=False, disabled=False, on_right_press=None, on_double_press=None, description=None, tooltip=None, theme=None, ) ``` Una fila de lista: icono, texto y un hueco al final. | Parámetro | Descripción | | --- | --- | | `content` | Texto o widget. | | `on_press` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `icon_color` | Color del icono: un token del tema o un color hex. | | `start` | Widgets al inicio (izquierda). | | `end` | Widgets al final (derecha). | | `selected` | Lo marca como seleccionado. | | `focused` | Dibuja un borde de foco. | | `indent` | Sangría a la izquierda, en píxeles. | | `height` | Alto de la fila. | | `color` | Color del texto. | | `inset` | Con margen y esquinas redondeadas (menús, paletas). | | `disabled` | Lo deshabilita (ignora los clics). | | `on_right_press` | Clic derecho. | | `on_double_press` | Se envía con un doble clic (abrir, renombrar…). | | `description` | Una segunda línea atenuada (la fila crece a 40 px salvo que fijes `height`). | | `tooltip` | Texto al pasar el ratón. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `list_header` ```python list_header(title, *, end=None, theme=None) ``` Un encabezado de sección para listas. | Parámetro | Descripción | | --- | --- | | `title` | Título. | | `end` | Widgets al final (derecha). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `TreeNode` ```python @dataclass class TreeNode(name, children=None, id=None, status=None, icon=None, icon_color=None) ``` Un archivo o carpeta para `ui.tree`. Las carpetas tienen `children` (lista). `status` es `"modified"`, `"created"`, `"deleted"`, `"conflict"` o `"ignored"`. `TreeNode.from_paths(paths, status=None)` construye el árbol a partir de rutas como `"src/main.rs"`, con las carpetas primero y el estado de git propagado a las carpetas. | Parámetro | Descripción | | --- | --- | | `name` | Nombre visible. | | `children` | Hijos (`None` si es un archivo). | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `status` | Estado de git. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `icon_color` | Color del icono: un token del tema o un color hex. | **Métodos y propiedades** ###### `TreeNode.is_folder` ```python TreeNode.is_folder # property ``` Si el nodo es una carpeta (tiene una lista `children`, aunque esté vacía). ###### `TreeNode.from_paths` ```python @classmethod TreeNode.from_paths(paths, status=None) ``` Crea un árbol a partir de rutas del estilo `"src/main.rs"`. Las carpetas van primero. ###### `TreeNode.sort` ```python TreeNode.sort() ``` Ordena los hijos recursivamente: primero las carpetas, luego por nombre sin distinguir mayúsculas. ##### `tree` ```python tree( nodes, *, expanded, selected=None, on_toggle=None, on_select=None, on_open=None, on_context=None, focused=None, indent=14, item_height=24, theme=None, ) ``` Un árbol de archivos plegable, como el panel de proyecto de un editor de código. | Parámetro | Descripción | | --- | --- | | `nodes` | Lista de `TreeNode`. | | `expanded` | Conjunto de ids de carpetas abiertas. | | `selected` | Un id o una colección de ids (selección múltiple). | | `on_toggle` | Se llama con el id de la carpeta pulsada. | | `on_select` | Se llama con el id del archivo pulsado. | | `on_open` | Recibe el id del archivo al que se hace doble clic. | | `on_context` | Recibe el id de la fila en la que se hace clic derecho (muestra un `ui.menu` ahí). | | `focused` | Resalta el borde de la fila en la que está el teclado. | | `indent` | Sangría a la izquierda, en píxeles. | | `item_height` | Alto de cada fila. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `MenuItem` ```python @dataclass class MenuItem(label, on_press=None, icon=None, keys=None, disabled=False, danger=False, checked=None, description=None, submenu=False) ``` Una entrada de `ui.menu`. | Parámetro | Descripción | | --- | --- | | `label` | Texto de la etiqueta. | | `on_press` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `keys` | Atajo que se muestra al final, escrito como texto: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `disabled` | Lo deshabilita (ignora los clics). | | `danger` | En rojo (acciones destructivas). | | `checked` | Muestra una marca (`True`/`False`) o nada (`None`). | | `description` | Una segunda línea atenuada. | | `submenu` | Termina la fila con una flecha; abre el siguiente menú al lado en `on_press`. | ##### `menu` ```python menu(items, *, width=240, max_height=None, theme=None) ``` Un menú contextual flotante. Usa `ui.SEPARATOR` entre grupos; un texto suelto es un encabezado. | Parámetro | Descripción | | --- | --- | | `items` | Lista de `MenuItem`, `ui.SEPARATOR` o textos. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `max_height` | Desplaza un menú más alto que esto. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `SEPARATOR` Separador para `ui.menu`. ##### `virtual_list` ```python virtual_list( items, render, *, item_height=26, spacing=0, width='fill', height='fill', padding=None, overscan=4, id=None, on_scroll=None, theme=None, ) ``` Una lista con scroll que solo construye las filas visibles, para miles o millones de elementos. `render(elemento)` devuelve el widget de un elemento y se llama cuando entra en la vista. Todas las filas tienen la misma altura. | Parámetro | Descripción | | --- | --- | | `items` | Los elementos: una lista o cualquier secuencia (otros iterables se convierten en lista). | | `render` | Función que recibe un elemento y devuelve su widget. | | `item_height` | Altura de cada fila, en píxeles. | | `spacing` | Separación entre hijos, en píxeles. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `padding` | Relleno interior: `8`, `(vertical, horizontal)` o `(arriba, derecha, abajo, izquierda)`. | | `overscan` | Filas extra construidas por encima y por debajo de las visibles. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `on_scroll` | Callable que recibe el viewport del scroll, como en `pz.scrollable`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | #### Navegación ##### `Tab` ```python @dataclass class Tab(id, label, icon=None, icon_color=None, dirty=False, preview=False, status=None, pinned=False, tooltip=None) ``` Una pestaña. `Tab.for_file(path, **kwargs)` la crea con el nombre y el icono del archivo. | Parámetro | Descripción | | --- | --- | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `label` | Texto de la etiqueta. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `icon_color` | Color del icono: un token del tema o un color hex. | | `dirty` | Cambios sin guardar (muestra un punto). | | `preview` | Pestaña de vista previa (en cursiva). | | `status` | Estado de git (tiñe el nombre). | | `pinned` | Muestra solo el icono, no tiene botón de cerrar y va primera en `tab_bar`. | | `tooltip` | Texto al pasar el ratón (la ruta completa…). | **Métodos y propiedades** ###### `Tab.for_file` ```python @classmethod Tab.for_file(path, **kwargs) ``` Una pestaña para el archivo `path`: su nombre como etiqueta y el icono y color de archivo correspondientes. ##### `tab` ```python tab(item, *, active, on_select=None, on_close=None, height=32, dimmed=False, theme=None) ``` Una sola pestaña. Normalmente se usa `tab_bar`. | Parámetro | Descripción | | --- | --- | | `item` | El `Tab`. | | `active` | Si es la activa. | | `on_select` | Callable que recibe el elemento elegido. | | `on_close` | Mensaje o callable que se envía al pulsar la X. | | `height` | Alto. | | `dimmed` | Apaga la etiqueta (la pestaña que se arrastra). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `tab_bar` ```python tab_bar( tabs, active, *, on_select=None, on_close=None, start=(), end=(), height=32, on_drag=None, on_drop=None, on_drag_over=None, dragging=None, drop_index=None, on_context=None, on_new=None, theme=None, ) ``` Una fila de pestañas con controles opcionales al principio y al final (navegación, dividir, +). El botón de cerrar aparece en la activa y al pasar el ratón. Las pestañas se pueden arrastrar para reordenarlas o sacarlas de la ventana: ver [Pestañas arrastrables](https://albakore.github.io/pyzed/es/guides/windows/#pestañas-arrastrables-y-ventanas-desprendibles). | Parámetro | Descripción | | --- | --- | | `tabs` | Lista de `Tab`. | | `active` | Id de la pestaña activa. | | `on_select` | Callable que recibe el elemento elegido. | | `on_close` | Mensaje o callable que se envía al pulsar la X. | | `start` | Widgets al inicio (izquierda). | | `end` | Widgets al final (derecha). | | `height` | Alto. | | `on_drag` | Se llama con el id de la pestaña cuando empieza a arrastrarse. | | `on_drop` | Se llama con el id de la pestaña y el `Event` al soltar la pestaña arrastrada: `event.window_x`/`window_y` (fuera de la ventana si se soltó fuera) y `event.screen`. | | `on_drag_over` | Mientras `dragging` tiene valor: se llama con el índice de inserción bajo el puntero, o `None` cuando sale de la barra. | | `dragging` | Id de la pestaña que se arrastra (la apaga y activa `on_drag_over`). | | `drop_index` | Dibuja la marca de inserción antes de la pestaña de este índice (`len(tabs)`: después de la última). | | `on_context` | Recibe el id de la pestaña en la que se hace clic derecho. | | `on_new` | Se envía con un doble clic en el espacio vacío tras las pestañas. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `breadcrumbs` ```python breadcrumbs( parts, *, separator='›', on_press=None, max_items=None, on_more=None, theme=None, ) ``` `src › main.rs › fn main`. Las partes pueden ser textos o tuplas `(icono, texto)`. | Parámetro | Descripción | | --- | --- | | `parts` | Las partes. | | `separator` | Separador. | | `on_press` | Se llama con el índice de la parte pulsada. | | `max_items` | Conserva la primera parte y las últimas y muestra `…` en lugar del resto. | | `on_more` | Hace pulsable el `…`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `toolbar` ```python toolbar(start=(), end=(), *, center=None, height=32, theme=None) ``` La barra bajo las pestañas: breadcrumbs a la izquierda y acciones del buffer a la derecha. | Parámetro | Descripción | | --- | --- | | `start` | Widgets al inicio (izquierda). | | `end` | Widgets al final (derecha). | | `center` | Se mantiene en el centro de la barra sean cuales sean los anchos de los lados. | | `height` | Alto. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | #### Workspace ##### `workspace` ```python workspace( center, *, title_bar=None, left=None, right=None, bottom=None, status_bar=None, left_width=240, right_width=320, bottom_height=220, on_resize=None, theme=None, ) ``` El layout de una ventana estilo editor: barra de título, docks izquierdo, derecho e inferior, centro y barra de estado. Pasa `None` para ocultar un dock. | Parámetro | Descripción | | --- | --- | | `center` | Contenido central (normalmente pestañas + editor). | | `title_bar` | Barra de título (`ui.title_bar`). | | `left` | Dock izquierdo. | | `right` | Dock derecho. | | `bottom` | Dock inferior. | | `status_bar` | Barra de estado (`ui.status_bar`). | | `left_width` | Ancho del dock izquierdo. | | `right_width` | Ancho del dock derecho. | | `bottom_height` | Alto del dock inferior. | | `on_resize` | Se llama con `(side, size)` cuando el usuario termina de cambiar el tamaño de un panel arrastrando su borde interior. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `title_bar` ```python title_bar(start=(), end=(), *, center=None, height=34, theme=None) ``` La barra superior: selectores de proyecto y rama a la izquierda, cuenta y acciones a la derecha. | Parámetro | Descripción | | --- | --- | | `start` | Widgets al inicio (izquierda). | | `end` | Widgets al final (derecha). | | `center` | Widget centrado. | | `height` | Alto. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `title_button` ```python title_button( content, on_press=None, *, icon=None, muted=False, chevron=False, theme=None, ) ``` Un botón compacto para la barra de título (`pyzed ▾`, `main`…). | Parámetro | Descripción | | --- | --- | | `content` | Texto. | | `on_press` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `muted` | Texto atenuado. | | `chevron` | Añade un ▾. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `status_bar` ```python status_bar(start=(), end=(), *, height=28, theme=None) ``` La barra de estado inferior. | Parámetro | Descripción | | --- | --- | | `start` | Widgets al inicio (izquierda). | | `end` | Widgets al final (derecha). | | `height` | Alto. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `status_item` ```python status_item( content=None, on_press=None, *, icon=None, color=None, icon_color=None, selected=False, tooltip=None, keys=None, theme=None, ) ``` Un icono y/o texto en la barra de estado (diagnósticos, posición del cursor, lenguaje…). | Parámetro | Descripción | | --- | --- | | `content` | Texto. | | `on_press` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `color` | Color del texto o del icono. | | `icon_color` | Color del icono: un token del tema o un color hex. | | `selected` | Lo marca como seleccionado. | | `tooltip` | Texto del tooltip. | | `keys` | Atajo que se muestra al final, escrito como texto: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `panel` ```python panel( content, *, header=None, width=None, height=None, side='left', resizable=True, on_resize=None, min_size=120, theme=None, ) ``` Un panel acoplable. `side` decide en qué borde va la línea; con un `width` numérico (`height` para el inferior) ese borde se arrastra para cambiar su tamaño. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `header` | Cabecera (`ui.panel_header`). | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `side` | `"left"`, `"right"` o `"bottom"`. | | `resizable` | `False` mantiene el tamaño fijo. | | `on_resize` | Se llama con el nuevo tamaño al terminar de arrastrar. | | `min_size` | Tamaño mínimo al redimensionar. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `panel_header` ```python panel_header(title, actions=(), *, icon=None, height=32, theme=None) ``` La cabecera de un panel: icono, título y acciones. | Parámetro | Descripción | | --- | --- | | `title` | Título. | | `actions` | Widgets extra (normalmente botones) que se muestran como acciones. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `height` | Alto. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | #### Marco de ventana ##### `window_frame` ```python window_frame( content, *, title=None, menus=None, menu_button=True, start=(), center=None, end=(), style=None, window=None, height=None, resize_border=6.0, resize_edges=True, bg=None, theme=None, ) ``` El marco entero de una ventana abierta con `frame="custom"`: una barra de título que mueve la ventana (doble clic maximiza, clic derecho abre el menú del sistema en Windows), los botones de minimizar, maximizar y cerrar con el estilo de una plataforma, menús a la izquierda y bordes que cambian el tamaño de la ventana. | Parámetro | Descripción | | --- | --- | | `content` | El contenido de la ventana, bajo la barra de título. | | `title` | Título de la barra. Por defecto el `title` de la ventana. | | `menus` | Menús a la izquierda de la barra: una lista de `ui.Menu`. | | `menu_button` | Muestra los menús tras un botón ☰ (`False` muestra siempre sus nombres). | | `start` | Elementos después de los menús (proyecto, rama…). | | `center` | Tu propio widget en el centro en lugar del título (un buscador, Run y Stop…); la barra sigue arrastrándose a ambos lados. | | `end` | Elementos antes de los botones de ventana. | | `style` | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"` o `"kde"`. Por defecto el `frame` de la ventana, o el del sistema. | | `window` | La ventana sobre la que actúan los botones. Por defecto aquella cuyo `view()` se está ejecutando. | | `height` | Alto de la barra. Por defecto el de la plataforma. | | `resize_border` | Ancho en píxeles de los bordes que cambian el tamaño de la ventana. | | `resize_edges` | `False` deja los bordes de redimensionar a tu cargo (`pz.window_edges`), para ponerlos encima de menús y diálogos. | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ```python class Notes(pz.App): frame = "custom" def view(self): return ui.window_frame(self.editor(), menus=[ui.Menu("Archivo", [ui.MenuItem("Abrir", self.open)])], end=[ui.icon_button("settings", self.settings)]) ``` ##### `window_title_bar` ```python window_title_bar( title=None, *, style=None, start=(), center=None, end=(), on_drag=None, on_minimize=None, on_maximize=None, on_close=None, on_menu=None, maximized=False, focused=True, width='fill', height=None, bg=None, border=True, theme=None, ) ``` La barra de título de una ventana, sola: botones de ventana con el estilo de `style` (a la izquierda en macOS), elementos `start`, el título y elementos `end`. Sin callbacks solo se muestra, que es como Pyzed Design previsualiza un formulario con el marco de cada plataforma. | Parámetro | Descripción | | --- | --- | | `title` | Título (centrado, o a la izquierda en Windows). | | `style` | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"` o `"kde"` (`None`: el del sistema). | | `start` | Elementos al principio. | | `center` | Widget en el centro en lugar del título; la barra sigue arrastrándose a ambos lados. | | `end` | Elementos al final. | | `on_drag` | Se llama al arrastrar la parte vacía de la barra (devuelve `Command.drag_window()`). | | `on_minimize` | Botón de minimizar. | | `on_maximize` | Botón de maximizar y doble clic en la barra. | | `on_close` | Botón de cerrar. | | `on_menu` | Clic derecho en la barra. | | `maximized` | Muestra el símbolo de restaurar. | | `focused` | `False` la atenúa como una ventana en segundo plano. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `height` | Alto. Por defecto el de la plataforma. | | `bg` | Color de fondo (`"#rrggbb[aa]"`, `(r, g, b[, a])` o `"transparent"`). | | `border` | Línea bajo la barra. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `window_controls` ```python window_controls( style=None, *, on_minimize=None, on_maximize=None, on_close=None, maximized=False, focused=True, height=None, theme=None, ) ``` Los botones de minimizar, maximizar y cerrar con el estilo de una plataforma. Un botón sin callback se dibuja pero no hace nada. | Parámetro | Descripción | | --- | --- | | `style` | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"` o `"kde"` (`None`: el del sistema). | | `on_minimize` | Botón de minimizar. | | `on_maximize` | Botón de maximizar. | | `on_close` | Botón de cerrar. | | `maximized` | Muestra el símbolo de restaurar. | | `focused` | `False` los atenúa. | | `height` | Alto. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `Menu` ```python @dataclass class Menu(label, items=[]) ``` Un menú de la barra de menús de un marco de ventana: un nombre y sus elementos (`ui.MenuItem`, `ui.SEPARATOR` o un texto de cabecera). | Parámetro | Descripción | | --- | --- | | `label` | El nombre del menú. | | `items` | Sus elementos: `ui.MenuItem`, `ui.SEPARATOR` o un texto de cabecera. | ```python ui.Menu("Archivo", [ui.MenuItem("Abrir…", self.open, keys="ctrl-o"), ui.SEPARATOR, ui.MenuItem("Salir", self.close)]) ``` ##### `system_platform` ```python system_platform() ``` El estilo de los botones de ventana de este sistema: `"macos"`, `"windows"`, `"kde"`, `"ubuntu"` o `"gnome"` (otros escritorios Linux). En WSL es `"windows"`. #### Overlays ##### `layer` ```python layer( base, overlay=None, *, on_dismiss=None, backdrop=None, align_x='center', align_y='start', offset=72, ) ``` Apila `overlay` sobre `base`. Hacer clic fuera envía `on_dismiss`. Con `overlay=None` devuelve `base` tal cual. | Parámetro | Descripción | | --- | --- | | `base` | La interfaz de debajo. | | `overlay` | Lo que flota encima (modal, paleta, menú). | | `on_dismiss` | Mensaje o callable que se envía al cerrar. | | `backdrop` | Color del fondo que oscurece la interfaz. | | `align_x` | Alineación horizontal: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | | `align_y` | Alineación vertical: `"start"`/`"top"`, `"center"`, `"end"`/`"bottom"`. | | `offset` | Distancia desde arriba cuando `align_y` es `"start"`. | ##### `modal` ```python modal( content, *, title=None, on_close=None, width=480, footer=(), description=None, icon=None, max_height=None, theme=None, ) ``` Un diálogo. Úsalo como `overlay` de `ui.layer`. | Parámetro | Descripción | | --- | --- | | `content` | Contenido: un `Widget`, un texto o un número (se convierte en texto). | | `title` | Título. | | `on_close` | El botón de cerrar; también lo envía Escape mientras el diálogo tiene el foco. | | `width` | Píxeles o `"sm"` (360), `"md"` (480), `"lg"` (640) o `"xl"` (820). | | `footer` | Botones del pie, alineados a la derecha. | | `description` | Línea atenuada bajo el título. | | `icon` | Icono antes del título. | | `max_height` | Desplaza el contenido más alto que esto, sin ocultar la cabecera ni el pie. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `PaletteItem` ```python @dataclass class PaletteItem(label, on_press=None, keys=None, icon=None, detail=None) ``` Una entrada de `command_palette` o `picker`. | Parámetro | Descripción | | --- | --- | | `label` | Texto de la etiqueta. | | `on_press` | Mensaje o callable que se envía al pulsar. `None` deshabilita el control. | | `keys` | Atajo que se muestra al final, escrito como texto: `"ctrl-s"`, `"cmd-k cmd-s"`. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `detail` | Texto secundario (por ejemplo, la ruta de un archivo). | ##### `command_palette` ```python command_palette( query, on_query, commands, *, selected=0, on_submit=None, placeholder='Execute a command…', input_id='command-palette', theme=None, **kwargs, ) ``` Una paleta de comandos: filtra `commands` por `query` con búsqueda difusa y muestra sus atajos. | Parámetro | Descripción | | --- | --- | | `query` | Texto buscado. | | `on_query` | Callable que recibe el texto nuevo. | | `commands` | Lista de `PaletteItem`. | | `selected` | Índice resaltado (para navegar con flechas). | | `on_submit` | Mensaje o callable que se envía al pulsar Enter. | | `placeholder` | Texto de ayuda cuando está vacío. | | `input_id` | Id del campo (para `Command.focus`). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | | `kwargs` | Los de `picker`. | ##### `picker` ```python picker( query, on_query, items, *, selected=0, placeholder='Search…', input_id='picker', on_submit=None, width=560, max_height=340, empty_text='No matches', theme=None, ) ``` Una lista con buscador en una superficie flotante. Es la base de la paleta y del buscador de archivos. | Parámetro | Descripción | | --- | --- | | `query` | Texto buscado. | | `on_query` | Callable que recibe el texto nuevo. | | `items` | Pares `(PaletteItem, posiciones)`, por ejemplo de `fuzzy_filter`. | | `selected` | Índice resaltado. | | `placeholder` | Texto de ayuda cuando está vacío. | | `input_id` | Id del campo. | | `on_submit` | Mensaje o callable que se envía al pulsar Enter. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `max_height` | Alto máximo en píxeles. | | `empty_text` | Texto cuando no hay resultados. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `fuzzy_match` ```python fuzzy_match(query, candidate) ``` Devuelve `(puntuación, posiciones)` si todas las letras de `query` aparecen en orden en `candidate`; si no, `None`. Premia los inicios de palabra y las letras consecutivas. | Parámetro | Descripción | | --- | --- | | `query` | Texto buscado. | | `candidate` | Texto candidato. | ##### `fuzzy_filter` ```python fuzzy_filter(query, items, key=) ``` Filtra y ordena `items` por puntuación difusa. Devuelve `[(item, posiciones), ...]`. | Parámetro | Descripción | | --- | --- | | `query` | Texto buscado. | | `items` | Elementos. | | `key` | Función que da el texto de cada elemento. | ##### `highlighted` ```python highlighted(text, positions, *, color, match_color, size=13.5, weight=None) ``` Texto con los caracteres de `positions` resaltados (coincidencias de la búsqueda difusa). | Parámetro | Descripción | | --- | --- | | `text` | Texto. | | `positions` | Índices a resaltar. | | `color` | Color del texto o del icono. | | `match_color` | Color de las coincidencias. | | `size` | Tamaño. | | `weight` | Peso del texto. | ##### `toast` ```python toast( title, message=None, *, kind='info', icon=None, on_dismiss=None, actions=(), width=340, progress=None, on_press=None, theme=None, ) ``` Una notificación. | Parámetro | Descripción | | --- | --- | | `title` | Título. | | `message` | Texto secundario. | | `kind` | `"info"`, `"success"`, `"warning"` o `"error"`. | | `icon` | Nombre de un icono de Lucide (`pyzed icons` los lista). | | `on_dismiss` | Mensaje o callable que se envía al cerrar. | | `actions` | Widgets extra (normalmente botones) que se muestran como acciones. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `progress` | De 0 a 1: una barra fina abajo (tiempo restante, una descarga). | | `on_press` | Hace pulsable la tarjeta. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `toasts` ```python toasts(base, items, *, spacing=8) ``` Muestra `items` (normalmente `toast(...)`) apilados en la esquina inferior derecha sobre `base`. | Parámetro | Descripción | | --- | --- | | `base` | La interfaz de debajo. | | `items` | Lista de toasts. | | `spacing` | Separación entre hijos, en píxeles. | ##### `popover` ```python popover(base, content=None, *, x=0, y=0, on_dismiss=None, align_x='start') ``` Muestra `content` (un `ui.menu`, una tarjeta…) sobre `base` en `(x, y)`. Hacer clic en cualquier otro sitio llama a `on_dismiss`. Para un menú de clic derecho, guarda la posición del ratón con `mouse_area(on_move=...)` y abre el popover desde `on_right_press` (ver `examples/context_menu.py`). | Parámetro | Descripción | | --- | --- | | `base` | La interfaz de debajo. | | `content` | Lo que flota (o `None`). | | `x` | Posición horizontal. | | `y` | Posición vertical. | | `on_dismiss` | Mensaje o callable que se envía al cerrar. | | `align_x` | Alineación horizontal: `"start"`/`"left"`, `"center"`, `"end"`/`"right"`. | #### Código ##### `editor` ```python editor( id, text='', *, language=None, path=None, on_change=None, on_cursor=None, markers=None, font_size=None, line_height=None, gutter=True, wrap=False, active_line=True, height='fill', placeholder=None, annotations=None, show_whitespace=False, tab_size=None, hard_tabs=False, backspace_unindent=True, repeat_delay=None, repeat_rate=None, capture_keys=(), on_scroll=None, auto_close=True, folding=True, minimap=False, cursor_shape='bar', cursor_blink=True, cursor_width=2.0, cursor_color=None, on_zoom=None, on_action=None, highlights=None, match_brackets=True, selection_highlight=True, font=None, keymap=None, scrollbar=True, readonly=False, on_hover=None, hover_delay=500, sticky_scroll=0, theme=None, ) ``` Un editor de código: resaltado con los colores del tema, números de línea y marcadores de git. | Parámetro | Descripción | | --- | --- | | `id` | Identifica el buffer (usa la ruta del archivo). | | `text` | Texto inicial. | | `language` | Lenguaje. Si falta, se deduce de `path`. | | `path` | Ruta del archivo. | | `on_change` | Callable que recibe el nuevo valor. | | `on_cursor` | Callable que recibe `(línea, columna)`. | | `markers` | `{línea: "created" | "modified" | "deleted"}`. | | `font_size` | Tamaño de la fuente (por defecto, el del tema). | | `line_height` | Alto de línea. | | `gutter` | Muestra los números de línea. | | `wrap` | Ajuste de línea. | | `active_line` | Color de fondo de la línea del cursor. | | `height` | Alto: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `placeholder` | Texto de ayuda cuando está vacío. | | `annotations` | `{línea: "texto"}` o `{línea: ("texto", color)}`: texto virtual al final de la línea (code lens, diagnósticos). | | `show_whitespace` | Dibuja los espacios como `·` y los tabuladores como `→`. | | `tab_size` | Tab inserta espacios hasta el siguiente múltiplo; Enter mantiene la sangría (un nivel más tras `:`); Backspace quita un nivel. | | `hard_tabs` | Con `tab_size`: Tab, Enter y sangrar (Ctrl+]) usan tabulaciones en vez de espacios. | | `backspace_unindent` | Con `tab_size`: Backspace en los espacios iniciales quita un nivel entero (`False`: un espacio). | | `repeat_delay` | Milisegundos que esperan Backspace y Suprimir, mantenidos, antes de repetirse (`None`: los del sistema). | | `repeat_rate` | Borrados por segundo como máximo mientras se mantiene Backspace o Suprimir (`None`: el ritmo del sistema). | | `capture_keys` | Teclas que el editor deja al `on_key` de la app (`"up"`, `"enter"`…), por ejemplo con una lista de autocompletado abierta. | | `on_scroll` | Se llama al desplazar. | | `auto_close` | Cierra paréntesis, corchetes, llaves y comillas al escribir (y envuelve la selección). | | `folding` | Flechas de plegado en el margen; Ctrl+Shift+[ pliega y Ctrl+Shift+] despliega. | | `minimap` | `True` (o un ancho en píxeles) muestra un minimapa a la derecha. | | `cursor_shape` | `"bar"`, `"block"`, `"underline"` o `"hollow"`. | | `cursor_blink` | Si el cursor parpadea. | | `cursor_width` | Ancho del cursor de barra. | | `cursor_color` | Color del cursor (por defecto, el color de jugador del tema). | | `on_zoom` | Callable que recibe `1`, `-1` o `0` con Ctrl+= / Ctrl+- / Ctrl+0 y Ctrl+rueda. | | `on_action` | Callable que recibe el nombre de cada acción del editor que se ejecuta (`"undo"`, `"select_next"`…), por ejemplo para contarlas. | | `highlights` | Rangos pintados detrás del texto con el color de búsqueda del tema: `[(línea, columna, línea_final, columna_final)]`, desde 1, con el final excluido. | | `match_brackets` | Remarca el corchete pareja del que está junto al cursor (Ctrl+M salta a él). | | `selection_highlight` | Resalta las demás apariciones del texto seleccionado (Ctrl+D añade la siguiente como cursor). | | `font` | Tipografía: `"mono"` por defecto; `"sans"` hace un cuadro de texto de varias líneas, como el cuadro de mensaje de un chat. | | `keymap` | Diccionario `{atajo: acción}` al estilo de Zed (`{"ctrl-shift-d": "duplicate_line_down"}`); `None` como acción quita una tecla por defecto. Créalo a partir de cambios por acción con `pyzed.ui.editor.keymap_for`. | | `scrollbar` | `False` oculta las barras de desplazamiento. | | `readonly` | `True` deja seleccionar y copiar el texto, pero no cambiarlo (un log, un traceback). | | `on_hover` | Función que recibe `{line, column, start, end, x, y, height}` (la palabra bajo el ratón, desde 1, y su lugar en el editor) cuando el ratón se detiene sobre ella, y `None` cuando se va (tarjetas de información). | | `hover_delay` | Milisegundos que el ratón debe quedarse sobre una palabra antes de llamar a `on_hover`. | | `sticky_scroll` | Cuántas líneas de los bloques que rodean al código visible (`class`, `def`, `if`…) quedan fijas arriba al desplazarse (`0`: ninguna). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `language_token` ```python language_token(path) ``` Devuelve el token de lenguaje del resaltador para una ruta (`"main.rs"` → `"rs"`). | Parámetro | Descripción | | --- | --- | | `path` | Ruta del archivo. | ##### `terminal` ```python terminal(lines, *, font_size=13, id=None, theme=None) ``` Una vista de terminal de solo lectura con colores ANSI. Se mantiene desplazada al final. | Parámetro | Descripción | | --- | --- | | `lines` | Texto con escapes ANSI o lista de líneas de segmentos `(texto, color)`. | | `font_size` | Tamaño de la fuente. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ##### `ansi` ```python ansi(text) ``` Convierte texto con colores ANSI en líneas de segmentos `(texto, nombre_de_color)`. | Parámetro | Descripción | | --- | --- | | `text` | Texto con escapes ANSI. | #### Selector de color ##### `ColorPicker` ```python class ColorPicker ``` Un componente para elegir un color con alfa, un gradiente lineal (paradas que se añaden, arrastran y quitan, más un ángulo) o un relleno con máscara de opacidad. `on_change` recibe un texto `"#rrggbb[aa]"`, un `pz.Gradient` o un `pz.Paint`, todos válidos como `background`. Devuelve el valor elegido en `value`. Ver [Estilos](https://albakore.github.io/pyzed/es/theming/styles/#selector-de-color). | Parámetro | Descripción | | --- | --- | | `value` | Un color, token del tema, `pz.Gradient` o `pz.Paint`. | | `alpha` | Muestra la barra y el campo de alfa. | | `gradients` | Muestra el cambio Sólido/Gradiente. | | `masks` | Muestra el editor de máscara. | | `swatches` | Colores rápidos (por defecto: el acento y los colores de estado del tema). | | `on_change` | Recibe `event.value`. | ```python ui.ColorPicker(value=self.fill, on_change=lambda e: setattr(self, "fill", e.value)) ``` **Métodos y propiedades** ###### `ColorPicker.parts` ```python ColorPicker.parts() ``` `(stops, angle, mask, opacity)`: una sola parada y sin ángulo para un color liso. ###### `ColorPicker.build` ```python ColorPicker.build(stops, angle, mask, opacity) ``` Convierte `(stops, angle, mask, opacity)` de nuevo en un color, un `Gradient` o un `Paint`. ###### `ColorPicker.publish` ```python ColorPicker.publish(*parts) ``` Fija el valor a partir de las partes, descarta el texto hex a medio escribir y emite `change`. ###### `ColorPicker.current` ```python ColorPicker.current() ``` Devuelve el color hex que se está editando (el de la parada seleccionada). ###### `ColorPicker.set_color` ```python ColorPicker.set_color(color) ``` Cambia el color de la parada seleccionada por `color` y publica el resultado. ###### `ColorPicker.hsva` ```python ColorPicker.hsva() ``` Devuelve el color editado como `(h, s, v, a)`, cada uno de 0 a 1. ###### `ColorPicker.pick_sv` ```python ColorPicker.pick_sv(event) ``` Fija la saturación y el valor al pulsar o arrastrar en el área de saturación/valor. ###### `ColorPicker.pick_hue` ```python ColorPicker.pick_hue(event) ``` Fija el tono al pulsar o arrastrar en la barra de tono. ###### `ColorPicker.pick_alpha` ```python ColorPicker.pick_alpha(event) ``` Fija el alfa al pulsar o arrastrar en la barra de alfa. ###### `ColorPicker.type_hex` ```python ColorPicker.type_hex(text) ``` Gestiona lo que se escribe en el campo hex: aplica el texto en cuanto es un color hex válido. ###### `ColorPicker.set_alpha_percent` ```python ColorPicker.set_alpha_percent(value) ``` Fija el alfa desde el campo de porcentaje (0 a 100). ###### `ColorPicker.set_mode` ```python ColorPicker.set_mode(mode) ``` Cambia entre `"Solid"` y `"Gradient"`. ###### `ColorPicker.set_angle` ```python ColorPicker.set_angle(value) ``` Fija el ángulo del gradiente en grados (o el de la máscara, sobre un color liso). ###### `ColorPicker.select_stop` ```python ColorPicker.select_stop(index) ``` Selecciona para editar la parada del gradiente en `index`. ###### `ColorPicker.move_stop` ```python ColorPicker.move_stop(index, event) ``` Mueve una parada del gradiente adonde se pulsa la barra o según el `dx` de un arrastre. ###### `ColorPicker.add_stop` ```python ColorPicker.add_stop(event) ``` Añade una parada del gradiente donde se hace clic en la barra, con el color que el gradiente tiene ahí. ###### `ColorPicker.remove_stop` ```python ColorPicker.remove_stop() ``` Quita la parada del gradiente seleccionada (un gradiente conserva al menos dos). ###### `ColorPicker.toggle_mask` ```python ColorPicker.toggle_mask(on) ``` Con `on` añade una máscara que va de opaco a transparente; si no, quita la máscara. ###### `ColorPicker.select_mask_stop` ```python ColorPicker.select_mask_stop(index) ``` Selecciona para editar la parada de la máscara en `index`. ###### `ColorPicker.move_mask_stop` ```python ColorPicker.move_mask_stop(index, event) ``` Mueve una parada de la máscara adonde se pulsa la barra o según el `dx` de un arrastre, manteniendo las paradas ordenadas. ###### `ColorPicker.add_mask_stop` ```python ColorPicker.add_mask_stop(event) ``` Añade una parada de la máscara donde se hace clic en la barra, con la opacidad que la máscara tiene ahí (como mucho 8). ###### `ColorPicker.set_mask_opacity` ```python ColorPicker.set_mask_opacity(value) ``` Fija la opacidad de la parada de la máscara seleccionada (0 a 1). ###### `ColorPicker.remove_mask_stop` ```python ColorPicker.remove_mask_stop() ``` Quita la parada de la máscara seleccionada (una máscara conserva al menos dos). ###### `ColorPicker.marker` ```python ColorPicker.marker(x, y, color, *, size=14, selected=True, **events) ``` Un tirador redondo de color `color` centrado en `(x, y)`. ###### `ColorPicker.area` ```python ColorPicker.area(layers, height, markers, on_pick, *, width=248) ``` Una franja redondeada de capas apiladas que informa de dónde se pulsa y se arrastra. ###### `ColorPicker.sv_area` ```python ColorPicker.sv_area() ``` El cuadrado de saturación/valor del tono actual, con un marcador en el color actual. ###### `ColorPicker.hue_bar` ```python ColorPicker.hue_bar() ``` La barra de tono arcoíris con un marcador en el tono actual. ###### `ColorPicker.alpha_bar` ```python ColorPicker.alpha_bar() ``` La barra de alfa (sobre un damero) con un marcador en el alfa actual. ###### `ColorPicker.stops_bar` ```python ColorPicker.stops_bar(points, preview, selected, on_select, on_move, on_add, *, colors=True) ``` Una barra que previsualiza `preview` con un tirador arrastrable por cada punto `(offset, value)`. ##### `color_swatch` ```python color_swatch(value, *, size=20, on_press=None) ``` Un cuadrito que muestra un color, gradiente o pintura sobre un damero. | Parámetro | Descripción | | --- | --- | | `value` | Qué mostrar. | | `size` | Lado en píxeles. | | `on_press` | Lo hace pulsable. | ##### `checkerboard` ```python checkerboard(width, height, size=6) ``` Un damero claro/oscuro para dibujar bajo colores translúcidos. | Parámetro | Descripción | | --- | --- | | `width` | Ancho. | | `height` | Alto. | | `size` | Tamaño de celda. | #### Fechas y horas ##### `Calendar` ```python class Calendar ``` Un componente con un mes donde elegir un día (`mode="single"`), un rango (`"range"`: el primer clic lo empieza y el segundo lo termina) o varios días (`"multiple"`). El título abre una cuadrícula de meses y después de años. Tras un clic recibe el teclado: las flechas mueven, Enter elige y Re Pág/Av Pág cambian de mes. Ver la página [Calendario](https://albakore.github.io/pyzed/es/library/calendar/). | Parámetro | Descripción | | --- | --- | | `value` | Una fecha (`datetime.date` o `"2026-10-22"`), un par `(inicio, fin)` o una lista de fechas, según `mode`. | | `mode` | `"single"`, `"range"` o `"multiple"`. | | `min` | El primer día que se puede elegir. | | `max` | El último día que se puede elegir. | | `disabled` | Días que no se pueden elegir: fechas, nombres de día en inglés (`"sunday"`) o `"weekends"`. | | `first_weekday` | `"monday"`, `"sunday"` o `"saturday"`. | | `months` | Meses que se ven uno al lado del otro. | | `week_numbers` | Muestra el número de semana ISO de cada fila. | | `show_outside` | Muestra los días de los meses vecinos. | | `today_button` | Añade un enlace Hoy. | | `on_change` | Recibe `event.value` (una fecha, una tupla o una lista). | | `on_month` | Recibe `event.value`, el `(año, mes)` que se muestra. | ```python ui.Calendar(self.day, on_change=lambda e: setattr(self, "day", e.value)) ui.Calendar(self.stay, mode="range", months=2, on_change=lambda e: setattr(self, "stay", e.value)) ``` ##### `DatePicker` ```python class DatePicker ``` Un componente: un campo de fecha que abre un calendario debajo, en una ventana emergente que ningún panel recorta. Se puede escribir en `format` (Enter o salir del campo lo lee); tras un clic, ↑/↓ mueven la fecha un día y Re Pág/Av Pág un mes sin abrir nada, Alt+↓ abre el calendario y Escape lo cierra. Ver la página [Selector de fecha](https://albakore.github.io/pyzed/es/library/date-picker/). | Parámetro | Descripción | | --- | --- | | `value` | La fecha (`datetime.date`, texto ISO o `None`). | | `format` | Cómo se escribe: `"DD/MM/YYYY"`, `"MMM D, YYYY"`... (ver `format_date`). | | `placeholder` | Pista mientras está vacío (por defecto, el formato). | | `min` | La primera fecha. | | `max` | La última fecha. | | `disabled_dates` | Días que no se pueden elegir (como en `Calendar`). | | `first_weekday` | `"monday"`, `"sunday"` o `"saturday"`. | | `week_numbers` | Muestra los números de semana en el calendario. | | `clearable` | Muestra una x que lo vacía. | | `today_button` | Añade un enlace Hoy al calendario. | | `disabled` | Lo atenúa. | | `invalid` | Pinta el borde con el color de error. | | `on_change` | Recibe `event.value`: un `datetime.date` o `None`. | | `on_open` | Se abrió el calendario. | | `on_close` | Se cerró el calendario. | ```python ui.DatePicker(self.due, format="DD/MM/YYYY", on_change=lambda e: setattr(self, "due", e.value)) ``` ##### `DateRangePicker` ```python class DateRangePicker ``` Un componente: un campo para una fecha de inicio y otra de fin, con dos meses y rangos rápidos (Hoy, Últimos 7 días, Este mes...) en su ventana emergente. El texto escrito son dos fechas separadas por `separator`. Ver la página [Selector de rango de fechas](https://albakore.github.io/pyzed/es/library/date-range-picker/). | Parámetro | Descripción | | --- | --- | | `value` | `(inicio, fin)`: fechas, texto ISO o `None`. | | `format` | Cómo se escribe cada fecha. | | `separator` | Texto entre las dos fechas. | | `placeholder` | Pista mientras está vacío. | | `min` | La primera fecha. | | `max` | La última fecha. | | `disabled_dates` | Días que no se pueden elegir. | | `first_weekday` | `"monday"`, `"sunday"` o `"saturday"`. | | `months` | Meses que se ven uno al lado del otro. | | `presets` | Muestra los rangos rápidos. | | `clearable` | Muestra una x que lo vacía. | | `disabled` | Lo atenúa. | | `invalid` | Pinta el borde con el color de error. | | `on_change` | Recibe `event.value`: `(inicio, fin)`. | | `on_open` | Se abrió el calendario. | | `on_close` | Se cerró el calendario. | ```python ui.DateRangePicker(self.period, on_change=lambda e: setattr(self, "period", e.value)) ``` ##### `TimePicker` ```python class TimePicker ``` Un componente: un campo de hora con columnas de horas y minutos (y segundos, y AM/PM con un `format` de 12 horas) en una ventana emergente. Tras un clic, ↑/↓ la cambian `minute_step` minutos. Ver la página [Selector de hora](https://albakore.github.io/pyzed/es/library/time-picker/). | Parámetro | Descripción | | --- | --- | | `value` | La hora (`datetime.time`, `"14:30"` o `None`). | | `format` | `"HH:mm"`, `"HH:mm:ss"`, `"h:mm A"`... | | `placeholder` | Pista mientras está vacío. | | `minute_step` | Minutos entre los que se ofrecen. | | `min` | La primera hora. | | `max` | La última hora. | | `clearable` | Muestra una x que lo vacía. | | `disabled` | Lo atenúa. | | `invalid` | Pinta el borde con el color de error. | | `on_change` | Recibe `event.value`: un `datetime.time` o `None`. | | `on_open` | Se abrió la lista. | | `on_close` | Se cerró la lista. | ```python ui.TimePicker(self.alarm, format="h:mm A", minute_step=15, on_change=lambda e: setattr(self, "alarm", e.value)) ``` ##### `format_date` ```python format_date(value, fmt='YYYY-MM-DD', *, language=None) ``` Escribe una fecha con un patrón de marcas: `YYYY` 2026, `YY` 26, `MMMM` octubre, `MMM` oct, `MM` 10, `M` 10, `DD` 05, `D` 5, `dddd` lunes, `ddd` lun, `HH`/`hh` horas, `mm` minutos, `ss` segundos, `A` AM/PM y `[texto]` tal cual. Un patrón con `%` es de `strftime`. Los nombres de meses y días siguen el idioma de `pyzed.i18n`. | Parámetro | Descripción | | --- | --- | | `value` | Una fecha, un datetime, texto ISO o `None` (da `""`). | | `fmt` | El patrón. | | `language` | Un idioma para los nombres en lugar del actual. | ```python ui.format_date(day, "dddd, D [de] MMMM [de] YYYY") # "lunes, 5 de octubre de 2026" ``` ##### `parse_date` ```python parse_date(text, fmt='YYYY-MM-DD', *, language=None) ``` Lee una fecha escrita con un patrón de `format_date` (meses en el idioma actual o en inglés); el ISO `YYYY-MM-DD` siempre funciona. Devuelve `None` si el texto no es una fecha. | Parámetro | Descripción | | --- | --- | | `text` | El texto. | | `fmt` | El patrón. | | `language` | El idioma de los nombres. | ##### `format_time` ```python format_time(value, fmt='HH:mm') ``` Escribe una hora (o `"14:30"`) con un patrón como `"h:mm A"`. | Parámetro | Descripción | | --- | --- | | `value` | Un `datetime.time` o texto. | | `fmt` | El patrón. | ##### `parse_time` ```python parse_time(text, fmt='HH:mm') ``` Lee una hora: `"14:30"`, `"2:30 PM"`, `"14:30:05"`, primero con `fmt`. `None` si no lo es. | Parámetro | Descripción | | --- | --- | | `text` | El texto. | | `fmt` | El patrón. | #### Sistema ##### `file_input` ```python file_input( value, on_change=None, *, mode='open', filters=None, title=None, directory=None, file_name='', placeholder=None, button=None, editable=True, disabled=False, invalid=False, id=None, width='fill', theme=None, ) ``` Un campo de ruta con un botón que abre el diálogo de archivos del sistema, para que nadie escriba una ruta a mano. Ver la página [Selector de archivos](https://albakore.github.io/pyzed/es/library/file-input/). | Parámetro | Descripción | | --- | --- | | `value` | Valor actual. | | `on_change` | Recibe la ruta (una lista con `"open_many"`). | | `mode` | `"open"` (un archivo), `"open_many"` (una lista), `"save"` o `"folder"`. | | `filters` | Archivos que se muestran: `{"Imágenes": "png jpg"}` o `"Imágenes: png, jpg"`. | | `title` | El título del diálogo. | | `directory` | Dónde se abre el diálogo (por defecto, la carpeta de la ruta actual). | | `file_name` | El nombre que propone al guardar. | | `placeholder` | Texto de ayuda cuando está vacío. | | `button` | Texto para el botón en lugar de un icono de carpeta. | | `editable` | `False` impide escribir la ruta. | | `disabled` | Lo deshabilita (ignora los clics). | | `invalid` | Pinta el borde con el color de error. | | `id` | Identificador estable para enfocarlo o desplazarlo con un `Command`. | | `width` | Ancho: píxeles, `"fill"`, `"shrink"` o `"fill:N"` (proporción). | | `theme` | Tema a usar en lugar del actual (`pyzed.theme.current()`). | ```python ui.file_input(self.photo, self.set_photo, filters={"Imágenes": "png jpg"}) ``` ##### `pick` ```python pick(then, *, mode='open', title=None, filters=None, directory=None, file_name='') ``` El diálogo de archivos del sistema como un `Command`: devuélvelo desde un callback y `then` recibe lo que eligió el usuario (una ruta, una lista de rutas con `"open_many"`, o `None` si cancela). `ui.file_input` lo usa. | Parámetro | Descripción | | --- | --- | | `then` | Se llama con el resultado. | | `mode` | `"open"`, `"open_many"`, `"save"` o `"folder"`. | | `title` | El título del diálogo. | | `filters` | Archivos que se muestran: `{"Imágenes": "png jpg"}` o `"Imágenes: png, jpg"`. | | `directory` | Dónde se abre el diálogo. | | `file_name` | El nombre que propone al guardar. | ```python ui.button("Abrir…", lambda: ui.system.pick(self.cargar, filters="Texto: txt, md")) ``` ##### `DropZone` ```python class DropZone ``` Un componente que recibe archivos arrastrados desde el explorador de archivos del sistema y soltados en la ventana, y abre el diálogo de archivos con un clic. Se ilumina mientras se arrastran archivos sobre la ventana y guarda las rutas soltadas en `files`. El gancho `on_file_drop` de la ventana entrega cada archivo a la zona bajo el puntero, si no a una cuyo `accept` incluya su tipo. Ver la página [Zona para soltar](https://albakore.github.io/pyzed/es/library/drop-zone/). | Parámetro | Descripción | | --- | --- | | `text` | El texto principal (por defecto: Suelta archivos aquí). | | `hint` | La línea de debajo. | | `icon_name` | Su icono. | | `accept` | Extensiones (`["png", "jpg"]`), patrones (`"*.tar.gz"`) o `"folder"`; vacío acepta todo. | | `multiple` | `False` se queda solo con el último archivo. | | `browse` | Un clic abre el diálogo de archivos. | | `show_files` | Lista los archivos bajo el texto (cada uno con una x). | | `files` | Las rutas soltadas. | | `disabled` | No recibe nada. | | `live` | `False` lo dibuja sin recibir archivos (el lienzo del diseñador). | | `on_drop` | Recibe `event.value`: una ruta soltada (una vez por archivo). | | `on_files` | Recibe `event.value`: la lista de archivos. | | `on_reject` | Recibe una ruta que no acepta. | | `on_enter` | Se arrastran archivos sobre la ventana. | | `on_leave` | El arrastre salió sin soltar. | ```python ui.DropZone(accept=["png", "jpg"], on_drop=lambda e: self.photos.append(e.value)) ``` #### Agente ##### `ToolCall` ```python class ToolCall ``` Un componente para una acción de un agente de IA, dibujada como en el panel Agent de Zed: una línea con su icono, lo que hace y dónde (se despliega para mostrar la entrada y la salida), o una tarjeta para las ediciones y los comandos, con el diff o la salida. Mientras `status` es `"waiting"` pregunta en la propia tarjeta: Permitir siempre, Permitir o Rechazar. Mira [Agentes de IA](https://albakore.github.io/pyzed/es/editor/ai-agents/). | Parámetro | Descripción | | --- | --- | | `title` | Lo que hace ("Editar archivo"). | | `detail` | Sobre qué trabaja, en letra monoespaciada. | | `kind` | El tipo de ACP: `read`, `edit`, `delete`, `move`, `search`, `execute`, `think`, `fetch` u `other` (su icono). | | `status` | `pending`, `running`, `waiting`, `done`, `failed`, `rejected` o `canceled`. | | `input` | Los argumentos (un dict o texto). | | `output` | La respuesta (texto o un dict). | | `images` | Bytes PNG que se muestran bajo la salida. | | `diffs` | Los cambios que hizo: dicts `{"path", "old", "new"}`, que se muestran como diffs de colores (mira `ui.diff_view`). | | `progress` | Lo que imprimió hasta ahora (la salida de un comando); sus últimas líneas se ven bajo la cabecera mientras corre. | | `allow_always` | Muestra Permitir siempre. | | `theme` | Tema. | | `on_allow` | Se pulsó Permitir. | | `on_allow_always` | Se pulsó Permitir siempre. | | `on_deny` | Se pulsó Rechazar. | ```python ui.ToolCall(title="Editar archivo", detail="ui/main.py", kind="edit", status=self.status, on_allow=lambda e: self.answer(True), on_deny=lambda e: self.answer(False)) ``` **Métodos y propiedades** ###### `ToolCall.toggle` ```python ToolCall.toggle() ``` Despliega o pliega la entrada y la salida de la herramienta. ###### `ToolCall.card` ```python ToolCall.card # property ``` Si se dibuja como tarjeta: una edición, un comando o una llamada que espera tu permiso. ##### `Plan` ```python class Plan ``` Un componente para el plan de un agente: una lista de pasos con `hechos/total` en la cabecera, que la pliega. | Parámetro | Descripción | | --- | --- | | `steps` | Dicts `{"step": texto, "status": "pending" | "in_progress" | "done"}`. | | `running` | Hace girar el paso en curso. | | `theme` | Tema. | ```python ui.Plan(steps=[{"step": "Añadir el formulario", "status": "done"}, {"step": "Conectarlo", "status": "in_progress"}]) ``` **Métodos y propiedades** ###### `Plan.toggle` ```python Plan.toggle() ``` Despliega o pliega la lista de pasos. ##### `EditsBar` ```python class EditsBar ``` Un componente con los archivos que cambió un agente, como la barra de cambios de Zed: `3 archivos cambiados` con Rechazar todo y Conservar todo, que se despliega en una fila por archivo con sus líneas, Rechazar y Conservar. | Parámetro | Descripción | | --- | --- | | `files` | Dicts u objetos con `path` y, opcionalmente, `added` y `removed` (líneas). | | `theme` | Tema. | | `on_keep` | Conservar todo. | | `on_reject` | Rechazar todo. | | `on_keep_file` | Conservar en un archivo (`event.value` es su ruta). | | `on_reject_file` | Rechazar en un archivo. | | `on_open` | Se hizo clic en un archivo. | ```python ui.EditsBar(files=[{"path": "ui/main.py", "added": 12, "removed": 3}], on_keep=lambda e: self.keep()) ``` **Métodos y propiedades** ###### `EditsBar.toggle` ```python EditsBar.toggle() ``` Despliega o pliega la lista de archivos cambiados. ##### `ThreadHistory` ```python class ThreadHistory ``` Un componente para conversaciones anteriores: un buscador y los hilos agrupados por día (Hoy, Ayer, Esta semana, Este mes, Antes), cada uno con un botón para eliminarlo. | Parámetro | Descripción | | --- | --- | | `threads` | Dicts u objetos con `id`, `title`, `updated` (una marca de tiempo) y un `detail` opcional. | | `selected` | El id del hilo abierto. | | `theme` | Tema. | | `on_open` | Se eligió un hilo (`event.value` es su id). | | `on_delete` | Se pulsó eliminar en un hilo. | ```python ui.ThreadHistory(threads=self.threads, on_open=lambda e: self.open(e.value)) ``` **Métodos y propiedades** ###### `ThreadHistory.set_query` ```python ThreadHistory.set_query(text) ``` Filtra el historial por `text`. ###### `ThreadHistory.visible` ```python ThreadHistory.visible() ``` Las conversaciones cuyo título tiene todas las palabras de la búsqueda. ##### `AuthPrompt` ```python class AuthPrompt ``` Un componente que pide iniciar sesión, como Zed cuando un agente lo necesita: un botón por cada forma de entrar y, mientras una está en curso, qué pasa (el navegador o una terminal está abierto), un código para escribir si lo hay, Abrir la página otra vez y Cancelar. | Parámetro | Descripción | | --- | --- | | `title` | El título. | | `detail` | Una línea debajo. | | `methods` | Dicts con `id`, `name` y, opcionalmente, `description` e `icon`; el primero es el principal. | | `waiting` | El id del método en curso (`""` si no hay). | | `waiting_text` | Lo que se muestra mientras espera. | | `code` | Un código para escribir (inicio de sesión por dispositivo). | | `url` | La página que se abrió. | | `error` | Un error a mostrar. | | `icon_name` | El icono. | | `theme` | Tema. | | `on_pick` | Se eligió un método (`event.value` es su id). | | `on_cancel` | Cancelar. | | `on_open_url` | Abrir la página otra vez. | | `on_copy` | Se copió el código. | ```python ui.AuthPrompt(title="Entrar en Gemini", methods=[{"id": "google", "name": "Entrar con Google"}], on_pick=self.sign_in) ``` ##### `ServiceRow` ```python class ServiceRow ``` Un componente para algo que usa un agente (un servidor MCP, un agente externo, una skill), como las filas de los ajustes de agentes de Zed: un punto de estado, el nombre y el comando, un interruptor, un botón principal y un menú `⋯`. | Parámetro | Descripción | | --- | --- | | `name` | Nombre. | | `detail` | El comando o la URL, en letra monoespaciada. | | `summary` | Una línea sobre él. | | `icon_name` | El icono. | | `status` | `running`, `starting`, `error`, `off`, `installed`, `missing` o `update` (el color del punto). | | `status_text` | El tooltip del punto; se muestra bajo el nombre en errores y actualizaciones. | | `version` | Una versión o un origen. | | `enabled` | El interruptor (`None` lo oculta). | | `busy` | Muestra un indicador de carga. | | `primary` | Texto del botón principal (Actualizar, Instalar…). | | `actions` | Entradas `ui.MenuItem` del menú `⋯`. | | `theme` | Tema. | | `on_toggle` | Se cambió el interruptor. | | `on_primary` | Se pulsó el botón principal. | ```python ui.ServiceRow(name="github", detail="npx -y @modelcontextprotocol/server-github", status="running", enabled=True) ``` **Métodos y propiedades** ###### `ServiceRow.toggle_menu` ```python ServiceRow.toggle_menu() ``` Abre o cierra el menú de acciones de la fila. ##### `Catalog` ```python class Catalog ``` Un componente para cosas que instalar con un buscador (agentes de un registro, servidores, skills…): cada fila muestra Instalar, Instalado o Actualizar a una versión nueva. | Parámetro | Descripción | | --- | --- | | `items` | Dicts u objetos con `id`, `name` y, opcionalmente, `version`, `description`, `installed` y `update`. | | `busy` | El id que se está instalando (un indicador de carga). | | `height` | Alto de la lista. | | `theme` | Tema. | | `on_install` | Se pulsó Instalar o Actualizar (`event.value` es el id). | ```python ui.Catalog(items=self.registry, busy=self.installing, on_install=lambda e: self.install(e.value)) ``` **Métodos y propiedades** ###### `Catalog.set_query` ```python Catalog.set_query(text) ``` Filtra el catálogo por `text`. ###### `Catalog.visible` ```python Catalog.visible() ``` Los elementos cuyo id, nombre o descripción tienen todas las palabras de la búsqueda. ##### `context_meter` ```python context_meter(used, limit, *, theme=None) ``` Cuánto se llenó la ventana de contexto de un modelo, como el contador de tokens de Zed: un anillo pequeño y `48k / 200k`, amarillo pasado el 80 % y rojo pasado el 95 %. | Parámetro | Descripción | | --- | --- | | `used` | Tokens en uso. | | `limit` | La ventana del modelo. | | `theme` | Tema. | ##### `format_tokens` ```python format_tokens(count) ``` Un número de tokens corto: `950`, `4.5k`, `48k`, `1.25M`. | Parámetro | Descripción | | --- | --- | | `count` | Tokens. | ##### `diff_view` ```python diff_view(diff, *, theme=None, max_lines=120) ``` Un cambio tal como lo muestra Zed en un hilo: el nombre del archivo y las líneas cambiadas (en verde las añadidas, en rojo las quitadas) con unas pocas líneas alrededor. | Parámetro | Descripción | | --- | --- | | `diff` | Un dict con `path`, `old` y `new` (los textos antes y después). | | `theme` | Tema. | | `max_lines` | Líneas que se muestran antes de `… N líneas más`. | ```python ui.diff_view({"path": "main.py", "old": "x = 1\n", "new": "x = 2\n"}) ``` #### Selector de iconos ##### `IconPicker` ```python class IconPicker ``` Un componente para elegir un icono de Lucide: un buscador (nombres y etiquetas), un desplegable de categorías y una cuadrícula que solo construye las filas visibles. Enter elige la primera coincidencia. `on_change` recibe el nombre del icono (`""` si se quita); devuélvelo en `value`. Ver la página [Selector de iconos](https://albakore.github.io/pyzed/es/library/icon-picker/). | Parámetro | Descripción | | --- | --- | | `value` | El nombre del icono elegido. | | `columns` | Iconos por fila. | | `height` | Alto de la cuadrícula. | | `clearable` | Muestra un botón que quita el icono. | | `autofocus` | Da el foco al buscador al aparecer. | | `on_change` | Recibe `event.value`. | ```python ui.IconPicker(value=self.icon, on_change=lambda e: setattr(self, "icon", e.value)) ``` **Métodos y propiedades** ###### `IconPicker.set_query` ```python IconPicker.set_query(text) ``` Filtra la cuadrícula por `text` (nombres y etiquetas). ###### `IconPicker.set_group` ```python IconPicker.set_group(key) ``` Muestra solo los iconos de la categoría `key` (`""` para todos). ###### `IconPicker.matches` ```python IconPicker.matches() ``` Los iconos que se muestran: los de la categoría elegida que coinciden con la búsqueda, los mejores primero. ###### `IconPicker.pick` ```python IconPicker.pick(name) ``` Selecciona `name` y emite `change`. ##### `icon_picker_dialog` ```python icon_picker_dialog( value, on_pick, *, on_close=None, title=None, columns=10, clearable=True, key='icon-picker', theme=None, ) ``` Un diálogo con un `IconPicker` que elige con un clic; úsalo como diálogo de `ui.dialog_layer`. El buscador recibe el foco. | Parámetro | Descripción | | --- | --- | | `value` | El icono actual. | | `on_pick` | Se llama con el nombre del icono pulsado (`""` si se quita). | | `on_close` | El botón de cerrar. | | `title` | Por defecto: "Elegir un icono", traducido. | | `columns` | Iconos por fila. | | `clearable` | Muestra el botón "Sin icono". | | `key` | Clave del componente, para varios selectores en una vista. | | `theme` | Tema. | ```python ui.dialog_layer(page, ui.icon_picker_dialog(self.icon, self.pick, on_close=self.close) if self.picking else None, on_dismiss=self.close) ``` ##### `icon_grid` ```python icon_grid( names, on_pick, *, selected=None, columns=8, cell=34, height=288, id=None, theme=None, ) ``` La cuadrícula de iconos sola, virtualizada: solo se construyen las filas visibles, así que miles de iconos cuestan lo mismo que unos pocos. | Parámetro | Descripción | | --- | --- | | `names` | Nombres de iconos, en orden. | | `on_pick` | Se llama con el nombre pulsado. | | `selected` | El nombre resaltado. | | `columns` | Iconos por fila. | | `cell` | Lado de cada celda en píxeles. | | `height` | Alto de la cuadrícula. | | `id` | Id del desplazable, para `Command.scroll_to`. | | `theme` | Tema. | ### Temas > Referencia de la API de Theme en Pyzed: los temas incluidos, los tokens de color, cómo derivar temas propios y utilidades de color. Source: https://albakore.github.io/pyzed/es/reference/theme/ Ver la guía de [temas](https://albakore.github.io/pyzed/es/theming/themes/) para la lista de tokens y cómo crear los tuyos. #### Theme ##### `Theme` Un tema: un dataclass inmutable con los tokens de color (superficies, bordes, elementos, texto, estado, editor, sintaxis, colores de colaboradores y ANSI) y métricas (`radius`, `radius_sm`, `ui_font_size`, `buffer_font_size`). La propiedad `is_dark` indica si es oscuro. **Métodos y propiedades** ###### `Theme.is_dark` ```python Theme.is_dark # property ``` Si el aspecto del tema es oscuro. ##### `derive` ```python derive(self, **changes) ``` Devuelve una copia con algunos tokens cambiados. Los diccionarios `syntax` y `ansi` se combinan en lugar de reemplazarse. | Parámetro | Descripción | | --- | --- | | `changes` | Tokens a cambiar. | ```python oceano = pz.themes.ONE_DARK.derive(name="Océano", accent="#4fb3ff", syntax={"keyword": "#ff79c6"}) ``` ##### `color` ```python color(self, name) ``` Resuelve un token (`"accent"`, `"text_muted"`…) a su color. Los colores hex y `"transparent"` pasan tal cual. | Parámetro | Descripción | | --- | --- | | `name` | Token o color. | ##### `tokens` ```python tokens(self) ``` Todos los tokens como diccionario. ##### `syntax_scopes` ```python syntax_scopes(self) ``` Traduce los tokens de sintaxis del tema a scopes de TextMate para el resaltador. ##### `palette` ```python palette(self) ``` La paleta base que se pasa a Iced para sus estilos internos. De uso interno. ##### `with_accent` ```python with_accent(self, accent) ``` Una copia con otro color de acento y todo lo que depende de él (foco, selección, enlaces, botones principales, texto sobre acento). | Parámetro | Descripción | | --- | --- | | `accent` | El color. | ##### `generate` ```python generate( name, *, background, text, accent, appearance=None, error=None, warning=None, success=None, syntax=None, radius=6.0, ) ``` Crea un tema completo a partir de unos pocos colores. Superficies, bordes, estados de hover y textos atenuados se mezclan a partir de `background` y `text`; la apariencia se deduce del brillo del fondo si no se indica. | Parámetro | Descripción | | --- | --- | | `name` | Nombre. | | `background` | Color del editor/fondo. | | `text` | Color del texto. | | `accent` | Color de acento. | | `appearance` | `"dark"` o `"light"`. | | `error` | Color de error. | | `warning` | Color de aviso. | | `success` | Color de éxito. | | `syntax` | Colores de sintaxis extra. | | `radius` | Radio de las esquinas. | ```python pz.Theme.generate("Propio", background="#1b1d23", text="#e6e6e6", accent="#ff8a65") ``` ##### `from_zed` ```python from_zed(source, name=None) ``` Lee un tema de un archivo de familia de temas de Zed (`.json`) o un dict. Una familia suele tener varios (Ayu Dark/Light/Mirage); `name` elige uno (por defecto, el primero). | Parámetro | Descripción | | --- | --- | | `source` | Ruta, texto JSON o dict. | | `name` | Nombre del tema dentro de la familia. | ##### `to_dict` ```python to_dict(self) ``` Un dict apto para JSON; `Theme.from_dict` lo vuelve a leer. ##### `from_dict` ```python from_dict(data, *, base=None) ``` Crea un tema a partir de `to_dict`. Los tokens que falten salen de `base` (One Dark u One Light según `appearance`). | Parámetro | Descripción | | --- | --- | | `data` | Los tokens. | | `base` | Tema base. | ##### `save` ```python save(self, path) ``` Guarda el tema como JSON (formato propio de Pyzed). | Parámetro | Descripción | | --- | --- | | `path` | Ruta del archivo. | ##### `load` ```python load(path) ``` Lee un tema escrito con `save` (para temas de Zed usa `from_zed`). | Parámetro | Descripción | | --- | --- | | `path` | Ruta del archivo. | #### Temas incluidos ##### `themes` Espacio de nombres con los temas incluidos (las familias One, Ayu y Gruvbox) y el registro de temas. Los incluidos también están como atributos: `themes.ONE_DARK`, `themes.ONE_LIGHT`, `themes.AYU_DARK`, `themes.AYU_LIGHT`, `themes.AYU_MIRAGE`, `themes.GRUVBOX_DARK`, `themes.GRUVBOX_LIGHT`… ##### `all` ```python all() ``` Lista con todos los temas incluidos. ##### `names` ```python names() ``` Nombres de todos los temas registrados. ##### `get` ```python get(name) ``` Busca un tema por nombre (sin distinguir mayúsculas). Lanza `KeyError` si no existe. | Parámetro | Descripción | | --- | --- | | `name` | Nombre. | ##### `register` ```python register(theme) ``` Añade (o reemplaza) un tema para poder usarlo por su nombre. | Parámetro | Descripción | | --- | --- | | `theme` | El tema. | ##### `load_zed` ```python load_zed(source) ``` Carga y registra todos los temas de un archivo de familia de Zed. | Parámetro | Descripción | | --- | --- | | `source` | Ruta, texto JSON o dict. | ##### `auto` ```python auto(light='One Light', dark='One Dark') ``` Un tema que sigue el modo claro/oscuro del sistema. | Parámetro | Descripción | | --- | --- | | `light` | Tema (o nombre) para modo claro. | | `dark` | Tema (o nombre) para modo oscuro. | ##### `AutoTheme` Lo que devuelve `themes.auto`: un par `(light, dark)`. `pick(dark)` devuelve el que toca. **Métodos y propiedades** ###### `AutoTheme.pick` ```python AutoTheme.pick(dark) ``` Devuelve el tema oscuro si `dark` es verdadero; si no, el claro. #### Utilidades (pyzed.theme) ##### `current` ```python current() ``` El tema que usan los componentes cuando no se pasa `theme=`. ##### `use` ```python use(theme) ``` Cambia el tema actual. `App` lo hace antes de cada `view()`, así que normalmente no lo necesitas. | Parámetro | Descripción | | --- | --- | | `theme` | El tema. | ##### `alpha` ```python alpha(color, value) ``` Devuelve `color` con su opacidad multiplicada por `value`. | Parámetro | Descripción | | --- | --- | | `color` | Color hex. | | `value` | Factor de 0 a 1. | ```python pz.theme.alpha("#74ade8", 0.2) # "#74ade833" ``` ##### `mix` ```python mix(a, b, amount=0.5) ``` Mezcla dos colores: `amount=0` es `a` y `amount=1` es `b`. | Parámetro | Descripción | | --- | --- | | `a` | Primer color. | | `b` | Segundo color. | | `amount` | Proporción de `b`. | ##### `resolve` ```python resolve(value, *, dark=True) ``` Convierte un valor de `App.theme` (tema, nombre, `AutoTheme` o `None`) en un `Theme`. | Parámetro | Descripción | | --- | --- | | `value` | El valor. | | `dark` | Si el sistema está en modo oscuro. | ##### `lighten` ```python lighten(color, amount=0.1) ``` Aclara un color. | Parámetro | Descripción | | --- | --- | | `color` | Color hex. | | `amount` | Cantidad de 0 a 1. | ##### `darken` ```python darken(color, amount=0.1) ``` Oscurece un color. | Parámetro | Descripción | | --- | --- | | `color` | Color hex. | | `amount` | Cantidad de 0 a 1. | ##### `luminance` ```python luminance(color) ``` Luminancia relativa (WCAG) de un color, de 0 a 1. | Parámetro | Descripción | | --- | --- | | `color` | Color hex. | ##### `contrast` ```python contrast(a, b) ``` Relación de contraste WCAG entre dos colores (1 a 21). | Parámetro | Descripción | | --- | --- | | `a` | Primer color. | | `b` | Segundo color. | ##### `readable_on` ```python readable_on(background, light='#fafafa', dark='#1c2025') ``` Devuelve `light` u `dark`, el que más contraste tenga sobre `background`. | Parámetro | Descripción | | --- | --- | | `background` | Color de fondo. | | `light` | Texto claro. | | `dark` | Texto oscuro. | ### Dibujo (pyzed.draw) > Referencia de la API de pyzed.draw: formas, trazados, texto y transformaciones para dibujar gráficos 2D propios en un pz.canvas de tu app. Source: https://albakore.github.io/pyzed/es/reference/draw/ Las formas de `pyzed.draw` son diccionarios simples que se pasan a [`pz.canvas`](https://albakore.github.io/pyzed/es/reference/widgets/#canvas). Los ángulos van en grados y `y` crece hacia abajo. Ver la guía de [dibujo con canvas](https://albakore.github.io/pyzed/es/guides/canvas/). ```python from pyzed import draw pz.canvas([ draw.background("#1e2127"), draw.circle(100, 100, 40, fill="#74ade8"), draw.text(100, 180, "Hola", color="#dce0e5", align_x="center"), ], width=200, height=200) ``` #### Formas ##### `background` ```python background(color) ``` Rellena todo el canvas. | Parámetro | Descripción | | --- | --- | | `color` | Color. | ##### `rect` ```python rect(x, y, w, h, *, fill=None, stroke=None, width=1.0, radius=0.0, dash=None) ``` Un rectángulo, relleno y/o con trazo. | Parámetro | Descripción | | --- | --- | | `x` | Izquierda. | | `y` | Arriba. | | `w` | Ancho. | | `h` | Alto. | | `fill` | Color de relleno. | | `stroke` | Color del trazo. | | `width` | Grosor del trazo. | | `radius` | Radio de las esquinas. | | `dash` | Patrón de guiones, p. ej. `[6, 4]`. | ##### `rects` ```python rects(items, *, fill) ``` Muchos rectángulos rellenos de un color en una sola forma (rápido para rejillas y barras). | Parámetro | Descripción | | --- | --- | | `items` | Tuplas `(x, y, w, h)`. | | `fill` | Color. | ##### `circle` ```python circle(x, y, r, *, fill=None, stroke=None, width=1.0, dash=None) ``` Un círculo centrado en `(x, y)`. | Parámetro | Descripción | | --- | --- | | `x` | Centro x. | | `y` | Centro y. | | `r` | Radio. | | `fill` | Color de relleno. | | `stroke` | Color del trazo. | | `width` | Grosor del trazo. | | `dash` | Patrón de guiones, p. ej. `[6, 4]`. | ##### `line` ```python line(x1, y1, x2, y2, *, stroke='#ffffff', width=1.0, dash=None, cap=None) ``` Una línea recta. | Parámetro | Descripción | | --- | --- | | `x1` | Inicio x. | | `y1` | Inicio y. | | `x2` | Fin x. | | `y2` | Fin y. | | `stroke` | Color del trazo. | | `width` | Grosor del trazo. | | `dash` | Patrón de guiones, p. ej. `[6, 4]`. | | `cap` | `"round"`, `"square"` o `"butt"`: forma de los extremos. | ##### `polyline` ```python polyline( points, *, stroke='#ffffff', width=1.0, closed=False, fill=None, dash=None, cap='round', ) ``` Une `points` con líneas rectas (`closed=True` cierra la figura). | Parámetro | Descripción | | --- | --- | | `points` | Lista de `(x, y)`. | | `stroke` | Color del trazo. | | `width` | Grosor del trazo. | | `closed` | Cierra la figura. | | `fill` | Color de relleno. | | `dash` | Patrón de guiones, p. ej. `[6, 4]`. | | `cap` | `"round"`, `"square"` o `"butt"`: forma de los extremos. | ##### `polygon` ```python polygon(points, *, fill=None, stroke=None, width=1.0) ``` Una figura cerrada que pasa por `points`. | Parámetro | Descripción | | --- | --- | | `points` | Lista de `(x, y)`. | | `fill` | Color de relleno. | | `stroke` | Color del trazo. | | `width` | Grosor del trazo. | ##### `arc` ```python arc(x, y, r, start, end, *, stroke=None, fill=None, width=1.0, pie=False, cap=None) ``` Un arco de circunferencia de `start` a `end` grados (0 = las 3 en punto, sentido horario). `pie=True` lo cierra por el centro (porción de tarta). | Parámetro | Descripción | | --- | --- | | `x` | Centro x. | | `y` | Centro y. | | `r` | Radio. | | `start` | Ángulo inicial. | | `end` | Ángulo final. | | `stroke` | Color del trazo. | | `fill` | Color de relleno. | | `width` | Grosor del trazo. | | `pie` | Cierra por el centro. | | `cap` | `"round"`, `"square"` o `"butt"`: forma de los extremos. | ##### `path` ```python path(ops, *, fill=None, stroke=None, width=1.0, dash=None, cap=None) ``` Un trazado libre con operaciones tipo SVG: `("M", x, y)` mover, `("L", x, y)` línea, `("Q", cx, cy, x, y)` curva cuadrática, `("C", c1x, c1y, c2x, c2y, x, y)` curva cúbica, `("A", cx, cy, r, inicio, fin)` arco, `("Z",)` cerrar. | Parámetro | Descripción | | --- | --- | | `ops` | Lista de operaciones. | | `fill` | Color de relleno. | | `stroke` | Color del trazo. | | `width` | Grosor del trazo. | | `dash` | Patrón de guiones, p. ej. `[6, 4]`. | | `cap` | `"round"`, `"square"` o `"butt"`: forma de los extremos. | ##### `text` ```python text( x, y, content, *, color='#ffffff', size=14, font='sans', weight=None, align_x='start', align_y='top', ) ``` Texto anclado en `(x, y)`; `align_x`/`align_y` eligen qué lado del texto queda en ese punto. | Parámetro | Descripción | | --- | --- | | `x` | Posición x. | | `y` | Posición y. | | `content` | El texto. | | `color` | Color. | | `size` | Tamaño. | | `font` | Familia. | | `weight` | Peso. | | `align_x` | `"start"`, `"center"` o `"end"`. | | `align_y` | `"top"`, `"center"` o `"bottom"`. | ##### `image` ```python image(source, x, y, w, h, *, rotation=0.0, opacity=1.0, pixelated=False, size=None) ``` Un mapa de bits (sprite) dibujado en la caja `(x, y, w, h)`. Se decodifica una vez por origen y queda en caché, así que dibujarlo en cada fotograma es barato. Ver [Crear videojuegos](https://albakore.github.io/pyzed/es/games/). | Parámetro | Descripción | | --- | --- | | `source` | Una ruta, bytes de una imagen (PNG, JPEG…) o bytes RGBA con `size`. | | `x` | Izquierda. | | `y` | Arriba. | | `w` | Ancho. | | `h` | Alto. | | `rotation` | Grados, alrededor de su centro. | | `opacity` | De 0 a 1. | | `pixelated` | Escalado por vecino más cercano, para pixel art. | | `size` | `(ancho, alto)` de los bytes RGBA. | #### Transformaciones ##### `translate` ```python translate(x, y) ``` Desplaza las formas que vienen después. | Parámetro | Descripción | | --- | --- | | `x` | Horizontal. | | `y` | Vertical. | ##### `rotate` ```python rotate(degrees) ``` Gira las formas que vienen después alrededor del origen actual. | Parámetro | Descripción | | --- | --- | | `degrees` | Grados. | ##### `scale` ```python scale(x, y=None) ``` Escala las formas que vienen después. | Parámetro | Descripción | | --- | --- | | `x` | Factor horizontal. | | `y` | Factor vertical (por defecto, igual que `x`). | ##### `save` ```python save() ``` Guarda la transformación actual (úsalo con `restore`). ##### `restore` ```python restore() ``` Vuelve a la transformación guardada por el último `save`. ##### `group` ```python group(shapes, *, x=0.0, y=0.0, rotation=0.0, scale_by=None) ``` Dibuja `shapes` desplazadas, giradas y escaladas sin afectar a las formas siguientes. Devuelve una lista: expándela con `*`. | Parámetro | Descripción | | --- | --- | | `shapes` | Las formas. | | `x` | Desplazamiento x. | | `y` | Desplazamiento y. | | `rotation` | Grados. | | `scale_by` | Escala. | ### Iconos > Referencia de la API de pyzed.icons: el conjunto de iconos de Lucide incluido, sus nombres y las utilidades de iconos por tipo de archivo. Source: https://albakore.github.io/pyzed/es/reference/icons/ Pyzed incluye la fuente de [Lucide](https://lucide.dev) (licencia ISC) con unos 2000 iconos. Busca nombres con `pyzed icons ` o en la web de Lucide. #### pyzed.icons ##### `icon` ```python icon(name, *, size=16, color=None) ``` Un widget de icono. También está en `pyzed.icon`. | Parámetro | Descripción | | --- | --- | | `name` | Nombre del icono. | | `size` | Tamaño. | | `color` | Color (por defecto, el del texto). | ##### `glyph` ```python glyph(name) ``` Devuelve el carácter de un icono, para usarlo en cualquier texto con `font="icons"`. Lanza `ValueError` si no existe. | Parámetro | Descripción | | --- | --- | | `name` | Nombre del icono. | ##### `names` ```python names() ``` Lista ordenada de todos los nombres de iconos. ##### `search` ```python search(query, among=None) ``` Los iconos que coinciden con una búsqueda por nombre y etiqueta, los mejores primero: nombres exactos, luego los que empiezan por la búsqueda, los que tienen todas las palabras y los encontrados por sus etiquetas (`search("person")` encuentra `user`). | Parámetro | Descripción | | --- | --- | | `query` | Palabras que deben aparecer todas en el nombre o las etiquetas. | | `among` | Buscar solo en estos nombres (por defecto, todos los iconos). | ```python icons.search("arrow up")[:3] # ["arrow-up", "arrow-up-01", "arrow-up-10"] ``` ##### `tags` ```python tags(name) ``` Las etiquetas de búsqueda de un icono, de Lucide (`tags("user")` es `["person", "account", "contact"]`). | Parámetro | Descripción | | --- | --- | | `name` | Nombre del icono. | ##### `category` ```python category(key, among=None) ``` Los iconos de una categoría de `CATEGORIES`, ordenados. Un icono puede estar en varias. | Parámetro | Descripción | | --- | --- | | `key` | Una clave de `CATEGORIES` (`"arrows"`, `"files"`, `"people"`...). | | `among` | Mirar solo estos nombres. | ##### `CATEGORIES` Grupos para explorar iconos (flechas, texto, archivos, diseño, multimedia, comunicación, personas, dispositivos, desarrollo, formas, clima, comercio, mapas, estado), cada uno con las palabras de nombres y etiquetas que meten un icono en él. ##### `for_file` ```python for_file(name) ``` Devuelve `(icono, color_o_None)` para un nombre de archivo, como en el panel de proyecto de un editor de código. | Parámetro | Descripción | | --- | --- | | `name` | Nombre del archivo. | ##### `language_for` ```python language_for(name) ``` Nombre legible del lenguaje de un archivo (`"main.py"` → `"Python"`), útil para la barra de estado. | Parámetro | Descripción | | --- | --- | | `name` | Nombre del archivo. | ### CLI > Todos los comandos de la CLI pyzed: crea proyectos, gestiona dependencias, ejecuta y compila apps, genera iconos e inicia el servidor MCP. Source: https://albakore.github.io/pyzed/es/reference/cli/ Al instalar Pyzed se añade el comando `pyzed` (también disponible como `python -m pyzed`). Los comandos que trabajan sobre un proyecto buscan el `pyproject.toml` más cercano desde la carpeta actual hacia arriba. Consulta [Proyectos con uv](https://albakore.github.io/pyzed/es/guides/projects/). #### Proyectos ##### `pyzed new ` Crea un proyecto de uv en una carpeta nueva: `uv init --package`, la tabla `[tool.pyzed]`, `ui/main.pzform` con sus handlers, `pyzed-ui` como dependencia, `ruff` y `ty` como dependencias de desarrollo y un repositorio git. | Opción | Efecto | | --- | --- | | `--app` | Backend junto a `pyproject.toml` en lugar de en `src/` (`uv init --app`). | | `--python VERSIÓN` | Versión de Python del proyecto. | | `--no-git` | Sin repositorio git. | | `--no-sync` | Solo escribe `pyproject.toml` (no resuelve ni instala nada). | | `--script` | Un único `main.py` con workspace, barra lateral y paleta de comandos, más `requirements.txt`; sin uv. | ##### `pyzed init [ruta]` Añade Pyzed al proyecto de uv en `ruta` (por defecto, la carpeta actual), con las mismas opciones que `new` salvo `--script`. Si no hay `pyproject.toml`, ejecuta antes `uv init`. No modifica el código existente. #### Dependencias | Comando | Hace | | --- | --- | | `pyzed add PAQUETE...` | `uv add`. Una restricción (`"httpx==0.27.2"`) cambia la versión de un paquete que ya está. `--dev`, `--group NOMBRE`, `--editable` (rutas locales). | | `pyzed remove PAQUETE...` | `uv remove`, en el grupo donde esté el paquete. | | `pyzed upgrade [PAQUETE...]` | Actualiza esos paquetes (todos si no se indica ninguno) dentro de sus restricciones y sincroniza. | | `pyzed sync` | `uv sync`. | | `pyzed deps [--outdated]` | Lista las dependencias directas, su grupo y la versión fijada (`--outdated`: solo las que tienen una versión más nueva). | | `pyzed check [--fix]` | `ruff check` y `ty check`; termina con 1 si alguno falla. | #### Ejecutar | Comando | Hace | | --- | --- | | `pyzed design [form o carpeta]` | Abre el [diseñador visual](https://albakore.github.io/pyzed/es/editor/). Dentro de un proyecto y sin form, abre su form principal. | | `pyzed run [form] [--reload]` | Ejecuta un form con sus handlers; por defecto, el form principal del proyecto. `--reload` actualiza la ventana al guardar. | | `pyzed dev script.py [--watch RUTA]` | Ejecuta un script con hot reload: al guardar un archivo se actualiza la ventana abierta. | #### `pyzed build` Empaqueta el proyecto para la plataforma actual con PyInstaller o Nuitka. Consulta [Cómo funciona la compilación](https://albakore.github.io/pyzed/es/packaging/build/) y, en Pyzed Design, [Exportar tu aplicación](https://albakore.github.io/pyzed/es/editor/exporting/). | Opción | Efecto | | --- | --- | | `--backend pyinstaller\|nuitka` | El empaquetador (por defecto: `backend` en `pyproject.toml`, si no PyInstaller). | | `--onefile` | Un único ejecutable en lugar de una carpeta. | | `--console` | Mantiene la ventana de consola. | | `--name NOMBRE` | Nombre del ejecutable. | | `--archive zip\|tar.gz` | Además comprime la aplicación, con el nombre que busca el actualizador (`shop-windows-x86_64.zip`). | | `--target DESTINO` | Falla si este equipo no compila `DESTINO` (`windows-x64`, `macos-arm64`, `linux-x64`...); lo usa la CI. | | `--dry-run` | Muestra el comando del empaquetador. | | `--workflow` | Escribe `.github/workflows/build.yml`, que compila cada destino de `targets` en GitHub Actions. | #### `pyzed icons [filtro]` Lista los nombres de los iconos de Lucide incluidos que contienen `filtro`. ```bash pyzed icons arrow ``` #### `pyzed mcp` Arranca el [servidor MCP para agentes de IA](https://albakore.github.io/pyzed/es/guides/mcp/) por stdio, hasta que el cliente se desconecta. Necesita el extra `mcp`: `pip install "pyzed-ui[mcp]"`. | Opción | Efecto | | --- | --- | | `--transport streamable-http` | Sirve por HTTP en lugar de stdio. | | `--allow-code` | Ofrece `screenshot` y las herramientas de proyecto también por HTTP (por stdio siempre están). | | `--root RUTA` | La carpeta donde trabajan las herramientas de proyecto (por defecto, la actual). | ### Diseñador visual (pyzed.designer) > Referencia de la API de pyzed.designer: ejecuta, abre y crea formularios desde código Python, y registra componentes y plantillas de la caja de herramientas. Source: https://albakore.github.io/pyzed/es/reference/designer/ `pyzed.designer` es el lado en código del [diseñador visual](https://albakore.github.io/pyzed/es/editor/): ejecuta archivos `.pzform`, abre formularios como ventanas, crea formularios en código y amplía la caja de herramientas con tus propios componentes y plantillas. Los [componentes diseñados](https://albakore.github.io/pyzed/es/editor/component-designer/) (`.pzcomp`) también se cargan desde aquí. ```python from pyzed import designer designer.run("ui/main.pzform", reload=True) ``` #### Ejecutar formularios ##### `run` ```python run(path, *, reload=None, splash=None) ``` Ejecuta el formulario de `path` con toda la lógica conectada en el diseñador. `reload=True` activa el hot reload: guardar el formulario (desde el diseñador o a mano) o su módulo de handlers actualiza la ventana en marcha sin reiniciarla. `None` sigue a `PYZED_RELOAD`, como `pz.run`. ##### `load` ```python load(path) ``` Lee un archivo `.pzform`. ##### `open_form` ```python open_form(path) ``` Carga un `.pzform` y sus handlers en una `FormApp` lista para `run()` o `screenshot()`. Si el formulario pertenece a un proyecto (ver `pyzed.project`), primero se activa el proyecto para que sus handlers puedan importar el backend y los paquetes de su `.venv`. ##### `open_window` ```python open_window(form, *, data=None, handlers=None, folder=None) ``` Un `Command` que abre `form` en una ventana nueva (los argumentos son los de `form_window`). ##### `form_window` ```python form_window(form, *, data=None, handlers=None, folder=None) ``` Una `FormWindow` para `form` (una ruta o nombre de `.pzform`, o un `Form`), lista para abrirse. Un formulario cargado de un archivo recibe su módulo de handlers; un `Form` creado en código usa `handlers` (un dict de funciones). `data` llega al formulario nuevo como `form.data`. ##### `new_form` ```python new_form( path, *, title=None, template='blank', width=None, height=None, overwrite=False, ) ``` Crea `path` (se añade `.pzform` si falta) y su módulo de handlers a partir de `template`. El formulario toma el nombre del archivo (`order-details.pzform` -> `OrderDetails`). Devuelve la ruta del formulario. Lanza `FileExistsError` si ya existe, salvo con `overwrite`. ##### `save` ```python save(form, path) ``` Escribe `form` en `path` (se añade `.pzform` cuando no tiene extensión). ##### `open_document` ```python open_document(path) ``` Abre un `.pzform` como `Document` o un `.pzcomp` como `SceneDocument`. ##### `design` ```python design(path=None, *, theme=None) ``` Abre el diseñador visual, opcionalmente sobre un archivo `.pzform` (que se crea al guardar por primera vez). #### Formularios y controles ##### `Form` ```python @dataclass class Form(name='Form1', title='Form1', width=640.0, height=420.0, theme='One Dark', accent=None, background=None, radius=None, font_size=14.0, resizable=True, grid=8, style={}, controls=[], events={}, code=None, animations=[], autoplay='') ``` Una ventana diseñada visualmente: sus ajustes, sus controles (en orden z) y sus eventos. `theme` es cualquier nombre de tema registrado; `accent`, `radius` y `font_size` lo ajustan sin escribir un archivo de tema. `style` es el estilo global del formulario (`pz.Style.to_dict()`), aplicado a toda la ventana. `grid` es el paso de ajuste del diseñador. **Métodos y propiedades** ###### `Form.get` ```python Form.get(name) ``` Devuelve el control llamado `name`, o `None` si no hay ninguno. ###### `Form.names` ```python Form.names() ``` Devuelve los nombres de todos los controles, en orden z. ###### `Form.children` ```python Form.children(name) ``` Los controles directamente dentro de `name` (el propio formulario con `None`), en orden z. ###### `Form.descendants` ```python Form.descendants(name) ``` Todos los controles dentro de `name`, a cualquier profundidad (los padres antes que sus hijos). ###### `Form.ancestors` ```python Form.ancestors(name) ``` Los contenedores que contienen a `name`, del más cercano al más lejano. ###### `Form.walk` ```python Form.walk(parent=None, depth=0) ``` `(control, depth)` en orden de dibujo: cada control y luego lo que contiene. ###### `Form.is_inside` ```python Form.is_inside(name, container) ``` Si el control `name` está dentro de `container`, a cualquier profundidad. ###### `Form.unique_name` ```python Form.unique_name(base) ``` El primer `base1`, `base2`... libre (al estilo de Visual Basic). ###### `Form.add` ```python Form.add(control) ``` Añade `control` (encima del orden z) y lo devuelve. ###### `Form.remove` ```python Form.remove(names) ``` Quita controles y todo lo que contienen. ###### `Form.rename` ```python Form.rename(old, new) ``` Renombra un control y devuelve el nombre usado en realidad (único y válido). ###### `Form.reorder` ```python Form.reorder(names, where) ``` Mueve controles al frente (`"front"`) o al fondo (`"back"`) del orden z. ###### `Form.to_dict` ```python Form.to_dict() ``` Devuelve el formulario como datos listos para JSON: `{"format", "form": ajustes, "controls": [...]}`. ###### `Form.from_dict` ```python @classmethod Form.from_dict(data) ``` Crea un formulario a partir de `data` guardados (ver `to_dict`), ignorando ajustes desconocidos y reparando padres rotos; lanza `ValueError` si no es un formulario de Pyzed. ###### `Form.to_json` ```python Form.to_json() ``` Devuelve el formulario como texto JSON indentado (el contenido de un archivo `.pzform`). ###### `Form.from_json` ```python @classmethod Form.from_json(text) ``` Crea un formulario a partir del texto JSON `text` de un archivo `.pzform`. ###### `Form.copy` ```python Form.copy() ``` Devuelve una copia profunda del formulario y sus controles. ##### `Control` ```python @dataclass class Control(name, type, x=0.0, y=0.0, width=100.0, height=28.0, props={}, events={}, anchor=('left', 'top'), visible=True, enabled=True, locked=False, parent=None, layout={}, style={}, animations=[], bind='', rules={}, hidden_in_editor=False) ``` Un componente colocado en un formulario. `x`, `y`, `width` y `height` están en píxeles lógicos relativos al área cliente del formulario, o a la esquina superior izquierda de `parent` cuando el control está dentro de un contenedor (un panel, un group box, una fila flex...). `props` guarda las propiedades propias del componente (`text`, `checked`...) y `events` asocia nombres de evento (`"click"`) a nombres de función handler. `layout` guarda opciones que lee el layout del padre, como `grow` en una fila flex o `span` en una grid. `style` guarda propiedades de estilo tipo CSS (`pz.Style.to_dict()`: fondo, borde, radio, fuente...) que se dibujan por encima de las props. **Métodos y propiedades** ###### `Control.min_size` ```python Control.min_size # property ``` El `(ancho, alto)` mínimo cuando el control se estira (0: sin límite). ###### `Control.max_size` ```python Control.max_size # property ``` El `(ancho, alto)` máximo cuando el control se estira (0: sin límite). ###### `Control.pivot` ```python Control.pivot # property ``` El punto `(x, y)` de 0 a 1 alrededor del que giran la rotación y la escala, y que se queda quieto cuando un límite de tamaño frena el estiramiento. ###### `Control.rect` ```python Control.rect # property ``` El `(x, y, width, height)` del control, relativo a su padre (o al formulario). ###### `Control.to_dict` ```python Control.to_dict() ``` Devuelve el control como datos listos para JSON; `parent`, `layout` y `style` vacíos se omiten. ###### `Control.from_dict` ```python @classmethod Control.from_dict(data) ``` Crea un control a partir de `data` guardados, ignorando claves desconocidas y anclajes no válidos. ###### `Control.copy` ```python Control.copy() ``` Devuelve una copia profunda del control. ##### `Event` ```python @dataclass class Event(sender, name, value=None) ``` Lo que recibe un handler además del formulario: quién lo lanzó, qué evento y su valor. ##### `FormApp` ```python class FormApp ``` Un formulario como ventana principal de la aplicación. `path` (lo fija `open_form`) es el archivo `.pzform`; con el hot reload activo, guardarlo o guardar su módulo de handlers actualiza la ventana en marcha. **Métodos y propiedades** ###### `FormApp.on_start` ```python FormApp.on_start() ``` Vigila el formulario y su módulo de handlers para el hot reload (si está activo) y luego lanza `load`. ##### `FormWindow` ```python class FormWindow ``` Un formulario en una ventana extra (`form.open(...)`, `designer.open_window` y la vista previa del diseñador). **Métodos y propiedades** ###### `FormWindow.on_open` ```python FormWindow.on_open() ``` Lanza el evento `load` del formulario al abrirse la ventana. ##### `Document` ```python class Document ``` Un formulario abierto en el diseñador. `code` es el texto del módulo de handlers, guardado en memoria y escrito junto al formulario al hacer `save`. `selection` guarda nombres de controles, el principal primero. **Métodos y propiedades** ###### `Document.clipboard` ```python Document.clipboard # property ``` Los controles copiados (como datos de `Control.to_dict()`), compartidos por todos los documentos. ###### `Document.open` ```python @classmethod Document.open(path) ``` Carga en un documento nuevo el formulario de `path` y el texto de su módulo de handlers (si el archivo existe). ###### `Document.code_path` ```python Document.code_path # property ``` Dónde se guarda el módulo de handlers, o `None` mientras el formulario no tenga archivo. ###### `Document.display_name` ```python Document.display_name # property ``` El nombre que se muestra para el documento: su nombre de archivo, o `.pzform` si no está guardado. ###### `Document.save` ```python Document.save(path=None) ``` Escribe el formulario y su módulo de handlers; devuelve la ruta del formulario. ###### `Document.commit` ```python Document.commit(group=None) ``` Guarda un paso de deshacer tras una edición (`group` une ediciones repetidas de un mismo campo). ###### `Document.undo` ```python Document.undo() ``` Retrocede un paso; devuelve `False` si no había nada que deshacer. ###### `Document.redo` ```python Document.redo() ``` Rehace un paso deshecho; devuelve `False` si no había nada que rehacer. ###### `Document.selected` ```python Document.selected # property ``` Los controles seleccionados, el principal primero (se saltan los nombres que ya no existen). ###### `Document.primary` ```python Document.primary # property ``` El control seleccionado principal (el primero), o `None` si no hay nada seleccionado. ###### `Document.select` ```python Document.select(names, *, add=False) ``` Selecciona los controles `names` (un nombre, varios, o `None` para ninguno); los nombres desconocidos se ignoran. ###### `Document.select_all` ```python Document.select_all() ``` Selecciona todos los controles del formulario. ###### `Document.is_selected` ```python Document.is_selected(name) ``` Si el control `name` está seleccionado. ###### `Document.add` ```python Document.add(type_, x, y, width=None, height=None, *, snap=True, parent=None) ``` Coloca un control nuevo de tipo `type_` (ajustado a la cuadrícula) y lo selecciona. ###### `Document.rects` ```python Document.rects() ``` El rectángulo absoluto de cada control (ver `layout.resolve`). ###### `Document.can_contain` ```python Document.can_contain(container, name) ``` Si el control `name` puede moverse dentro de `container` (`None` es el formulario, que admite cualquiera): debe ser un contenedor distinto del propio control y que no esté dentro de él. ###### `Document.reparent` ```python Document.reparent(names, parent, *, index=None, at=None, commit=True, rects=None) ``` Mueve controles dentro de `parent` (o al formulario con `None`), dejándolos donde están. ###### `Document.wrap` ```python Document.wrap(type_, names=None, props=None) ``` Mete controles (por defecto, la selección) dentro de un contenedor nuevo de tipo `type_` que los rodea. ###### `Document.ungroup` ```python Document.ungroup(names=None) ``` Saca los hijos de los contenedores seleccionados (donde están en pantalla) y quita los contenedores; los hijos quedan seleccionados. Devuelve los nombres de los contenedores quitados. ###### `Document.move_out` ```python Document.move_out(name) ``` Saca un control de su contenedor y lo pasa al padre de ese contenedor. ###### `Document.set_layout` ```python Document.set_layout(name, key, value) ``` Fija una opción que lee el layout del padre (`grow`, `span`). ###### `Document.delete` ```python Document.delete() ``` Quita los controles seleccionados (y lo que contienen); devuelve los nombres quitados. ###### `Document.copy` ```python Document.copy() ``` Copia la selección, con lo que haya dentro de los contenedores seleccionados. ###### `Document.clipboard_text` ```python Document.clipboard_text() ``` Los controles copiados como JSON para el portapapeles del sistema, para que otro formulario, otra ventana de Pyzed Design o un archivo de texto puedan tomarlos (ver `paste_text`). ###### `Document.parse_clipboard` ```python @staticmethod Document.parse_clipboard(text) ``` Los controles del texto del portapapeles escrito por `clipboard_text`, o `None` si contiene otra cosa. Se descartan los controles de un tipo que este Pyzed no conoce. ###### `Document.paste_text` ```python Document.paste_text(text, offset=16) ``` Pega los controles del texto del portapapeles del sistema (ver `clipboard_text`); devuelve los nombres nuevos, o `None` si el texto no contiene controles de Pyzed. ###### `Document.cut` ```python Document.cut() ``` Copia la selección al portapapeles y la borra; devuelve cuántos controles se copiaron. ###### `Document.paste` ```python Document.paste(offset=16) ``` Añade copias del portapapeles (desplazadas `offset`) sin sus eventos; conservan su nombre si está libre en este formulario. ###### `Document.duplicate` ```python Document.duplicate() ``` Pega al momento una copia de la selección sin tocar el portapapeles; devuelve los nombres nuevos. ###### `Document.size` ```python Document.size # property ``` El `(width, height)` del formulario en píxeles. ###### `Document.nudge` ```python Document.nudge(dx, dy) ``` Mueve `dx`, `dy` píxeles los controles seleccionados (los que no coloca un layout), sin salirse de su contenedor cuando comparten uno; los empujones repetidos se unen en un solo paso de deshacer. ###### `Document.align` ```python Document.align(edge, *, to_form=False) ``` Alinea los controles seleccionados en `edge` (ver `geometry.align`): entre sí, o al formulario con `to_form` o cuando solo hay un control seleccionado. ###### `Document.center` ```python Document.center(axis='both') ``` Centra la selección (como grupo) en el formulario. ###### `Document.distribute` ```python Document.distribute(axis) ``` Reparte los controles seleccionados con el mismo espacio a lo largo de `axis` (ver `geometry.distribute`). ###### `Document.same_size` ```python Document.same_size(which='both') ``` Da a los controles seleccionados el ancho, el alto o ambos (`which`) del principal. ###### `Document.snap_to_grid` ```python Document.snap_to_grid() ``` Ajusta a la cuadrícula del formulario los controles seleccionados (o todos si no hay selección). ###### `Document.reorder` ```python Document.reorder(where) ``` Mueve los controles seleccionados en el orden z (`where`: ver `Form.reorder`). ###### `Document.set_rect` ```python Document.set_rect(name, rect, *, group=None) ``` Fija la posición y el tamaño del control `name` a partir de `rect` `(x, y, width, height)`, redondeados a píxeles enteros; las ediciones con el mismo `group` se unen en un paso de deshacer. ###### `Document.set_prop` ```python Document.set_prop(name, key, value) ``` Fija la propiedad `key` del control `name` a `value` (las ediciones repetidas se unen en un paso de deshacer). ###### `Document.set_style` ```python Document.set_style(name, key, value) ``` Fija una propiedad de estilo de un control, o del estilo global del formulario si `name` es `None`; `None` la quita. ###### `Document.set_animations` ```python Document.set_animations(name, animations, *, group=None) ``` Fija la lista de animaciones del control `name` (dicts con `preset` y, opcionalmente, `on`, `duration`, `delay`, `easing` y `repeat`); las ediciones con el mismo `group` se unen en un paso de deshacer. ###### `Document.set_clip` ```python Document.set_clip(clip, *, old=None, group=None) ``` Guarda el `pz.Clip` `clip` en el formulario (en lugar del clip `old` cuando se renombra). ###### `Document.remove_clip` ```python Document.remove_clip(name) ``` Quita el clip `name` (y deja de reproducirse al abrir el formulario). ###### `Document.set_autoplay` ```python Document.set_autoplay(name) ``` Reproduce el clip `name` al abrir el formulario (`""`: ninguno). ###### `Document.clear_style` ```python Document.clear_style(name) ``` Quita todas las propiedades de estilo de un control (o del estilo global del formulario). ###### `Document.set_field` ```python Document.set_field(name, key, value) ``` Fija un atributo de `Control` (x, y, width, height, visible, enabled, locked, anchor). ###### `Document.set_bind` ```python Document.set_bind(name, bind) ``` Enlaza el control `name` al campo (o, en los controles de visualización, a la expresión) `bind` de `form.data`; `None` o `""` lo desenlaza. ###### `Document.set_rule` ```python Document.set_rule(name, rule, value) ``` Fija la regla de validación `rule` (`required`, `min_length`, `pattern`...) del control `name`; `None`, `""` o `False` la quita. ###### `Document.toggle_anchor` ```python Document.toggle_anchor(name, edge) ``` Añade `edge` a los anclajes del control `name`, o lo quita si ya estaba. ###### `Document.parent_size` ```python Document.parent_size(name) ``` El tamaño del padre del control `name` (su contenedor o el formulario). ###### `Document.set_anchor` ```python Document.set_anchor(name, anchor, *, move=False, margin=0.0) ``` Ancla el control `name` con un preset, palabras o bordes; con `move=True` también lo lleva a ese sitio, a `margin` píxeles del borde. ###### `Document.set_offset` ```python Document.set_offset(name, side, value) ``` Fija la distancia de un lado (`left`, `top`, `right`, `bottom`) del control `name` al de su padre. ###### `Document.set_pivot` ```python Document.set_pivot(name, x, y) ``` Fija el pivote del control `name` (de 0 a 1 en cada eje). ###### `Document.set_form` ```python Document.set_form(key, value) ``` Fija el atributo `key` del formulario (un `name` nuevo se convierte antes en un identificador válido). ###### `Document.reflow` ```python Document.reflow(parent, size) ``` Tras cambiar el tamaño del formulario (`parent` `None`) o del contenedor `parent` desde `size`, mueve y dimensiona lo que hay dentro según sus anclajes (salvo con `follow_anchors` desactivado). ###### `Document.rename` ```python Document.rename(old, new) ``` Renombra un control, su entrada en la selección y los handlers que siguen la convención de nombres. ###### `Document.owner` ```python Document.owner(name) ``` El nombre visible y el mapa de eventos de un control, o del formulario si `name` es `None`. ###### `Document.events_of` ```python Document.events_of(name) ``` Devuelve los eventos del control `name`, o los del formulario si `name` es `None`. ###### `Document.wire` ```python Document.wire(name, event) ``` Conecta `event` a un handler (creando su esqueleto si hace falta) y devuelve el nombre de la función. ###### `Document.unwire` ```python Document.unwire(name, event, *, delete_code=False) ``` Desconecta `event`; con `delete_code` también quita el esqueleto si no se ha tocado. ###### `Document.rename_handler` ```python Document.rename_handler(old, new) ``` Hace que los eventos conectados al manejador `old` apunten a `new` (después de renombrar su función en el código); devuelve si algún evento lo usaba. ###### `Document.handler_source` ```python Document.handler_source(handler) ``` Devuelve el código de `handler`, o un esqueleto nuevo si aún no existe. ###### `Document.set_handler_source` ```python Document.set_handler_source(handler, source) ``` Reemplaza el código de un handler (lanza `SyntaxError` y no cambia nada si no es válido). ###### `Document.set_code` ```python Document.set_code(source) ``` Reemplaza todo el texto del módulo de handlers y marca el documento como modificado. #### Enlace de datos y validación ##### `check` ```python check(value, rules, type_='input') ``` El mensaje de la primera regla que `value` incumple, o `None` si las cumple todas. `required` (una casilla marcada para casillas e interruptores), `min_length`, `max_length`, `pattern` (una expresión regular que todo el texto debe cumplir), `min` y `max`. `message`, si está, sustituye el mensaje predeterminado. Los valores vacíos solo incumplen `required`. ##### `translate` ```python translate(value, language=None) ``` `value` con los textos `@clave` buscados en el idioma actual (o el indicado); las listas se traducen elemento a elemento y `@@` representa una `@` literal. ##### `read` ```python read(data, path, default=None) ``` El campo en `path` de `data` (dicts, listas y objetos), o `default` si falta. ##### `write` ```python write(data, path, value) ``` Fija el campo en `path` de `data`, creando por el camino los dicts que falten. ##### `has` ```python has(data, path) ``` Si `data` tiene un campo en `path`. ##### `evaluate` ```python evaluate(expression, data, **extra) ``` El valor de `bind` para `data`: el campo si es una ruta, si no la expresión de Python evaluada sobre los campos de los datos. Lanza un error si falta el campo o la expresión es incorrecta. ##### `bound_prop` ```python bound_prop(type_) ``` La propiedad que enlaza un control de tipo `type_` (`None` si no se puede enlazar). ##### `two_way` ```python two_way(type_) ``` Si un control de tipo `type_` escribe en los datos lo que cambia el usuario. ##### `rules_for` ```python rules_for(type_) ``` Las reglas de validación que tienen sentido para `type_` (ninguna para los controles de visualización). ##### `load_texts` ```python load_texts(path) ``` Carga los catálogos de traducción del formulario en `path`: `locales/` junto a él y en las carpetas superiores, hasta la raíz del proyecto. Devuelve los idiomas encontrados. ##### `BIND_PROPS` La propiedad que enlaza cada tipo de control y si el usuario la edita (en los dos sentidos): `{tipo: (propiedad, dos_sentidos)}`. #### Caja de herramientas ##### `Component` ```python @dataclass class Component(type, label, icon, category, render, size=(120, 28), props=(), events=(), description='', visual=True, container=None, child_layout=(), children=(), role=None) ``` Una entrada de la caja de herramientas: cómo se ve, se comporta y se configura un control de este `type`. **Métodos y propiedades** ###### `Component.default_event` ```python Component.default_event # property ``` El evento que se abre al hacer doble clic en el control en el diseñador. ###### `Component.all_events` ```python Component.all_events # property ``` Los eventos propios del control seguidos, en los componentes visuales, de los eventos de ratón comunes. ###### `Component.defaults` ```python Component.defaults() ``` Devuelve un dict nuevo con el valor por defecto de cada propiedad, por nombre. ###### `Component.prop` ```python Component.prop(name) ``` Devuelve la propiedad llamada `name`, o `None` si el componente no la tiene. ###### `Component.create` ```python Component.create(name, x=0, y=0, width=None, height=None) ``` Un control nuevo de este tipo con las propiedades por defecto. ##### `Prop` ```python @dataclass class Prop(name, kind, default=None, label='', choices=(), min=None, max=None, step=1.0, group='Appearance', type='', accept='') ``` Una propiedad que se muestra en la rejilla de propiedades del diseñador. `kind` elige el editor: text, multiline, number, bool, color, choice (de `choices`), list (un elemento por línea), rows (filas separadas por comas, la primera es la cabecera) o icon (un nombre de icono de Lucide). **Métodos y propiedades** ###### `Prop.title` ```python Prop.title # property ``` La etiqueta que se muestra en la rejilla de propiedades: `label`, o el nombre con espacios en lugar de guiones bajos. ##### `RenderContext` ```python class RenderContext ``` Lo que necesita el `render` de un componente: el tema y una forma de lanzar eventos. `live` es `False` en el diseñador, donde los callbacks son marcadores inertes para que los controles se vean habilitados pero no hagan nada. **Métodos y propiedades** ###### `RenderContext.on` ```python RenderContext.on(control, event, prop=None) ``` Un callback para `event`; si se da `prop`, el valor también se guarda ahí. ###### `RenderContext.set` ```python RenderContext.set(control, prop, value) ``` Guarda `value` en `prop` de un control en ejecución sin lanzar ningún evento (las carpetas abiertas de un árbol). ###### `RenderContext.key` ```python RenderContext.key(control) ``` Un id estable para widgets con estado interno (editores, campos de texto). ##### `register` ```python register(component) ``` Añade (o reemplaza) un componente en la caja de herramientas. ##### `component` ```python component(type_) ``` Devuelve el componente registrado para `type_`; lanza `KeyError` con la lista de tipos conocidos. ##### `all_components` ```python all_components() ``` Devuelve todos los componentes registrados, en orden de registro. ##### `categories` ```python categories() ``` Devuelve los componentes registrados agrupados por `category` (secciones de la caja de herramientas), en orden de registro. ##### `Template` ```python @dataclass class Template(name, label, icon, description, build=None, size=(640, 420), handlers=(), form=None) ``` Un punto de partida para formularios nuevos. ##### `register_template` ```python register_template(template) ``` Añade (o reemplaza, por nombre) una plantilla del diálogo "Nuevo formulario"; la devuelve. ##### `templates` ```python templates() ``` Devuelve las plantillas de formulario registradas, en orden de registro. #### Componentes diseñados ##### `new_component` ```python new_component( name='Component1', path=None, *, width=240, height=120, label='', icon='component', category='Custom', description='', extends=None, ) ``` Un componente diseñado nuevo (aún sin guardar; llama a `save()`): su escena y una clase vacía. `extends` es el `.pzcomp` de un componente del que heredar: la clase nueva deriva de él y empieza con sus controles, propiedades, eventos y métodos. ##### `load_component` ```python load_component(path) ``` Importa la clase del componente guardado en `path` (de nuevo, para recoger los cambios). Definir la clase la registra en la caja de herramientas, reemplazando una versión anterior. Devuelve la clase, o `None` (con el motivo en `errors`) si su código falla al importarse. ##### `import_component` ```python import_component(scene) ``` La clase del componente diseñado guardado en `scene`, para heredar de ella. Una ruta relativa es relativa al módulo que la llama, así que un componente puede extender otro que esté a su lado: ```python Rating = import_component("rating.pzcomp") class BigRating(Rating, scene="big_rating.pzcomp"): ... ``` La clase se importa una vez y se reutiliza hasta que cambia su código. ##### `discover` ```python discover(root=None, *, fresh=True) ``` Importa todos los componentes diseñados bajo `root` (por defecto, la carpeta actual). Los componentes pueden usarse entre sí, así que una importación fallida se reintenta cuando los demás ya cargaron (`errors` guarda lo que siga fallando). ##### `SceneDocument` ```python class SceneDocument ``` Un componente abierto en el diseñador: su escena, el código de su clase, la selección y el deshacer. Funcionan todos los comandos de `Document` (colocar, mover, alinear controles...). Los eventos de los controles se convierten en métodos de la clase (`def ok_click(self, event)`), y los eventos propios del componente son sus métodos de ciclo de vida (`setup`, `on_mount`...). Las propiedades y los eventos personalizados se leen de la clase y se escriben de vuelta en ella. **Métodos y propiedades** ###### `SceneDocument.scene` ```python SceneDocument.scene # property ``` La escena del componente (el `form` del documento, tipado como `Scene`). ###### `SceneDocument.base_file` ```python SceneDocument.base_file # property ``` El `.pzcomp` que extiende este componente, o `None`. ###### `SceneDocument.base` ```python SceneDocument.base # property ``` La escena que extiende este componente, con todo lo que ella misma hereda (`None` si no hay). ###### `SceneDocument.inherited` ```python SceneDocument.inherited(name) ``` `True` para un control que viene del componente base. ###### `SceneDocument.own_scene` ```python SceneDocument.own_scene() ``` Lo que guarda el `.pzcomp` propio de este componente: sus controles y los heredados que cambió. ###### `SceneDocument.set_base` ```python SceneDocument.set_base(path) ``` Hace que este componente herede del guardado en `path` (`None` no hereda de nada). ###### `SceneDocument.base_info` ```python SceneDocument.base_info() ``` Las clases de las que hereda este componente (la más cercana primero) como `(nombre del archivo de escena, info de la clase)`. ###### `SceneDocument.all_members` ```python SceneDocument.all_members # property ``` Las propiedades de este componente y las que hereda (los bindings pueden usar ambas). ###### `SceneDocument.info` ```python SceneDocument.info # property ``` La clase tal como se analizó por última vez (el último análisis correcto mientras el código tenga un error de sintaxis). ###### `SceneDocument.code_ok` ```python SceneDocument.code_ok # property ``` Si el código de la clase se puede analizar ahora (si no, `info` recurre al último análisis correcto). ###### `SceneDocument.members` ```python SceneDocument.members # property ``` Las propiedades y el estado privado del componente tal como se declaran en su clase, en orden (una copia). ###### `SceneDocument.member` ```python SceneDocument.member(name) ``` Devuelve la propiedad o el estado declarado llamado `name`, o `None`. ###### `SceneDocument.unique_member` ```python SceneDocument.unique_member(base='value') ``` Devuelve `base` (convertido en identificador), o `base2`, `base3`... si ya lo usa un miembro, método, control o nombre reservado. ###### `SceneDocument.add_member` ```python SceneDocument.add_member(name='', type_='str', default=None, *, public=True, description='') ``` Declara una propiedad nueva (`public=False` la convierte en estado privado). ###### `SceneDocument.update_member` ```python SceneDocument.update_member(member, **changes) ``` Cambia una propiedad (`name`, `type`, `default`, `public`, `description`); devuelve su nombre. ###### `SceneDocument.set_member_source` ```python SceneDocument.set_member_source(name, source) ``` Pone como valor por defecto de la propiedad `name` la expresión de Python `source` (`[1, 2]`, `Point(x=1)`...). ###### `SceneDocument.remove_member` ```python SceneDocument.remove_member(name) ``` Quita la propiedad `name` de la clase. ###### `SceneDocument.move_member` ```python SceneDocument.move_member(name, offset) ``` Mueve la propiedad `name` `offset` posiciones en el orden de declaración de la clase (dentro de los límites). ###### `SceneDocument.custom_events` ```python SceneDocument.custom_events # property ``` Los eventos que declara el componente, como `{nombre: descripción}` (una copia). ###### `SceneDocument.add_event` ```python SceneDocument.add_event(name='changed', description='') ``` Declara un evento nuevo llamado `name` (hecho único) con `description`; devuelve su nombre. ###### `SceneDocument.update_event` ```python SceneDocument.update_event(name, *, new_name=None, description=None) ``` Renombra el evento `name` a `new_name` (si es válido y está libre) y/o cambia su `description`, manteniendo su posición; devuelve el nombre del evento. Lanza `KeyError` si no existe. ###### `SceneDocument.remove_event` ```python SceneDocument.remove_event(name) ``` Quita de la clase el evento declarado `name`. ###### `SceneDocument.set_class_name` ```python SceneDocument.set_class_name(name) ``` Renombra la clase del componente (convertido en un nombre de clase válido) en el código y la escena; devuelve el nombre nuevo. ###### `SceneDocument.set_description` ```python SceneDocument.set_description(text) ``` Fija la descripción del componente, también como docstring de su clase. ###### `SceneDocument.set_scene` ```python SceneDocument.set_scene(key, value) ``` Fija cómo aparece el componente en la caja de herramientas (`label`, `icon`, `category`) o su tamaño. ###### `SceneDocument.set_binding` ```python SceneDocument.set_binding(control, prop, expression) ``` Hace que `prop` de `control` siga a `expression` (`None` o `""` quita el binding). ###### `SceneDocument.add_method` ```python SceneDocument.add_method(name='do_something', args='self', doc='') ``` Añade a la clase un método reutilizable (no un handler de evento); devuelve su nombre. ##### `Scene` ```python @dataclass class Scene(name='Component1', title='Component1', width=240.0, height=120.0, theme='One Dark', accent=None, background=None, radius=None, font_size=14.0, resizable=False, grid=8, style={}, controls=[], events={}, code=None, animations=[], autoplay='', label='', icon='component', category='Custom', description='', extends='', bindings={}) ``` El layout de un componente diseñado: un `Form` cuyo `name` es el nombre de la clase. `bindings` asocia un nombre de control a `{propiedad del control: expresión}`; la expresión (normalmente una propiedad del componente, como `value`, o cualquier expresión de Python sobre ellas, como `f"{value} stars"`) se evalúa en cada render. **Métodos y propiedades** ###### `Scene.class_name` ```python Scene.class_name # property ``` El nombre de la clase del componente (el `name` de la escena). ###### `Scene.bound` ```python Scene.bound(control) ``` Devuelve los bindings de `control` como `{propiedad: expresión}` (vacío si no tiene). ##### `SceneComponent` ```python class SceneComponent ``` Clase base de los componentes creados en el modo componente del diseñador. `scene` es el archivo `.pzcomp` con el layout, relativo al módulo. La etiqueta, el icono, la categoría, la descripción y el tamaño por defecto en la caja de herramientas salen de la escena salvo que la clase los fije. **Métodos y propiedades** ###### `SceneComponent.scene_path` ```python @classmethod SceneComponent.scene_path() ``` Devuelve el archivo `.pzcomp` al que está ligada la clase, o `None`. ###### `SceneComponent.scene_model` ```python @classmethod SceneComponent.scene_model() ``` La escena tal como está guardada (o previsualizada por el diseñador), o `None`. ###### `SceneComponent.ctx` ```python SceneComponent.ctx # property ``` Un contexto de render para esta instancia, con el tema de su escena (lo que necesita `ControlProxy.focus()`). ##### `render` ```python render(control, ctx) ``` Dibuja `control` a su tamaño (con sus props, o los valores por defecto del componente si faltan). #### Ayudas de layout ##### `align` ```python align(controls, edge, *, to=None) ``` Alinea controles en `edge` (left, center, right, top, middle, bottom). Con `to` (p. ej. el rectángulo del formulario) se alinean a él; si no, a los límites de la selección, o al formulario cuando se pasa un solo control mediante `to`. ##### `distribute` ```python distribute(controls, axis) ``` Reparte tres o más controles con el mismo espacio en `"horizontal"` o `"vertical"`. ##### `same_size` ```python same_size(controls, which='both', *, reference=None) ``` Da a todos los controles el `"width"`, `"height"` o `"both"` de `reference` (el primero). ##### `snap` ```python snap(value, grid, enabled=True) ``` Redondea `value` al múltiplo más cercano de `grid` (sin cambios si está desactivado). ##### `smart_guides` ```python smart_guides(rect, others, size, threshold=4.0) ``` Ajusta un rectángulo en movimiento a los bordes y centros de `others` y del formulario. Devuelve `(dx, dy, lines)`: la corrección a aplicar y las líneas guía a dibujar (como cajas finas `(x1, y1, x2, y2)`), como las guías de alineación de Figma o Qt Designer. ##### `anchored` ```python anchored(control, design, actual) ``` Dónde va un control cuando su formulario pasa del tamaño `design` al tamaño `actual`. Como los anclajes de WinForms: un borde anclado al formulario mantiene su distancia a ese lado; anclado a ambos lados se estira; anclado a ninguno mantiene su posición relativa (los controles centrados siguen centrados). #### App del diseñador ##### `Designer` ```python class Designer ``` El editor visual de formularios. ##### `EditorSettings` ```python @dataclass class EditorSettings(theme='', font_family='', font_size=14.0, tab_size=4, hard_tabs=False, convert_indentation=False, backspace_unindent=True, repeat_delay=500.0, repeat_rate=20.0, show_whitespace=False, wrap=False, line_numbers=True, highlight_line=True, autocomplete=True, code_lens=True, inline_diagnostics=True, format_on_apply=False, auto_close=True, folding=True, minimap=True, cursor_shape='bar', cursor_blink=True, cursor_width=2.0, line_height='standard', scrollbar=True, match_brackets=True, selection_highlight=True, hover=True, sticky_scroll=True, ty=True, ruff=True, keymap={}) ``` Los ajustes del editor de código del diseñador (un dataclass): `theme`, `font_size`, `tab_size`, `hard_tabs`, `convert_indentation`, `backspace_unindent`, `repeat_delay`, `repeat_rate`, `show_whitespace`, `wrap`, `line_numbers`, `highlight_line`, `autocomplete`, `code_lens`, `inline_diagnostics`, `format_on_apply`, `auto_close`, `folding`, `minimap`, `cursor_shape`, `cursor_blink`, `ty` y `ruff`. **Métodos y propiedades** ###### `EditorSettings.key_repeat` ```python EditorSettings.key_repeat # property ``` Los `repeat_delay` y `repeat_rate` del editor (`None` donde se mantiene la repetición de teclas del sistema). ###### `EditorSettings.to_dict` ```python EditorSettings.to_dict() ``` Devuelve los ajustes como un dict listo para JSON. ###### `EditorSettings.from_dict` ```python @classmethod EditorSettings.from_dict(data) ``` Crea los ajustes a partir de `data` guardados, con los valores por defecto para los que falten o no sean válidos, y limitando el tamaño de tabulación, el de fuente y la forma del cursor a valores válidos. ##### `History` ```python class History ``` Instantáneas de un formulario: `push` tras una edición, y luego `undo`/`redo` para moverse entre ellas. **Métodos y propiedades** ###### `History.push` ```python History.push(form, group=None) ``` Guarda el estado actual del formulario; devuelve `False` si nada cambió. ###### `History.steps` ```python History.steps # property ``` Cuántos pasos de deshacer hay (una marca para `merge`). ###### `History.merge` ```python History.merge(mark) ``` Convierte en un solo paso de deshacer todo lo guardado desde `mark` (`steps` antes de una tanda de cambios). ###### `History.reset` ```python History.reset(form) ``` Olvida todos los pasos de deshacer y rehacer y empieza de nuevo desde el estado actual de `form`. ###### `History.can_undo` ```python History.can_undo # property ``` Si hay un paso que deshacer. ###### `History.can_redo` ```python History.can_redo # property ``` Si hay un paso deshecho que rehacer. ###### `History.undo` ```python History.undo() ``` Retrocede una instantánea; devuelve el formulario anterior (un objeto nuevo) o `None` si no hay. ###### `History.redo` ```python History.redo() ``` Avanza una instantánea; devuelve el formulario restaurado (un objeto nuevo) o `None` si no hay. ##### `theme_for` ```python theme_for(form, theme_name=None) ``` El tema con el que se ejecuta un formulario: su tema base más los ajustes de acento, radio, fuente y fondo. ##### `COMMON_EVENTS` Los eventos que cualquier control puede manejar en el panel Eventos del diseñador. ##### `FORM_EVENTS` Los eventos del propio formulario (`load`, `close`, `resize`, `key`...). ### Servicios (pyzed.services) > Referencia de la API de pyzed.services: temporizadores, atajos, vigilantes de archivos, procesos, tareas en segundo plano, peticiones web, portapapeles, ajustes, diálogos de archivos, notificaciones e iconos de bandeja. Source: https://albakore.github.io/pyzed/es/reference/services/ Los servicios lanzan eventos pero no dibujan nada: son los componentes no visuales de Pyzed Design ([la bandeja bajo el formulario](https://albakore.github.io/pyzed/es/editor/non-visual-components/)) y funcionan igual en un `pz.App` o en un script. Los handlers van en `on_=`; los eventos que lanza un hilo en segundo plano les llegan en el hilo de la app. Mira la [guía de servicios](https://albakore.github.io/pyzed/es/guides/services/). ```python watcher = pz.FileWatcher("inbox", pattern="*.csv", on_created=self.import_file) watcher.start() ``` #### Tiempo y teclas ##### `Timer` ```python class Timer ``` Lanza `tick` (con la cuenta) cada `interval` milisegundos mientras está en marcha; con `repeat=False` hace un solo tick. Devuelve `subscriptions()` desde el `subscriptions` de tu app. ##### `Shortcut` ```python class Shortcut ``` Lanza `activate` al pulsar una de sus `keys` (`"ctrl-s"`, `"Ctrl+S"`, varias separadas por comas; `mod-` es Cmd en macOS y Ctrl en el resto). Llama a `handle(combo)` desde `on_key`. #### Archivos y programas ##### `FileWatcher` ```python class FileWatcher ``` Vigila una carpeta o un archivo y lanza `created`, `modified` y `deleted` (una ruta) y `change` (las rutas cambiadas), revisando cada `interval` milisegundos. `start()`, `stop()`, `running`. ##### `Process` ```python class Process ``` Ejecuta un programa y lanza `output` y `error_output` por cada línea que imprime, `finished` con su código de salida, `started` y `failed`. `start()`, `write()`, `kill()`, `wait()`. ##### `Worker` ```python class Worker ``` Ejecuta `work(value)` en un hilo aparte con `run(value)`; `report()` lanza `progress`, el resultado llega en `done` y una excepción en `failed`. `cancel()` activa `cancelled`. #### Red ##### `HttpRequest` ```python class HttpRequest ``` Llama a una dirección web en segundo plano: `send()` lanza `response` con un `HttpResponse` (también con estados de error) o `failed` si no hubo respuesta. `json=` envía JSON. ##### `HttpResponse` ```python @dataclass class HttpResponse(status, content=b'', headers={}, url='') ``` La respuesta de un servidor web: `status`, `ok`, `headers`, `content` (bytes), `text` y `json()`. #### Sistema ##### `Clipboard` ```python class Clipboard ``` El portapapeles del sistema: `copy(text)` y `paste()` devuelven comandos; con `watch=True` lanza `change` cuando se copia algo nuevo. ##### `Settings` ```python class Settings ``` Valores que se guardan entre ejecuciones: un diccionario guardado como JSON en la carpeta de ajustes del usuario en cada cambio, con `defaults`; lanza `change` con `(clave, valor)`. ##### `FileDialog` ```python class FileDialog ``` El diálogo de archivos del sistema: `show()` devuelve el comando que lo abre en el modo `"open"`, `"open_many"`, `"save"` o `"folder"`; lanza `chosen` o `cancelled`. ##### `Notification` ```python class Notification ``` Una notificación de escritorio: `show(text)` la muestra y devuelve si pudo; lanza `shown` o `failed`. ##### `TrayIcon` ```python class TrayIcon ``` Un icono en la bandeja del sistema con `tooltip` y `menu` (una lista, o un texto con un elemento por línea: `-` separador, `[x]` casilla, con sangría un submenú): `show()` devuelve si pudo, `hide()`, `checked(item)`; lanza `click`, `double_click` (Windows), `menu` y `failed`. En Linux es un StatusNotifierItem, sin GTK. ##### `TrayItem` ```python @dataclass class TrayItem(text, id=None, enabled=True, checked=None, items=[]) ``` Un elemento del menú de un `TrayIcon`: `text`, `id` (lo que recibe `menu`), `enabled`, `checked` e `items` para un submenú. #### Sistema operativo (pyzed.system) ##### `SystemInfo` ```python class SystemInfo ``` Qué es el equipo: `os` (`"Windows"`, `"macOS"`, `"Linux"`), `os_version`, `machine`, `hostname`, `user`, `home`, `language`, `cpu_count`, `python_version`, `memory_total`, más `memory_available`, `disk_free` y `disk_total`, que se leen al pedirlos. `env(name)` lee una variable de entorno y `folder("documents")` encuentra las carpetas del usuario. Con `interval` mayor que 0, `start()` lanza `update` con `snapshot()` cada cierto tiempo. ##### `Launcher` ```python class Launcher ``` Abre archivos, carpetas y enlaces con la aplicación que usa el sistema para ellos: `open(target)`, `reveal(path)` (seleccionado en el explorador de archivos) y `email(to, subject, body)`; lanza `opened` o `failed`. ##### `NetworkMonitor` ```python class NetworkMonitor ``` Avisa cuando el equipo se conecta o se desconecta probando `host` cada `interval` milisegundos en segundo plano: `online`, `check()`, `start()`, `stop()`; lanza `change`, `online` y `offline`. ##### `PowerMonitor` ```python class PowerMonitor ``` La batería: `percent`, `plugged` y `has_battery`; cada `interval` milisegundos lanza `change`, `low` (por debajo de `low_level` sin enchufar), `plugged` y `unplugged`. ### Juegos (pyzed.game) > Referencia de la API de pyzed.game: teclas mantenidas, movimiento, colisiones, sprites de píxeles, partículas y temblor de pantalla para juegos en un pz.canvas. Source: https://albakore.github.io/pyzed/es/reference/game/ `pyzed.game` reúne pequeñas ayudas para juegos en tiempo real: teclas mantenidas, movimiento, colisiones, sprites de píxeles, partículas y temblor de pantalla. Son Python puro sobre [`pyzed.draw`](https://albakore.github.io/pyzed/es/reference/draw/), así que también funcionan en capturas y tests. Mira [Crear videojuegos](https://albakore.github.io/pyzed/es/games/) para ver ejemplos completos. ```python from pyzed import draw, game def tick(self, dt): # pz.frames(self.tick) en subscriptions() self.x += game.axis(("left", "a"), ("right", "d")) * 300 * dt self.chispas.update(dt) def view(self): return pz.canvas([draw.background("#0b0e14"), *game.glow(self.x, 200, 18, "#74ade8"), *self.chispas.shapes()], width=400, height=300) ``` #### Teclado ##### `pressed` ```python pressed() ``` Las teclas pulsadas ahora mismo, como un `frozenset` de nombres sin modificadores (`"left"`, `"space"`, `"a"`…). También está disponible como `pz.pressed()`. ##### `is_pressed` ```python is_pressed(*keys, held=None) ``` Si alguna de las teclas de `keys` está pulsada (`held` sustituye a `pressed()`, para tests). También está disponible como `pz.is_pressed()`. ##### `axis` ```python axis(negative, positive, *, held=None) ``` `-1`, `0` o `1` a partir de dos grupos de teclas: `axis(("left", "a"), ("right", "d"))`. ##### `vector` ```python vector( left=('left', 'a'), right=('right', 'd'), up=('up', 'w'), down=('down', 's'), *, held=None, ) ``` Una dirección `(x, y)` de longitud 0 o 1 a partir de cuatro grupos de teclas (flechas y WASD por defecto); las diagonales se normalizan. #### Cálculo ##### `clamp` ```python clamp(value, low, high) ``` `value` limitado a `[low, high]`. ##### `lerp` ```python lerp(a, b, t) ``` El valor que está a una fracción `t` del camino de `a` a `b` (`t` de 0 a 1). #### Colisiones ##### `Rect` ```python @dataclass class Rect(x, y, w, h) ``` Una caja alineada con los ejes (`x`, `y` es la esquina superior izquierda) con `left`, `right`, `top`, `bottom`, `center`, `overlaps(other)`, `contains(x, y)`, `moved(dx, dy)` y `shape(**paint)` (un `draw.rect`). ##### `circle_rect` ```python circle_rect(cx, cy, r, rect) ``` Dónde choca un círculo con un `Rect`: la dirección hacia la que empujarlo `(nx, ny)`, o `None`. Una pelota rebota con `if nx: vx = abs(vx) * nx` y `if ny: vy = abs(vy) * ny`. #### Dibujo ##### `rgba` ```python rgba(color, alpha=1.0) ``` `color` (`"#rrggbb[aa]"` o una tupla) como una tupla `(r, g, b, a)` con `alpha` aplicado. ##### `glow` ```python glow(x, y, radius, color, *, layers=4, strength=0.5, core=True) ``` Una luz suave: círculos translúcidos que crecen alrededor de `(x, y)` (bloom en un lienzo). `strength` es la opacidad del halo interior; `core=False` omite el círculo sólido. ##### `sprite` ```python sprite(rows, palette, x=0.0, y=0.0, *, scale=1.0, flip=False) ``` Pixel art a partir de texto: cada carácter de `rows` es un píxel con el color de `palette` (los caracteres que no están son transparentes). Una forma por color, así que un sprite sigue siendo barato sea cual sea su tamaño. #### Efectos ##### `Particles` ```python class Particles ``` Chispas, humo, explosiones: puntos de vida corta que se mueven, caen y se desvanecen. Llama a `burst(x, y, …)` cuando pase algo, a `update(dt)` en cada fotograma y dibuja `shapes()`; `gravity`, `drag` y `limit` los ajustan. ##### `Shake` ```python class Shake ``` Temblor de pantalla: `kick(8)` al recibir un golpe, `update(dt)` en cada fotograma, y empieza las formas del lienzo con `*shake.shapes()` (un translate) para que todo se mueva a la vez. ### Grafos de shaders (pyzed.shading) > Referencia de la API de pyzed.shading: carga grafos de shaders .pzshad, cambia sus variables y crea grafos de nodos en código que se compilan al WGSL de pz.shader. Source: https://albakore.github.io/pyzed/es/reference/shading/ `pyzed.shading` es el sistema de shaders por nodos que hay detrás de la [vista de shaders](https://albakore.github.io/pyzed/es/shading/shader-designer/) del diseñador: grafos `.pzshad` de nodos que se compilan al WGSL de [`pz.shader`](https://albakore.github.io/pyzed/es/reference/widgets/). Mira [Grafos de shaders](https://albakore.github.io/pyzed/es/shading/shader-graphs/) para un recorrido. ```python from pyzed import shading fuego = shading.load("shaders/fuego.pzshad") fuego["speed"] = 2.0 # una variable mutable: no recompila pz.shader(fuego, width=320, height=200) # o pz.shader("shaders/fuego.pzshad") ``` #### Usar shaders ##### `load` ```python load(path, *, fresh=False) ``` El `Shader` de un archivo `.pzshad`, en caché por ruta (`fresh=True` lo vuelve a leer). Se recarga solo cuando el archivo cambia. ##### `Shader` ```python class Shader ``` Un grafo de shader listo para dibujar, con sus propios valores de las variables: `shader["speed"] = 2`, `shader.set(...)`, `shader.variables`, `shader.wgsl` y `shader.widget(...)`. `pz.shader` lo acepta directamente. ##### `widget` ```python widget(source, **options) ``` Un widget `pz.shader` para una ruta `.pzshad`, un `Graph` o un `Shader`. #### Crear grafos ##### `Graph` ```python class Graph ``` Un grafo de shader: variables, nodos y las conexiones entre ellos. Llama a un tipo de nodo sobre él para añadirlo (`g.noise(scale=4)`, `g.sin(x)`), elige salidas con `nodo["nombre"]` y combínalas con aritmética. `save`, `compile`, `arrange`, `connect` y los métodos de variables lo editan. ##### `new` ```python new(name='shader') ``` Un grafo nuevo con UV alimentando un degradado hacia el nodo Output: la plantilla Blank del diseñador. ##### `load_graph` ```python load_graph(path) ``` El `Graph` de un archivo `.pzshad`, para editarlo. ##### `Variable` ```python @dataclass class Variable(name, type='float', value=None, mutable=True, min=None, max=None, step=None, description='') ``` Una variable de un shader: `name`, `type` (float, int, bool, vec2, vec3, vec4, color, texture), `value`, `mutable` (un uniform) o constante, `min`, `max`, `step` y `description`. ##### `Node` ```python @dataclass class Node(id, type, x=0.0, y=0.0, inputs={}, params={}, title='', preview=True) ``` Un nodo de un grafo: su `type`, su posición, `inputs` (valores fijos de las entradas sin conectar), `params` (ajustes), `title` y si se muestra su `preview`. ##### `NodeRef` ```python class NodeRef ``` Lo que devuelve `Graph.add`: `ref["puerto"]` es una salida para conectar en otro sitio; la aritmética usa su primera salida. ##### `GraphError` ```python class GraphError ``` Se lanza con conexiones imposibles (tipos, ciclos), tipos de nodo desconocidos y nombres de variable no válidos. #### Compilar ##### `compile` ```python compile(graph, target=None, *, values=None) ``` Compila un grafo (o la salida de un nodo, para su vista previa) a WGSL: el código `Compiled`, uniforms, texturas y problemas por nodo. ##### `Compiled` ```python @dataclass class Compiled(code, uniforms={}, textures={}, names={}, errors={}, lines={}, uses_time=False) ``` El resultado de `compile`: `code`, `uniforms`, `textures`, `names` (el uniform de cada variable), `errors` por id de nodo, `uses_time`, y `check()`, que ejecuta el compilador de la GPU y pone sus mensajes en el nodo que escribió la línea. #### El catálogo de nodos ##### `register` ```python register(node_type, *, preset=True) ``` Añade un tipo de nodo propio al catálogo (y a la barra Shading del diseñador). ##### `NodeType` ```python @dataclass class NodeType(id, title, category, description, inputs=(), outputs=(), params=(), exprs={}, functions=(), min_type='bool', build=None, ports=None, resolve=None, label=None, icon='', keywords='') ``` Un tipo de nodo: id, título, categoría, entradas, salidas, ajustes y cómo escribe su WGSL. ##### `Preset` ```python @dataclass class Preset(type, title, category, description, params={}, keywords='') ``` Una entrada de la barra Shading: un tipo de nodo con algunos ajustes (`func:sin` es el nodo Function con sin). ## Ejemplos ### Ejemplos > Apps de ejemplo de Pyzed listas para ejecutar: listas de tareas, un editor de código, el Juego de la vida, reloj, cronómetro, varias ventanas y datos en vivo. Source: https://albakore.github.io/pyzed/es/examples/ Todos los ejemplos están en [`examples/`](https://github.com/albakore/pyzed/tree/main/examples). Ejecútalos desde la raíz del repositorio: .py `} python={` {py} -m pip install pyzed-ui {py} examples/.py `} /> | Ejemplo | Qué muestra | Tutorial | | --- | --- | --- | | [`counter.py`](https://github.com/albakore/pyzed/blob/main/examples/counter.py) | Lo mínimo: estado, mensajes y callables. | [Inicio rápido](https://albakore.github.io/pyzed/es/getting-started/quickstart/) | | [`todo.py`](https://github.com/albakore/pyzed/blob/main/examples/todo.py) | Formularios, listas, filtros, foco y atajos. | [Lista de tareas](https://albakore.github.io/pyzed/es/tutorials/basic/todo-app/) | | [`downloads.py`](https://github.com/albakore/pyzed/blob/main/examples/downloads.py) | `Command.perform`, `pz.post` desde hilos, progreso y notificaciones. | [Tareas en segundo plano](https://albakore.github.io/pyzed/es/tutorials/intermediate/background-tasks/) | | [`mini_editor.py`](https://github.com/albakore/pyzed/blob/main/examples/mini_editor.py) | Un editor de código con árbol de archivos y pestañas en ~150 líneas. | [Mini editor](https://albakore.github.io/pyzed/es/tutorials/advanced/code-editor/) | | [`gallery.py`](https://github.com/albakore/pyzed/blob/main/examples/gallery.py) | Todos los componentes, interactivos, con selector de tema. | [Componentes](https://albakore.github.io/pyzed/es/components/overview/) | | [`zed_editor.py`](https://github.com/albakore/pyzed/blob/main/examples/zed_editor.py) | Editor completo: buscador de archivos, paleta, docks, terminal, temas y marcadores de git. | | | [`tutorial/`](https://github.com/albakore/pyzed/tree/main/examples/tutorial/) | Cinco pasos: ventana, estado, layout y temas, async, app de notas. | [Tutorial en 5 pasos](https://albakore.github.io/pyzed/es/tutorials/basic/step-by-step/) | | [`todos.py`](https://github.com/albakore/pyzed/blob/main/examples/todos.py) | Las *todos* de Iced: añadir, editar, completar, filtrar y guardar. | | | [`game_of_life.py`](https://github.com/albakore/pyzed/blob/main/examples/game_of_life.py) | `pz.canvas` con miles de celdas y `pz.every` como motor. | [Canvas](https://albakore.github.io/pyzed/es/guides/canvas/) | | [`games/snake.py`](https://github.com/albakore/pyzed/blob/main/examples/games/snake.py) | Snake con temporizador fijo, cola de teclas y tarjeta de cristal de game over. | [Snake](https://albakore.github.io/pyzed/es/games/snake/) | | [`games/breakout.py`](https://github.com/albakore/pyzed/blob/main/examples/games/breakout.py) | Bucle a 60 fps con `pz.frames`, teclas mantenidas, colisiones, partículas y temblor. | [Breakout](https://albakore.github.io/pyzed/es/games/breakout/) | | [`games/neon_invaders.py`](https://github.com/albakore/pyzed/blob/main/examples/games/neon_invaders.py) | Un shooter espacial: sprites pixel art, brillo neón y un HUD con cristal y bloom. | [Neon Invaders](https://albakore.github.io/pyzed/es/games/neon-invaders/) | | [`clock.py`](https://github.com/albakore/pyzed/blob/main/examples/clock.py) | Reloj analógico dibujado en un canvas. | [Canvas](https://albakore.github.io/pyzed/es/guides/canvas/) | | [`stopwatch.py`](https://github.com/albakore/pyzed/blob/main/examples/stopwatch.py) | Cronómetro con vueltas y atajos. | [Async](https://albakore.github.io/pyzed/es/guides/async/) | | [`download_progress.py`](https://github.com/albakore/pyzed/blob/main/examples/download_progress.py) | Descargas concurrentes con generadores `async` y progreso. | [Async](https://albakore.github.io/pyzed/es/guides/async/) | | [`websocket_stream.py`](https://github.com/albakore/pyzed/blob/main/examples/websocket_stream.py) | Datos en vivo con `pz.listen` y un generador `async`. | [Async](https://albakore.github.io/pyzed/es/guides/async/) | | [`multi_window.py`](https://github.com/albakore/pyzed/blob/main/examples/multi_window.py) | Varias `pz.Window` con estado compartido y temas distintos. | [Varias ventanas](https://albakore.github.io/pyzed/es/guides/windows/) | | [`styling.py`](https://github.com/albakore/pyzed/blob/main/examples/styling.py) | Selector de temas, modo del sistema, `Theme.generate` y acentos. | [Temas](https://albakore.github.io/pyzed/es/theming/themes/) | | [`image_viewer.py`](https://github.com/albakore/pyzed/blob/main/examples/image_viewer.py) | `pz.image` y el visor con zoom `pz.image_viewer`. | [Contenido](https://albakore.github.io/pyzed/es/components/content/#imágenes) | | [`markdown_viewer.py`](https://github.com/albakore/pyzed/blob/main/examples/markdown_viewer.py) | Editor de Markdown con vista previa en vivo. | [Contenido](https://albakore.github.io/pyzed/es/components/content/) | | [`qr_code.py`](https://github.com/albakore/pyzed/blob/main/examples/qr_code.py) | Genera un código QR mientras escribes. | [Contenido](https://albakore.github.io/pyzed/es/components/content/) | | [`panes.py`](https://github.com/albakore/pyzed/blob/main/examples/panes.py) | Paneles redimensionables con `ui.split` anidados. | [Contenido](https://albakore.github.io/pyzed/es/components/content/#paneles-redimensionables) | | [`context_menu.py`](https://github.com/albakore/pyzed/blob/main/examples/context_menu.py) | Menú contextual con `ui.popover` y `ui.menu`. | [Contenido](https://albakore.github.io/pyzed/es/components/content/#popover) | | [`screenshots.py`](https://github.com/albakore/pyzed/blob/main/examples/screenshots.py) | Regenera las capturas de la documentación. | [Tests y capturas](https://albakore.github.io/pyzed/es/guides/testing/) | #### Integraciones [`examples/integrations/`](https://github.com/albakore/pyzed/tree/main/examples/integrations) reúne apps de escritorio hechas con librerías de backend y de datos. Cada una es un proyecto de uv: entra en su carpeta y ejecuta `uv run main.py`. | Ejemplo | Librerías | Tutorial | | --- | --- | --- | | [`fastapi_heroes`](https://github.com/albakore/pyzed/tree/main/examples/integrations/fastapi_heroes) | FastAPI, SQLModel, httpx | [FastAPI](https://albakore.github.io/pyzed/es/integrations/fastapi/) | | [`django_polls`](https://github.com/albakore/pyzed/tree/main/examples/integrations/django_polls) | Django, httpx | [Django](https://albakore.github.io/pyzed/es/integrations/django/) | | [`sqlalchemy_quickstart`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlalchemy_quickstart) | SQLAlchemy 2.0, aiosqlite | [SQLAlchemy](https://albakore.github.io/pyzed/es/integrations/sqlalchemy/) | | [`sqlalchemy_contacts`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlalchemy_contacts) | SQLAlchemy 2.0 | [SQLAlchemy](https://albakore.github.io/pyzed/es/integrations/sqlalchemy/#muchas-filas-la-demo-de-contactos) | | [`sqlmodel_heroes`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlmodel_heroes) | SQLModel | [SQLModel](https://albakore.github.io/pyzed/es/integrations/sqlmodel/) | | [`sqlmodel_inventory`](https://github.com/albakore/pyzed/tree/main/examples/integrations/sqlmodel_inventory) | SQLModel, Pydantic | [SQLModel](https://albakore.github.io/pyzed/es/integrations/sqlmodel/#un-modelo-para-el-formulario-y-la-tabla-la-demo-de-inventario) | | [`httpx_pypi`](https://github.com/albakore/pyzed/tree/main/examples/integrations/httpx_pypi) | httpx, Pydantic | [httpx](https://albakore.github.io/pyzed/es/integrations/httpx/) | | [`httpx_download`](https://github.com/albakore/pyzed/tree/main/examples/integrations/httpx_download) | httpx | [httpx](https://albakore.github.io/pyzed/es/integrations/httpx/#descargas-en-streaming-con-progreso) | | [`pydantic_signup`](https://github.com/albakore/pyzed/tree/main/examples/integrations/pydantic_signup) | Pydantic | [Pydantic](https://albakore.github.io/pyzed/es/integrations/pydantic/) | | [`aiosqlite_journal`](https://github.com/albakore/pyzed/tree/main/examples/integrations/aiosqlite_journal) | aiosqlite | [Otras librerías](https://albakore.github.io/pyzed/es/integrations/other-libraries/#aiosqlite) | #### Editor de código Atajos: Ctrl+Shift+P paleta, Ctrl+P buscador de archivos, Ctrl+S guardar, Ctrl+B, Ctrl+J y Ctrl+Alt+B docks. (Image: Un editor de código hecho con Pyzed, en One Dark) | Paleta de comandos | Buscador de archivos (Gruvbox) | | --- | --- | | (Image: Paleta) | (Image: Buscador) | #### Galería de componentes (Image: Galería en One Light) #### Descargas (Image: Descargas) #### Demos de Iced Recreaciones en Python de las [demos de Iced](https://github.com/iced-rs/iced/tree/master/examples). | Game of Life | Estilos | | --- | --- | | (Image: Game of Life) | (Image: Estilos) | | Reloj | Cronómetro | | --- | --- | | (Image: Reloj) | (Image: Cronómetro) | | Todos | Código QR | | --- | --- | | (Image: Todos) | (Image: Código QR) | | Visor de imágenes | Markdown | | --- | --- | | (Image: Visor de imágenes) | (Image: Markdown) | ##### Varias ventanas (Image: Varias ventanas) #### IA y Bloques | Ejemplo | Qué muestra | Página | | --- | --- | --- | | [`ai_chat.py`](https://github.com/albakore/pyzed/blob/main/examples/ai_chat.py) | Un chat con IA en streaming sobre cualquier API compatible con OpenAI, con un modo demo que no necesita red. | [Crear apps con IA](https://albakore.github.io/pyzed/es/ai/ai-apps/) | | [`pomodoro.py`](https://github.com/albakore/pyzed/blob/main/examples/pomodoro.py) | El temporizador que construye un agente con las herramientas MCP en la guía de prompts. | [Guía de prompts](https://albakore.github.io/pyzed/es/ai/prompting/) | | [`blocks/`](https://github.com/albakore/pyzed/tree/main/examples/blocks) | Nueve pantallas listas: login, dashboard, ajustes, tabla de datos, chat, IDE, bienvenida, reproductor de música y kanban. | [Bloques](https://albakore.github.io/pyzed/es/blocks/) | Las capturas de esta página se generan con `python docs/scripts/example_shots.py`. ## Preguntas frecuentes ### Preguntas frecuentes > Preguntas frecuentes de Pyzed: qué es, cómo se compara con tkinter, Kivy, PyQt y Flet, licencia, plataformas, rendimiento, diseñador visual y agentes de IA. Source: https://albakore.github.io/pyzed/es/faq/ Respuestas cortas a las preguntas más habituales sobre Pyzed. Cada una enlaza a la página que lo explica en detalle. #### ¿Qué es Pyzed? Pyzed es un framework de código abierto para crear aplicaciones de escritorio nativas en Python. Tu código Python describe la interfaz; [Iced](https://iced.rs), un toolkit de GUI escrito en Rust, la organiza y la dibuja con la GPU (o con un renderizador por software si no la hay). Incluye más de 90 componentes inspirados en el editor [Zed](https://zed.dev) (pestañas, árbol de archivos, paleta de comandos, editor de código, terminal, tablas, gráficos, diálogos, un workspace con docks) y Pyzed Design, un diseñador visual de arrastrar y soltar. Mira la [introducción](https://albakore.github.io/pyzed/es/getting-started/introduction/). #### ¿Pyzed tiene relación con el paquete pyzed de las cámaras ZED? No. Pyzed (`pip install pyzed-ui`, `import pyzed as pz`) es un framework de GUI de escritorio para Python, y su nombre viene del editor [Zed](https://zed.dev), que inspira sus componentes. El módulo `pyzed` del SDK de ZED de Stereolabs (`pyzed.sl`) es una biblioteca distinta para las cámaras estéreo ZED. Los dos se importan como `pyzed`, así que no se pueden instalar en el mismo entorno. #### ¿Cómo instalo Pyzed? Ejecuta `pip install pyzed-ui` o `uv add pyzed-ui` y luego `import pyzed as pz`. El paquete se llama `pyzed-ui` en PyPI pero se importa como `pyzed`. Necesita Python 3.10 o superior, y los wheels precompilados para Windows, macOS y Linux incluyen el motor en Rust, así que no hay nada que compilar. Mira [Instalación](https://albakore.github.io/pyzed/es/getting-started/installation/). #### ¿Pyzed es gratis y de código abierto? Sí. Pyzed y Pyzed Design son gratuitos y se publican con licencia MIT, así que puedes usarlos en aplicaciones comerciales y de código cerrado. El código está en [GitHub](https://github.com/albakore/pyzed). #### ¿Qué plataformas soporta Pyzed? Windows (x64 y ARM), macOS (Intel y Apple Silicon) y Linux (x86_64 y ARM, con glibc 2.28 o superior, y musl para Alpine). En Alpine el motor de audio solo renderiza sin dispositivos de sonido. Móviles y web todavía no son objetivos. Mira [Estabilidad y limitaciones conocidas](https://albakore.github.io/pyzed/es/guides/stability/). #### ¿En qué se diferencia Pyzed de tkinter? tkinter viene con Python y sirve para utilidades pequeñas, pero sus widgets se ven anticuados, el estilo es limitado y los layouts complejos requieren mucho código. Pyzed dibuja una interfaz moderna y con temas usando la GPU, trae componentes de alto nivel como un editor de código, tablas, árboles, paleta de comandos y notificaciones, tiene un diseñador visual y usa un modelo declarativo de `view()` y estado en lugar de modificar widgets a mano. tkinter no necesita instalación; Pyzed necesita `pip install pyzed-ui`. #### ¿En qué se diferencia Pyzed de Kivy? Kivy está pensado para apps táctiles y móviles y usa su propio lenguaje KV. Pyzed está pensado para apps de escritorio (interfaces densas y manejadas con teclado, como editores, paneles y herramientas), se escribe en Python puro sin otro lenguaje y renderiza a través de Rust e Iced. Si necesitas Android o iOS, hoy Kivy encaja mejor. #### ¿En qué se diferencia Pyzed de PyQt y PySide? Qt es un toolkit de C++ grande y maduro, con widgets de aspecto nativo y una API enorme. Pyzed es mucho más pequeño y sencillo: describes la interfaz a partir de tu estado con funciones de Python, los estilos funcionan como CSS y los componentes tienen un aspecto moderno desde el principio. Pyzed tiene licencia MIT, mientras que PyQt es GPL o comercial y PySide es LGPL. Qt sigue teniendo más widgets, soporte de accesibilidad y años de uso en producción. #### ¿En qué se diferencia Pyzed de Flet? Flet construye la interfaz con Flutter y la controla desde Python, y además puede apuntar a web y móviles. Pyzed ejecuta la interfaz en el mismo proceso mediante una extensión de Rust, sin cliente aparte, y se centra en apps de escritorio con aspecto de IDE, un diseñador visual y un componente de editor de código. #### ¿Pyzed es rápido? El layout, el texto, el resaltado de sintaxis y el dibujo ocurren en Rust, y el árbol de widgets que devuelve tu `view()` está hecho de objetos de Rust inmutables y baratos. El GIL se libera mientras corre el bucle de eventos, así que tus hilos de Python siguen trabajando. Para muchos datos, las listas y tablas virtuales muestran cientos de miles de filas y `@pz.memo` evita reconstruir las partes de la vista que no cambiaron. Mira [Rendimiento](https://albakore.github.io/pyzed/es/guides/performance/) y [Cómo funciona](https://albakore.github.io/pyzed/es/guides/how-it-works/). #### ¿Hay un diseñador visual de interfaces? Sí. [Pyzed Design](https://albakore.github.io/pyzed/es/editor/) te permite dibujar ventanas y componentes reutilizables arrastrando y soltando, editar sus propiedades y estilos, escribir los manejadores de eventos en un editor de Python con autocompletado y diagnósticos, y ejecutarlos con F5. Los formularios se guardan como un archivo `.pzform` legible más un archivo de Python. [Descárgalo](https://albakore.github.io/pyzed/es/download/) para Windows, macOS o Linux. #### ¿Puedo hacer interfaces con efectos y animaciones? Sí. Cualquier widget se puede animar con presets, keyframes y transiciones, y los controles pueden tener rotación, desenfoque, glow, bloom, glass y opacidad, con temas claros, oscuros y propios. Mira [Animación y efectos](https://albakore.github.io/pyzed/es/animation/) y [Temas](https://albakore.github.io/pyzed/es/theming/themes/). #### ¿Pyzed funciona con agentes de IA para programar? Sí. `pyzed mcp` (instala `pyzed-ui[mcp]`) es un [servidor MCP](https://albakore.github.io/pyzed/es/guides/mcp/) que da a agentes como Cursor, VS Code o Zed la firma exacta de cada componente, esta documentación, buenas prácticas, comprobación de código y capturas del resultado. Esta documentación también se publica para modelos de lenguaje en [llms.txt](https://albakore.github.io/pyzed/es/llms.txt) y [llms-full.txt](https://albakore.github.io/pyzed/es/llms-full.txt), y Pyzed Design tiene un [panel del agente](https://albakore.github.io/pyzed/es/editor/ai-agents/) integrado. #### ¿Puedo añadir funciones de IA a mi propia app? Sí. [`pyzed.agents`](https://albakore.github.io/pyzed/es/guides/agents/) habla con Anthropic, OpenAI, Gemini, OpenRouter, Ollama y servidores compatibles con OpenAI, deja que el modelo llame a tus funciones de Python como herramientas, transmite su respuesta a la interfaz y guarda las claves de API en el llavero del sistema. #### ¿Cómo distribuyo una app de Pyzed? `pyzed build` empaqueta un proyecto como app independiente (una carpeta o un solo archivo) para Windows, macOS o Linux que funciona en equipos sin Python. Usa PyInstaller por debajo; compila cada plataforma en esa plataforma o en una matriz de CI. Mira [Empaquetado](https://albakore.github.io/pyzed/es/packaging/build/). #### ¿Puedo probar una app de Pyzed sin ventana? Sí. `app.screenshot(path)` renderiza el estado actual fuera de pantalla con el renderizador por software, sin pantalla ni GPU, así que funciona en CI. Tu estado y tus manejadores son Python normal y se prueban como cualquier otro código. Mira [Tests](https://albakore.github.io/pyzed/es/guides/testing/). #### ¿Pyzed soporta lectores de pantalla? Todavía no. Pyzed respeta el ajuste de reducir movimiento del sistema y admite atajos de teclado, pero el soporte de lectores de pantalla depende de Iced y aún falta. Mira [Accesibilidad](https://albakore.github.io/pyzed/es/guides/accessibility/). #### ¿Necesito saber Rust? No. Solo escribes Python: clases, funciones y lambdas. Rust solo hace falta si compilas Pyzed desde el código fuente o [contribuyes](https://albakore.github.io/pyzed/es/contributing/) con un widget nativo nuevo. ## Contribuir ### Contribuir a Pyzed > Contribuye a Pyzed: prepara un entorno de desarrollo con Python y Rust, ejecuta las comprobaciones de la CI y sigue las convenciones de commits y PRs. Source: https://albakore.github.io/pyzed/es/contributing/ ¡Gracias por ayudar! Esta sección es la guía de desarrollo de Pyzed: cómo compilarlo desde el código fuente, cómo está organizado, cómo [añadir un componente al editor visual](https://albakore.github.io/pyzed/es/contributing/adding-components/), cómo [documentarlo](https://albakore.github.io/pyzed/es/contributing/documenting-components/) y cómo [editar esta web](https://albakore.github.io/pyzed/es/contributing/docs/). La versión corta también está en [`CONTRIBUTING.md`](https://github.com/albakore/pyzed/blob/main/CONTRIBUTING.md), en la raíz del repositorio. #### Entorno de desarrollo Necesitas **Python 3.10+**, **Rust 1.88+** (`rustup update` actualiza una toolchain más antigua) y, en Linux, las bibliotecas de desarrollo habituales de X11/Wayland. Pyzed es una extensión de Rust (PyO3 + Iced) con un paquete de Python encima, compilada con [maturin](https://www.maturin.rs). Con [uv](https://docs.astral.sh/uv/): ```bash git clone https://github.com/albakore/pyzed cd pyzed uv sync # crea .venv, compila el núcleo de Rust (release) e instala pyzed editable uv run pytest tests uv run python examples/gallery.py uv run pyzed design # el editor visual, Pyzed Design ``` Con pip: ```bash python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install maturin pytest ty maturin develop --release # compila el núcleo de Rust e instala pyzed en el venv pytest tests ``` - Compila siempre con `--release`: las compilaciones de depuración de Iced son demasiado lentas para usar una ventana real. - En Linux, una build desde el código no suena por defecto, así que compila sin paquetes extra del sistema: `pyzed.audio` renderiza offline y con su reloj silencioso. Para dispositivos de audio y MIDI, instala las cabeceras de ALSA (`sudo apt install libasound2-dev pkg-config`; Fedora: `sudo dnf install alsa-lib-devel`) y compila con `maturin develop --release --features alsa` (con uv, deja exportado `MATURIN_PEP517_ARGS="--features alsa"` y ejecuta `uv sync --reinstall-package pyzed-ui`). - Los cambios en los archivos de Python de `python/pyzed/` se aplican directamente (el paquete se instala en modo editable). - Después de cambiar código de **Rust** en `src/`, vuelve a compilar: `maturin develop --release` (con uv, el siguiente `uv run` o `uv sync` recompila el núcleo solo). #### Comprobaciones Ejecuta las mismas comprobaciones que la CI antes de hacer push: ```bash cargo fmt --check # formato de Rust cargo clippy -- -D warnings # lints de Rust; los avisos son errores pytest tests # o: uv run pytest tests ``` Los tests se ejecutan sin pantalla: las ventanas se dibujan con `App.screenshot` en archivos PNG, así que no necesitan GPU ni display. `tests/test_visual.py` compara capturas con imágenes de referencia; si cambias el aspecto de un componente, regenéralas con `pytest tests/test_visual.py --update-snapshots` (mira [Tests](https://albakore.github.io/pyzed/es/contributing/testing/)). Opcionales: ```bash uv run ty check # comprueba los tipos del paquete de Python python docs/scripts/gen_api.py --check # nombres públicos sin documentación en la API ``` La CI ejecuta el lint, los tests en Linux, macOS y Windows, una vista previa de las notas de la versión y la comprobación del **título del PR**. Los cambios que solo tocan la documentación no compilan el paquete. #### La web de la documentación La documentación es una web de [Astro Starlight](https://starlight.astro.build) en `docs/`: ```bash cd docs npm install npm run dev # http://localhost:4321/pyzed/ npm run build # la web estática en docs/dist (lo que publica GitHub Pages) ``` Los dos scripts ejecutan primero `scripts/sync-changelog.mjs`, que convierte `CHANGELOG.md` en la página [Novedades](https://albakore.github.io/pyzed/es/changelog/). Lee [Editar la documentación](https://albakore.github.io/pyzed/es/contributing/docs/) para la estructura de la web, las traducciones y los scripts que regeneran la referencia y las imágenes. #### Arquitectura del proyecto ```mermaid flowchart LR subgraph RS["src/ (Rust, PyO3 + Iced)"] W["widget.rs
Widget, Kind"] --> R["render.rs
Kind → iced::Element"] A["app.rs
mensajes, Command, run, screenshot"] end subgraph PY["python/pyzed/"] UI["ui/
componentes listos"] --> CORE["pyzed._core"] D["designer/
Pyzed Design"] --> UI APP["app.py, theme.py,
component.py…"] --> CORE end CORE --> W ``` | Ruta | Qué contiene | | --- | --- | | `src/` | Núcleo en Rust (PyO3 + Iced): el árbol de widgets, el puente con el renderer, el runtime y el editor de código | | `python/pyzed/` | Paquete de Python: `App`, temas, iconos, `pz.Component`, CLI | | `python/pyzed/ui/` | Los componentes listos para usar (`ui.button`, `ui.select`, `ui.table`…), funciones que devuelven widgets | | `python/pyzed/designer/` | El editor visual: modelo del formulario, registro de la caja de herramientas, comandos del documento, runtime, paneles | | `tests/` | Tests de pytest (sin pantalla ni GPU) | | `examples/` | Ejemplos ejecutables y el código de los tutoriales | | `docs/` | Esta web, con sus scripts generadores en `docs/scripts/` | | `scripts/` | Scripts de publicación y mantenimiento | Lee [Cómo funciona](https://albakore.github.io/pyzed/es/guides/how-it-works/) para el puente entre Python e Iced y [Pyzed Design → Archivos, plantillas y scripts](https://albakore.github.io/pyzed/es/editor/files-and-scripting/) para las clases del editor. #### Commits y pull requests Pyzed sigue el estilo de [FastAPI](https://github.com/fastapi/fastapi): cada commit en `main`, es decir, cada **título de PR**, empieza con un [gitmoji](https://gitmoji.dev) seguido de una frase corta en imperativo, en inglés y sin punto final: ```text ✨ Add a Rating component to the designer 🐛 Fix line numbers when the editor wraps lines 📝 Document the Rating component ``` Los PR se integran con **Squash and merge**, así que el título se convierte en el commit de `main` y en la línea de las notas de la versión. El emoji decide la sección de las notas: | Emoji | Úsalo para | Sección de las notas | | --- | --- | --- | | 💥 | Un cambio incompatible (explícalo también en el PR) | Breaking Changes | | ✨ | Una funcionalidad, widget o componente nuevo | Features | | 🐛 🚑️ 🔒️ | Una corrección, un hotfix crítico, una corrección de seguridad | Fixes | | ♻️ ⚡️ 🎨 🔥 | Refactorizaciones, rendimiento, estructura, eliminar código | Refactors | | ⬆️ ⬇️ 📌 | Actualizar, bajar de versión o fijar una dependencia | Upgrades | | 📝 ✏️ | Documentación, erratas | Docs | | 🌐 | Traducciones de la documentación | Translations | | ✅ 👷 💚 🔧 🔨 | Tests, CI, configuración, scripts de desarrollo | Internal | El workflow **PR title** (`.github/workflows/pr-title.yml`) rechaza los títulos que no empiezan con un gitmoji (o un shortcode como `:sparkles:`) y un espacio. El flujo de un cambio: 1. Haz un fork del repositorio y crea una rama desde `main` (`feature/rating`, `fix/editor-scroll`, `docs/tutorial-typo`). Para cualquier cosa más grande que una corrección pequeña, abre antes un issue. 2. Mantén cada PR centrado en un solo cambio. Añade o actualiza tests para los cambios de comportamiento y documentación para los que ve el usuario. 3. Ejecuta `cargo fmt --check`, `cargo clippy -- -D warnings` y `pytest tests`. 4. Abre el PR con un título con gitmoji y completa la plantilla: qué cambia, cómo se probó (capturas para los cambios de interfaz) y la lista de comprobación. Enlaza el issue que cierra (`Closes #123`). 5. Sube los commits de seguimiento a la misma rama; no hace falta que los juntes tú. Los commits deben ser de tu autoría: no añadas trailers `Co-authored-by` ni pies de «generado por». #### Siguientes pasos - [Añadir un componente al editor](https://albakore.github.io/pyzed/es/contributing/adding-components/): de una función de `pyzed.ui` a una entrada de la caja de herramientas con propiedades, eventos y tests, con un ejemplo completo. - [Documentar un componente](https://albakore.github.io/pyzed/es/contributing/documenting-components/): docstrings, referencia de la API, biblioteca de componentes, capturas, traducciones y el changelog. - [Editar la documentación](https://albakore.github.io/pyzed/es/contributing/docs/): cómo está organizada y cómo se compila esta web. - [Tests](https://albakore.github.io/pyzed/es/contributing/testing/): probar componentes de `ui` y los tests de regresión visual con sus imágenes de referencia. ### Añadir un componente al editor > Añade un componente a Pyzed paso a paso: la función de pyzed.ui y su widget en Rust, la entrada en la caja de herramientas, eventos y tests. Source: https://albakore.github.io/pyzed/es/contributing/adding-components/ Esta guía explica cómo añadir un control nuevo a **Pyzed Design**, el editor visual de `python/pyzed/designer/`, de principio a fin. El ejemplo es un control **Rating** (valoración): una fila de estrellas que lanza un evento `change` con el número de la estrella pulsada. (Image: El control Rating en un formulario en ejecución) #### El recorrido de un componente Todo lo que el editor sabe de un control sale de un registro `Component` en `python/pyzed/designer/components.py`. La misma función `render` dibuja el control en el lienzo (inerte) y en la aplicación en ejecución (activo), así que lo que diseñas es exactamente lo que se ejecuta. ```mermaid flowchart LR UI["función de pyzed.ui
(ui/*.py)"] --> SPEC["designer.Component
+ Prop, eventos"] SPEC -->|register| REG["registro
categories()"] REG --> TB["Caja de herramientas
(panels.toolbox)"] TB -->|arrastrar / Document.add| CV["Lienzo
RenderContext (inerte)"] CV --> PG["Propiedades
editores según Prop.kind"] CV -->|Document.save| PZ[".pzform
props + events"] CV -->|Document.wire| PY["handlers .py
stub control_evento()"] PZ --> RT["FormApp / pyzed run
LiveContext"] PY --> RT ``` | Etapa | Código | Qué pasa | | --- | --- | --- | | Widget | `python/pyzed/ui/` | Una función normal devuelve un `Widget` construido con `pyzed._core` (Rust). | | Entrada de la caja | `designer/components.py` | `Component(...)` describe el tipo, la etiqueta, el icono, la categoría, el tamaño, las propiedades y los eventos; `register()` lo añade. | | Caja de herramientas | `designer/panels.py` | `toolbox()` lista `categories()` con el `icon` y el `label` de cada entrada (traducido con `tr`) y su `description` como tooltip. | | Lienzo | `designer/surface.py` | Dibuja cada control con `components.render(control, RenderContext(...))`; los callbacks son inertes. | | Propiedades | `designer/panels.py` | Un editor por cada `Prop`, elegido por su `kind` y agrupado por su `group`. | | Guardado | `designer/model.py` | `Control.to_dict()` escribe `type`, la geometría, `props` y `events` en el JSON del `.pzform`. | | Handlers | `designer/document.py`, `designer/code.py` | `Document.wire(nombre, evento)` llama al handler `_` y añade un stub al archivo `.py`. | | Ejecución | `designer/runtime.py` | `LiveContext.on()` guarda el valor nuevo en la propiedad y llama al handler conectado con `(form, event)`. | :::tip[¿Integrado o solo para tu aplicación?] Esta guía añade un componente **integrado** en Pyzed. Para usar un control solo en tu aplicación no hace falta ningún cambio en el repositorio: llama a `designer.register(...)` desde tu código (mira el [ejemplo completo](#ejemplo-completo-un-control-rating)), escribe una clase [`pz.Component`](https://albakore.github.io/pyzed/es/guides/components/) (se une a la caja de herramientas sola) o dibuja uno en el [diseñador de componentes](https://albakore.github.io/pyzed/es/editor/component-designer/). ::: #### 1. El componente de `pyzed.ui` La mayoría de los controles del diseñador envuelven una función de `pyzed.ui`, así que el control también está disponible para quien escribe su interfaz en código. Ponla en el módulo que corresponda (`inputs.py`, `controls.py`, `display.py`…) y sigue las convenciones del paquete: - Es una **función normal** que devuelve un `Widget`; primero el valor, después el callback y luego las opciones solo por nombre. - El último parámetro es `theme: Optional[Theme] = None`, que se resuelve con `t = _theme(theme)`; todos los colores salen del tema (`t.color("accent")`, `t.text_muted`, `t.ghost_hover`…), nunca un hexadecimal fijo. - Los callbacks reciben el valor nuevo; sin callback, el control es de solo lectura. - Un docstring de una línea que diga qué dibuja y, después, qué recibe el callback. ```python # python/pyzed/ui/controls.py def rating( value: float, on_change: Any = None, *, max: int = 5, # noqa: A002 - mirrors ui.progress size: float = 16, tint: str = "accent", theme: Optional[Theme] = None, ) -> _core.Widget: """A row of ``max`` stars with the first ``value`` lit, in the ``tint`` color. ``on_change`` is called with the number of the clicked star (1 to ``max``); without it the stars are read only. """ t = _theme(theme) stars = [] for i in range(1, int(max) + 1): lit = i <= value star = _icon("star", size=size, color=t.color(tint) if lit else alpha(t.text_muted, 0.5)) stars.append(_core.button(star, (lambda i=i: on_change(i)) if on_change else None, padding=2, bg="transparent", hover_bg=t.ghost_hover, radius=t.radius_sm)) return _core.row(stars, spacing=0, align="center") ``` `controls.py` ya importa `_core`, `_icon`, `alpha`, `Theme` y `_theme`. Después, expórtala en `python/pyzed/ui/__init__.py`: añade `rating` a la lista `from .controls import (...)` **y** a `__all__`. Todo nombre de `__all__` debe estar documentado (mira [Documentar un componente](https://albakore.github.io/pyzed/es/contributing/documenting-components/)). ##### Cuando el widget necesita Rust Solo tocas `src/` cuando ninguna pieza existente puede dibujar el control: un widget nuevo de Iced, una interacción nativa o algo que tiene que ser rápido. `pyzed._core` expone un conjunto pequeño de primitivas (`text`, `row`, `column`, `container`, `button`, `text_input`, `slider`, `progress_bar`, `mouse_area`, `stack`, `svg`, `code_editor`…), y la mayoría de los componentes de `ui`, como Rating, son combinaciones de ellas. Un widget nativo se conecta en cuatro sitios. Así lo hace `progress_bar`: 1. **Una variante de `Kind`** en `src/widget.rs`, con datos de Rust normales (el árbol de widgets es inmutable y vive en Rust): ```rust pub enum Kind { // ... ProgressBar { min: f32, max: f32, value: f32, width: Option, height: Option, style: ToggleStyle, radius: f32 }, } ``` 2. **Una función `#[pyfunction]`** en `src/widget.rs` (o `src/extra.rs`) que convierte los valores de Python con las ayudas de `src/style.rs` y devuelve `new(py, Kind::...)`. Los callbacks se guardan como `PyMsg` (`PyMsg::maybe(&on_change)`): ```rust #[pyfunction] #[pyo3(signature = (value, *, min=0.0, max=1.0, width=None, height=None, accent=None, bg=None, radius=2.0))] pub fn progress_bar(py: Python<'_>, value: f32, min: f32, max: f32, width: Any<'_>, height: Any<'_>, accent: Any<'_>, bg: Any<'_>, radius: f32) -> PyResult> { new(py, Kind::ProgressBar { min, max, value, width: style::opt_length(&width)?, height: style::opt_length(&height)?, style: toggle_style(&accent, &bg, &None, &None, &None)?, radius }) } ``` 3. **El registro** en la función `register()` del módulo (`widget::register`, que llama `src/lib.rs`): `m.add_function(wrap_pyfunction!(progress_bar, m)?)?;`. A partir de ahí es `pyzed._core.progress_bar`. 4. **Una rama del `match`** en `build()` de `src/render.rs` que convierte el `Kind` en un `iced::Element` en cada frame. Las interacciones se convierten en un `Message` (`Message::Py(msg)`, `Message::Slide(msg, value)`, `Message::Toggle`…), que `src/app.rs` traduce en una llamada al callback de Python. Recompila con `maturin develop --release`, ejecuta `cargo fmt` y `cargo clippy -- -D warnings`, y envuelve la primitiva nueva en una función de `pyzed.ui` como antes. [Cómo funciona](https://albakore.github.io/pyzed/es/guides/how-it-works/) explica el puente con más detalle. #### 2. La entrada de la caja de herramientas Los controles integrados viven en `python/pyzed/designer/components.py`: una función de dibujo y una entrada en la tupla que se registra al final del módulo. ##### La función de dibujo ```python def _rating(c: Control, ctx: RenderContext) -> Any: p = c.props return _fill(rating(_num(p["value"]), ctx.on(c, "change", "value") if c.enabled else None, max=int(_num(p["max"], 5)), size=_num(p["size"], 16), tint=p["tint"], theme=ctx.theme), c) ``` (`from ..ui.controls import rating`, o llámala como `ui.rating` cuando esté exportada). - `c` es el `Control` que se dibuja. `c.props` ya tiene todas las propiedades: `components.render()` mezcla los valores por defecto del componente debajo de los guardados, así que `p["max"]` nunca lanza `KeyError`, ni siquiera en formularios guardados antes de que existiera la propiedad. - **Los valores del panel de propiedades o del JSON pueden ser textos o `None`.** Conviértelos con las ayudas del módulo: `_num(valor, por_defecto)`, `_lines(valor)` para las propiedades `list`, `_align(valor)`. - La función recibe el tamaño del control: rellénalo (`_fill(widget, c)` envuelve el widget en un contenedor `width="fill", height="fill"`, alineado al inicio y centrado en vertical). - **Usa `ctx.on(c, evento, prop)` para cada callback.** En el diseñador devuelve una función inerte. En ejecución (`LiveContext`) guarda el argumento del callback en `c.props[prop]` y después llama al handler conectado a `evento`, con ese valor como `event.value`. Pasa `None` cuando el control está deshabilitado (`c.enabled`). - `ctx.theme` es el tema del formulario; `ctx.live` es `False` en el lienzo (úsalo para dibujar ayudas de diseño, como hacen los contenedores de layout); `ctx.key(c)` es un id estable para widgets con estado interno (campos de texto, editores). - No dibujes tú el estilo del usuario: `components.render()` aplica la sección **Estilo** del control (`designer/styling.py`) sobre lo que devuelves. ##### El registro `Component` ```python Component( type="rating", label="Rating", icon="star", category="Common", render=_rating, size=(132, 28), props=(Prop("value", "number", 3, min=0, max=10, group="Data"), Prop("max", "number", 5, min=1, max=10, group="Data"), Prop("size", "number", 16, min=8, max=48), Prop("tint", "choice", "accent", choices=TINTS)), events=("change",), description="A row of stars to rate something; change gives the number of stars.", ), ``` Añádelo a la tupla `for _component in (...)`, junto a los controles de su categoría; la caja de herramientas muestra las categorías, y los componentes dentro de ellas, en el orden de registro. | Campo | Significado | | --- | --- | | `type` | Id estable que se guarda en los `.pzform` y con el que se nombran los controles nuevos (`rating1`, `rating2`…). **No lo cambies nunca** después de publicarlo: los formularios antiguos dejarían de cargar (`KeyError: unknown component type`). | | `label` | Nombre en la caja de herramientas y arriba del panel de propiedades. | | `icon` | Un icono de [Lucide](https://lucide.dev), para la caja de herramientas y la estructura (`pyzed icons` los lista). | | `category` | Sección de la caja: `Common`, `Display`, `Containers`, `Layout`, `Navigation`, `Components` o una nueva. | | `render` | `render(control, ctx) -> Widget`, como arriba. | | `size` | `(ancho, alto)` por defecto al soltarlo en el formulario. | | `props` | Las propiedades del panel de propiedades, en orden (ver abajo). | | `events` | Los eventos propios del control. El primero es el **evento por defecto**, que se abre con doble clic en el control. | | `description` | Una frase; es el tooltip de la caja de herramientas y el subtítulo del panel de propiedades. | | `visual` | `False` para controles que no dibujan nada en ejecución (como `timer`): no reciben eventos del ratón. | | `container` | `"free"`, `"flex"` o `"grid"` cuando se pueden soltar otros controles dentro (mira `designer/layout.py`). | | `child_layout` | Opciones de layout para los controles nuevos de este tipo, como `(("grow", 1),)` en el espaciador. | ##### Propiedades `Prop(name, kind, default, label="", choices=(), min=None, max=None, step=1.0, group="Appearance")`: | `kind` | Editor en el panel de propiedades | Valor | | --- | --- | --- | | `text` | cuadro de texto de una línea | `str` | | `multiline` | cuadro de texto de varias líneas | `str` | | `number` | cuadro numérico con botones (`min`, `max`, `step`) | número, o `None` si el valor por defecto es `None` | | `bool` | interruptor | `bool` | | `color` | muestra de color y selector | token del tema o hexadecimal (`""` para ninguno) | | `choice` | desplegable con `choices` | `str` | | `list` | editor de lista, un elemento por línea | `str` con un elemento por línea | | `rows` | editor de tabla, filas separadas por comas, la primera es el encabezado | `str` | | `icon` | selector de iconos de Lucide | nombre del icono (`""` para ninguno) | - `group` la coloca en una sección del panel: `Appearance` (por defecto), `Data` (los valores que cambia el usuario, como `text`, `value`, `selected`), `Layout` o `Behavior`. - El panel muestra `label`, o el nombre con los guiones bajos como espacios (`line_numbers` → *Line numbers*). - Llama a la propiedad que cambia con la interacción del usuario según lo que guarda (`value`, `checked`, `selected`, `text`) y pasa ese nombre como tercer argumento de `ctx.on`, así `form.rating1.value` siempre está al día. ##### Eventos - Usa los verbos existentes cuando encajen: `click`, `double_click`, `change`, `submit`, `release`, `tick`. `EVENT_DOCS` en `components.py` guarda el docstring del stub generado para cada evento; añade una entrada si introduces uno nuevo. - Todo componente visual recibe también los `COMMON_EVENTS` (`mouse_enter`, `mouse_leave`, `right_click`) automáticamente (`Component.all_events`); el runtime envuelve el control en un `mouse_area` cuando están conectados. No los listes tú. ##### Traducciones y recarga en caliente - Los textos del propio diseñador se traducen en `python/pyzed/designer/locales/es.json`. Añade la etiqueta en español y cualquier categoría o título de propiedad nuevo bajo `"designer"` (la clave es el texto en inglés): `"Rating": "Valoración"`. Los títulos de propiedad se comparten, así que `Value`, `Max`, `Size` y `Tint` ya están. - Si el usuario cambia una propiedad al interactuar (como el `value` de Rating), añádela a `_VALUE_PROPS` en `python/pyzed/designer/runtime.py` (`"rating": ("value",)`): así la recarga en caliente conserva lo que eligió el usuario al guardar el formulario o su código. #### 3. Qué guarda y genera el editor Colocar un Rating, ponerle nombre y hacer doble clic en él deja esto en el `.pzform` (solo `type`, la geometría, `props` y `events` dependen de tu componente): ```json { "name": "rating1", "type": "rating", "x": 16, "y": 48, "width": 132, "height": 28, "props": { "value": 3, "max": 5, "size": 16, "tint": "accent" }, "events": { "change": "rating1_change" }, "anchor": ["left", "top"], "visible": true, "enabled": true, "locked": false } ``` y este stub en el módulo de handlers del formulario (`Document.wire` → `code.ensure_function`, con el docstring de `EVENT_DOCS`): ```python def rating1_change(form, event): """Runs when the value of rating1 changes (``event.value``).""" pass ``` En ejecución, los handlers leen y escriben las propiedades como atributos: `form.rating1.value`, `form.rating1.max = 10`, `form.rating1.hide()`. Como las `props` se guardan como JSON, usa valores compatibles con JSON (textos, números, booleanos, `None`), y cuando añadas una propiedad más adelante dale un valor por defecto que no cambie el aspecto de los formularios antiguos. #### 4. Tests Los tests viven en `tests/` y se ejecutan sin pantalla. `test_every_component_has_defaults_and_renders`, en `tests/test_designer.py`, ya dibuja cada componente registrado con sus valores por defecto, así que un `render` roto falla enseguida. Añade tests de lo propio de tu control: ```python import pyzed as pz from pyzed import designer, ui from pyzed.designer.document import Document from pyzed.designer.runtime import FormApp def test_rating_ui_component(): assert isinstance(ui.rating(3, lambda stars: None, max=5), pz.Widget) assert isinstance(ui.rating(0), pz.Widget) # read only def test_rating_in_the_toolbox(): spec = designer.component("rating") assert spec in designer.categories()["Common"] assert spec.default_event == "change" control = spec.create("stars", 0, 0) assert control.props == {"value": 3, "max": 5, "size": 16, "tint": "accent"} assert isinstance(designer.render(control, designer.RenderContext(pz.themes.ONE_DARK)), pz.Widget) def test_rating_runs_its_handler(tmp_path): doc = Document(designer.Form(name="review")) doc.add("rating", 16, 16) handler = doc.wire("rating1", "change") doc.set_handler_source(handler, "def rating1_change(form, event):\n form.rating1.max = 10\n") path = doc.save(tmp_path / "review.pzform") assert designer.load(path)["rating1"].events == {"change": "rating1_change"} rt = designer.open_form(path).runtime rt.ctx.on(rt.form["rating1"], "change", "value")(4) # what a click on the 4th star does assert rt.form["rating1"].props["value"] == 4 assert rt.form["rating1"].props["max"] == 10 assert rt.error is None def test_rating_screenshot(tmp_path): form = designer.Form(width=200, height=60) form.add(designer.component("rating").create("stars", 16, 16)) FormApp(form).screenshot(tmp_path / "rating.png", scale=1) assert (tmp_path / "rating.png").stat().st_size > 0 ``` - `rt.ctx.on(control, evento, prop)(valor)` simula al usuario: es exactamente el callback que llama el widget. - `Document` maneja el editor sin ventana: `add`, `set_prop`, `wire`, `save`, `undo`… - `FormApp(...).screenshot(ruta, scale=1)` y `designer.Designer(ruta).screenshot(...)` dibujan sin pantalla el formulario en ejecución y el editor completo. - El fixture de `conftest.py` mantiene las preferencias del editor fuera de tu carpeta de configuración real. Ejecuta `pytest tests/test_designer.py -q` mientras trabajas y todos los tests antes de hacer push. #### Ejemplo completo: un control Rating Este es el ejemplo completo, en un solo archivo que puedes ejecutar sin tocar el repositorio: crea la función de `ui`, registra la entrada de la caja de herramientas, crea un formulario con `Document`, conecta el evento, ejecuta el formulario y hace capturas. (En un pull request, cada pieza va al archivo indicado en los pasos de arriba). ```python # rating.py """A Rating control for Pyzed Design.""" from typing import Any, Optional from pyzed import _core from pyzed.designer import Component, Prop, register from pyzed.designer.components import TINTS, RenderContext from pyzed.designer.model import Control from pyzed.icons import icon as _icon from pyzed.theme import Theme, alpha from pyzed.ui.base import _theme def rating( value: float, on_change: Any = None, *, max: int = 5, # noqa: A002 - mirrors ui.progress size: float = 16, tint: str = "accent", theme: Optional[Theme] = None, ) -> _core.Widget: """A row of ``max`` stars with the first ``value`` lit, in the ``tint`` color. ``on_change`` is called with the number of the clicked star (1 to ``max``); without it the stars are read only. """ t = _theme(theme) stars = [] for i in range(1, int(max) + 1): lit = i <= value star = _icon("star", size=size, color=t.color(tint) if lit else alpha(t.text_muted, 0.5)) stars.append(_core.button(star, (lambda i=i: on_change(i)) if on_change else None, padding=2, bg="transparent", hover_bg=t.ghost_hover, radius=t.radius_sm)) return _core.row(stars, spacing=0, align="center") def _rating(c: Control, ctx: RenderContext) -> Any: p = c.props widget = rating(p["value"], ctx.on(c, "change", "value") if c.enabled else None, max=int(p["max"]), size=p["size"], tint=p["tint"], theme=ctx.theme) return _core.container(widget, width="fill", height="fill", align_y="center") RATING = register(Component( "rating", "Rating", "star", "Common", _rating, (132, 28), (Prop("value", "number", 3, min=0, max=10, group="Data"), Prop("max", "number", 5, min=1, max=10, group="Data"), Prop("size", "number", 16, min=8, max=48), Prop("tint", "choice", "accent", choices=TINTS)), ("change",), "A row of stars to rate something; change gives the number of stars.", )) ``` ```python # demo.py from pathlib import Path import rating # noqa: F401 - registra el componente from pyzed import designer from pyzed.designer import Document HERE = Path(__file__).resolve().parent doc = Document(designer.Form(name="review", title="Review", width=360, height=150)) doc.add("label", 16, 16, 320, 24).props["text"] = "How was your stay?" doc.add("rating", 16, 48) doc.add("label", 16, 88, 320, 24) doc.rename("label2", "status") handler = doc.wire("rating1", "change") # "rating1_change", con un stub en review.py doc.set_handler_source(handler, ( "def rating1_change(form, event):\n" ' form.status.text = f"You gave it {event.value} stars"\n')) path = doc.save(HERE / "review.pzform") # escribe review.pzform y review.py app = designer.open_form(path) rt = app.runtime rt.ctx.on(rt.form["rating1"], "change", "value")(4) # un clic en la cuarta estrella assert rt.form["status"].props["text"] == "You gave it 4 stars" app.screenshot(HERE / "rating-run.png", scale=2) # la imagen del principio de esta página editor = designer.Designer(path) editor.doc.select("rating1") editor.screenshot(HERE / "rating-designer.png", size=(1280, 760), scale=2) ``` `python demo.py` escribe el formulario, sus handlers y las dos capturas. Para usar el control de forma interactiva, importa `rating` antes de abrir el editor o ejecutar el formulario: ```python import rating # noqa: F401 from pyzed import designer designer.design("review.pzform") # el editor, con Rating en la caja de herramientas # designer.run("review.pzform") # o ejecuta el formulario ``` El Rating seleccionado en el lienzo, con sus propiedades **Data** y **Appearance** en el panel: (Image: El control Rating seleccionado en Pyzed Design) #### Lista de comprobación - [ ] Función de `pyzed.ui` con docstring, exportada en `ui/__init__.py` y en `__all__` - [ ] Cambios de Rust (si los hay) formateados y sin avisos de clippy, recompilados con `maturin develop --release` - [ ] Entrada `Component` en `designer/components.py`, con un `type` estable, un icono de Lucide y una descripción de una frase - [ ] Callbacks con `ctx.on(c, evento, prop)`; valores convertidos con `_num`/`_lines` - [ ] Textos en español en `designer/locales/es.json`; propiedades interactivas en `_VALUE_PROPS` - [ ] Tests en `tests/`, y `pytest tests` pasa - [ ] Documentación: mira [Documentar un componente](https://albakore.github.io/pyzed/es/contributing/documenting-components/) - [ ] PR con el título `✨ Add a Rating component` ### Documentar un componente > Documenta un componente nuevo de Pyzed: docstrings, referencia de la API, página de la biblioteca, capturas, traducciones y el changelog. Source: https://albakore.github.io/pyzed/es/contributing/documenting-components/ Un componente está terminado cuando la gente puede encontrarlo y aprender a usarlo sin leer su código. Documéntalo en el mismo pull request que lo añade; en la revisión se comprueba. Esta página continúa el ejemplo de Rating de [Añadir un componente al editor](https://albakore.github.io/pyzed/es/contributing/adding-components/). ```mermaid flowchart LR DS["Docstring
(el código)"] --> API["api_docs_en.py
api_docs_es.py"] --> GEN["gen_api.py"] --> REF["Referencia de la API
reference/*.md"] LIB["library_data.py"] --> LS["library_shots.py"] --> IMG["public/library/*.webp"] LIB --> GL["gen_library.py"] --> PAGES["Páginas de la biblioteca"] CS["component_shots.py"] --> CIMG["src/assets/components/*.png"] --> GUIDE["Guías de componentes"] CL["CHANGELOG.md
[Unreleased]"] --> NEW["Novedades"] ``` | Qué | Dónde | Lo genera | | --- | --- | --- | | Docstring | la función o clase en `python/pyzed/` | se escribe a mano | | Referencia de la API | `docs/src/content/docs/[es/]reference/*.md` | `docs/scripts/gen_api.py` a partir de `api_docs_en.py` / `api_docs_es.py` | | Biblioteca de componentes | la sección *Biblioteca de componentes* de la web | `docs/scripts/gen_library.py` a partir de `docs/scripts/library_data.py` | | Vistas previas de la biblioteca (animadas) | `docs/public/library/*.webp` | `docs/scripts/library_shots.py` | | Ilustraciones de las guías | `docs/src/assets/components/*.png` | `docs/scripts/component_shots.py` | | Capturas del editor | `docs/screenshots/*.png` | `examples/screenshots.py` | | Notas de versión | `CHANGELOG.md` → [Novedades](https://albakore.github.io/pyzed/es/changelog/) | `docs/scripts/sync-changelog.mjs` en cada compilación | Instala antes Pyzed en tu entorno (`uv sync` o `maturin develop --release`): todos los scripts importan el paquete, así que las firmas y las imágenes siempre coinciden con el código. #### 1. Docstrings Los docstrings son la fuente de verdad: los muestra el editor, los muestra el autocompletado y la referencia de la API los usa cuando falta una descripción. El estilo del repositorio (en inglés, como el código): - **La primera línea es una frase completa** que dice qué es o qué hace, en presente: *"A row of ``max`` stars with the first ``value`` lit, in the ``tint`` color."* El primer párrafo es el resumen que usa `gen_api.py`. - Después, si hace falta, un párrafo corto sobre **los callbacks y el comportamiento**: qué recibe `on_change` y qué pasa sin él. - Al final, una **sección `Events:`** que enumera cada parámetro `on_*` con lo que recibe su handler, escrito como una llamada (``` ``on_change(stars)`` ```, ``` ``on_press()`` ```). Los editores la muestran al pasar el ratón, y es la misma lista que la sección **Eventos** de la página de la biblioteca. - Los nombres de código van entre comillas invertidas dobles de reST (```` ``value`` ````) y los ejemplos en un bloque `::`; `gen_api.py` convierte las dos cosas a Markdown (código en línea y un bloque `python`). - Sin secciones `Args:`/`Returns:`: los parámetros se describen en `api_docs_*.py` (abajo), así el docstring sigue siendo corto. - Las `Prop` del diseñador no tienen docstring: la `description` del `Component` (una frase) es el tooltip de la caja de herramientas y el subtítulo del panel de propiedades. ```python def rating(value: float, on_change: Any = None, *, max: int = 5, size: float = 16, tint: str = "accent", theme: Optional[Theme] = None) -> _core.Widget: """A row of ``max`` stars with the first ``value`` lit, in the ``tint`` color. ``on_change`` is called with the number of the clicked star (1 to ``max``); without it the stars are read only. Events: ``on_change(stars)``: Called with the number of the clicked star. """ ``` Los widgets escritos en Rust (`pz.button`, `pz.slider`…) llevan el mismo texto en comentarios `///` sobre su `#[pyfunction]`; `scripts/gen_stubs.py` los copia a `_core.pyi` después de `maturin develop`. #### 2. Referencia de la API Las páginas de la [Referencia de la API](https://albakore.github.io/pyzed/es/reference/ui/) se generan. No edites nunca `docs/src/content/docs/reference/*.md` a mano: describe la función en `docs/scripts/api_docs_en.py` y su traducción en `docs/scripts/api_docs_es.py`, y vuelve a generar. Cada archivo tiene una lista `PAGES`; cada página tiene `sections`, una lista de pares `(título, {nombre: entrada})`. Añade la entrada a la sección que le corresponde (para los componentes de `pyzed.ui`: `Basics`, `Forms`, `Lists, tree and menus`, `Navigation`, `Workspace`, `Overlays`, `Code`; en `api_docs_es.py` los títulos están traducidos: `Básicos`, `Formularios`…): ```python # docs/scripts/api_docs_en.py, page "ui", section "Forms" "pyzed.ui.rating": { "desc": "A row of stars to rate something.", "params": { "max": "Number of stars.", "size": "Size of each star, in pixels.", "on_change": "Called with the number of the clicked star (1 to `max`). Without it the stars are read only.", }, }, ``` ```python # docs/scripts/api_docs_es.py, page "ui", section "Formularios" "pyzed.ui.rating": { "desc": "Una fila de estrellas para valorar algo.", "params": { "max": "Número de estrellas.", "size": "Tamaño de cada estrella, en píxeles.", "on_change": "Recibe el número de la estrella pulsada (de 1 a `max`). Sin él, las estrellas son de solo lectura.", }, }, ``` - **La firma no se escribe a mano**: `gen_api.py` la lee con `inspect`, tanto de Python como de las funciones de Rust (PyO3 expone sus firmas). - Los parámetros que faltan en `params` se buscan en `COMMON`, al principio del archivo (`value`, `tint`, `theme`, `width`, `on_press`…), así que describe solo lo específico. - Una entrada también puede tener `"example"` (un bloque de código) y `"extra"` (Markdown después de la tabla). - Las entradas de la API del diseñador (`pyzed.designer.*`) se listan con `_doc(...)`, que usa el docstring. ```bash python docs/scripts/gen_api.py # los dos idiomas python docs/scripts/gen_api.py --lang es # solo español (o --lang en) python docs/scripts/gen_api.py --check # lista los nombres públicos sin documentación ``` `--check` revisa todo lo exportado en `__all__` de `pyzed`, `pyzed.ui` y `pyzed.designer`. La CI lo ejecuta y muestra lo que falta como avisos, así que una exportación nueva sin entrada se ve en el PR. #### 3. La biblioteca de componentes La [Biblioteca de componentes](https://albakore.github.io/pyzed/es/library/) tiene una página por componente, al estilo de shadcn/ui o Chakra UI: una descripción, una vista previa en los dos temas con pestañas **Vista previa/Código**, «Cuándo usarlo», uso, ejemplos, consejos, una tabla de la API y componentes relacionados, además de una galería con todos ellos agrupados por categoría. Las páginas se generan a partir de un único archivo de datos, `docs/scripts/library_data.py`, con `docs/scripts/gen_library.py`; no edites nunca `library/*.mdx` a mano. Documenta primero la función en la referencia de la API (paso 2): las tablas de la API de la biblioteca reutilizan `gen_api.py`, así que la firma y las descripciones de los parámetros salen de `api_docs_.py`. Después añade una entrada con `add(...)` a `library_data.py` (el orden del archivo da igual; las páginas se ordenan por nombre). Cada texto es un par `T(en, es)`: ```python add( slug="rating", name="Rating", category="forms", api=["pyzed.ui.rating"], blurb=T("A row of stars to rate something.", "Una fila de estrellas para valorar algo."), desc=T( "Stars the user clicks to give a score. `on_change` receives the number of the clicked star; " "without it the stars are read only.", "Estrellas en las que el usuario hace clic para puntuar. `on_change` recibe el número de la " "estrella pulsada; sin él, las estrellas son de solo lectura.", ), when=T( ["To collect a quick score (reviews, feedback).", "To show a score next to an item, read only."], ["Para recoger una puntuación rápida (reseñas, opiniones).", "Para mostrar una puntuación junto a un elemento, en solo lectura."], ), preview={"code": """pz.column([ ui.rating(4, on_rate), ui.rating(3, on_rate, tint="warning", size=20), ui.rating(7, max=10), ], spacing=10)""", "size": (720, 200)}, usage="""class Review(pz.App): def __init__(self): self.stars = 0 def view(self): return ui.rating(self.stars, lambda stars: setattr(self, "stars", stars))""", related=["slider", "segmented"], badge="new", ) ``` | Clave | Significado | | --- | --- | | `slug` | URL y nombre de archivo: `library/.mdx` y `public/library/.*.webp`. | | `name` | Título de la página y de su tarjeta en la galería. | | `category` | Una clave de `CATEGORIES` (`actions`, `display`, `forms`, `feedback`, `data`, `lists`, `navigation`, `overlays`, `dialogs`, `workspace`, `code`, `layout`, `content`). | | `api` | Los nombres públicos que documenta la página, el principal primero. | | `blurb`, `desc`, `when` | La línea de la galería, el párrafo inicial y las viñetas de «Cuándo usarlo». | | `preview` | `{"code": ..., "size": (w, h), "fill": False, "start": None}`: el fragmento que se muestra en la pestaña Código **y** que se dibuja como vista previa. | | `usage` | Opcional: un fragmento más completo, normalmente una `App` que muestra dónde vive el estado. | | `examples` | Lista opcional de `{"title": T(...), "text": T(...), "code": ..., "size": (w, h)}`, cada uno con su imagen (`"shot": False` para mostrar solo el código). | | `notes` | «Consejos» opcionales (Markdown). | | `related`, `badge` | Slugs relacionados opcionales; `"new"` añade una insignia en la barra lateral y en la galería. | **Eventos.** Cada página tiene una sección **Eventos**, también generada: una tabla con los eventos propios del componente (sus parámetros `on_*`, con la descripción de la referencia de la API), los eventos comunes de `.on()` y un ejemplo **Manejar los eventos** en tres pestañas: imperativo, declarativo y funcional. Lo que recibe cada handler (`(text)`, `(x, y)`…) y los tres ejemplos están en `docs/scripts/library_events.py`: ```python HANDLERS = {"on_change": "(value)", "ui.rating.on_change": "(stars)", ...} ex("rating", """class Review(pz.App): ...""", # imperativo: los métodos cambian self """@dataclass\nclass Rate: ...""", # declarativo: mensajes y update() """@dataclass(frozen=True)\nclass State: ...""") # funcional: view(state, set_state) ``` Los ejemplos imperativo y declarativo terminan con una clase `App`; el funcional define `view(state, set_state)` y un `State` congelado. `tests/test_library_events.py` ejecuta los tres de cada página, y `--check` falla si un componente con eventos no tiene ejemplos. La página [Eventos](https://albakore.github.io/pyzed/es/library/events/) enumera cada componente con su número de eventos. Los fragmentos se ejecutan con `pz`, `ui` y `draw` ya importados; cualquier otro nombre libre (`on_rate`, `save`…) es un callback que no hace nada, así que escríbelos como los escribiría alguien en su aplicación; **la última expresión es el widget**. Los textos son Markdown dentro de MDX: escribe `{`, `}` y `<` solo entre comillas invertidas. Dibuja las vistas previas, genera las páginas y compruébalas: ```bash python docs/scripts/library_shots.py rating # public/library/rating.dark.webp y rating.light.webp python docs/scripts/gen_library.py # library/*.mdx en los dos idiomas python docs/scripts/gen_library.py --check # cobertura y coherencia, no escribe nada ``` Las vistas previas son WebP animados grabados sin pantalla a escala 2 con [`anim_webp.py`](https://albakore.github.io/pyzed/es/contributing/docs/#capturas-animadas), centrados sobre el fondo del tema, en One Dark y One Light (ejemplos: `-..webp`; cada uno con un `*.still.webp` quieto para el movimiento reducido). Por defecto el puntero pasa por encima del centro de la vista previa y lo pulsa, y cada variante se sobrevuela y se pulsa (o se hace clic y se escribe si tiene un campo de texto); un clip en el que nada cambia recorre algunos temas. Dale al componente nuevo un guion en `docs/scripts/library_anim.py` cuando haga algo más (abrir un menú, arrastrar un deslizador, escribir una búsqueda); `library_shots.py --probe rating` imprime a qué puede apuntar un guion. `--check` falla cuando un nombre de `pyzed.ui.__all__` no está en ninguna página de la biblioteca, cuando una entrada apunta a un nombre, categoría, slug relacionado o clip desconocidos, cuando falta un clip o no está animado, o cuando una página generada no está al día. Sube los clips y `public/library/manifest.json` junto con el archivo de datos. #### 4. Capturas para las guías Las guías temáticas de *Componentes* (`docs/src/content/docs/components/*.md`) muestran una imagen por página, dibujada por `docs/scripts/component_shots.py`. Para ilustrar ahí el componente nuevo, añade una función que devuelva los widgets y una entrada en `SHOTS` con el tamaño de la imagen en píxeles lógicos: ```python def rating(): return pz.column([ ui.rating(3, noop), ui.rating(4, noop, tint="warning", size=20), ui.rating(2, max=10), ], spacing=10) SHOTS = { # ... "rating": (rating, (300, 140)), } ``` ```bash python docs/scripts/component_shots.py rating # docs/src/assets/components/rating.png ``` y enlázala desde la página con una ruta relativa: `![Rating](../../../../assets/components/rating.png)` (en la página en inglés, un `../` menos). Las capturas grandes del editor (`docs/screenshots/`) las hace `examples/screenshots.py`. Su `designer-components.png` coloca **todos los componentes registrados** en un formulario, así que vuelve a generarla cuando añadas uno a la caja de herramientas: ```bash python examples/screenshots.py ``` Consejos para buenas imágenes: - Muestra el componente **en contexto y en sus estados** (vacío, seleccionado, deshabilitado, cada variante), no solo el valor por defecto. - Usa textos realistas («How was your stay?»), nunca «foo» o «test». - Ajusta el tamaño: los scripts dibujan a escala 2, así que 300×140 píxeles lógicos son una imagen de 600×280. - One Dark es el tema por defecto de las imágenes; las páginas de la biblioteca también tienen One Light. #### 5. Guías y documentación del editor Además de las páginas generadas, actualiza las escritas a mano a las que afecta el componente: - La página de la guía de **Componentes** de su familia (`components/forms.md`, `components/controls.md`…): añádelo al ejemplo y a la tabla *Componente / Para qué sirve*. - La lista de controles integrados del [Diseñador visual](https://albakore.github.io/pyzed/es/guides/designer/) y la página [Caja de herramientas y lienzo](https://albakore.github.io/pyzed/es/editor/toolbox-and-canvas/) del manual de Pyzed Design, cuando se une a la caja de herramientas. - El manual de [Pyzed Design](https://albakore.github.io/pyzed/es/editor/), si el control añade comportamiento al editor (un editor de propiedades nuevo, un tipo de contenedor nuevo). #### 6. Traducciones La web está primero en inglés, con la traducción al español en `docs/src/content/docs/es/` en las mismas rutas. Para cada página que añadas o cambies: - Escribe la página en inglés y después la española con el mismo nombre de archivo. - Los enlaces incluyen el idioma: `/pyzed/guides/designer/` en inglés, `/pyzed/es/guides/designer/` en español. - Las rutas relativas de las imágenes llevan **un `../` más** en las páginas en español, porque están una carpeta más abajo. - Las anclas salen del texto del título, así que un enlace a una sección de una página en español usa el título en español (`/pyzed/es/guides/components/#en-el-diseñador`). - El contenido generado tiene los dos idiomas en sus datos: `api_docs_es.py`, los textos en español de `library_data.py` y los textos del propio editor en `python/pyzed/designer/locales/es.json`. Si no puedes escribir la versión en español, dilo en el PR: una página que falta se muestra en inglés con un aviso, y alguien del equipo u otra persona puede traducirla en un PR 🌐. #### 7. El changelog Las notas de cada versión se generan a partir de los títulos de los PR al publicarla, así que un título `✨ Add a Rating component` suele bastar. Para un cambio que merece más de una línea, escribe la nota a mano bajo `## [Unreleased]` en `CHANGELOG.md`, en la sección que le corresponde (en inglés): ```markdown ## [Unreleased] ### Features - **Rating**: a row of stars in `pyzed.ui` (`ui.rating`) and in the designer's toolbox, with a `change` event. ``` Las notas escritas bajo `[Unreleased]` se conservan y se combinan al publicar la versión. La página [Novedades](https://albakore.github.io/pyzed/es/changelog/) de la web se regenera a partir de este archivo en cada compilación. #### 8. Compilar y comprobar la web ```bash cd docs npm install npm run dev # http://localhost:4321/pyzed/, se recarga mientras editas npm run build # la web estática en docs/dist, tal como se publica en GitHub Pages ``` Antes de abrir el PR: - `npm run build` termina sin errores (falla con Markdown o MDX roto o con imágenes que no existen). - `python docs/scripts/gen_api.py --check` no lista tus nombres nuevos, y `python docs/scripts/gen_library.py --check` pasa. - Las páginas nuevas aparecen en la barra lateral en los dos idiomas, y sus enlaces e imágenes funcionan en los dos. - Las imágenes se regeneraron con los scripts, no se capturaron a mano. Lee [Editar la documentación](https://albakore.github.io/pyzed/es/contributing/docs/) para la estructura de la web, los diagramas de mermaid y el flujo de publicación. ### Editar la documentación > Cómo está organizada la documentación de Pyzed, cómo previsualizarla, cómo funcionan las traducciones y cómo regenerar la referencia y las capturas. Source: https://albakore.github.io/pyzed/es/contributing/docs/ La web está hecha con [Astro Starlight](https://starlight.astro.build) y vive en `docs/`. Se publica en GitHub Pages en cada push a `main` y en cada release. ``` docs/ ├── astro.config.mjs # secciones (topics), redirecciones, tema, idiomas ├── screenshots/ # capturas grandes (examples/screenshots.py) ├── scripts/ │ ├── api_docs_en.py # descripciones de la API en inglés │ ├── api_docs_es.py # descripciones de la API en español │ ├── gen_api.py # genera reference/*.md en ambos idiomas │ ├── library_data.py # la biblioteca de componentes: una entrada por componente │ ├── gen_library.py # genera library/*.mdx en ambos idiomas │ ├── shading_nodes_data.py # textos y ejemplos del catálogo de nodos (en/es) │ ├── gen_shading_nodes.py # genera shading/nodes/*.mdx en ambos idiomas │ ├── anim_webp.py # graba capturas WebP animadas (biblioteca compartida + CLI) │ ├── library_anim.py # qué hace la vista previa animada de cada componente │ ├── library_shots.py # genera public/library/*.webp (vistas previas animadas) │ ├── component_shots.py # genera src/assets/components/*.png │ ├── example_shots.py # genera capturas de ejemplos y tutoriales │ └── sync-changelog.mjs # CHANGELOG.md → página de novedades (ambos idiomas) + versión estable └── src/content/docs/ # las páginas (Markdown / MDX) ├── guides/ ... # inglés (idioma por defecto, raíz de la web) └── es/ # traducciones al español, mismas rutas ``` #### Previsualizar ```bash cd docs npm install npm run dev # http://localhost:4321/pyzed/ ``` #### Idiomas La documentación es multilingüe. El inglés es el idioma por defecto y se sirve en la raíz de la web; cada traducción vive en una carpeta con el nombre de su idioma: | Idioma | Archivo | URL | | --- | --- | --- | | Inglés | `src/content/docs/guides/canvas.md` | `/pyzed/guides/canvas/` | | Español | `src/content/docs/es/guides/canvas.md` | `/pyzed/es/guides/canvas/` | - Una traducción usa **la misma ruta** que la página en inglés, dentro de `es/`. - Cuando añadas una página, créala en los dos idiomas. Si falta la traducción, Starlight muestra la página en inglés con un aviso en lugar de un 404. - Los enlaces internos llevan el idioma: `/pyzed/guides/...` en las páginas en inglés y `/pyzed/es/guides/...` en las de español. - Las rutas relativas a imágenes y ejemplos llevan un `../` más en los archivos en español, porque están una carpeta más abajo. #### Secciones y navegación Las secciones principales (Aprender, Biblioteca, Diseñador, Shading, Temas, Distribuir, Showcase, Integraciones, IA y Referencia) son la lista `topics` de `astro.config.mjs`, con el plugin [starlight-sidebar-topics](https://starlight-sidebar-topics.netlify.app). Cada una es un enlace en la barra de navegación de la cabecera (`src/components/Header.astro`) y tiene su propia barra lateral; en el móvil aparecen arriba del menú. Una página pertenece a la sección cuya barra lateral la lista, normalmente por la carpeta donde está: | Sección | Carpetas | | --- | --- | | Aprender | `getting-started/`, `tutorials/{basic,intermediate,advanced}/`, `guides/`, `why/`, `examples`, `faq` | | Biblioteca | `library/`, `blocks/` (y la antigua `components/`) | | Diseñador | `editor/`, `download` | | Shading, Temas, Distribuir | `shading/`, `theming/`, `packaging/` | | Showcase | `showcase/`, `games/` | | Integraciones, IA | `integrations/`, `ai/` | | Referencia | `reference/`, `changelog`, `contributing/` | - La página de inicio de una sección lleva un hero cuando su frontmatter tiene `section: { eyebrow, icon, lead }` (`src/components/PageTitle.astro`). - Enlaza páginas con `` (`src/components/NavCards.astro`); una tarjeta sin `href` se muestra como "próximamente". - Muestra capturas y grabaciones cortas con `` (`src/components/Media.astro`): una imagen de `src/assets` (opcionalmente con una versión `light`) o un bucle mp4/webm con `poster`, más un pie de foto. - Escribe los comandos de terminal con `` `` (`src/components/Commands.astro`, en una página `.mdx`). Dibuja las pestañas `uv`, `pip` y `python` (sincronizadas en todo el sitio) y separa cada una en bloques para macOS/Linux y para Windows PowerShell cuando cambian. Escribe `{py}`, `{venv}`, `{activate}`, `{vpy}` y `{env NOMBRE=valor}` donde los sistemas difieren; el componente pone `python3`/`py`, `source .venv/bin/activate`/`.venv\Scripts\Activate.ps1`, etc. Los comandos que solo existen en un sistema van en ``. - Los colores de los tokens de tema salen de `src/components/theme-tokens.json` (lo escribe `docs/scripts/gen_theme_tokens.py`), y las capturas de temas de `docs/scripts/theming_shots.py`. - **Mover una página:** muévela con `git mv` en los dos idiomas, añade la ruta antigua a `MOVED` en `astro.config.mjs` para que la URL antigua redirija, y actualiza los enlaces (busca la ruta antigua con `git grep`, también en `docs/scripts` y `python/`). #### Diagramas Escribe los diagramas como bloques de código `mermaid`; la integración `astro-mermaid` los dibuja en la página y respeta el tema claro/oscuro: ````md ```mermaid flowchart LR view["App.view()"] --> tree[Árbol de Widget] --> render[render.rs] --> iced[Iced] ``` ```` #### Regenerar la referencia y las imágenes Necesitan Pyzed instalado (`uv sync`, o `maturin develop --release`). Para un componente nuevo, sigue [Documentar un componente](https://albakore.github.io/pyzed/es/contributing/documenting-components/). ```bash python docs/scripts/gen_api.py # referencia de la API, ambos idiomas python docs/scripts/gen_api.py --lang es # solo español (o --lang en) python docs/scripts/gen_api.py --check # informa de nombres públicos sin documentar python docs/scripts/gen_shading_nodes.py # catálogo de nodos de shader, ambos idiomas python docs/scripts/gen_shading_nodes.py --check # falla si las páginas no están al día o un ejemplo falla python docs/scripts/library_shots.py # vistas previas animadas de la biblioteca python docs/scripts/component_shots.py # ilustraciones de componentes python docs/scripts/example_shots.py # capturas de ejemplos y tutoriales python examples/screenshots.py # capturas grandes ``` Cuando añadas una función pública, describe sus parámetros en `docs/scripts/api_docs_en.py` y `docs/scripts/api_docs_es.py`, y vuelve a generar la referencia. La CI ejecuta `gen_api.py --check`. #### Capturas animadas `docs/scripts/anim_webp.py` graba una vista de Pyzed como un WebP animado en bucle, con un puntero, un teclado y un reloj guionizados. Todos los componentes de la [biblioteca](https://albakore.github.io/pyzed/es/library/) se graban con él (`library_shots.py`), y cualquier otra página puede usarlo para sus propios clips: animaciones, widgets de audio, un formulario rellenándose. Usa el mismo renderizador fuera de pantalla que `pz.screenshot` (sin ventana, pantalla ni GPU), a través de `pyzed._core.record`: cada fotograma recibe la posición del puntero, así que se ven los estados al pasar por encima y al pulsar y los tooltips; los eventos (pulsar, soltar, rueda, teclas, texto) llegan a los widgets entre fotogramas; y el tiempo avanza exactamente un fotograma por fotograma (las animaciones, spinners, cursores de texto y presencias lo siguen), así que un clip sale igual en cualquier máquina, tarde lo que tarde cada fotograma. ```python import pyzed as pz from pyzed import ui from anim_webp import record_webp, hover, press, type_text, drag, wait, leave class Login(pz.App): def __init__(self): self.email = "" def view(self): return pz.column([ ui.input(self.email, lambda v: setattr(self, "email", v), placeholder="Email"), ui.button("Sign in", lambda: None, variant="solid"), ], spacing=8, width=280) record_webp( Login(), [press("input"), type_text("ada@example.com"), hover("Sign in"), press(), wait(600), leave()], out="docs/public/media/login.webp", size=(360, 160), # lienzo, en píxeles lógicos (se graba a 2x) theme="One Light", crop="content", # recorta a lo que cambia, más un margen poster="docs/public/media/login.still.webp", ) ``` `record_webp` recibe una `App` (instancia o clase) o una función que devuelve el widget. Pasos: | Paso | Qué hace | | --- | --- | | `wait(ms)` | pasa el tiempo (se mueven animaciones, spinners y cursores) | | `move(target, ms)`, `hover(target, ms, hold)` | el puntero se desliza hasta `target` (y se queda) | | `press(target=None, hold)`, `click` | un clic izquierdo, mantenido `hold` ms | | `drag(start, end, ms)` | pulsar, deslizar, soltar (deslizadores, divisores, campos de color) | | `type_text(text, cps)` | escribe en el campo con el foco | | `key(name, mods="")` | una tecla: `"Enter"`, `"Tab"`, `"Down"`, `"a"` con `"ctrl"`... | | `scroll(lines, target=None)` | la rueda sobre `target` | | `leave(ms)` | el puntero se va (terminan los estados de hover) | | `call(fn)`, `set_state(**attrs)` | cambian el estado de la app entre fotogramas | | `theme(name)` | cambia el tema de la app | Un **target** es un punto `(x, y)`, un texto visible en la vista (`"Sign in"`: su centro), `"input"` / `"input:1"` (el primer / segundo campo de texto), `"center"` (el centro de lo dibujado), `"@0.25,0.5"` (un punto relativo a lo dibujado), `(target, dx, dy)` para apuntar al lado de uno, o `fn(ctx) -> (x, y)`. A los widgets que se dibujan sin texto (interruptores, deslizadores, opciones de un desplegable) se apunta con puntos. Codificación: los fotogramas consecutivos idénticos se funden en uno más largo, el clip se repite sin fin y cada fotograma es con pérdida (`quality=80`, `method=6`) o sin pérdida, lo que ocupe menos (`allow_mixed` de WebP), lo que mantiene nítido el texto. Apunta a **150 KB o menos** por clip y nunca más de **400 KB**; acorta el clip, recórtalo o baja `fps` (20 por defecto) si crece. Las vistas pueden leer `clip_time()` (segundos desde que empezó el clip) para mover cosas que si no estarían quietas. También están las piezas de más bajo nivel: `record_frames()` (los fotogramas, en RGBA), `images()`, `crop_box()`, `encode_webp()`, `fit_webp()` (baja la calidad hasta que el archivo cabe en un tamaño; `record.py` codifica con ella sus clips `anim=True`), `info(path)` (fotogramas, duración y tamaño de un WebP). Desde la línea de comandos: ```bash python docs/scripts/anim_webp.py out.webp --code 'ui.button("Save", None, variant="solid")' \ --steps 'hover("Save"); press(); wait(500)' --size 320x120 --crop --theme "One Light" ``` **En una página**, sirve los archivos tal cual desde `docs/public/`: el procesado de imágenes de Astro convertiría un WebP animado en una imagen quieta. `src/components/AnimatedImage.astro` escribe el marcado: el `` con su `width`, `height`, `alt`, `loading="lazy"` y `decoding="async"`, y el fotograma quieto para quien pida movimiento reducido: ```html Escribiendo un email e iniciando sesión ``` `` lo usa para los clips de `public/media/`, y `src/components/library/Clip.astro` de la biblioteca para los de `public/library/` (tamaños de `public/library/manifest.json`). Lo que hace cada vista previa está en `docs/scripts/library_anim.py`; `library_shots.py --probe ` imprime los textos y campos a los que puede apuntar un guion. #### Novedades y versiones ```mermaid flowchart LR C["Commits (gitmoji)"] --> PR["Pull request"] --> CI["CI: lint, tests, wheels"] PR --> MAIN["main"] MAIN --> CL["CHANGELOG.md"] T["tag vX.Y.Z"] --> R["GitHub Release + PyPI"] CL --> S["sync-changelog.mjs"] --> WEB["Novedades + versión estable"] MAIN --> D["docs.yml → GitHub Pages"] ``` `docs/scripts/sync-changelog.mjs` genera la página [Novedades](https://albakore.github.io/pyzed/es/changelog/) en ambos idiomas desde `CHANGELOG.md` (raíz del repositorio) en cada build. Cada versión es un encabezado `## [X.Y.Z] - AAAA-MM-DD`; las que llevan sufijo (`1.0.0-rc.1`, `0.2.0b1`) se marcan como preliminares. La más reciente sin sufijo aparece como **última estable** en la cabecera, la portada y la página de novedades. ### Tests > Cómo se organizan los tests de Pyzed, cómo probar componentes de UI sin ventana y cómo funcionan los tests de regresión visual con imágenes de referencia. Source: https://albakore.github.io/pyzed/es/contributing/testing/ Los tests están en `tests/` y se ejecutan con pytest sin pantalla: no necesitan GPU ni display. ```bash pytest tests # todo (uv run pytest tests) pytest tests -m "not visual" # sin las comparaciones de capturas pytest tests/test_ui_overlays.py # un archivo ``` #### Probar componentes de `ui` Los componentes de `pyzed.ui` son funciones que devuelven widgets. Los widgets son objetos opacos de Rust, así que para mirar dentro usa el ayudante `ui_tree`: mientras dura el test sustituye `pyzed._core` por un grabador, y cada widget que construye un componente pasa a ser un `Node` con su `kind`, sus `args` posicionales y sus `props` con nombre. ```python import pytest import pyzed as pz from pyzed.ui import navigation as nav from ui_tree import install @pytest.fixture def tree(monkeypatch): return install(monkeypatch) def test_tab_bar_selects_by_id(tree): selected = [] bar = nav.tab_bar([nav.Tab("a", "a.py")], "a", on_select=selected.append, theme=pz.themes.ONE_DARK) bar.button_with("a.py").args[1]() # pulsa la pestaña assert selected == ["a"] assert bar.props["bg"] == pz.themes.ONE_DARK.tab_bar ``` `Node.find(kind, where)`, `find_all`, `walk`, `texts()` y `button_with(text)` buscan en el árbol. Pasa un `theme=` explícito para que los colores esperados no dependan del tema actual. Para comprobar que un componente se dibuja de verdad, constrúyelo dentro de una `App` y llama a `App.screenshot` (mira el final de `tests/test_ui_workspace.py`). #### Tests de regresión visual `tests/test_visual.py` dibuja cada escena de `tests/visual/scenes.py` con `App.screenshot` y la compara, píxel a píxel, con su imagen de referencia en `tests/visual/references/`. Un píxel cuenta como cambiado si un canal difiere en más de 16; el test falla si cambia más del 0,05 % de los píxeles. Las referencias se dibujan en **Linux x86-64 con el renderizador por CPU** (tiny-skia; `tests/conftest.py` pone `WGPU_BACKEND=dx12`, que no existe en Linux, así que iced recurre a él). Otras plataformas rasterizan el texto de forma algo distinta (también ARM), así que allí las comparaciones se omiten salvo con `PYZED_VISUAL=1`. Los tests visuales necesitan Pillow (`pip install pillow`; está en el grupo `test` de `pyproject.toml`). ##### Cuando falla una comparación El test escribe dos imágenes en `tests/visual/output/` (ignorada por git): - `.actual.png`: lo que dibuja el código ahora. - `.diff.png`: la referencia, atenuada, con los píxeles cambiados en rojo. En la CI se suben como el artefacto **visual-diffs-…** del job de tests de Linux. ##### Actualizar las referencias Si el cambio es intencionado (cambiaste el aspecto de un componente a propósito, o fusionaste un cambio que lo hizo), regenera las referencias y súbelas con tu cambio: ```bash pytest tests/test_visual.py --update-snapshots # todas las escenas que cambiaron pytest tests/test_visual.py --update-snapshots -k palette # solo las que coinciden con "palette" git add tests/visual/references ``` `--update-snapshots` solo reescribe las imágenes que cambiaron de verdad, así que las demás no se tocan. Ejecútalo en Linux para que las imágenes coincidan con lo que dibuja la CI. Si dos ramas cambian la misma referencia, resuelve el conflicto regenerándola después del merge. ##### Añadir una escena Añade a `tests/visual/scenes.py` una función que devuelva una `App`, decorada con `@scene`: ```python @scene("badges", size=(320, 120)) def badges(): return _app(lambda: pz.container(pz.row([ui.badge("new", tint="accent"), ui.count_badge(3)], spacing=8), padding=16)) ``` Después crea su referencia con `pytest tests/test_visual.py --update-snapshots -k badges` y sube el PNG. Mantén las escenas pequeñas y deterministas: sin relojes, datos aleatorios ni archivos de fuera del repositorio. Las animaciones se dibujan en su estado final. Una referencia cuya escena se borró hace fallar `test_every_reference_has_a_scene`, así que borra la imagen junto con la escena. ### SEO y llms.txt > Cómo se optimiza la documentación de Pyzed para buscadores y motores de respuesta con IA: metadatos, tarjetas sociales, JSON-LD, hreflang, sitemap y llms.txt. Source: https://albakore.github.io/pyzed/es/contributing/seo/ La documentación está pensada para que la encuentren los buscadores (Google, Bing...) y para que la lean y la citen los motores de respuesta con IA y los asistentes de programación. Casi todo se genera al compilar a partir de las propias páginas, así que una página nueva lo recibe gratis. Tu parte es escribir un buen `title` y una buena `description`. #### Escribir una buena descripción Cada página necesita una `description` en su frontmatter. Se convierte en la ``, el texto bajo el título en los resultados de búsqueda y en las vistas previas al compartir, el resumen de la página en `llms.txt` y la `description` de sus datos estructurados. - **Entre 120 y 160 caracteres.** Las más cortas se completan con una frase genérica; las más largas las cortan los buscadores. - **Di qué obtiene el lector**, con las palabras que la gente busca: *GUI de escritorio en Python*, *nativa*, *Rust*, *diseñador visual*, *alternativa a tkinter*, el nombre del componente o la función. Úsalas con naturalidad, una vez. - **Única por página**, y traducida (no copiada) en la página en inglés. - Ponla entre comillas si tiene dos puntos: `description: "Temas de Pyzed: One, Ayu y Gruvbox..."`. Las páginas de componentes de la biblioteca se generan: su descripción es el `blurb` del componente en `docs/scripts/library_data.py` más una frase que añade `seo_description()` en `gen_library.py`. Las descripciones del changelog están en `docs/scripts/sync-changelog.mjs`. #### Qué se genera | Qué | De dónde sale | | --- | --- | | ``, meta description, URL canónica, Open Graph, tarjetas de Twitter y keywords | `docs/src/routeData.ts` ([route middleware](https://starlight.astro.build/guides/route-data/) de Starlight) | | Imágenes de las tarjetas sociales (1200×630) | `docs/scripts/gen_og.py` escribe `docs/public/og/<sección>[.es].png` y los iconos de la app; vuelve a ejecutarlo si cambian las capturas | | Datos estructurados JSON-LD | `routeData.ts`: `Organization` y `WebSite` en todas, `SoftwareSourceCode` y `SoftwareApplication` (Pyzed Design, con la versión de `src/generated/release.json`) en la portada, `BreadcrumbList` y `TechArticle` en cada página, `FAQPage` en las [preguntas frecuentes](https://albakore.github.io/pyzed/es/faq/) | | Alternativas `hreflang` | Starlight, filtradas por `routeData.ts` para que una página solo liste los idiomas a los que está traducida. Una página sin traducir mostrada en español apunta su URL canónica a la inglesa y lleva `noindex` | | `sitemap-index.xml` | `@astrojs/sitemap` en `astro.config.mjs`, con los dos idiomas y el `<lastmod>` de cada página sacado del historial git de su archivo (el workflow de docs descarga el historial completo; el mismo historial da la fecha de *Última actualización* de cada página) | | `robots.txt`, `humans.txt`, `.well-known/security.txt`, `manifest.webmanifest` | Archivos estáticos en `docs/public/` | | `llms.txt`, `llms-full.txt`, `llms-small.txt`, `llms/<sección>.txt`, `es/llms.txt`, `es/llms-full.txt` | Endpoints en `docs/src/pages/`, generados desde la colección de contenido por `docs/src/lib/llms.ts` | | `<página>.md` (el Markdown de cada página) | `docs/src/pages/[...page].md.ts` | #### llms.txt y páginas en Markdown [llms.txt](https://llmstxt.org) es un mapa del sitio en Markdown para modelos de lenguaje. Pyzed publica: - [`/pyzed/es/llms.txt`](https://albakore.github.io/pyzed/es/llms.txt): qué es Pyzed, datos clave y cada página con una descripción de una línea, agrupadas por sección. En inglés: [`/pyzed/llms.txt`](https://albakore.github.io/pyzed/llms.txt). - [`/pyzed/es/llms-full.txt`](https://albakore.github.io/pyzed/es/llms-full.txt): toda la documentación en español en un archivo. En inglés: [`/pyzed/llms-full.txt`](https://albakore.github.io/pyzed/llms-full.txt). - [`/pyzed/llms-small.txt`](https://albakore.github.io/pyzed/llms-small.txt): solo lo necesario para escribir una app (empezar, tutoriales, guías, IA y preguntas frecuentes), en inglés. - `/pyzed/llms/<sección>.txt`: una sección, por ejemplo [`llms/library.txt`](https://albakore.github.io/pyzed/llms/library.txt). - Cada página en Markdown: cambia la barra final por `.md`, por ejemplo [`/pyzed/es/guides/layout.md`](https://albakore.github.io/pyzed/es/guides/layout.md) (`/pyzed/es/index.md` para la portada). Cada página HTML lo enlaza con `<link rel="alternate" type="text/markdown">`. El Markdown se limpia: se quitan el frontmatter, los imports y los componentes MDX, las pestañas y los avisos pasan a texto, los ejemplos que los tutoriales importan con `?raw` se incluyen como bloques de código, las imágenes conservan solo su texto alternativo y los enlaces pasan a ser absolutos. Las secciones nuevas aparecen solas; para darles nombre y lugar en el orden, añádelas a `SECTIONS` en `docs/src/lib/llms.ts`. Revisa el resultado después de compilar: ```bash cd docs npm run build less dist/es/llms.txt dist/es/guides/layout.md ``` #### Otras comprobaciones - **Un solo `h1` por página.** Starlight muestra el título como `h1`; empieza el contenido en `##`. - **Texto alternativo en cada imagen**: `![Qué muestra la captura](...)`. Es también lo que leen los asistentes en lugar de la imagen. - **Enlaces internos** con la ruta completa y la base: `/pyzed/es/guides/layout/`. - **robots.txt** solo funciona en la raíz de un dominio. Mientras la documentación se sirva desde `albakore.github.io/pyzed/`, la copia de `docs/public/robots.txt` documenta la política (todos los buscadores y rastreadores de IA son bienvenidos); el sitemap también se enlaza desde cada página y se puede enviar en Google Search Console y Bing Webmaster Tools. #### Que los buscadores encuentren la web Los buscadores solo muestran páginas que conocen, y un proyecto nuevo todavía no tiene enlaces que lleven a él. En cada despliegue de `main` (`.github/workflows/docs.yml`) pasan dos cosas: - **IndexNow.** `docs/scripts/indexnow.py` calcula un hash de cada página del sitemap, los publica en `page-hashes.json` y, cuando la web ya está desplegada, envía a [IndexNow](https://www.indexnow.org) las páginas que cambiaron respecto a la versión publicada. Bing (que también alimenta la búsqueda de ChatGPT, Copilot y DuckDuckGo), Yandex, Seznam, Naver y Yep las rastrean en horas. La clave es el archivo `docs/public/<clave>.txt` y es pública a propósito. - **Verificación de propiedad.** Las variables del repositorio `GOOGLE_SITE_VERIFICATION`, `BING_SITE_VERIFICATION` y `YANDEX_VERIFICATION` (*Settings → Secrets and variables → Actions → Variables*) se convierten en las etiquetas `google-site-verification`, `msvalidate.01` y `yandex-verification` de cada página. Para dar de alta la web una sola vez: 1. En [Google Search Console](https://search.google.com/search-console), añade la propiedad de prefijo de URL `https://albakore.github.io/pyzed/`, elige **Etiqueta HTML** y guarda el valor de `content` como la variable `GOOGLE_SITE_VERIFICATION`. Vuelve a lanzar el workflow **Docs**, verifica y envía `https://albakore.github.io/pyzed/sitemap-index.xml` en **Sitemaps**. 2. En [Bing Webmaster Tools](https://www.bing.com/webmasters), importa la web desde Search Console (o usa la variable `BING_SITE_VERIFICATION` del mismo modo) y envía el mismo sitemap. 3. Dale al proyecto enlaces en los que los buscadores confíen: en el repositorio, **About → Website** apuntando a la documentación y temas como `python`, `gui`, `desktop-app`, `rust`, `iced`; la página de PyPI (sus enlaces salen de `[project.urls]` en `pyproject.toml`); y artículos o listados que enlacen a la documentación. Pyzed comparte el nombre de importación con el módulo `pyzed` del SDK de ZED de Stereolabs, así que la documentación deja claro que el paquete es `pyzed-ui` y usa las palabras que la gente busca: *GUI en Python*, *app de escritorio*, *Rust*, *Iced*. ## Novedades ### Novedades > Novedades y notas de versión de Pyzed: componentes nuevos, funciones del diseñador, mejoras, correcciones y breaking changes de cada versión. Source: https://albakore.github.io/pyzed/es/changelog/ <!-- Archivo generado por docs/scripts/sync-changelog.mjs a partir de CHANGELOG.md. No lo edites a mano. --> La última versión estable es **1.9.0** (2026-10-22). ```bash pip install --upgrade pyzed-ui==1.9.0 ``` Las notas de cada versión también están en [GitHub Releases](https://github.com/albakore/pyzed/releases). #### [1.9.0] - 2026-10-22 <span class="release-tag stable">Última estable</span> ##### Highlights - **A code editor that knows your code**: hover cards with types and docs, go to definition, find references, rename across the project, quick fixes (ruff, ty, auto-imports, `uv add`) and sticky scroll. - **A Figma-style Properties panel**: numbers you drag, position and size first, auto layout for flex and grid containers, a margin/padding box model, file and folder pickers and animations on events. - **Dates, files and the system**: date, range and time pickers, a calendar, a file picker, a drop zone, popups and `pyzed.system` (system info, launcher, network and power monitors). - **Settings that follow you**: one Settings dialog with search, user profiles, per-project settings, Reset to default and the project's renderer (GPU or CPU) in `pyproject.toml`. ##### Features - ✨ IDE features in the code editor: hover, go to definition (F12, Ctrl+click), type definition, back/forward, find references, rename symbol (F2), quick fixes (Ctrl+.) and sticky scroll; the language servers fall back to the designer's Python when the project has no `.venv`. PR [#183](https://github.com/albakore/pyzed/pull/183) by [@albakore](https://github.com/albakore). - ✨ Component icons take the color of their role (layout, input, action, navigation, display, media, system, custom) in the toolbox, the outline, the tray and the Properties/Events headers, and the Design tab is now called Toolbox. PR [#184](https://github.com/albakore/pyzed/pull/184) by [@albakore](https://github.com/albakore). - ✨ One Settings dialog with search, user profiles, per-project settings and Reset to default; a project picks its renderer (GPU or CPU, graphics API, GPU and vsync) in `pyproject.toml`, and the MCP server reads and changes it. PR [#185](https://github.com/albakore/pyzed/pull/185) by [@albakore](https://github.com/albakore). - ✨ Keyboard focus for selects, switches, check boxes, sliders, segmented controls and radio groups, `pz.focusable` for any control, and tooltip shortcuts shown as key caps from the keymap. PR [#186](https://github.com/albakore/pyzed/pull/186) by [@albakore](https://github.com/albakore). - ✨ A Figma-style Properties panel: draggable numbers, Position first with align buttons, Text/Fill/Border/Spacing sections, auto layout, group and ungroup (Ctrl+G, Ctrl+Shift+G), path pickers for every file or folder property and animations on events. PR [#188](https://github.com/albakore/pyzed/pull/188) by [@albakore](https://github.com/albakore). - ✨ `ui.Calendar`, `ui.DatePicker`, `ui.DateRangePicker`, `ui.TimePicker`, `ui.file_input`, `ui.DropZone`, `pz.popup` and `pyzed.system` (`SystemInfo`, `Launcher`, `NetworkMonitor`, `PowerMonitor`), with the toolbox split into Forms, Date & time and System. PR [#189](https://github.com/albakore/pyzed/pull/189) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 A WSL project opened from Windows still lists its tools when Windows can't reach the distro. PR [#180](https://github.com/albakore/pyzed/pull/180) by [@albakore](https://github.com/albakore). - 🐛 A project's components load whenever it opens, and a missing one is drawn as a warning instead of breaking the form. PR [#182](https://github.com/albakore/pyzed/pull/182) by [@albakore](https://github.com/albakore). - 🐛 Windows updates no longer keep the old version: the installer clears the old files before copying, and Restart to update stops child processes, retries the folder swap and says so if it fails. PR [#192](https://github.com/albakore/pyzed/pull/192) by [@albakore](https://github.com/albakore). - 🐛 Component descriptions show without raw reST marks: inline code in the Properties header and clean tooltips. PR [#187](https://github.com/albakore/pyzed/pull/187) by [@albakore](https://github.com/albakore). ##### Refactors - ♻️ Custom events have their own place in the Events tab, apart from the built-in ones. PR [#181](https://github.com/albakore/pyzed/pull/181) by [@albakore](https://github.com/albakore). #### [1.8.1] - 2026-10-22 ##### Highlights - **Updates that get out of the way**: the update button moves to the top right of the title bar with a subtle blink, new versions download in the background, and Pyzed Design restarts cleanly on Windows after updating. ##### Features - ✨ The update button sits in the title bar next to View, Help and Settings and blinks softly while it waits; the version found by the automatic check downloads in the background, the check runs again every 4 hours while the designer is open, a finished download is reused and old downloads are cleaned up. PR [#178](https://github.com/albakore/pyzed/pull/178) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Restarting after an update on Windows no longer fails with `'NoneType' object has no attribute 'flush'` or opens several console windows. PR [#178](https://github.com/albakore/pyzed/pull/178) by [@albakore](https://github.com/albakore). ##### Docs - 📝 The README is in English (Spanish in `README.es.md`), the repository has a `CITATION.cff`, and the docs sitemap carries real dates, unique titles and image descriptions. PR [#177](https://github.com/albakore/pyzed/pull/177) by [@albakore](https://github.com/albakore). #### [1.8.0] - 2026-10-22 ##### Highlights - **A faster designer**: Pyzed draws only what changed (cached components, form controls and panels), skips rebuilds for pointer moves that change nothing and redraws big forms for less. - **Projects that stay in sync**: Pyzed Design warns when a project needs a newer `pyzed-ui` or a sync, and the Dependencies tab keeps itself up to date. ##### Features - ✨ Pyzed Design warns when a project needs a newer `pyzed-ui` or a sync, and keeps the Dependencies tab up to date. PR [#172](https://github.com/albakore/pyzed/pull/172) by [@albakore](https://github.com/albakore). - ✨ Hidden dialogs and drawers stay off the designer canvas until one is selected. PR [#171](https://github.com/albakore/pyzed/pull/171) by [@albakore](https://github.com/albakore). - ✨ Select and copy text in the Agent, Logs and Console panels, and close any tab. PR [#173](https://github.com/albakore/pyzed/pull/173) by [@albakore](https://github.com/albakore). - ⚡️ Pointer moves that change nothing no longer rebuild the view, and big forms are cheaper to redraw. PR [#174](https://github.com/albakore/pyzed/pull/174) by [@albakore](https://github.com/albakore). - ⚡️ Only what changed is drawn again: cached components, form controls and designer panels. PR [#175](https://github.com/albakore/pyzed/pull/175) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 New projects are no longer created in the installed app's folder (where they failed, or were deleted by the next update): Pyzed Design offers the last project's folder or `~/Pyzed Projects`, and refuses folders it cannot write to with a clear message. PR [#170](https://github.com/albakore/pyzed/pull/170) by [@albakore](https://github.com/albakore). ##### Docs - 📝 The docs reach search engines and AI answer engines sooner: new and changed pages are sent to IndexNow on every deploy, Google, Bing and Yandex ownership checks come from repository variables, the PyPI page links the docs, and the FAQ tells Pyzed apart from the ZED camera SDK's `pyzed` module. PR [#176](https://github.com/albakore/pyzed/pull/176) by [@albakore](https://github.com/albakore). #### [1.7.0] - 2026-10-22 1.6.0 was tagged but never published (its wheels failed to build), so this release carries all of it. ##### Highlights - **Sound in your apps**: `pyzed.audio`, an audio engine in Rust with 23 modules joined by cables (oscillators, filters, effects, sampler, drums, sequencer, MIDI) and low-latency output on every system, plus waveform, spectrum, knobs, faders, a webcam view and a VCV-style rack with animated cables. - **Export your app**: Pyzed Design builds apps for Windows, macOS and Linux on x64 and ARM with PyInstaller or Nuitka, with an icon and a splash screen. An Android prototype builds APKs too. - **Components that do work**: timers, shortcuts, file watchers, processes, workers and web requests as non-visual components in Pyzed Design, and `pyzed.services` in code. - **Hot reload that keeps your app running**: the state stays and new attributes, initial values and methods are picked up without restarting. - **A fuller designer**: every library component in the toolbox (Dock included) with its properties and events, the startup form, any Pyzed file open in the editor, and animations you start from code with an easier timeline. - **Tabs anywhere and WSL as Windows**: any tab goes to any dock or window of its own, and under WSL Pyzed opens its windows as native Windows programs. ##### Features - ✨ Put any tab anywhere: panels next to the forms, forms in docks, and a clearer tab drag. PR [#136](https://github.com/albakore/pyzed/pull/136) by [@albakore](https://github.com/albakore). - ✨ Interface and code fonts and sizes in the editor settings, and duplicating lines on every cursor. PR [#135](https://github.com/albakore/pyzed/pull/135) by [@albakore](https://github.com/albakore). - ✨ Non-visual components in Pyzed Design and `pyzed.services`: timers, shortcuts, file watchers, processes, workers, web requests and more. PR [#138](https://github.com/albakore/pyzed/pull/138) by [@albakore](https://github.com/albakore). - ✨ An audio engine, `pyzed.audio`: modules joined by cables, low-latency devices (WASAPI, CoreAudio, PulseAudio/PipeWire, ALSA), MIDI, offline rendering to WAV and analysis for visualizers. PR [#139](https://github.com/albakore/pyzed/pull/139) by [@albakore](https://github.com/albakore). - ✨ Audio UI components, a webcam view, a VCV-style rack with animated cables, audio demos and the Audio docs section. PR [#142](https://github.com/albakore/pyzed/pull/142) by [@albakore](https://github.com/albakore). - ✨ Windows open as native Windows windows when Pyzed runs in WSL (`pyzed windows setup`, `PYZED_WINDOWS=0` for WSLg). PR [#150](https://github.com/albakore/pyzed/pull/150) by [@albakore](https://github.com/albakore). - ✨ Every demo in the welcome gallery of Pyzed Design: audio, games, animation, styling, shaders, AI and more. PR [#153](https://github.com/albakore/pyzed/pull/153) by [@albakore](https://github.com/albakore). - ⚡️ Faster views with shared button parts, and the CPU renderer when the GPU is emulated. PR [#157](https://github.com/albakore/pyzed/pull/157) by [@albakore](https://github.com/albakore). - ✨ Export apps from Pyzed Design for Windows, macOS and Linux on x64 and ARM, with PyInstaller or Nuitka, icons and a splash screen. PR [#158](https://github.com/albakore/pyzed/pull/158) by [@albakore](https://github.com/albakore). - ✨ Android prototype: build APKs of Pyzed apps. PR [#159](https://github.com/albakore/pyzed/pull/159) by [@albakore](https://github.com/albakore). - ✨ Hot reload keeps the state and picks up new attributes, initial values and methods without restarting. PR [#164](https://github.com/albakore/pyzed/pull/164) by [@albakore](https://github.com/albakore). - ✨ Editor: choose the startup form, open every Pyzed file, indent with tabs and pace a held Backspace. PR [#166](https://github.com/albakore/pyzed/pull/166) by [@albakore](https://github.com/albakore). - ✨ The library's missing components in the designer's toolbox (Dock included), with their properties and events. PR [#165](https://github.com/albakore/pyzed/pull/165) by [@albakore](https://github.com/albakore). - ✨ Clearer animations: start them from code, an easier timeline and a "Start here" guide. PR [#167](https://github.com/albakore/pyzed/pull/167) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Claude Code, Codex and other agents start when Pyzed Design is opened from the desktop's menu (Node.js found where nvm, volta and fnm put it), **Retry** no longer repeats the message, and Pyzed's MCP server is added to Zed's settings even when they have comments. PR [#137](https://github.com/albakore/pyzed/pull/137) by [@albakore](https://github.com/albakore). - 🐛 The silent audio clock keeps real time when the system timer wakes late. PR [#144](https://github.com/albakore/pyzed/pull/144) by [@albakore](https://github.com/albakore). - 🐛 Pyzed builds from source on Linux without ALSA's headers: audio devices and MIDI come with the `alsa` feature, which the released wheels turn on. PR [#149](https://github.com/albakore/pyzed/pull/149) by [@albakore](https://github.com/albakore). - 🐛 Claude Code and Codex run their current ACP adapters, so the new models and effort levels show. PR [#151](https://github.com/albakore/pyzed/pull/151) by [@albakore](https://github.com/albakore). - 🐛 Your checkout and everything Pyzed Design starts run on Windows when they come from WSL. PR [#152](https://github.com/albakore/pyzed/pull/152) by [@albakore](https://github.com/albakore). - 🐛 The Windows core builds without Windows' folders on PATH, so maturin does not pick the Microsoft Store's `python.exe`. PR [#154](https://github.com/albakore/pyzed/pull/154) by [@albakore](https://github.com/albakore). - 🐛 Pyzed Design on Windows no longer freezes when it opens or creates a WSL project. PR [#155](https://github.com/albakore/pyzed/pull/155) by [@albakore](https://github.com/albakore). - 🐛 The agents' local servers no longer print dropped connections (`WinError 10054`) on Windows. PR [#156](https://github.com/albakore/pyzed/pull/156) by [@albakore](https://github.com/albakore). - 🐛 A release stays a draft until its files are attached, and the download page links only files that exist. PR [#163](https://github.com/albakore/pyzed/pull/163) by [@albakore](https://github.com/albakore). ##### Refactors - ♻ The new app template is centered with a flex box now that `pz.box` centers on the cross axis. PR [#132](https://github.com/albakore/pyzed/pull/132) by [@albakore](https://github.com/albakore). - ♻ The agent settings show the everyday ones first and fold the rest under **Advanced**. PR [#137](https://github.com/albakore/pyzed/pull/137) by [@albakore](https://github.com/albakore). ##### Docs - 📝 Game tutorials and the Animation docs section. PR [#146](https://github.com/albakore/pyzed/pull/146) by [@albakore](https://github.com/albakore). - 🍱 Pyzed Design captures retaken with the event tray. PR [#148](https://github.com/albakore/pyzed/pull/148) by [@albakore](https://github.com/albakore). - 📝 The events of every component, with imperative, declarative and functional examples. PR [#168](https://github.com/albakore/pyzed/pull/168) by [@albakore](https://github.com/albakore). ##### Internal - 📦 `pyzed-ui` installs only from the prebuilt wheels, never compiling the Rust core. PR [#140](https://github.com/albakore/pyzed/pull/140) by [@albakore](https://github.com/albakore). - ✅ The real-time audio test waits for the audio clock instead of a fixed sleep. PR [#141](https://github.com/albakore/pyzed/pull/141) by [@albakore](https://github.com/albakore). - 👷 CI decides which jobs run from `git diff`, so pull requests with over 300 files still run their tests. PR [#147](https://github.com/albakore/pyzed/pull/147) by [@albakore](https://github.com/albakore). - 💚 The release wheels build and test again: tests no longer share styles or need maturin, and the musllinux wheels build without ALSA. PRs [#162](https://github.com/albakore/pyzed/pull/162) and [#163](https://github.com/albakore/pyzed/pull/163) by [@albakore](https://github.com/albakore). #### [1.5.0] - 2026-10-20 ##### Highlights - **Program apps like the web**: hover, pressed and focus styles with transitions, CSS-like stylesheets, `pz.box` (block, flex, grid and stack layouts), media queries with `pz.viewport()`, and components with children and context. - **Errors never take the editor down**: a Logs panel, error boundaries, an error dialog and a selectable console. - **Agents you can follow**: every agent step shows live, each agent's modes and models in the composer, and Pyzed's MCP server is added to the coding agents and editors on this computer. ##### Features - ✨ Every agent step shows live, each agent's modes, models and options in the composer, a Thinking toggle, remembered Always allow permissions, and Pyzed's MCP server added to the AI agents and editors of this computer (Claude Desktop, Cursor, Windsurf, Gemini CLI, Codex, VS Code, Zed and more). PR [#123](https://github.com/albakore/pyzed/pull/123) by [@albakore](https://github.com/albakore). - ✨ Logs panel, error boundaries and an error dialog, a selectable console, the `ui.error_boundary`, `ui.error_card`, `ui.error_dialog` and `ui.ErrorInfo` components and a `readonly` code editor. PR [#124](https://github.com/albakore/pyzed/pull/124) by [@albakore](https://github.com/albakore). - ✨ State styles and transitions, CSS-like stylesheets, `pz.box`, media queries and breakpoints with `pz.viewport()`, children, context and `animate=` for components. PR [#125](https://github.com/albakore/pyzed/pull/125) by [@albakore](https://github.com/albakore). - ⚡ Pyzed Agent designs faster: it starts out knowing the toolbox, themes, `pz.ui` components and best practices, gets the open document with each message and lays out a whole form with one `add_controls` call. PR [#123](https://github.com/albakore/pyzed/pull/123) by [@albakore](https://github.com/albakore). - ✨ The agent's steps go to the Logs panel: each message, tool call and result, files, permissions and errors. PR [#126](https://github.com/albakore/pyzed/pull/126) by [@albakore](https://github.com/albakore). - ✨ MCP tools for timeline animations (`.pzanim`) and component states. PR [#127](https://github.com/albakore/pyzed/pull/127) by [@albakore](https://github.com/albakore). - ✨ The MCP writes apps like a web page: boxes, stylesheets, media queries and nesting components. PR [#128](https://github.com/albakore/pyzed/pull/128) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 `pz.box` shrinks, centers on the cross axis and sizes its children correctly. PR [#129](https://github.com/albakore/pyzed/pull/129) by [@albakore](https://github.com/albakore). - 🐛 Recent projects stay in `PYZED_CONFIG_DIR` when it is set. PR [#125](https://github.com/albakore/pyzed/pull/125) by [@albakore](https://github.com/albakore). ##### Docs - 📝 The error and AI agent components in the component library. PR [#130](https://github.com/albakore/pyzed/pull/130) by [@albakore](https://github.com/albakore). #### [1.4.0] - 2026-10-20 1.3.0 was tagged but never published: its Windows wheel build failed a test that is fixed in this version. Everything listed under 1.3.0 ships for the first time in 1.4.0. ##### Highlights - **Detachable panels and work layouts**: drag any panel out of Pyzed Design, stack panels together and save layouts for each kind of work. - **A Zed-like Agent panel**: new-thread menu, thread history, settings inside the panel, the ACP agent registry, skills, keep or reject each edit, external agent sign-in, follow-agent, a context meter, notifications, and threads and rules per project. - **Timeline animations**: `pz.Clip`, `pz.Track`, `pz.Call` and `AnimationPlayer`, saved as `.pzanim` and edited in the new Animation panel of `.pzform` files. - **Pyzed themes**: Midnight, Graphite, Forest, Rosé, Dawn and Glacier, plus property editors for each kind of value. - **Zed-like keymap**: `keymap.json` with contexts and a keymap editor. ##### Features - ✨ Zed-like keymap with contexts in `keymap.json` and a keymap editor. PR [#108](https://github.com/albakore/pyzed/pull/108) by [@albakore](https://github.com/albakore). - ✨ Forms kept in the project, mark or change the startup file and clone repositories. PR [#113](https://github.com/albakore/pyzed/pull/113) by [@albakore](https://github.com/albakore). - ✨ Detachable and stackable panels and work layouts. PR [#105](https://github.com/albakore/pyzed/pull/105), PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - ✨ Toolbar groups; New component moves to the toolbox and the New dialog. PR [#106](https://github.com/albakore/pyzed/pull/106), PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - ✨ Run/Stop and a search field in the middle of the title bar. PR [#107](https://github.com/albakore/pyzed/pull/107), PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - ✨ Zed-like Agent panel with history, settings, ACP registry, skills and keep/reject edits, and the reusable `ToolCall`, `Plan`, `EditsBar`, `ThreadHistory`, `AuthPrompt`, `ServiceRow`, `Catalog` and `context_meter` components. PR [#109](https://github.com/albakore/pyzed/pull/109), PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - ✨ Timeline animations with `pz.Clip`, `pz.Track`, `pz.Call`, `AnimationPlayer` and `.pzanim`. PR [#110](https://github.com/albakore/pyzed/pull/110), PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - ✨ States, sizes and events for the `ui` components; `pick_list` gains `on_open` and `on_close`. PR [#111](https://github.com/albakore/pyzed/pull/111), PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - ✨ WSL projects from Windows. PR [#115](https://github.com/albakore/pyzed/pull/115), PR [#119](https://github.com/albakore/pyzed/pull/119) by [@albakore](https://github.com/albakore). - ✨ External agent sign-in like Zed, follow-agent, context meter and notifications. PR [#116](https://github.com/albakore/pyzed/pull/116), PR [#119](https://github.com/albakore/pyzed/pull/119) by [@albakore](https://github.com/albakore). - ✨ Pyzed theme family and property editors for each kind of value. PR [#117](https://github.com/albakore/pyzed/pull/117), PR [#119](https://github.com/albakore/pyzed/pull/119) by [@albakore](https://github.com/albakore). - ✨ Animation panel in `.pzform`. PR [#118](https://github.com/albakore/pyzed/pull/118), PR [#119](https://github.com/albakore/pyzed/pull/119) by [@albakore](https://github.com/albakore). - ✨ Agents work on the active project: threads per project and project rules (`AGENTS.md`, `.rules`, `CLAUDE.md`...) in `pyzed.agents.rules`. PR [#120](https://github.com/albakore/pyzed/pull/120) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Agent sign-in on Windows sends the POST body correctly. PR [#114](https://github.com/albakore/pyzed/pull/114) by [@albakore](https://github.com/albakore). - 🐛 Clone target folder on Windows paths and the agent registry cache on Windows. PR [#119](https://github.com/albakore/pyzed/pull/119) by [@albakore](https://github.com/albakore). #### [1.3.0] - 2026-10-20 ##### Highlights - **Shader graphs**: build shaders from nodes in the new Shader view of Pyzed Design, like Godot's visual shaders, saved as `.pzshad` and compiled to WGSL (`pyzed.shading`). - **Pyzed Design as an app**: native installers for Windows (per user, no admin), macOS (`.dmg`) and Linux (`install.sh`), a preview update channel and a welcome window with 24 example projects. - **Agents that build whole apps**: sign in to Claude in the browser or with a Claude subscription, `run_app` and a plan checklist in the Agent panel, and MCP tools that plan an app (`plan_app`), show it live while it is built and work in the project's environment (`.venv`, WSL). - **Projects in the title bar**: the project, branch and environment, with a project picker for open and recent projects. - **New docs site**: sections with a header navbar, every Designer view documented, a shader node catalog, basic, intermediate and advanced tutorials, every variant of each component and retaken screenshots and clips. ##### Features - ✨ Update the MCP server with everything new up to 1.2.0 and add project tools to build whole apps. PR [#86](https://github.com/albakore/pyzed/pull/86) by [@albakore](https://github.com/albakore). - ✨ Sign in to Claude in the browser, use a Claude subscription, and let agents build apps and control the editor. PR [#87](https://github.com/albakore/pyzed/pull/87) by [@albakore](https://github.com/albakore). - ✨ Add a welcome window with example projects to Pyzed Design, and run commands without console windows on Windows. PR [#88](https://github.com/albakore/pyzed/pull/88) by [@albakore](https://github.com/albakore). - 📦 Make Pyzed Design an installable app: native installers, a preview channel, and templates and translations as data. PR [#89](https://github.com/albakore/pyzed/pull/89) by [@albakore](https://github.com/albakore). - ✨ Add node-based shader graphs (`.pzshad`) and a Shader view to Pyzed Design, like Godot's visual shaders. PR [#90](https://github.com/albakore/pyzed/pull/90) by [@albakore](https://github.com/albakore). - ✨ Open pages and sign-ins in the Windows browser when Pyzed Design runs under WSL. PR [#92](https://github.com/albakore/pyzed/pull/92) by [@albakore](https://github.com/albakore). - ✨ Guide agents with `plan_app`, show the app live while they build it, and run project tools in the project's environment. PR [#102](https://github.com/albakore/pyzed/pull/102) by [@albakore](https://github.com/albakore). - ✨ Show the project, branch and environment in the title bar with a project picker. PR [#103](https://github.com/albakore/pyzed/pull/103) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Fix the white Retro screen shader template and tidy shader graph layouts. PR [#91](https://github.com/albakore/pyzed/pull/91) by [@albakore](https://github.com/albakore). - 🐛 Keep the folder browser working next to folders without permission, and close a dialog that fails to draw. PR [#93](https://github.com/albakore/pyzed/pull/93) by [@albakore](https://github.com/albakore). - 🐛 Fix `ui.button` style overrides such as `radius`, rotated images and the alpha of SVG tints. PR [#101](https://github.com/albakore/pyzed/pull/101) by [@albakore](https://github.com/albakore). ##### Docs - 💄 Restyle the docs and split them into sections with a header navbar. PR [#94](https://github.com/albakore/pyzed/pull/94) by [@albakore](https://github.com/albakore). - 📝 Document every Designer view and add a shader node catalog with recipes. PR [#95](https://github.com/albakore/pyzed/pull/95) by [@albakore](https://github.com/albakore). - 📝 Expand Theming and Distribute, and show uv/pip/python commands per platform. PR [#96](https://github.com/albakore/pyzed/pull/96) by [@albakore](https://github.com/albakore). - 📝 Add basic, intermediate and advanced tutorials and turn the Showcase into a gallery. PR [#97](https://github.com/albakore/pyzed/pull/97) by [@albakore](https://github.com/albakore). - 📸 Retake every docs screenshot, add node captures and short clips, and place them in the pages. PR [#98](https://github.com/albakore/pyzed/pull/98) by [@albakore](https://github.com/albakore). - 📝 Show every variant of each library component, with plain-words intros and a gallery index. PR [#99](https://github.com/albakore/pyzed/pull/99) by [@albakore](https://github.com/albakore). - 📝 Update the README with Pyzed Design, shaders, games, AI, integrations and new screenshots. PR [#100](https://github.com/albakore/pyzed/pull/100) by [@albakore](https://github.com/albakore). ##### Internal - 👷 Speed up CI: cache the docs images, run jobs only for the files they check, a smaller PR matrix and parallel tests. PR [#104](https://github.com/albakore/pyzed/pull/104) by [@albakore](https://github.com/albakore). #### [1.2.0] - 2026-10-20 ##### Highlights - **Games**: a game loop (`pz.frames`), held keys (`pz.pressed`), `on_key_up`, sprites and `pyzed.game`, with Snake, Breakout and Neon Invaders demos and tutorials. - **Shaders**: `pz.shader` paints a widget with a WGSL function on the GPU, with showcase demos (game HUD, main menu, inventory, glass dashboard). - **Integrations**: tutorials and runnable demos for FastAPI, SQLAlchemy, SQLModel, Django, httpx and Pydantic, with `pz.validation_errors` and `ui.field(error=)`. - **A stronger AI agent**: images, external MCP servers, external agents through ACP (Gemini CLI, Codex...), GitHub Copilot, Google Vertex AI, Claude through identity federation, sign-in from Pyzed Design and a Zed-like composer. - **Pyzed Design feels like Zed**: the project's files with Zed's right-click menu, any text file in the code editor, tabs you drag out into their own windows, a custom window frame, anchors that follow while you design, a compact details panel for properties and an icon picker. - **Faster by default**: `import pyzed` takes 78 ms instead of 178 ms, lighter view rebuilds and mimalloc in the native core. - **Why Pyzed**: a new docs section comparing Pyzed with tkinter, Qt, Kivy, Flet and others, with measured benchmarks. ##### Features - ✨ Edit component properties of any type in a compact details panel, kept in sync with the code. PR [#61](https://github.com/albakore/pyzed/pull/61) by [@albakore](https://github.com/albakore). - ✨ Add an icon picker with search and categories to the library and Pyzed Design. PR [#62](https://github.com/albakore/pyzed/pull/62) by [@albakore](https://github.com/albakore). - ✨ Sign in to AI providers from Pyzed Design and add a Zed-like agent composer. PR [#63](https://github.com/albakore/pyzed/pull/63) by [@albakore](https://github.com/albakore). - ✨ Show the project's files in Pyzed Design, with file search, a Dependencies tab and a folder browser. PR [#65](https://github.com/albakore/pyzed/pull/65) by [@albakore](https://github.com/albakore). - ✨ Add anchor presets, offsets, size limits, pivot and rotation to Position, and `pz.anchor_layout`. PR [#66](https://github.com/albakore/pyzed/pull/66) by [@albakore](https://github.com/albakore). - ✨ Add images, MCP servers, external agents, GitHub and Google sign-in and Claude identity federation to the AI agent. PR [#68](https://github.com/albakore/pyzed/pull/68) by [@albakore](https://github.com/albakore). - ✨ Add a game loop, held keys and sprites, with three game demos and tutorials. PR [#69](https://github.com/albakore/pyzed/pull/69) by [@albakore](https://github.com/albakore). - ✨ Add `pz.shader` and showcase demos: game HUD, main menu, inventory, glass dashboard and shaders. PR [#70](https://github.com/albakore/pyzed/pull/70) by [@albakore](https://github.com/albakore). - ✨ Add integration demos and tutorials for FastAPI, SQLAlchemy, SQLModel, Django and httpx, with `pz.validation_errors`, `ui.field(error=)` and object options in `ui.select`. PR [#71](https://github.com/albakore/pyzed/pull/71) by [@albakore](https://github.com/albakore). - ✨ Open any text file from the Project tab in the code editor, with a tab per file. PR [#76](https://github.com/albakore/pyzed/pull/76) by [@albakore](https://github.com/albakore). - ✨ Follow anchors while designing: resizing the form, a component or a container moves what is inside. PR [#77](https://github.com/albakore/pyzed/pull/77) by [@albakore](https://github.com/albakore). - ✨ Drag tabs out into their own windows and stack them back in Pyzed Design. PR [#79](https://github.com/albakore/pyzed/pull/79) by [@albakore](https://github.com/albakore). - ✨ Give the Project tree Zed's right-click menu: cut, copy, paste, duplicate, undo, trash, terminal, .gitignore and permalinks, with its keys. PR [#81](https://github.com/albakore/pyzed/pull/81) by [@albakore](https://github.com/albakore). - ✨ Add custom window frames: own title bar, platform window buttons, Zed-like menus and resize borders, used by Pyzed Design. PR [#83](https://github.com/albakore/pyzed/pull/83) by [@albakore](https://github.com/albakore). - ✨ Wheel scrolling, configurable key bindings and a sectioned settings dialog for the code editor. PR [#84](https://github.com/albakore/pyzed/pull/84) by [@albakore](https://github.com/albakore). - ⚡ Start faster and rebuild views with less work by default. PR [#82](https://github.com/albakore/pyzed/pull/82) by [@albakore](https://github.com/albakore). - 💄 Normalize margins and spacing across Pyzed Design's panels with shared spacing tokens. PR [#80](https://github.com/albakore/pyzed/pull/80) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Restart Pyzed Design after an update even while background work is running. PR [#60](https://github.com/albakore/pyzed/pull/60) by [@albakore](https://github.com/albakore). - 🐛 Write Optional props as `X | None` on every Python version. PR [#64](https://github.com/albakore/pyzed/pull/64) by [@albakore](https://github.com/albakore). - 🐛 Pick color properties with the color picker and keep half-typed colors from breaking the view. PR [#67](https://github.com/albakore/pyzed/pull/67) by [@albakore](https://github.com/albakore). - 🐛 `ui.select` hands `on_select` the chosen object (a model, an enum) instead of its label, as documented, and `Command.perform` calls its `then` when the function returns `None`. PR [#71](https://github.com/albakore/pyzed/pull/71) by [@albakore](https://github.com/albakore). - 🐛 Build with Rust 1.88 again: drop the if-let match guard and check the MSRV in CI. PR [#75](https://github.com/albakore/pyzed/pull/75) by [@albakore](https://github.com/albakore). - 🐛 Keep completion rows inside the list and let the list scroll. PR [#78](https://github.com/albakore/pyzed/pull/78) by [@albakore](https://github.com/albakore). ##### Docs - 📝 Add SEO/GEO, llms.txt, Why Pyzed with measured benchmarks, AI agents and Blocks docs. PR [#74](https://github.com/albakore/pyzed/pull/74) by [@albakore](https://github.com/albakore). - 📝 Redo the integration demos following the official tutorials, with an architectures page. PR [#73](https://github.com/albakore/pyzed/pull/73) by [@albakore](https://github.com/albakore). - 📝 Document the window frame, the editor's `keymap` and `scrollbar`, held keys and `on_key_up`, `pyzed.game` and the release secrets, and re-measure Pyzed's benchmarks with 1.2.0. PR [#85](https://github.com/albakore/pyzed/pull/85) by [@albakore](https://github.com/albakore). ##### Internal - 👷 Bundle the Agent panel's GitHub and Google sign-in apps into the release builds from Actions secrets. PR [#72](https://github.com/albakore/pyzed/pull/72) by [@albakore](https://github.com/albakore). #### [1.1.0] - 2026-10-19 ##### Highlights - **Large data**: virtual lists and tables that only build the rows in view (200 000 rows in 0.2 ms per view) and `@pz.memo` to reuse view subtrees. - **Real effects**: animate rotation, size, padding and font size, with real blur, backdrop blur and bloom on the GPU and in software. - **A better Pyzed Design**: copy and paste between forms, data binding and validation, and a theme and language preview. - **Accessibility**: Tab focus between fields, the system's reduce motion setting and a screen reader guide. - **Input methods**: Chinese, Japanese and Korean input in the code editor. - **Safer by default**: private agent threads, hidden secret files, a hardened sign-in and checked updates. - **More platforms**: wheels for musllinux, tested on Python 3.10 to 3.14 and on ARM Linux, Pyzed Design builds for Linux ARM and Intel Macs, and build attestations. ##### Breaking Changes - ⬆️ Pyzed now requires Python 3.10 or newer. PR [#52](https://github.com/albakore/pyzed/pull/52) by [@albakore](https://github.com/albakore). ##### Features - ✨ Add virtual lists and tables and memoized view subtrees for large data. PR [#55](https://github.com/albakore/pyzed/pull/55) by [@albakore](https://github.com/albakore). - ✨ Add copy and paste between forms, data binding and validation, and theme/language preview to Pyzed Design. PR [#56](https://github.com/albakore/pyzed/pull/56) by [@albakore](https://github.com/albakore). - ✨ Support input methods (IME) for Chinese, Japanese and Korean in the code editor. PR [#57](https://github.com/albakore/pyzed/pull/57) by [@albakore](https://github.com/albakore). - ✨ Animate rotation, size, padding and font size, with real blur, backdrop blur and bloom. PR [#58](https://github.com/albakore/pyzed/pull/58) by [@albakore](https://github.com/albakore). - ♿ Add Tab focus between fields, follow the system's reduce motion setting and document screen reader support. PR [#54](https://github.com/albakore/pyzed/pull/54) by [@albakore](https://github.com/albakore). ##### Fixes - 🔒️ Keep agent threads private (`0600`, with a *Keep conversations* switch), hide secret files such as `.env` and keys from agents, ignore forged sign-in callbacks and refuse updates without a checksum. PR [#52](https://github.com/albakore/pyzed/pull/52) by [@albakore](https://github.com/albakore). - 🐛 Read component props on Python 3.14, where annotations are lazy. PR [#52](https://github.com/albakore/pyzed/pull/52) by [@albakore](https://github.com/albakore). ##### Internal - 👷 Test on Python 3.10, 3.12 and 3.14 and on ARM Linux; build musllinux wheels, Pyzed Design for Linux ARM and Intel Macs, optional signing and notarization, and build provenance attestations. PR [#52](https://github.com/albakore/pyzed/pull/52) by [@albakore](https://github.com/albakore). - ✅ Add visual regression tests and tests for workspace, navigation, overlays and agent threads. PR [#53](https://github.com/albakore/pyzed/pull/53) by [@albakore](https://github.com/albakore). #### [1.0.1] - 2026-10-19 ##### Fixes - 🔒️ AI agents refuse tools marked `writes=True` when `confirm_writes` is on (the default) and no `confirm` callback was given, instead of running them without asking. Pass `confirm=` or set `confirm_writes=False` to keep the old behavior. - 🔒️ `pyzed mcp --transport streamable-http` no longer offers the `screenshot` tool, which runs the code it receives, unless you add `--allow-code`. Over stdio nothing changes. - 🐛 Pyzed Design refuses a `.pzform` or component saved by a newer Pyzed instead of opening it and losing what it does not know. - 🏷️ Type stubs for the Rust core (`_core.pyi`) and `py.typed`, so editors and type checkers see every widget and command. ##### Docs - 📝 Add the *Stability and known limitations* guide: the versioning and deprecation policy and what Pyzed does not do yet. #### [1.0.0] - 2026-10-19 First stable version. From 1.0.0 on, the public API follows semantic versioning: breaking changes only land in a new major version. ##### Highlights - **Animations and effects**: animate any widget property, with ready-made animations, transitions between windows and visual effects such as glow, bloom, glass and transparency, in the library and in Pyzed Design. - **AI agents**: an Agent panel in Pyzed Design and `pyzed.agents` in the library, to write code and generate components and forms, with browser login or personal tokens kept in the system keychain. `pyzed mcp` also exposes Pyzed to external agents. - **Built-in reference**: a searchable, Godot-style reference of every component and function, inside Pyzed Design and from the library. - **A Zed-grade code editor**: Zed's actions and keys, multiple cursors, find and replace. - **Updates**: Pyzed Design updates itself like Zed, with a What's new tab. - **New docs site**: a component library, the Pyzed Design manual and a download page. ##### Features - ✨ Add Zed-style updates to Pyzed Design with a What's new tab. PR [#40](https://github.com/albakore/pyzed/pull/40) by [@albakore](https://github.com/albakore). - ✨ Add an MCP server for AI agents (`pyzed mcp`). PR [#41](https://github.com/albakore/pyzed/pull/41) by [@albakore](https://github.com/albakore). - ✨ Add Zed's editor actions, keys, multiple cursors, find and replace, and opt-in telemetry. PR [#44](https://github.com/albakore/pyzed/pull/44) by [@albakore](https://github.com/albakore). - ✨ Add AI agents to Pyzed Design and the library (`pyzed.agents`). PR [#47](https://github.com/albakore/pyzed/pull/47) by [@albakore](https://github.com/albakore). - ✨ Add a built-in, searchable reference to the library and the designer, like Godot's. PR [#46](https://github.com/albakore/pyzed/pull/46) by [@albakore](https://github.com/albakore). - ✨ Add animations, transitions and visual effects (glow, bloom, glass) to widgets, windows and Pyzed Design. PR [#48](https://github.com/albakore/pyzed/pull/48) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Fix the MCP docs index after the docs redesign and document self-updates. PR [#45](https://github.com/albakore/pyzed/pull/45) by [@albakore](https://github.com/albakore). ##### Docs - 📝 Redesign the docs site with a component library, the Pyzed Design manual and a download page. PR [#42](https://github.com/albakore/pyzed/pull/42) by [@albakore](https://github.com/albakore). - 📝 Add the reference browser to the component library. PR [#49](https://github.com/albakore/pyzed/pull/49) by [@albakore](https://github.com/albakore). ##### Translations - 🌐 Translate the designer's context menu, code panel, color picker and status bar, and rename it to Pyzed Design. PR [#43](https://github.com/albakore/pyzed/pull/43) by [@albakore](https://github.com/albakore). #### [0.4.0] - 2026-10-19 ##### Highlights - **Component Designer**: draw reusable components with typed properties, state, methods and events, inherit from them and use them like any widget, with a Component view, tabs and a New component button. - **A much richer designer**: containers, dockable panels, menus, templates, a debug console, a Style section with a floating color picker, list and table editors, drag and drop from the outline, and a code editor with completion, diagnostics, folding and a minimap. - **Styles and layout**: CSS-like styles for every widget with global styles, gradients and masks, plus track grids, flex, resizable panels and a Zed-style dock. - **Apps and projects**: common events for every widget, translations, ready-made dialogs, uv projects and `pyzed build` to package an app. - **Full API docs**: every function, method, class and component is documented. ##### Features - ✨ Add uv projects, a Project tab in the designer and `pyzed build`. PR [#18](https://github.com/albakore/pyzed/pull/18) by [@albakore](https://github.com/albakore). - ✨ Add common events for every widget, custom components and quiet Linux/WSL startup. PR [#19](https://github.com/albakore/pyzed/pull/19) by [@albakore](https://github.com/albakore). - ✨ Add completion, diagnostics, code lens and settings to the designer's code editor. PR [#20](https://github.com/albakore/pyzed/pull/20) by [@albakore](https://github.com/albakore). - ✨ Add track grids, flex, resizable panels and a Zed-style dock. PR [#25](https://github.com/albakore/pyzed/pull/25) by [@albakore](https://github.com/albakore). - ✨ Add a component designer mode: draw reusable components with properties, state, methods and events. PR [#24](https://github.com/albakore/pyzed/pull/24) by [@albakore](https://github.com/albakore). - ✨ Add translations, ready-made dialogs and opening forms from code. PR [#26](https://github.com/albakore/pyzed/pull/26) by [@albakore](https://github.com/albakore). - ✨ Add auto-closing pairs, code folding, a minimap, font zoom and cursor shapes to the code editor. PR [#27](https://github.com/albakore/pyzed/pull/27) by [@albakore](https://github.com/albakore). - ✨ Add containers, dockable panels, menus, templates, a debug console and translations to the designer. PR [#28](https://github.com/albakore/pyzed/pull/28) by [@albakore](https://github.com/albakore). - ✨ Drag components and outline rows onto the form, and open context menus where the pointer is. PR [#30](https://github.com/albakore/pyzed/pull/30) by [@albakore](https://github.com/albakore). - ✨ Make designed components responsive, add a New component button and component inheritance. PR [#31](https://github.com/albakore/pyzed/pull/31) by [@albakore](https://github.com/albakore). - ✨ Add a grip tab to select and drag containers in the designer. PR [#32](https://github.com/albakore/pyzed/pull/32) by [@albakore](https://github.com/albakore). - ✨ Add CSS-like styles for every widget, global styles, gradients, masks and a color picker. PR [#33](https://github.com/albakore/pyzed/pull/33) by [@albakore](https://github.com/albakore). - ✨ Add a Style section and the form's global style to the designer, with the color picker. PR [#35](https://github.com/albakore/pyzed/pull/35) by [@albakore](https://github.com/albakore). - ✨ Add a Component view with tabs, typed properties, reusable methods and child access. PR [#37](https://github.com/albakore/pyzed/pull/37) by [@albakore](https://github.com/albakore). - ✨ Float the color picker, add list and table editors, and line up the Properties and Events docks. PR [#36](https://github.com/albakore/pyzed/pull/36) by [@albakore](https://github.com/albakore). ##### Fixes - 🐛 Silence "MESA: error: ZINK" with `MESA_LOG=null` (`silent` is not a Mesa option). PR [#22](https://github.com/albakore/pyzed/pull/22) by [@albakore](https://github.com/albakore). - 🐛 Make a fixed grid height the height of each row with `columns=<int>`. PR [#29](https://github.com/albakore/pyzed/pull/29) by [@albakore](https://github.com/albakore). ##### Upgrades - ➕ Add ty to the development dependencies. PR [#23](https://github.com/albakore/pyzed/pull/23) by [@albakore](https://github.com/albakore). ##### Docs - 📝 Document setting up Pyzed with uv. PR [#21](https://github.com/albakore/pyzed/pull/21) by [@albakore](https://github.com/albakore). - 📝 Document every function, method, class and component. PR [#38](https://github.com/albakore/pyzed/pull/38) by [@albakore](https://github.com/albakore). #### [0.3.1] - 2026-10-18 ##### Fixes - 🐛 Read `pyzed.__version__` from the package metadata. PR [#17](https://github.com/albakore/pyzed/pull/17) by [@albakore](https://github.com/albakore). #### [0.3.0] - 2026-10-18 ##### Highlights - **Pyzed Designer**: a Visual Basic / Qt Designer style form editor (`pyzed design app.pzform`) with a toolbox, dot-grid snapping, alignment tools, property grid, events and per-event code editing. **F5** runs the form with all its logic. - **Hot reload**: `pyzed dev main.py` updates the open window on every save and keeps the app's state. - **Bilingual docs**: the site is in English by default, with a full Spanish translation under `/es/`. ##### Features - ✨ Add visual form designer with event code editing, grid snapping and alignment. PR [#15](https://github.com/albakore/pyzed/pull/15) by [@albakore](https://github.com/albakore). - ✨ Add hot reload for running apps (`pyzed dev`, `pz.run(App, reload=True)`, `PYZED_RELOAD=1`). PR [#14](https://github.com/albakore/pyzed/pull/14) by [@albakore](https://github.com/albakore). ##### Upgrades - ⬆️ Bump pyo3 from 0.27.2 to 0.30.1. PR [#11](https://github.com/albakore/pyzed/pull/11) by [@dependabot](https://github.com/apps/dependabot). - ⬆️ Bump two-face from 0.4.5 to 0.5.2. PR [#10](https://github.com/albakore/pyzed/pull/10) by [@dependabot](https://github.com/apps/dependabot). - ⬆️ Bump png from 0.17.16 to 0.18.1. PR [#9](https://github.com/albakore/pyzed/pull/9) by [@dependabot](https://github.com/apps/dependabot). - ⬆️ Bump the GitHub Actions group with 7 updates. PR [#12](https://github.com/albakore/pyzed/pull/12) by [@dependabot](https://github.com/apps/dependabot). ##### Docs - 📝 README images now show up on PyPI. ##### Translations - 🌐 Make the docs bilingual (English by default, Spanish under `/es`), with Mermaid diagrams and new screenshots. PR [#13](https://github.com/albakore/pyzed/pull/13) by [@albakore](https://github.com/albakore). ##### Internal - 👷 Speed up CI, gitmoji release notes, PyPI README images and contribution guidelines. PR [#8](https://github.com/albakore/pyzed/pull/8) by [@albakore](https://github.com/albakore). - 👷 Deploy the docs site only from `main`, not from release tags. PR [#7](https://github.com/albakore/pyzed/pull/7) by [@albakore](https://github.com/albakore). #### [0.2.0] - 2026-10-18 First version published on PyPI, as `pyzed-ui`. ##### Breaking Changes - 💥 `App.run()` raises an error when it is called a second time in the same process. ##### Features - ✨ Several windows per app: open and close extra windows, window hooks and window commands (`Window`). - ✨ Async support: coroutines, generators and async generators from `update` or `Command.perform`. - ✨ Subscriptions: timers with `pz.every` and streams with `pz.listen`. - ✨ New widgets: `image` (bitmaps), `image_viewer`, `canvas` with `pz.draw`, `rich_text`, `markdown`, `qr_code`, `pick_list`, `radio`, `split`, `pin`, `grid` and `responsive`; gradients and more style options. - ✨ New `pyzed.ui` components: `select`, `radio_group`, `number_field`, `split_button`, `chip`, `disclosure`, `callout`, `table`, `facepile`, `diff_stat`, charts, `spinner`, `popover`, `markdown`, `split` and `stat`. - ✨ Theming: import Zed themes, bundled Ayu, Gruvbox and One themes, `Theme.generate()` and automatic light/dark switching. - ✨ Editor: horizontal scrolling with a sticky line-number gutter, correct line numbers with `wrap`, active line, cursor autoscroll and PageUp/PageDown. ##### Fixes - 🐛 Images are clipped and rounded correctly in every renderer, including off-screen screenshots without a GPU. ##### Docs - 📝 Documentation site with guides, tutorials, API reference and component illustrations. - 📝 Examples based on Iced's demos (todos, game of life, clock, stopwatch, downloads, websocket, multiple windows, image viewer, markdown, QR code, styling, context menu, panes) and a five-step tutorial. ##### Internal - 👷 CI and release workflows: wheels for Linux, macOS and Windows published on PyPI. #### [0.1.0] - 2026-10-18 First version (not published on PyPI). ##### Features - ✨ Elm-style runtime on top of Iced 0.14 (`App`, `update`, `view`, `on_start`, `on_key`). - ✨ Messages can be any Python value, or callables that are called directly. - ✨ `Command`: `exit`, `focus`, `set_editor_text`, `set_editor_cursor`, `scroll_to_end`, `batch`, `message` and `perform` (work on a thread). - ✨ `pz.post()` to send messages from other threads. - ✨ Base widgets: `text`, `column`, `row`, `container`, `center`, `button`, `text_input`, `scrollable`, `space`, `rule`, `mouse_area`, `stack`, `opaque`, `hover`, `tooltip`, `checkbox`, `toggler`, `slider`, `progress_bar`, `code_editor`, `svg` and `icon`. - ✨ About 40 Zed-style components in `pyzed.ui`: buttons, forms, lists, file tree, tabs, workspace with docks, command palette, toasts, code editor and terminal. - ✨ One Dark, One Light and Gruvbox Dark themes with Zed's color tokens, and `Theme.derive()`. - ✨ Bundled IBM Plex fonts and Lucide icons. - ✨ Off-screen screenshots with `app.screenshot()` (no GPU or display needed). - ✨ CLI: `pyzed new` and `pyzed icons`.