Core concepts

A ComposeUI app is made of views, signals, an optional model, and studies, which a backend shows:

Concept

Role

View (composeui.core.view)

A typed attrs class that holds the state of a part of the UI, such as InputView, FormView, or TableView. You compose views by nesting view fields in other views. See Building views.

Main view

The root view, a subclass of MainView: the window, with its menus, its status bar, and its content. See The main view.

Signal (composeui.core.signal)

An event of a view or a model, such as clicked. signal.connect(callback) calls the callback on each event, and gives it the view, main_view, or model that it asks for.

Model (composeui.core.model)

The app’s data beside the views. It is optional. SqliteModel keeps the data in a SQLite database.

Study

The file that the File menu saves and opens: the state of the views, and the data of the model. The extension of the main view names its file type. See Saving and loading state.

Backend

The toolkit that shows the views: Qt, Textual, or the web. run(MainViewClass) starts the app on the backend that --backend names. See Backends.

Jobs (composeui.core.jobs)

main_view.jobs: functions that run in threads. Their results are emitted on the main thread.

Task

main_view.jobs.task(work): a job that a view runs again and again, with its state (is_running, progress, result), and its changed, finished, and failed signals. See Reusable loading tasks.

A backend binds each view to a widget with a renderer. The renderer keeps the two in sync in both directions, so the app’s code never calls it. The technical guide explains renderers.

How ComposeUI maps to MVVM

ComposeUI follows the Model-View-ViewModel pattern, but its names are not the usual ones:

MVVM

ComposeUI

Model

The model, a Model or a SqliteModel. It knows no view. It tells of its changes with signals.

ViewModel

The views of composeui.core.view. They hold the state of the UI as typed fields, and their signals and callbacks act on the model.

View

The widget of the backend: a Qt widget, a Textual widget, or an HTML element. Your code does not touch it.

Binding

The renderer. It pushes each change of a view to its widget, and copies the widget’s state back.

Each view has one widget, and views nest as their widgets do. So the views are the widget tree of the UI, without the toolkit. One app runs on each backend without a change.

Views are reactive

When code sets an attribute of a view (view.text = "..."), the view pushes the change to its renderer. You do not call update() or refresh() for normal state changes.

Signals inject arguments

Signal.connect and Application.connect read the callback’s parameter names. When a callback asks for view, main_view, or model, it gets them:

# `main_view` is injected automatically because it's a requested parameter.
def _on_name_changed(self, text: str, *, main_view: MainView) -> None:
    main_view.status_bar.show_message(f"Hello, {text}!" if text else "")

A callback can take fewer of the signal’s arguments than the signal emits. It then gets the first ones, or none. connect(callback, now=True) also calls the callback once, right away.