
# Hello World [Python]

## Introduction

Python is the easiest way to use Azul. You can write idiomatic Python — plain classes, 
plain `str`, plain method calls — and the binding (uses Rusts `pyo3`) takes care of the rest.

## Installation

Azul is not on the public PyPI (the `azul` package on pypi.org is an unrelated
project - do not run a plain `pip install azul`). There are two ways to install:
the self-hosted pip index on azul.rs, or a manual download.

### Self-hosted pip index

CI publishes Linux (manylinux x86_64) wheels to a self-hosted PEP 503 index
served from azul.rs. Pointing `--index-url` at it makes pip resolve `azul`
from azul.rs instead of pypi.org:

```sh
pip install azul --index-url https://azul.rs/ui
```

This is NOT the public PyPI - the index is generated by the same CI run that
builds the release binaries. On platforms without a wheel on that index, use
the manual download below.

### Manual download

Download the prebuilt extension module for 
your platform from the [release page](https://azul.rs/ui/release/0.2.0) and put 
it next to your script:

```sh
# macOS
curl -L -o azul.so https://azul.rs/ui/release/0.2.0/azul.so
# linux
curl -L -o azul.so https://azul.rs/ui/release/0.2.0/azul.cpython.so
# windows
curl.exe -L -O https://azul.rs/ui/release/0.2.0/azul.pyd
```

The module bundles the prebuilt native library, so there are no further system 
dependencies to worry about. The abi3 wheel targets **Python 3.10+** (pyo3 is
`abi3-py310`) - make sure you have a recent version.

> [!NOTE]
> If there is no prebuilt module for your platform, 
> see "[Building the extension](#building-the-extension)" below for the 
> manual route.

## Simple "Counter" Example

```python
from azul import *

# Plain Python class - "single source of truth" for app state
class DataModel:
    def __init__(self, counter):
        self.counter = counter

# Layout callback: f(DataModel, LayoutCallbackInfo) -> Dom. Runs once on
# startup and again after every callback that returns Update.RefreshDom.
def layout(data, info):

    # Rendered counter label: a text node wrapped in a styled div.
    # .with_css(...) consumes self and returns a new Dom, so builder
    # calls chain inline.
    label = (Dom.create_div()
             .with_child(Dom.create_text(str(data.counter)))
             .with_css("font-size: 32px;"))

    # Button widget with a click handler. Everything lives in the flat
    # `azul` module; with_on_click(data, callback) registers the handler
    # and .dom() turns the widget into a Dom node.
    button = (Button.create("Increase counter")
              .with_on_click(data, on_click)
              .dom()
              .with_css("flex-grow: 1;"))

    # Final wrapup - Dom.create_body builds the root, then .with_child(...)
    # appends children. Builder methods return a NEW Dom - keep chaining
    # (or re-assign the result); they do not mutate in place.
    return (Dom.create_body()
            .with_child(label)
            .with_child(button))

# Click callback: f(DataModel, CallbackInfo) -> Update. 'data' is the same
# Python instance you passed to App.create, it is mutated in place (thread
# safe). Update variants are plain class attributes - no parentheses:
# return Update.RefreshDom.
def on_click(data, info):
    data.counter += 1
    return Update.RefreshDom

# main function
if __name__ == "__main__":

    # Initialize the data model (here we set counter=5 on startup)
    model = DataModel(5)

    # Configure the window. layout is the "/" default route; SPA-style
    # routing is done later by swapping the layout callback.
    window = WindowCreateOptions.create(layout)

    # AppConfig discovers system-native styling, monitor layout, etc.
    # App.run blocks until the last window closes.
    app = App.create(model, AppConfig.create())
    app.run(window)
```

Three things to notice.

- **Pass plain Python objects.** No upcast, no downcast, no reflection macro. The binding wraps your `DataModel` instance for you and hands the *same* instance back to your callbacks. The framework holds a strong reference until you drop the `App`, so the GC will not eat it under your feet.
- **Strings are `str`, styles are CSS strings.** No `AzString`, no `String(...)` wrapper, no `AZ_CONST_STR` macro. Pass UTF-8 Python strings; the binding converts at the boundary.
- **Callbacks are regular functions** with the signature `(data, info) -> Update` (or `-> Dom` for layout). No `extern "C"`, no boxing, no decorators — just `def`.

Things we did not use that you may want to explore next.

- The `info` argument — read-only access to the system font cache, image cache, GL context, current window size, routing, and localization dictionaries in `layout`; lots of mutation helpers in `on_click` (DOM navigation, CSS overrides without rebuilding, computed-layout queries).
- `WindowCreateOptions` — the Python binding currently exposes only `WindowCreateOptions.create(layout)`; setting the window title, size, decorations etc. from Python is not wrapped yet. The underlying options are covered in [windowing](../windowing.md).

## Run it

```sh
python3 hello-world.py
```

You should see the window pictured on the [hello-world landing page](../hello-world.md). Click the button: the counter increments, the layout callback re-runs, and the new value renders.

1. `app.run(window)` opened a native window and ran `layout()` once with your `DataModel` on startup.
2. The returned `Dom` was styled, laid out, and rendered.
3. On click, the framework matched the button's event filter, called `on_click(data, info)`, observed the `Update.RefreshDom` return, and re-invoked `layout()`.
4. The new `Dom` was diffed against the previous one; only the changed text node was repainted.

## Building the extension

Only needed if there is no prebuilt module for your platform, or if you want to track `master`. From a checkout:

```sh
# git clone https://github.com/fschutt/azul
# cd myfolder/azul
cargo build -p azul-dll --release \
    --no-default-features --features python-extension
```

The resulting library is `target/release/libazul.{so,dylib}` (`azul.dll` on Windows). Python imports it as `azul`, so rename or symlink it:

```sh
# macOS
cp target/release/libazul.dylib target/release/azul.so
# Linux
cp target/release/libazul.so target/release/azul.so
# Windows
copy target\release\azul.dll target\release\azul.pyd
```

Then either run Python from the directory containing the file, or prepend that path to `sys.path`:

```python
import sys, os
sys.path.insert(0,
    os.path.join(os.path.dirname(__file__), 'target', 'release'))
import azul
```

## Common errors

- **`ModuleNotFoundError: No module named 'azul'`** — the downloaded `azul.so` / `azul.pyd` is not in the directory you are running from (or on `sys.path`). Run `python3` from the directory containing the file, or prepend that path to `sys.path`.
- **Blank window / dead button** — a callback raised an exception. The binding prints the Python traceback to stderr and falls back to a default return value (an empty `Dom` for `layout`, `DoNothing` for event callbacks), so check the terminal you started the app from.
- **Counter does not advance** — the click callback returned `Update.DoNothing`, or it implicitly returned `None` (which the binding treats as `DoNothing`). Always end a mutating handler with `return Update.RefreshDom`.
- **`TypeError: layout() takes 0 positional arguments but 2 were given`** — your callback signature is wrong. `layout` and click handlers must accept exactly `(data, info)`.
- **Mutation isn't sticking** — you mutated a *copy* of the model instead of the instance bound to the framework. The binding always passes the same instance back; check that you are not shadowing `data` with a fresh `DataModel(...)` somewhere inside the callback.

## Coming Up Next

- [Application Architecture](../architecture.md) — Explains the concepts of architecting a larger Azul application
- [Document Object Model](../dom.md) — The Dom tree - node types, hierarchy, and CSS
- [Hello World [Rust]](rust.md)
