# Virtual list

> A list that only builds the rows in view, for millions of items. A Pyzed component for Python desktop GUIs, with examples and its API.

Source: https://albakore.github.io/pyzed/library/virtual-list/

:::tip[In plain words]
A list that stays fast even with millions of rows, because it only draws what you see.
:::

A scrolling list for thousands or millions of items. Only the rows in view are built: `render(item)` is called when an item scrolls into view, so the list costs the same whatever its length. Every row has the same `item_height`. `pz.virtual_column` is the building block underneath (rows by index, inside any `scrollable`), and [Table](https://albakore.github.io/pyzed/library/table/) uses it for its rows too.

`ui.virtual_list` · Category: [Lists and menus](https://albakore.github.io/pyzed/library/#lists-and-menus) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#virtual_list)

**Code:**

```python
files = [f"src/module_{i:06}.py" for i in range(1_000_000)]
pz.container(ui.virtual_list(
    files, lambda f: ui.list_item(f, icon="file-code", selected=f.endswith("02.py")),
    height=240,
), width=420)
```

:::note[New]
This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues).
:::

## When to use

- For logs, search results, file lists or any list that can grow past a few hundred rows.
- For short lists, a `pz.column` of [List items](https://albakore.github.io/pyzed/library/list-item/) is simpler.

## Usage

```python
import pyzed as pz
from pyzed import ui

class Logs(pz.App):
    def __init__(self):
        self.lines = [f"[{i:06}] request handled" for i in range(500_000)]

    def view(self):
        return ui.virtual_list(self.lines, lambda line: pz.text(line, font="mono", size=12),
                               item_height=20, id="logs")
```

## Variants

:::note[About these captures]
Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks.
:::

### Options

```python
pz.container(ui.virtual_list([f"row {i:,}" for i in range(1_000_000)],
                             lambda r: ui.list_item(r, icon="file"), height=150), width=260)
```

```python
pz.container(ui.virtual_list([f"[{i:06}] request handled" for i in range(100_000)],
                             lambda line: pz.text(line, font="mono", size=12), item_height=20, height=150),
             width=260)
```

```python
pz.container(ui.virtual_list(list(range(10_000)), lambda i: ui.card(ui.label(f"Card {i}"), padding=8),
                             item_height=40, spacing=6, height=150), width=260)
```

## Examples

### Rows by index

`pz.virtual_column(count, item, item_height=...)` calls `item(index)`, handy when the rows come from an array, a database cursor or a computed value. Scroll it like any `scrollable`: row `i` starts at `i * (item_height + spacing)`.

**Code:**

```python
pz.container(pz.scrollable(
    pz.virtual_column(10_000_000, lambda i: pz.text(f"{i:>10,}  {i * i:>22,}", font="mono", size=12),
                      item_height=22),
    height=150,
), width=420)
```

## Events

Virtual list has **1 event of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/).

| Event | Handler gets | When |
| --- | --- | --- |
| `on_scroll` | `(info)` | Callable that receives the scroll viewport, as in `pz.scrollable`. |

**Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`.

### Handling the events

The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/).

**Imperative:**

```python
class Logs(pz.App):
    def __init__(self):
        self.lines = [f"[{i:06}] request handled" for i in range(100_000)]
        self.top = 0

    def scrolled(self, info):            # on_scroll: offsets and sizes of the viewport
        self.top = int(info["y"] // 20)

    def view(self):
        return pz.column([
            ui.label(f"Line {self.top + 1} of {len(self.lines)}"),
            ui.virtual_list(self.lines, lambda line: pz.text(line, font="mono", size=12),
                            item_height=20, height=200, on_scroll=self.scrolled),
        ], spacing=6)
```

**Declarative:**

```python
from dataclasses import dataclass

@dataclass
class Scrolled:
    info: dict

class Logs(pz.App):
    lines = [f"[{i:06}] request handled" for i in range(100_000)]
    top = 0

    def update(self, message):
        match message:
            case Scrolled(info):
                self.top = int(info["y"] // 20)

    def view(self):
        return ui.virtual_list(self.lines, lambda line: pz.text(line, size=12), item_height=20,
                               height=200, on_scroll=Scrolled)
```

**Functional:**

```python
from dataclasses import dataclass

LINES = [f"[{i:06}] request handled" for i in range(100_000)]

@dataclass(frozen=True)
class State:
    top: int = 0

def view(state, set_state):
    return pz.column([
        ui.label(f"Line {state.top + 1}"),
        ui.virtual_list(LINES, lambda line: pz.text(line, size=12), item_height=20, height=200,
                        on_scroll=lambda info: set_state(top=int(info["y"] // 20))),
    ], spacing=6)
```

## Tips

:::tip
- Rows are built again when they scroll back into view, so keep `render` cheap or wrap it in `@pz.memo`.
- `items` should be a list or another sequence: other iterables are copied into a list on every `view()`.
:::

## Properties and API

### `ui.virtual_list`

```python
virtual_list(
    items,
    render,
    *,
    item_height=26,
    spacing=0,
    width='fill',
    height='fill',
    padding=None,
    overscan=4,
    id=None,
    on_scroll=None,
    theme=None,
)
```

A scrolling list that only builds the rows in view, for thousands or millions of items. `render(item)` returns the widget for one item and is called when it scrolls into view. Every row has the same height.

| Property | Default | Description |
| --- | --- | --- |
| `items` | *required* | The items: a list or any sequence (other iterables are turned into a list). |
| `render` | *required* | Function that receives an item and returns its widget. |
| `item_height` | `26` | Height of every row, in pixels. |
| `spacing` | `0` | Space between children, in pixels. |
| `width` | `"fill"` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). |
| `height` | `"fill"` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). |
| `padding` | `None` | Inner padding: `8`, `(vertical, horizontal)` or `(top, right, bottom, left)`. |
| `overscan` | `4` | Extra rows built above and below the visible ones. |
| `id` | `None` | Stable identifier used to focus or scroll it with a `Command`. |
| `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). |

Events: `on_scroll` ([events](#events)).

[Full reference →](https://albakore.github.io/pyzed/reference/ui/#virtual_list)

### `pz.virtual_column`

```python
virtual_column(count, item, *, item_height, spacing=0.0, overscan=4, width=None)
```

A column of `count` rows of the same height that only builds the rows in view: `item(index)` is called when row `index` scrolls into view, and rows that leave it are dropped. A column of a million rows costs the same as one of fifty. Put it in a `scrollable` (it virtualizes against the visible area of the closest one, or the window).

| Property | Default | Description |
| --- | --- | --- |
| `count` | *required* | Number of rows. |
| `item` | *required* | Function that receives a row index and returns its widget. |
| `item_height` | *required* | Height of every row, in pixels. |
| `spacing` | `0.0` | Space between rows, in pixels. |
| `overscan` | `4` | Extra rows built above and below the visible ones, so fast scrolling shows no gaps. |
| `width` | `None` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). |

[Full reference →](https://albakore.github.io/pyzed/reference/widgets/#virtual_column)

## Related

- [List item](https://albakore.github.io/pyzed/library/list-item/): A list row with icon, text and an end slot.
- [Table](https://albakore.github.io/pyzed/library/table/): A data table with a fixed header, sorting and selection.
