# 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…).
