Widgets¶
This page covers the small views: a label, a button, the inputs that need more than the table of fields, and the views that show a result. The larger views have sections of their own: tables, charts, maps, dialogs, and the views that organize the window, from pages to the sidebar. The widget catalog lists every view.
Label¶
A LabelView shows a line of read-only text, for example a caption or a result. Code sets
the text. The user can’t edit it.
from composeui.core.view import LabelView, child
summary: LabelView = child(LabelView, text="No pipe yet")
Qt shows a QLabel, Textual a Label, and the web a line of text. For several lines with
headings and links, use a Markdown view.
Help on a form row¶
The help of a labelled row says what to enter. A “?” button beside the label shows it.
A click, a tap, or the keyboard opens it. Textual shows the help as a hint under the label.
Text area¶
A TextAreaView takes several lines of text, for example a comment. Its text holds the
lines, separated by "\n". In a form, use LabelRowTextAreaView.
Enter starts a new line. The field emits
editing_finishedwhen it loses the focus after a change.Tab goes to the next field, and does not go into the text.
Qt shows a
QPlainTextEdit, Textual aTextArea, and the web a<textarea>. Each one is four lines high.
The comment of src/examples/pipeform.py is a text area.
Integer input¶
An IntInputView takes a whole number between its minimum and maximum, for example
an hour of the day.
Qt shows a spin box, which keeps the number in range.
The other backends show a text field for digits. They do not refuse a number out of range (Textual marks it), so validate the number.
Date input¶
A DateInputView takes a datetime.date, for example the first day of a booking, or no
date (None).
The user types the date in the format that date_format gives (dd, MM, yyyy).
ISO 8601 is always understood. The catalogs translate date_format as text, so the format
follows the main view’s language: “MM/dd/yyyy” in English, “dd/MM/yyyy” in French.
The user can also pick the date from a calendar beside the field:
Qt shows its own calendar. It names the months and days in the system’s language.
The web shows Cally, in the colors of the theme and in the page’s language.
Textual shows ComposeUI’s own calendar. It names the months and days with the view’s
month_namesandday_names.
The calendar offers only the dates from minimum to maximum. But not every backend
refuses a typed date out of range, so validate the date. value_changed reports the dates
that the user enters.
File path input¶
A FilePathInputView takes the path of a file, for example a data file or a template file
for the app to read. The user types the path, or picks the file with the “…” button beside
the field.
from composeui.core.view import FormView, LabelRowFilePathInputView, child, define_view
@define_view
class DataForm(FormView):
measurements: LabelRowFilePathInputView = child(
LabelRowFilePathInputView,
label="Measurements",
extensions={"CSV": (".csv",)},
has_reload=True,
)
textholds the path as the user typed it.pathreads it as aPath, orNonewhile the field is empty.extensionsnames the file types that the “…” button offers. On the web, the backend refuses a file of another type.path_pickedemits the path of a file that the user picks with the “…” button. A typed path comes through the signals of any text field:text_editedandediting_finished.With
has_reload, a reload button shows beside the “…” button. It emitsreload_clicked, for example to read the file again after it changed.browse_textandreload_textname the two buttons in a tooltip and for a screen reader. The catalogs translate them.
The state saves text and has_reload, but not extensions, server_roots or base_folder,
which the app sets.
What the “…” button opens:
Qt shows its file dialog, in the directory of the field’s path.
Textual shows the file picker of textual-fspicker, in the directory of the field’s path.
The web shows the browser’s file picker. The browser cannot give the page a path, so it uploads the file. The session keeps a copy, and the field shows the path of that copy. The session deletes the copy when the field gets its next upload, or when the session ends. With
server_roots, the web lists the server’s files instead (see Files on the server).
src/examples/markdownviewer.py opens a Markdown file
with a file path input, and reads the file again when the user presses reload.
Files on the server¶
On the web, the “…” button uploads a copy of the file. A copy is not always what the app
needs: a study file can refer to other files beside it, or the files are on the server
already. Give the field server_roots, the folders whose files the web lists:
from pathlib import Path
from typing import Optional
from composeui.core.view import FormView, LabelRowFilePathInputView, child, define_view
STUDIES = Path.home() / "studies"
@define_view
class StudyForm(FormView):
study: LabelRowFilePathInputView = child(
LabelRowFilePathInputView,
label="Study",
extensions={"Code_Aster": (".export",)},
server_roots=(STUDIES,),
)
def __attrs_post_init__(self) -> None:
super().__attrs_post_init__()
self.study.set_validator(self._check_study, self.study.text_edited)
def _check_study(self) -> Optional[str]:
# `path` is None for a path typed outside the roots
if self.study.text != "" and self.study.path is None:
return f"Pick a study in {STUDIES}."
return None
What the web lists. The “…” button opens a dialog with the folders and the files of
extensionsin a folder, starting in the folder of the field’s file, else in the only root. A folder’s item shows its files, and “Parent folder” goes up, as far as a root. With several roots, the dialog starts with the list of the roots.Only the roots. The dialog shows nothing outside the roots. It leaves out hidden items, whose name starts with a dot, and items that a symbolic link leads out of the roots to. A root that doesn’t exist is left out. A folder of more than 2,000 items is listed only in part: the dialog asks the user to type the file’s path.
The path picked. A file’s item puts the file’s path on the server in the field, and
path_pickedemits it. The web copies nothing, so the files beside it stay beside it.A typed path. The user can still type any path of the server. On the web,
pathisNonefor a path outside the roots, symbolic links followed. Readpath, nottext, and show why it isNone, as_check_studydoes above.Qt and Textual open their dialogs in the first root while the field is empty. The user can leave the roots: the app runs on the user’s own machine.
The texts of the dialog are
parent_folder_text,cancel_text,unreadable_folder_textandpartial_folder_text. The catalogs translate them.
Every user of a web app sees the names of the folders and files in the roots. Give only the folders that every user may see.
Relative paths¶
A relative path, such as inputs/model.med, names a file from the process’s working folder.
A study often names its files from its own folder instead, and the app starts elsewhere. Give
the field base_folder, the folder that a relative path starts from:
from pathlib import Path
from composeui.core.view import FormView, LabelRowFilePathInputView, child, define_view
STUDIES = Path.home() / "studies"
@define_view
class MeshForm(FormView):
mesh: LabelRowFilePathInputView = child(
LabelRowFilePathInputView,
label="Mesh",
extensions={"MED": (".med",)},
server_roots=(STUDIES,),
base_folder=STUDIES / "beam",
)
textkeeps the relative path.pathreads it frombase_folder: “inputs/model.med” reads as~/studies/beam/inputs/model.med. An absolute path reads as it is.A picked path inside
base_foldershows relative to it, and one outside it shows whole.path_pickedemits the whole path in both cases.The roots check the whole path. On the web,
pathisNonefor a relative path that leads out ofserver_roots, with..or a symbolic link.The “…” button opens its dialog in
base_folderwhile the field is empty, rather than in the first root. On the web,base_foldermust be inside the roots, else the dialog opens as it would without it.When
base_folderchanges, the field keeps its text, andpathreads it from the new folder.
FolderPathInputView and SavePathInputView take base_folder too. The state does not save
it: the app sets it, for example from the study that it opens.
Folder path input¶
A FolderPathInputView takes the path of a folder, for example where the app writes its
results. The user types the path, or picks the folder with the “…” button beside the field.
from pathlib import Path
from composeui.core.view import FormView, LabelRowFolderPathInputView, child, define_view
@define_view
class OutputForm(FormView):
results: LabelRowFolderPathInputView = child(
LabelRowFolderPathInputView,
label="Results folder",
server_roots=(Path.home() / "studies",),
)
textholds the path as the user typed it.pathreads it as aPath, orNonewhile the field is empty.path_pickedemits the path of a folder that the user picks with the “…” button. A typed path comes through the signals of any text field:text_editedandediting_finished.browse_textnames the “…” button in a tooltip and for a screen reader. The catalogs translate it.
The state saves text, but not server_roots or base_folder, which the app sets.
What the “…” button opens:
Qt shows its folder dialog (
QFileDialog.getExistingDirectory), in the field’s folder, else in the first ofserver_roots.Textual shows the folder picker of textual-fspicker (
SelectDirectory), in the same folder.The browser cannot give the page a folder’s path. On the web, without
server_roots, the field has no “…” button: the user types the path. Withserver_roots, the button lists the server’s folders in them, as a file field lists its files (see Files on the server). A folder’s item shows its folders, and “Choose this folder” puts the folder shown in the field. On the web,pathisNonefor a path typed outside the roots.
Save path input¶
A SavePathInputView takes the path of a file to save, for example a report that the app
writes. The file may not exist yet. The user types the path, or picks it with the “…”
button beside the field.
from pathlib import Path
from composeui.core.view import FormView, LabelRowSavePathInputView, child, define_view
@define_view
class ReportForm(FormView):
report: LabelRowSavePathInputView = child(
LabelRowSavePathInputView,
label="Report",
extensions={"PDF": (".pdf",)},
server_roots=(Path.home() / "studies",),
)
textholds the path as the user typed it.pathreads it as aPath, with the first ofextensionsadded when the name has none of them: “report” reads asreport.pdf.pathisNonewhile the field is empty.path_pickedemits the path that the user picks with the “…” button. A typed path comes through the signals of any text field:text_editedandediting_finished.With
confirms_overwrite(on by default), the user agrees to replace a file that exists before it is picked.is_replace_agreedremembers it. A path typed in the field isn’t asked about: write the file with the main view’swrite_file, which asks when the user saves.browse_text(“Save as”),file_name_text,save_text,replace_question_textandreplace_textname the button and the texts of the dialog. The catalogs translate them.
The state saves text, but not extensions, confirms_overwrite, server_roots or
base_folder, which the app sets.
What the “…” button opens:
Qt shows its save dialog, on the field’s path, else in the first of
server_roots. It asks before it replaces a file. When the first extension, added to the name typed, names a file that exists, the field asks too.Textual shows the save picker of textual-fspicker, on the same path. Before it replaces a file, the field asks in a dialog of its own; Escape keeps the file.
The browser cannot give the page a path. On the web, without
server_roots, the field has no “…” button: the user types the path. Withserver_roots, the button lists the server’s folders in them and their files ofextensions(see Files on the server). A file’s item puts its name in the “File name” box. “Save”, or Enter in the box, picks the folder shown with that name. The dialog asks there before it replaces a file, and refuses a name that is not a file’s, such as one with a separator, or a folder’s. On the web,pathisNonefor a path typed outside the roots.
Slider¶
A SliderView picks a whole number when the user drags a handle, for example a number of
workers. The number goes from minimum to maximum by step. The handle stops only on a
step. The value shows beside the track, so the row is no taller than the other rows.
value_changed reports the user’s moves.
Qt shows a horizontal QSlider, Textual ComposeUI’s own slider, which the arrow keys
move, and the web a range input.
Progress bar¶
A ProgressBarView shows how far a long task has gone: value steps out of maximum.
While maximum is None, the bar shows only that the task runs.
Qt shows a QProgressBar, Textual a ProgressBar, and the web a <progress> bar.
src/examples/taskview.py follows tasks that run in
threads with a progress bar
(uv run python -m examples.taskview, or --backend qt, or textual).
Metric¶
A MetricView shows a figure to read at a glance, for example the number of jobs running.
It shows its label and its value in large type. A badge shows the change since some
point: delta is the change, and delta_text says the point, for example “since
yesterday”. The sign of the change sets the color of the badge. To show a row of metrics,
put them in a HorizontalView.
Qt and Textual show the value in bold, and the web as a daisyUI stat.
Log message¶
A LogView shows a message to the user in the page, for example the error of an import.
Set its log_message, and its log_type (LogType: ERROR, the default, WARNING, or
INFO):
from composeui.core.view import LogType
main_view.import_log.log_message = "The file has no column named radius."
main_view.import_log.log_type = LogType.WARNING
The view hides while log_message is empty. An error shows in red and a warning in orange. A
check of a row writes the same two fields of the row, and the row
shows its message under its field.
Qt shows a QLabel, Textual a Label, and the web a paragraph that screen readers read
when it changes (aria-live="polite").
A new message replaces the last one. To show a stream of output, such as the output of a process, use a console.
Expander¶
An ExpanderView is a collapsible section, for example the details of a record with its
Edit and Delete buttons. The view fields of a subclass are its content, one above the
other. They show only while is_expanded is true. The user clicks the title to expand
or collapse the section (toggled).
Qt shows a header button with an arrow, Textual a Collapsible, and the web a <details>
element.
Tree¶
A TreeView shows nodes in a tree, for example the buildings, floors, and rooms of a site.
Each node is a TreeNode with its label and children.
A path identifies a node: the keys from its top-level node down to it, for example
("Building A", "Floor 1"). The key of a node is its label, unless you give one.
The user expands and collapses the nodes (expanded, toggled), and selects one node
(selected, selection_changed). When code sets new nodes, the tree keeps the expanded
and selected paths that are still in them.
A node can also show the kind and the state of what it stands for, as a study tree does:
from composeui.core.view import NodeMark, TreeNode
TreeNode(
"Study 1",
[TreeNode("Monte Carlo", mark=NodeMark.DONE), TreeNode("Sobol", mark=NodeMark.ERROR)],
icon="icons/study.svg",
is_bold=True,
)
iconis the path of an image file before the label, as theiconof an action is (see Toolbars). Qt and the web show it, a terminal does not. The app’s code sets it, so it is never saved or loaded: build the nodes again from the model after a load.markis aNodeMarkafter the label:DONE(a green check),WARNING(an orange “!”) orERROR(a red cross). The tree’sdone_text,warning_textanderror_textname the marks: Qt shows the name as the mark’s tooltip, and the web gives it to screen readers as the node’s description.is_boldandis_underlinedstyle the label.
Qt shows the marks in a second column of the QTreeWidget, Textual as a colored glyph
(✓, !, ✗), and the web as an icon in the node’s button.
On the web, a label that is too long for the tree ends in an ellipsis.
src/examples/pipecatalogue.py shows its pipes in a
tree, under their material. It builds the tree again from the database whenever the pipes
change.
Markdown¶
A MarkdownView shows read-only Markdown text, for example a help page or the summary of
a report. Its links open in the browser. The view never reads raw HTML in the text as
markup: Qt and the web show it as text, and Textual leaves out its tags.
src/examples/pipeform.py has a help page.
src/examples/markdownviewer.py shows a Markdown file
(uv run python -m examples.markdownviewer --markdown README.md --backend qt).
Console¶
A ConsoleView shows plain text that the app adds to as it comes, for example the output of a
solver that a job runs. To show one message to the user, such as an error, use a
log message instead.
Call append with the new text, such as each line that the job reports as its progress:
import io
import subprocess
import sys
from composeui.core.jobs import Job
from composeui.core.view import ConsoleView, MainView, child, define_view
SOLVER = "print('Step 1'); print('50%', end='\\r'); print('Done')"
def run_solver(job: Job[int, str], command: list[str]) -> int:
with subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
assert process.stdout is not None
# newline="": each line keeps its "\r" or "\n", which text mode reads as "\n"
for line in io.TextIOWrapper(process.stdout, encoding="utf-8", newline=""):
job.report_progress(line)
return process.returncode
@define_view
class SolverView(MainView):
output: ConsoleView = child(ConsoleView, max_lines=5_000)
def run(self) -> None:
self.output.clear()
job = self.jobs.start(run_solver, [sys.executable, "-c", SOLVER])
job.progress.connect(self.output.append)
main_view = SolverView()
main_view.run()
main_view.jobs.wait() # Only for a main view with no toolkit.
assert main_view.output.text == "Step 1\nDone\n"
Lines. “\n” ends a line. “\r” starts the line again: the next text replaces it, as a terminal shows a progress line. “\r\n” ends the line. Python reads a “\r” in text mode as “\n”, so read the output with
newline="", as the example does.The last lines. The view keeps the last
max_lineslines (10,000 by default) and drops the oldest.linesholds them, andlast_linethe line being written.line_countcounts the lines ended since the view was built, the dropped ones too.Replace and clear.
textholds the lines, then the line being written. Set it to replace them, or callclear.The text as it is. The view never reads the text as markup: a tag shows as text.
Scrolling. The view scrolls to its end as text comes. When the user scrolls up to read, it stays there, until the user scrolls back to the end.
Not study data. The text is the app’s output: a study never saves, loads or resets it.
Save or copy the output. The user saves the text to a file, named
export_filename(output.txtby default) at first, or copies the whole text to the clipboard. In code,export_text(path)writes it in UTF-8, and the status bar says how it went.texts["save"]andtexts["copy"]name the controls, “Save output” and “Copy all” by default.Search the output. The user searches the text: plain text, in any case. Every match is highlighted, the current one darker, and the count says which one it is: “2 of 5”, or “No match” (
texts["matches"]andtexts["no_match"]). Typing shows the first match, and Enter the next. New lines keep the current match, or the next one if it went with the oldest lines.find_in_linesfinds the matches as the search does.Colors. The view removes the escape sequences of a terminal from the text, such as the ANSI codes that color a solver’s output: the text reads, searches, copies and saves as plain text. With
ansi_colors, the view shows the colors and the bold, italic and underlined text that those codes ask for: 16, 256, or 24-bit colors.line_stylesandlast_line_stylesgive each line’s styled parts, asTextStyles.from composeui.core.view import ConsoleView, TextStyle output = ConsoleView(ansi_colors=True) output.append("\x1b[1;31mFAILED\x1b[0m test_beam\n") assert output.text == "FAILED test_beam\n" # the codes are gone from the text assert output.line_styles[0] == ((0, 6, TextStyle(foreground="#cd3131", is_bold=True)),)
Qt shows a read-only QPlainTextEdit in a fixed-width font, Textual a Log, and the web a
box of preformatted text. Qt and the web wrap a long line; Textual scrolls sideways. The user
selects and copies the text on each backend. On the web, a change sends only the new lines.
To save the text, Qt has a “Save output…” button above it, which opens a save dialog;
Textual has the key s, shown in the footer while the console has the focus; the web has a
“Save output” link above it, which downloads the text. To copy it, Qt has a “Copy all”
button, Textual the key c, and the web a “Copy all” button. Textual copies through the
terminal, which must allow it. The web needs a secure page (HTTPS, or localhost) to write
the clipboard: elsewhere, the button selects the text, for the user’s Ctrl+C. To search it,
Qt and the web have a search field above it, where Shift+Enter shows the previous match;
Escape clears it on Qt. On Textual, / shows a search field, Escape hides it, and n and
N show the next and the previous match.