-
v0.3.1
Stablereleased this
2026-10-06 09:07:43 +02:00 | 1 commits to main since this release0.3.1 - 2026-10-06
What apps ported to 0.3 asked for: the run's clipboard in the app's hands,
with or without a window, to copy a secret and clear it again, and to
watch it for copies made in any app; fields and popups that give the
keyboard back as they found it; keys a component claims decided when they
arrive; shortcuts that work through modal popups; a sheet for modal
content; a chooser of the app's own items and completion in text fields;
more say over the password, tag, search and number fields, the toolbar,
the status bar, tree rows, toasts and the Markdown editor; a desktop id
per window, the window's size as it changes, and printing that works on
KDE; and a force-directed graph, behind the newgraphfeature. All
additive: nothing is removed or renamed and no signature changes. Where
behavior changes, it is listed under Changed; charts take new colors.forma-winit depends directly on two crates it built already, on Linux and
the BSDs only:image(PNG only), for pictures over the data-control
clipboard, andrustix(event), to read a watched copy against a
deadline. No crate is new to the build.forma-ui
Changed
-
A press inside a modal popup on something that takes no focus (a
button, a label, the background) keeps the focus where it was, instead
of clearing it so that the modal handed it to its first widget again. A
focused text field there still reportsEvent::Submitted, so a click on
a dialog's button still commits its edit. -
A modal popup that takes the focus as it opens shows the focus ring
only if the last press was a key (or there was none yet), not when it
was opened with the pointer. -
A text area that does not wrap scrolls sideways as a single-line field
now does: in the width its real right padding leaves (it used to assume
the left padding on both sides), and not at all when its lines fit,
where it used to scroll and show a scrollbar for the 6 px margin past
their end. -
Harness::find_text,try_find_textandfind_all_textmatch a
RichTextwidget too, so a test with a label and a rich text showing
the same text now finds two:find_textandtry_find_textpanic with
"2 widgets show ...". Tell them apart withfind_all_text, or by the
ids the build returned. -
SearchField's result count stands inside the field, before the clear
button, instead of after the field, so the field keeps its width when
the count appears. The field keeps room for the count (at least 80
pixels, more for a longer count or translation), and the typed text
stops short of it.SearchParts::countis the same label, inside the
field now. -
Enter in a single-line text input now reports
Event::Entered(id)just
before itsEvent::Submitted(id). A test that asserts the exact events
after Enter sees one more:Before:
assert_eq!(ui.drain_events(), vec![Event::Submitted(field)]);After:
assert_eq!( ui.drain_events(), vec![Event::Entered(field), Event::Submitted(field)] ); -
The command palette shows an action's description as a muted line under
its label, and as the row's accessible description. It is not searched. -
ActionSet::bind_shortcutsreplaces the bindings of the set's ids that
are not scoped - its own, everywhere or through modals, and an app's
Shortcuts::bindon those ids - and keeps an app'sShortcuts::bind_in
binding of an action's id inside a widget, which it used to remove. A set
bound where a bigger one with the same base was takes down the ids it no
longer has.Shortcuts::unbindstill removes every binding of an id,
scoped ones included, as it now says. -
Toolbar::with_styleis laid over the toolbar's own look instead of
under it: a background, the padding of a side, a gap or an alignment the
style sets now wins, and the toolbar counts that gap and padding when it
decides what fits. What the style leaves asStyle::new()has it stays
the toolbar's (with_plainleaves the bar's look out). The bar stays a
row that clips. -
A status section's icon is as big as the theme's text (
TextStyle::size,
15 pixels by default) instead of 14 pixels. -
ActionSet::revision()numbers are unique across sets (they come from
one counter) rather than counting from 0 in each set; they still only
grow, and compare as before. -
The command palette's list scrolls: it builds only the rows in view of a
scroll view of at most ten rows, with a scrollbar and the wheel, instead
of the ten rows around the highlight. Page Up and Page Down move the
highlight a page, Ctrl+Home and Ctrl+End (Home and End on a Mac) to the
first and the last match. Its rows have a fixed height, a row with a
description a taller one; the palette's card is keyed, so a rebuild
keeps its scroll. Its public API and its ordering (recent actions,
disabled ones last, categories, shortcuts,hidden_from_palette) are
unchanged, andPaletteParts::rowsstill pairs each row with its
action's index in the set. Also:- The list is also no taller than the window leaves room for below the
card's top (at least three rows). - While the list scrolls, rows keep 16 pixels clear on the right for the
scrollbar, so the shortcut column sits 8 pixels further in. - With no match there is no list box: "No matching commands" is a label
in the card, beside the field, and the accessibility tree has no
ListBoxnode then. - Wheel scrolling past the rows built is reported as
PaletteEvent::UpdatedfromComponent::poll, which
Mounted::handle_all(andFrame::offer) call; an app that calls
onlyhandlefor each event shows unbuilt rows after a scroll until
it rebuilds for another reason. matches()andhighlighted_action()are empty while the palette is
closed; they were the last search's.
- The list is also no taller than the window leaves room for below the
-
Mounted::build_keyed's documentation, and the guide, name the command
palette, the chooser and completion besidePopoveras components that
key their card themselves and take no key. -
A toast stays up while the pointer (or a finger) rests on it, or while
the keyboard is on its action button, and its whole time starts again
when they leave, or when the stack moves it from under a still pointer. It used to go at its time whatever the user was doing. The card
looks and reads to assistive technology as before. -
Charts (
Plot,PieChart) take their series colors from the theme's
newPalette::seriesinstead of the accent, the status colors and two
fixed colors: a chart without colors of its own looks different, in
colors people with the common kinds of color blindness tell apart.
Added
-
Style::claims_when(commands, ClaimIf)and the non-exhaustive
ClaimIf(HasText,Empty): a claim that holds only while the
claiming widget has text, or has none, read when the key arrives rather
than at the last build (ADR 0087). -
Ui::refocus(id): gives the keyboard back after a popup closes. A text
input gets back the selection and every caret it had when it lost the
focus (moved with edits made since), instead of having its whole text
selected asUi::focusdoes; the focus ring shows unless the last
press was the pointer's. -
Harness::press_shortcut(Shortcut): presses a shortcut with its
modifiers (both keys of a chord) and holds the modifiers held before
again after. -
Harness::announced(): whatUi::announcewas asked to say since the
last call, each with itsPoliteness, with or without theaccesskit
feature. AUitaken back withHarness::into_uikeeps no record. -
Harness::textreads aRichTextwidget's text (and the finders match
it, see Changed). -
ClaimIfis informa::prelude. -
PasswordField::set_text(ui, &parts, text)puts a password into the
field as the app's own change: the old text is wiped, nothing is
reported, and it shows at once. It takes the parts, as
MarkdownEditor::set_textdoes: the component keeps no copy of the
password, so it writes into the field it built. It panics if the parts'
field is not in theUi(not built, or not built again). -
Mounted::build_keyed(ui, parent, key)builds a component with its root
keyed, so a rebuild keeps its widgets (typed text, focus, scroll) when
the components beside it come and go. It panics, saying so, if the build
added no widget to take the key or keyed a widget of its own first (a
Popoverkeys its card itself). Twopub(crate)helpers in
ui/rebuild.rs(key_pending,set_key_giver) and a field ofUi
serve the checks. -
NumberField::integer(value, min, max): whole numbers, stepping by 1,
no decimals; the value stays anf64. Panics beyond 2^53. -
NumberField::with_label_width(width), so the fields of a column line
up, andNumberField::with_name(name), an accessible name without a
visible label. -
NumberField::with_style_fn(|s| ..)sizes and places the field's root:
the function gets the root's own style (a row, centered, a gap of 4) and
returns it changed. A function rather than aStyle, because aStyle
is notSendandNumberFieldis (and must stay so). -
Event::Entered(id): Enter was pressed in a single-line text input. It
comes just before theSubmittedof the same Enter, so an app can tell
Enter from the focus leaving. A text area's Enter and a field that
claims Enter report noEntered.Actions::on_enter(id, |text| ..)
builds on it. -
TagInputgrows:with_normalize(|tag| ..)makes each tag the user adds what the
function returns (Nonerefuses it); duplicates are compared after
normalizing, and suggestions are offered as they normalize. The tags of
newandset_tagsare the app's, taken as they are.sorted()keeps the tags sorted,set_tags's too.set_suggestions(..); open suggestions are narrowed to the new ones.set_text(ui, &parts, text)andclear_text(ui, &parts), public now:
the typed text, set at once, nothing reported.with_chip_color(|tag| ..)fills a plain chip with the app's color,
its text black or white, whichever reads better, unless the theme's
text does. The chip stepped onto and a failing one keep their look.TagEvent::Activated(index): a chip was clicked, or Enter was pressed
on the chip stepped onto (the field claims Enter while a chip is
stepped onto, so the focus stays). The click still steps onto the
chip, reported asTagEvent::Updatedafter it, as before.keep_placeholder()keeps the placeholder beside the tags.
-
SearchField::set_query(text)puts a query in the field and searches
for it: the nexthandle_allreportsSearchEvent::SearchRequested,
and the next build asks for that frame. -
Shortcuts::bind_through_modals(id, shortcut): a shortcut that works
while a modal popup is shown (a dialog, a sheet, the command palette),
after the popup's own keys and the bindings scoped inside it. Chords
bound so work too.Action::through_modals()(and the public field
Action::through_modals) makesActionSet::bind_shortcutsbind an
action's shortcut that way. -
ActionSet::sync_shortcuts(&mut Shortcuts): binds asbind_shortcuts
does, but only when the set changed since it was bound there (the
Shortcutsremember the revision per set, by its first id), so it can be
called in every build - where a test'sHarness, which runs no
App::start, builds too. -
Action::hidden_from_palette()(and the field
Action::hidden_from_palette): the command palette leaves the action out. -
ActionSet::label_with_shortcut(&id), asAction::label_with_shortcut. -
ActionandEntryare re-exported at the crate root (forma::Action,
forma::Entry). -
Entry::Spacer: free space that pushes the entries after it to the end
of aToolbar. Menus (ActionSet::menu, the "more" menu, the platform's
menu bar) leave it out, and a separator it leaves right after another. -
Toolbar::with_emphasis(&id, Emphasis)andEmphasis(Flat,
Primary,Danger): an action's button filled with the accent or the
danger color, as a dialog's default and destructive answers are. -
StatusSection::tone(Tone)andTone(Neutral,Info,Success,
Warning,Danger): a section's text and icon in the theme's color for
how things stand. -
StatusSection::fill(): the section takes the room the others leave and
ends its text in "..." when it does not fit; the bar's spacer then takes
none. It works among the start sections and among the end ones. -
Toolbar::with_plain(): the bar leaves out its own background, bottom
border and padding, for a toolbar inside a header of the app's own. -
Sheet,SheetEventandSheetParts: a modal card over the whole
window with a body the app builds into. Escape closes it
(SheetEvent::Closed), and a press beside the card does too with
with_dismiss_on_outside(true);set_busy(true)keeps it open while a
job runs. It keeps its widgets between builds, gives the focus back on
closing, and is a named modal dialog to a screen reader (with_label). -
The guide:
unbindandbind_inbeside an action set, binding in a
test withsync_shortcutsinbuild, shortcuts through modals, the
palette's descriptions, spacers and emphasis on toolbars, tones and
fillin the status bar, and a section onSheet. -
ClipboardforArc<Mutex<C>>of any clipboardC: a test keeps one
handle and gives the other to aUi, aHarnessor forma-winit's
OsClipboard::from_clipboard, then reads back what was copied and
whether it was marked secret (MemoryClipboard::last_was_secret). A
call fails when a panic poisoned the lock. -
widgets::Chooser<K>: a searchable, modal list of the app's own items,
with a search field.ChooserItem::new(key, title)withdetail(..),
shown muted after the title, andicon(..);set_items,open,
open_with_query,close,is_open,query,matches,
highlighted;with_placeholder(default "Search",
Phrase::SearchPlaceholder),with_empty_text(default "No results",
Phrase::SearchResults { count: 0 }),with_width. It reports
ChooserEvent::{Chosen(K), Closed, Updated}(non-exhaustive) and builds
ChooserParts { root, input, list, rows },rowspairing each row with
its item's index.- The letters typed are matched in order, not necessarily side by side,
over the title and the detail, the title counting more; best match
first, equal scores in the order given, the letters found highlighted.
It is the palette's matcher, moved into a crate-private module. ChooserAnchor::{Top, At(Vec2), Caret(WidgetId)}(non-exhaustive):
near the top of the window where the palette opens; with its top-left
at a point in physical pixels, asUi::popuptakes it, nudged into the
window; or below the caret of a text field, lined up with it and above
it where there is more room, below the field when the field does not
have the keyboard as it opens.with_anchor,set_anchor,anchor.- Up and Down (wrapping), Page Up, Page Down, Ctrl+Home and Ctrl+End;
Enter or a click chooses, Escape or a press outside closes. The list
scrolls with the wheel and a scrollbar and builds only the rows in
view, so thousands of items stay quick. - A screen reader hears a dialog holding a combo box (the field, which
speaks for the highlighted option) over a list box, each option named
by its title and described by its detail. - Closing gives the keyboard back to where it was, with
Ui::refocus.
- The letters typed are matched in order, not necessarily side by side,
-
CommandPalette::with_placeholderandwith_empty_textreplace "Type a
command" and "No matching commands" for one palette.
PaletteParts::listis the scrolled list. -
widgets::Completion<K>: the app's items below the caret of a text
field, opened by typing a trigger (Completion::new("[["),"#","@")
and built at the field:completion.build(ui, field). It reports
CompletionEvent::{Query(String), Chosen { key, range }, Closed, Updated}(non-exhaustive);rangecovers the trigger and the query, in
bytes, for the app to replace, in one undo step withUi::record_edit
aroundUi::replace_range.set_items,items,is_open,query,
range,matches,highlighted,close,trigger,
with_empty_text,with_width;CompletionParts { field, root, list, rows }.- The items are searched as the chooser searches them.
- The list sits below the caret, follows it as it moves, and flips above
it near the bottom of the window. - The keyboard stays in the field: Up, Down, Page Up, Page Down, Enter
and Tab are claimed only while the list shows matches, so Enter makes
a new line otherwise. A click on a row leaves the keyboard in the
field. - Escape, a press outside, the caret leaving the range, an edit outside
the range (the text before and after it on its line is compared), a
second caret, or the field losing the keyboard closes it and leaves
the text as typed; a trigger typed with several carets opens nothing.
Nothing is shown while nothing matches, unlesswith_empty_textgives
a text. - A key the card claimed while it had matches, pressed after a key in
the same frame left none (Enter after a last letter that matches
nothing), goes to the field as if unclaimed, through
Ui::run_command: Enter makes its new line. - A screen reader hears the list as a list box, and the highlighted item
is announced politely as it changes ("Groceries, Personal, 2 of 5").
-
Chooser<K>andCompletion<K>areSendfor aSendkey, checked in
send.rs. -
MarkdownEditor::with_monospace(bool): the source stays monospace by
default;falseshows it in the theme's proportional font. While it is
on, the text is monospace whatever family the style names. -
MarkdownEditor::with_area_style(style)styles the text area in its
half of the split, with a preview (its size there, padding, font size),
in place of the default, which fills the half. Without a preview the
text area is the editor's root and takeswith_style; the area style is
not used then. -
MarkdownEditor::with_preview_view(view)shows the app's own
MarkdownViewas the preview - with its highlighter, images, look,
hard breaks and style - in the splitwith_preview()makes. Its source
becomes the editor's text, and it follows the text as before. A
with_preview()after it keeps the app's view.
MarkdownEditor::preview_mut()changes it later (a new highlighter for
a new theme), shown from the next build. -
MarkdownEditor::replace_range(ui, &parts, range, text) -> Range<usize>:
an app's edit in the middle of the user's (a completion chosen, a link
put in) as one undo step, colored and previewed at once, and reported
asMarkdownEditorEvent::Changedby the editor's nextpoll(the next
Mounted::handle_all), as typing is; typing in the same round makes it
oneChanged, not two. The range is in bytes and taken
asUi::replace_rangetakes it: its ends move back to a character
boundary and into the text, and a backwards range is empty at its start.
With the text focused, the caret lands after the new text. It returns
the range the new text occupies, and panics if the parts' text area is
not in theUi.set_textkeeps its meaning: a new text, without
history or a report. -
MarkdownView::with_hard_breaks(bool)shows each soft line break inside
a paragraph as a line break, in a heading too (a setext heading written
over two lines shows on two). The source and its offsets do not change,
soTaskToggledandscroll_to_sourcework as before. An editor's
preview gets it throughwith_preview_view.
MarkdownView::set_hard_breaks(bool)changes it on a view held
elsewhere, as throughMarkdownEditor::preview_mut. -
With the
syntaxfeature,syntax::Highlighter::highlight_str(language, code, background) -> Vec<(Range<usize>, Color)>colors a string: the
grammar by name or extension, asHighlighter::newtakes it, and the
theme picked bybackgroundasupdatepicks it by the UI's (dark or
light), every color held to the same contrast againstbackground. An
unknown or empty language colors nothing. -
syntax::Highlighter::for_markdown(&theme)returns the closure
MarkdownView::with_highlightertakes, coloring each code block by the
language after its fence (up to a comma, as inrust,ignore) for a view
intheme, on its sunken surface. It takes the theme because the
closure cannot see the UI's: when the theme changes, the app gives the
view a new one. -
Ui::style(id): the style a widget was last given, by a build or
set_style, in pixels at 1x;Nonefor a removed widget. -
Harness::find_tooltip(text),try_find_tooltipandfind_all_tooltip:
the widgets whose tooltip saystext(each one's own tooltip, or else
why its value is invalid). With theaccesskitfeature,Harness::find_role(role),
try_find_roleandfind_all_role: the widgets assistive technology
knows as thataccesskit::Role, as their style names them or as forma
names their kind. Both panic asfind_textdoes. -
Ui::command_modifiers(scope, command): the modifier keys held when a
claimed command was routed toscope, kept while the app handles the
events it came with, so a widget that claims Enter tells Shift+Enter
(which the keymap turns intoCommand::Enter) from Enter, even when
Shift was let go before the events were read. A commandrun_command
ran comes with none.Ui::command_modifiers_eachgives each one's when
the same command reached the same widget twice in one batch. -
TreeView::with_row(|ui, row, node, state| ..): the app builds what a
row shows after its indent and arrow (an icon, the label, a count), from
the node and itsTreeRowState(non-exhaustive:selected,open,
depth,branch,matches). The keys, the selection, the arrow and
the tree item roles stay; the accessible name stays the node's label
unless the app sets another on the row. -
TreeView::with_drop_kinds(mask): rows take drags from outside the
tree whose kind shares a bit withmask(the app's own draggable
widgets, or another tree's rows), highlighted while hovered, and report
a drop asTreeEvent::DroppedOn { source, node }. Reordering goes on
as before. -
TreeView::DRAG_KIND, the drag kind reorderable rows carry (it was
private), so the app's own drop targets can take tree rows. -
TreeParts::rows: each built row and the node it shows, busy rows left
out. -
ToastMessage::duration(d), how long one toast shows, and
Toasts::with_duration_for(kind, d), how long a kind shows; the
defaults stay as they are, and a message's own time wins over its
kind's, which wins overwith_duration. -
InputCx::click_count(): how many presses in a row the last primary
press on a custom widget was (2 for the second of a double click), for
itsPointerPressedand the drag and release that follow; and
InputCx::now(), the time on the UI's clock. A custom widget can tell
a double click from two clicks without a clock of its own. -
Palette::series: [Color; 8]: colors that tell categories of data apart
(a chart's series, a graph's groups), in every bundled theme. They are
the Okabe-Ito palette, lightened or darkened the least that holds them
at 3:1 against the theme's background and surfaces (4.5:1 in the
high-contrast themes). A theme saved before reads with the bundled
series that fits it: light or dark by its colors, and the high-contrast
one when its muted text holds 7:1, as the high-contrast themes' does. -
Accessible items in a custom widget:
CustomWidget::access_children
lists the parts of a widget a user tells apart (a graph's nodes, a
map's pins), each a node of its own with the bounds, label,
description, selected state and actions the widget gives it, and
CustomWidget::access_action(item, action, cx)takes what assistive
technology asks of an item. While the widget is focused, its selected
item is what a screen reader speaks as focused. Both have defaults, so
existing custom widgets are unchanged (ADR 0089). -
LayoutCx::now()andLayoutCx::reduced_motion(): a custom widget that
moves by itself works out where it has got to inlayout, on the UI's
clock (which a test'sHarness::advancemoves), and shows where it
would come to rest when the user asked for less motion. -
ForceGraph<K>, behind the newgraphfeature (forma-winit'sgraph
turns it on): a force-directed graph of the app's nodes and links.- Data:
GraphNode::new(key, label)with.weight(w),.group(g)and
.description(text);GraphEdge::new(from, to)with.directed().
Edges to unknown keys and from a node to itself are left out, and two
edges between the same nodes are one link.ForceGraph::new(nodes, edges), andset_data(ui, &parts, nodes, edges), which keeps the
nodes it had where they were. - Layout:
ForceLayout(repulsion,link_distance,link_strength,
gravity,cooling;Defaultandwith_*builders) through
with_layout. Repulsion, springs, a pull to the middle and collision,
with a Barnes-Hut quadtree above 300 nodes, stepped in layout at 60
steps a second on the UI's clock and asking for frames only while it
moves. Deterministic: nodes start where a stable hash of their key
puts them, and it uses no randomness and no trigonometry. With reduced
motion nothing moves on screen: the layout settles out of sight, a few
milliseconds of it a frame, and the nodes jump to where it came to
rest.settle,is_at_rest,positions,
set_positions,node_rect. 2000 nodes and 5000 links take about
2.4 ms a frame while they move (cargo bench -p forma-ui --features graph --bench graph). - Interaction: dragging the background pans; the wheel, a pinch,
+and
-zoom about the pointer (+and-about the middle while the
pointer is elsewhere); a double click on the background and0
fit (fit); hovering a node shows its links and neighbors and dims
the rest; a dragged node moves and rejoins the layout, or stays where
it is let go withwith_pinned_drags(true); a click selects, a double
click or Enter opens; the arrow keys select the nearest node that way;
Escape clears the selection.selected,set_selected(announced to
a screen reader),focus_on. - Events:
GraphEvent::{Selected, Activated, Hovered, Moved}. - Look: dots in the theme's
seriescolors by group (or
with_palette), links in the border color with arrowheads, an accent
ring on the selection, labels on a halo of the background where they
do not overlap, long ones cut short with an ellipsis;set_dimmed,
with_isolated_hidden,with_legend. - Accessibility: a list labeled with
with_label, each shown node an
item with its label and a description (its own, or how many links it
has and its group's name from the legend), the selected node in
focus; a screen reader's focus selects a node and its click opens it. - The gallery's Graph page, a screenshot scene, a section in the
guide's "Documents and charts" chapter, and ADR 0090.
- Data:
-
ClipboardErrorKind::StaleandClipboardStale(ClipboardStale::new(),
non-exhaustive), the error it is told by: a copy asked about after a
newer one took the clipboard's place.ClipboardErrorKind::ofreturns it
for aClipboardStale.
Fixed
-
A single-line field's text, selection and caret are clipped to its
content box sideways, so a long text no longer runs under what lies over
the field's padding (PasswordField's eye,SearchField's icon and
clear button). -
A single-line field scrolls its caret into the room its real right
padding leaves: it used to assume the left padding on both sides, so the
caret of a long password could sit under the eye (and, right to left,
the text stopped short of the end). Text that fits no longer scrolls for
the margin past its end, so a narrow field such as a time picker's shows
its digits whole. -
CommandPalette,Drawer,Popoverand the newSheetgive the
focus back withUi::refocus: a text field or text area they return to
keeps its selection and carets instead of being selected whole, and no
focus ring shows after a click.SelectandSplitButtondo the same, so a pick
with the pointer draws no ring on the button. -
CommandPaletteclosed and opened again before a build keeps where the
focus goes back to: it used to note its own old search field instead.
Opened and closed again before a build, it leaves the focus alone. -
TagInput: text typed and Enter pressed in the same frame make a tag and
keep the focus in the field, where Enter used to move the focus on;
Backspace and Left after typing in the same frame edit the text instead
of taking out or stepping onto a chip. -
SearchField: text typed and Escape pressed in the same frame empty the
field instead of dismissing the popup around it, and Escape after
emptying the field in the same frame is no longer swallowed. -
A press on a
TagInputchip inside a modal popup, which keeps the focus
in the text field, no longer makes what is typed a tag. -
In a right-to-left UI,
SearchField's magnifying glass, clear button
and result count sat on the wrong sides: they are mirrored now, with the
glass at the field's start (the right) and the clear button at its end. -
With a preview,
MarkdownEditor::with_stylewas ignored: the split
pane always filled its parent. It now sizes and places the editor's
root, the split pane, as it does the text area without a preview. The
default (filling) is as before; an app that passed a style with a
preview gets the editor sized by it now. -
The editor colored Markdown without footnotes while the view read them:
Said so.[^1]with a[^1]: ...definition was colored as a link in
the source, and shown as a footnote in the preview. The view's set is
the right one (it shows footnote references and definitions on
purpose), and the editor now uses the same: tables, task lists,
strikethrough and footnotes. The docs now say footnotes are read. -
Input or an action reaching a custom widget that had no laid-out
rectangle dropped the widget and its state.
forma-winit
Changed
-
With the
system-settingsfeature on Linux and the BSDs,runwaits for
the desktop portal's first answer beforeApp::launchedrather than as
the first window opens - the same quarter of a second at most from the
start - soContext::system_theme()andContext::system_settings()
say the system's inlaunchedalready, also in an app that starts in
the tray with no window. An app that opens its main window as it starts
waits no longer than before. -
OsClipboard'sset_textandset_secret_textfail over the core
Wayland protocol (GNOME) while no window of the app has the keyboard
focus - a copy from the tray's menu, say - where the compositor dropped
the copy without a word before. The error is a
ClipboardErrorKind::Failedsaying so. A copy with the focus but before
any key press or click in the app since the clipboard was opened or
cleared can still be dropped silently: forma cannot see it. Onlyrun
tells the clipboard the focus; with an event loop of the app's own,
nothing is refused. -
TrayError::BadIconfor an icon drawn at 0 pixels says "an icon cannot
be drawn 0 pixels square" (was "no picture 0 pixels square"); for SVG
that does not read it says "the icon is not valid SVG: ..." as before. -
Printing on Linux and the BSDs goes through the portal in two steps:
PreparePrintshows the desktop's dialog, andPrintprints with the
dialog's token. Where the dialog fails (the call failing, answer 2, an
answer without a readable token), the PDF opens in the desktop's PDF
viewer and the app hearsPrintOutcome::Shown, with the reason on
stderr. A desktop that does not allow printing (PreparePrintrefused
withNotAllowed, as under a lockdown, orAccessDenied) is
PrintError::Servicesaying "the desktop does not allow printing", and
no viewer opens. A print that fails after the dialog is
PrintError::Servicesaying "the desktop could not print" and the
portal's reason where it gave one (ADR 0065, updated). -
A window whose display scale changes is built again in its next frame,
together with whatApp::resizedasks for, rather than at once. -
Over Wayland's data-control protocol (KDE Plasma, wlroots desktops)
forma writes its copies through wl-clipboard-rs itself instead of
through arboard, offering the same types as before; a secret still
offersx-kde-passwordManagerHint: secret. While the app watches the
clipboard, each copy offers one type more,
application/x-forma-own.<process id>.<random number>, by which it knows
its own copies; that type holds the copy's main content again (its
text, HTML or PNG), since the types are offered in no set order and a
program that reads whatever type comes first then still gets the
content - under that type's name. An app that does not watch offers
exactly what it did. Reads, X11, the core Wayland protocol, macOS and
Windows are unchanged. -
While an app watches the clipboard on X11, each copy it makes waits
until the watcher has seen the clipboard's new owner, which tells the
app's own copies - usually a moment, at most half a second; a copy the
X server did not show by then is reported as another app's, with a line
on stderr. Copies made throughOsClipboardon the UI thread wait so,
also a window's Copy.
Added
-
Context::clipboard(): a handle to the OS clipboard every window's
Uihas, also before the first window opens (App::launched) and with
none open (App::woken, the tray's menu), to copy a secret
(set_secret_text) and clear it again. It fails, saying why, where the
clipboard cannot be opened, in a detached context that was given none,
and once the app has ended. With theclipboardfeature. -
Context::with_clipboard(OsClipboard)and
OsClipboard::from_clipboard(clipboard), to hand a detached context a
clipboard of the test's, such as anArc<Mutex<MemoryClipboard>>. -
OsClipboard::marks_secrets(): whether a secret goes on the clipboard
marked for clipboard managers - false over the core Wayland protocol
(GNOME), true on X11, Wayland's data-control protocol, macOS and
Windows. -
OsClipboardisDebug. -
Context::system_theme(): the system's light or dark appearance
without a window - the desktop portal's on Linux and the BSDs (the
system-settingsfeature), known inApp::launchedalready; elsewhere
what a window last reported,Noneuntil one opened. -
The
svg-iconsfeature, whichtrayturns on:IconExt::rasterize
(icon.rasterize(size, color)) draws a formaIconinto anRgbaImage
in one color, as the tray draws its icon, and
WindowIcon::from_svg(svg, size)draws an app's SVG in its own colors.
Both fail with the newIconError(InvalidSvg,BadSize). -
WindowSettings::always_on_top(bool)and its field: a window that stays
above the others (winit'sWindowLevel::AlwaysOnTop). Wayland ignores
it. -
WindowSettings::app_idand its setter: the desktop id of one window,
overridingSettings::app_idfor it - its Wayland app id and X11 class
on Linux and the BSDs (ignored elsewhere) - for a process that shows the
windows of two apps, such as a hub hosting another app's window.
Notifications, the tray, hotkeys, recent documents and single instance
keep the app's id. One id per app stays the default (ADR 0088,
superseding ADR 0047's "An id per window"). -
App::resized(frame, size): a window's size inside, in logical pixels,
once as it opens (aftertheme_changed, beforestart,
window_openedand its first build) and whenever it changes after
(before the nextupdate), so a build that chooses its layout by width
can be rebuilt when it crosses a threshold; the default does nothing.
Before, a resize rebuilt only when the window was maximized or restored. -
Frame::size(): the window's size inside, in logical pixels, as the
window system has it now rather than as it was at the last layout. -
Frame::print_pdf(pdf, title):Context::print_pdfwith the print
dialog over the frame's window - on X11 the portal is given the window
asx11:<id>; on Wayland the dialog still belongs to no window. It
shadowsContext::print_pdffor calls through a frame, with the same
signature;Context::print_pdfpasses no window, as before. -
Watching the clipboard, with the
clipboardandrunfeatures:
Context::watch_clipboard(ClipboardWatch)and
Context::unwatch_clipboard(). Every change - a copy in any app, this
one too, or the clipboard emptied - reaches the app as
App::clipboard_changed(cx, ClipboardChange), whether a window is open
or not, until the app stops watching or ends. AClipboardChangesays
which types the copy offers (formats, in the order offered), whether
another app marked it secret (secret, thex-kde-passwordManagerHint
type), whether this app made it (own) and which clipboard changed
(selection,ClipboardSelection::Clipboardor
ClipboardSelection::Primary). Itsoffer(ClipboardOffer::text,
html,image) hands out what was read of the copy as it came: the
formats theClipboardWatchnames (ClipboardFormat), up to its size
limit (ClipboardTooLargebeyond it;
ClipboardWatch::DEFAULT_SIZE_LIMITis 16 MiB). A secret copy is never
read. An offer kept past the next change of its clipboard, or one a
newer copy took the place of before it was read, fails with
ClipboardErrorKind::Stale. Each copy is read within three seconds in
all. On X11 through XFixes, on Wayland through
the data-control protocol (KDE Plasma, wlroots desktops); over the core
Wayland protocol (GNOME), on macOS and on Windowswatch_clipboard
fails withClipboardErrorKind::Unsupported, saying why. -
App::clipboard_watch_failed(cx, error): the watcher stopped without
the app asking, as when the connection to the display broke. The default
says so on stderr. -
MemoryClipboardWatchandContext::with_clipboard_watch, for tests:
copy_from_other_app(formats, contents, secret),copy_from_this_app,
select_in_other_app,clear,make_stale,fail, and
MemoryClipboardWatch::unsupported()for a clipboard that cannot be
watched; the changes reach the app atContext::dispatch. -
On Linux and the BSDs the
clipboardfeature depends on theimage
crate (0.25, PNG only, as arboard has it there) and onrustix(1.1,
the version wl-clipboard-rs has, forpoll); both were built already.
x11rb'sxfixesfeature is named, which winit builds already. Pictures
of a watched copy are read on Linux and the BSDs only: elsewhere a
MemoryClipboardWatch's picture fails to read.
Fixed
Clipboard::clearover the core Wayland protocol (GNOME) put an empty
text on the clipboard, which a clipboard manager kept as an entry; it
now gives the selection up. Without the keyboard focus it takes away
only the app's own copy.- Printing on KDE Plasma: its portal shows the print dialog only from
PreparePrintand refused thePrintforma sent alone, so nothing
printed. OsClipboard::get_textover Wayland's data-control protocol (KDE
Plasma, wlroots desktops) read a copy that offers no plain text as text:
arboard took any type wl-clipboard-rs counts as text - everytext/*
type, JSON, XML. A picture copied in a browser, which offers its link
(text/x-moz-url, in UTF-16), HTML and the picture, pasted its link and
title as text. It now reads only plain text, as on X11 and GNOME, from
the best type offered:text/plain;charset=utf-8,UTF8_STRING,
text/plain,STRING(Latin-1),TEXT, in any case and with spaces
around;and=(text/plain; charset=UTF-8), and answersNone
where none is offered. Over data-control a byte order mark before UTF-8
text is dropped. A watched copy'sClipboardOffer::texttakes the
plain-text types in other spellings too.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v0.3.0
Stablereleased this
2026-10-05 13:15:35 +02:00 | 127 commits to main since this release0.3.0 - 2026-10-05
0.3 takes forma from windows to whole desktop applications: one set of
actions for menus, toolbars, a command palette and key bindings; apps that
live in the tray, with notifications that are answered, global hotkeys,
printing and PDF, recent documents and one instance at a time; title bars
of the app's own, touch and pens, right-to-left layout and high-contrast
themes that follow the system. Text areas get several carets, folding,
syntax highlighting and spelling marks; tables are edited, arranged,
grouped, totalled and copied; charts add pies, heatmaps, histograms, box
plots, sparklines and export; there are a kanban board, a calendar and a
timeline; and widgets can slide to new places, come in and go out. It
breaks the API, mostly so that later releases need not:
the migration guide shows every break with
code before and after, and the types 0.3 renamed keep their 0.2 names as
deprecated aliases until 0.4. The minimum Rust version stays 1.90 for the
libraries; the repository's examples need 1.92, for their profiler (puffin
0.20).0.3 is tested by hand on Linux only, on X11 and Wayland. It compiles for
macOS and Windows, and CI checks that it does, but its platform services -
the tray, notifications, global hotkeys, printing, recent documents, single
instance, the system settings, title bars, touch - have not been run there
yet.forma-ui's
licensefield names what the package carries, not only its
code:(MIT OR Apache-2.0) AND Bitstream-Vera AND CC-BY-4.0 AND Apache-2.0 AND ISC AND MIT- the code under MIT or Apache 2.0 as before, the DejaVu
fonts (the Bitstream Vera license), Twemoji's graphics (CC-BY 4.0) in
Mozilla's font build (Apache 2.0), and Lucide's icons (ISC, and MIT for
those taken from Feather). It wasMIT OR Apache-2.0, which did not cover
the fonts, and the package carries DejaVu's license text now. An app with
bundled-emojicredits Twemoji (the README says how).New dependencies, each behind the feature named, under MIT or Apache-2.0
unless named:- forma-ui:
ropey(MIT) always, for the text of edited fields;
unicode-bidiandweb-time, in the tree before, are direct
dependencies now.pdf:krilla0.7 and the crates it brings, among
themzlib-rs(Zlib) andyoke,yoke-derive,zerofromand
zerofrom-derive(Unicode-3.0).syntax:syntect5.3, with
fancy-regex,bincode1.3 (MIT; no longer maintained upstream) and
aho-corasick(Unlicense or MIT).lucideandbundled-emojiadd no
crates: they carry Lucide's icons (ISC) and the Twemoji font (CC-BY-4.0). - forma-wgpu:
screenshotusespng, andswashis a direct dependency;
both were in the tree already. - forma-winit: on Linux and the BSDs the default
clipboardbrings
wl-clipboard-rsandsmithay-clipboard, withfoldhash(Zlib) among
theirs.settings-file:dirs7 andserde_json, withoption-ext
(MPL-2.0).openandprint:opener0.9, which on Linux and Windows
bringsurl,idnaand eighteen ICU4X crates (Unicode-3.0).tray:
ksni(Unlicense) on Linux and the BSDs,tray-iconon macOS and
Windows.hotkeys:global-hotkeyon macOS and Windows. The system
settings, notifications, recent documents, single instance andrunuse
zbus,x11rb,roxmltree,objc2andwindows-sys, all in the tree
already.dark-lightandwinregare gone, andnotify-rustis used on
macOS and Windows only.
With every feature on, on every target, that makes 67 crates 0.2.1 did not
have and 13 second versions of crates it had, through krilla, fancy-regex,
petgraph and global-hotkey.Not in 0.3, and planned for 0.4: CI on macOS and Windows and the fixes it
brings, and a pass with real screen readers (the accessibility tree is
tested through AccessKit's own consumer); the property grid; the log
viewer; the editors - a canvas, a node graph, curve and gradient editors, a
diff viewer; what needs winit 0.31 - drops onto a widget, dragging out of
the app and between windows, floating dock panels, raising an open window
on Wayland; andrunin a browser, for which forma-ui and forma-wgpu only
compile. Right to left does not mirror the calendar's week and day views
and the timeline, which run as their dates do, nor tab dragging, a text
area's gutter and lines that do not wrap. Touch has no long press for a
context menu, no flicks, no pen tilt and no touchpad pinches on Linux, and
cannot move or resize a window on Wayland.
The README lists what else is known.forma-ui
Changed (breaking)
Style,Palette,MetricsandTextStyleare#[non_exhaustive]:
their fields stay public to read and assign, but they are made from
Style::new(), a theme's palette, metrics or text style
(Metrics::DEFAULT,Metrics::HIGH_CONTRAST,TextStyle::DEFAULT) or
Default, not with a struct literal.Stylehas new fields -folding,
pasted_tabs,takes_typing,layout_transition,enter,leave,
children_enter,children_leave,window_area,gestures,
touch_drag,keep_focus- andMetricstwo,state_outlineand
selection_bar(0 in the standard themes; withserde, a theme saved by
0.2 still reads).Style::corner_radiiis anOption<[f32; 4]>:None, the default,
leaves the corners to the widget, andSome([0.0; 4])is square. The
builderscorner_radius,round_topandcorner_radiiare unchanged.Shortcut's fields are private, since a shortcut can be a chord of two
keys: make one withShortcut::new,primary,parseorthen, and
read it withfirst_key,first_modifiersandsecond. A character is
kept lowercase, soKey::Char('S')is Ctrl+S; written out uppercase, it
showed as "Ctrl+S" and never fired.- Shift no longer counts in a
Shortcuton a digit or symbol, which a
layout types with or without Shift:Shortcut::newdrops it there, and
binding and matching do too, so Ctrl++ fires on every layout. Ctrl+1 and
Ctrl+Shift+1 are one binding now. - A focused text field no longer takes Ctrl+Shift+A, C, V, X and Y
(Cmd+Shift+V on macOS) as Select all, Copy, Paste, Cut and Redo: the
keymap fell back from Shift and a character to the character alone, so
app shortcuts bound to them never fired. A character falls back only to
a caret motion now. To paste with Ctrl+Shift+V again, bind it:
ui.keymap_mut().bind(Key::Char('v'), Modifiers::CTRL | Modifiers::SHIFT, Command::Paste). - Ctrl+D (Cmd+D on macOS) in a focused text area selects the word at the
caret and then its next occurrences (Command::SelectNextOccurrence),
instead of reaching an app shortcut bound to it; anywhere else it still
reaches the app. - A focused
Tableclaims Ctrl+C, copying its selected rows, and Ctrl+V
where the row's cells can be edited: an app shortcut bound to them no
longer fires while a table has the keyboard. MenuItem::Actionhas a new field,icon, and it and
MenuItem::Submenuare#[non_exhaustive]: build menu items with
MenuItem::action,submenuand their builders, and match the variants
with...plot::Seriesis#[non_exhaustive]and has new fields,boxesand
axis: make one withSeries::new(orline,bar,histogram,
boxes) andcolor.WidgetKind::TextInputholds aFieldTextinstead of aString: an
edited field's text is kept in a rope.FieldTextderefs tostrand
compares withstrandString.TableStateis#[non_exhaustive]and keeps the column order, the
hidden and frozen columns and the grouping (order,hidden,frozen,
grouping): make one withTableState::default(). Withserde, a state
saved by 0.2 still reads.- Every component's parts (each
*Parts, andMenuLevel,DockBar,
DockStrip,ListRows,FindRows,PickerRows,TabBarRows) are
#[non_exhaustive]and#[must_use], and a component's outermost widget
is its parts'root:CollapsibleParts::section,DateParts::row, the
date picker calendar'scard,DateTimeParts::row,DialogParts::scrim,
DockParts::area,MarkdownParts::view,MenuBarParts::bar,
NumberParts::row,RadioParts::group, the split button'srow,
SplitPaneParts::container,TableParts::table,TimeParts::row,
ToastParts::stackandVirtualParts::vieware eachrootnow.
TablePartshas new fields, and itsheadersandhandlesare in the
order the columns are shown, without hidden ones (columnssays which
column each heads). form::Rule::checktakes a closure that isSend + Sync, so rules no
longer keep aForm, or a component that checks text by them, from
beingSend.Ui::copy_textreturns the clipboard's error,Result<(), ClipboardError>, instead of reporting it asEvent::ClipboardFailed.
The event is left for what forma does by itself - the keys,
run_command, a component's Copy or Paste - and says what kind of
failure it was:kind: ClipboardErrorKindbesidemessage.Ui::run_commandgoes where the command's key would go: to the topmost
popup or the focused widget that claims it (anEvent::Commandfor its
component), to a focused custom widget, to the focused widget's own
action, then to the nearest widget around it that claims it. It ran only
the focused widget's own action, so a claimed command never reached its
component: an Edit menu's Copy is now onerun_command(Command::Copy)
for a text field, aTableor anImageViewer. The events it gives ask
for the next frame at once.- A press on a
MenuBaror aMenu(a context menu, the open menu of a
menu bar, a select or a split button) leaves the keyboard focus where it
was (Style::keep_focus). It took the focus away from the field the user
was in, so an Edit menu's command found nothing to act on, and the field
reportedEvent::Submitted. CursorHinthas two new variants,ResizeDiagonalDownand
ResizeDiagonalUp, for the corners of a window's resize border: a
backend of its own maps them.Ui::set_localepanics for aLocalewhosegroupingis itsdecimal
separator: such a locale read "1.5" as 15 without a word.- An icon (
DrawCommand::Icon) keeps its proportions, scaled to fit its
rect and centered in it, where it was stretched to fill it: a renderer of
its own draws it so.
Deprecated
- Types renamed, their 0.2 names kept as deprecated aliases at the same
paths until 0.4: the date picker'swidgets::CalendarPartsis
DateCalendarParts, Markdown'sEditorEventandEditorPartsare
MarkdownEditorEventandMarkdownEditorParts,ViewerEventand
ViewerPartsareImageViewerEventandImageViewerParts,SplitEvent
andSplitPartsareSplitButtonEventandSplitButtonParts, and
RangeEventandRangePartsareRangeSliderEventand
RangeSliderParts.
Changed
- Editing a text input no longer copies its whole text: the edit, the undo
step, caret and line movement, the gutter and the color spans read around
where they look.Ui::text_ofstill gives the whole text - for an edited
field a copy, made on the first call after each edit - and
Ui::field_textreads it without one. - An input method's composition is shaped into the field's text at the
caret, underlined, instead of drawn over it: the text after it moves
along and wraps, and the caret andUi::ime_cursor_areasit at its own
cursor. In a hidden password field it is bullets. - The image viewer zooms out as far as fitting the whole picture needs, not
only to 1 %, and tells a screen reader a zoom below 1 % with a decimal. - Tabs in text put into a text input are no longer dropped. A text area
keeps them, so tab-indented code pasted into it keeps its indentation,
and so does a password field, so a pasted password arrives unchanged.
Another single-line field - a table cell's editor too - turns each into a
space, so a row copied from a sheet pastes as36 London Ada, not
36LondonAda. A mask checks the text as it is then:12, a tab and34
pasted into aMask::Digitsfield is refused, where it gave1234.
Style::pasted_tabs(PastedTabs::Drop)drops them as before.
Added
ActionSet<A>(forma::action_set, withActionandEntry): an app's
actions, each known by an id of the app's own type, with a label, icon,
shortcut, enabled and checked state, description and category, described
once.menuandmenusbuild menu items from a layout ofEntrys,
bind_shortcutsbinds their shortcuts (switched off for disabled
actions), andchosenandtriggeredturn menu choices and shortcut
events back into the app's ids. Setters,revision, and cheap copies.CommandPalette<A>(widgets::palette): every action of anActionSet,
found by typing letters of its name in order, best match first and the
last run first before anything is typed; keyboard driven and modal,
reportingPaletteEvent::Chosenwith the keyboard back where it was.Toolbar<A>(widgets::toolbar): buttons for a layout ofEntrys -
icons (with labels,with_labels), tooltips with the shortcut, disabled
actions greyed out, checked ones pressed, separators, submenus. What does
not fit moves into a "more" menu. ReportsToolbarEvent::Chosen; a press
on it keeps the keyboard focus where it is.StatusBar(widgets::status_bar):StatusSections at the start and
the end (with_start,with_end) and aStatusProgressbetween them
(with_progress); a status to screen readers.Undo<C>: undo and redo stacks of the app's own change records, with
groups, a limit,can_undoandnext_undofor a menu, and
mark_savedandis_modifiedfor unsaved changes.MenuItem::iconandMenuItem::is_enabled;Menu::set_itemsand
MenuBar::set_menus(andMenuBar::menus) show new items in place,
keeping an open menu open.- Chords (ADR 0070):
Shortcut::thenmakes a shortcut of two keys (Ctrl+K
Ctrl+S), withis_chord,firstandsecond; menus, the palette and
tooltips show both keys. The next key completes the chord or calls it
off - Escape, an unbound key, a press, the window losing the keyboard, or
the time out (Ui::set_chord_timeout, 1.5 s) - and types nothing.
Event::ChordPending,Ui::pending_chordandUi::cancel_chord. - A shortcut's text form:
Displaywrites "Ctrl+K Ctrl+S" on every
platform, andShortcut::parseandFromStrread it back
(ParseShortcutError); withserdea shortcut is saved as that text.
Shortcuts::bindingslists the bindings, andShortcuts::set_enabled
andis_enabledswitch one off as if it were unbound. widgets::KeyBindingEditor: an action set's shortcuts for the user to
change, filter, capture (a key, or two for a chord), remove or reset,
clashes named (ActionSet::conflicts_with), reporting
KeyBindingEvent::Changed;KeyBindingsandKeyBindingkeep, save
(serde) andapplywhat the user changed.Ui::set_key_captureand
Ui::key_capture: keys pressed in a widget are reported as
Event::KeyCaptured, and do nothing else.SegmentedControl(widgets::segmented): joined buttons choosing one
(SegmentEvent::Selected) or toggling any number
(SegmentedControl::multiple,SegmentEvent::Toggled), with labels,
icons and disabled segments; a radio group or toggle buttons to screen
readers.SearchField(widgets::search): a query field with a magnifying glass
and a clear button, reportingSearchEvent::Changedon each edit and
SearchEvent::SearchRequestedonce typing pauses (with_delay, 300 ms),
on Enter and on clearing;set_result_countshows and announces how many
results there are.PasswordField(widgets::password): a password input with an eye
button that shows and hides the password, keeping no copy of it;
PasswordEvent::Changed,Activated(Enter) andRevealed;
with_maskfor a PIN,with_label.MaskedInputandMaskPattern(widgets::masked): text of a fixed
shape, its pattern written as it looks ("(###) ###-####") - the places
still to fill shown dimmed, typing stepping over separators, characters
that fit no place refused, pastes read through the pattern, undo and redo
by value;raw,formatted,is_complete,with_label.TagInput(widgets::tags): several values as chips in one field, made
on Enter, a separator, a suggestion taken or the focus leaving, and
checked by a form'sRules (with_rules); keys over the chips;
TagEvent::ChangedandUpdated.widgets::Breadcrumb: the path to the current place, each place above it
a button (BreadcrumbEvent::Selected), those that do not fit moved into
a "..." menu; a navigation landmark of links to screen readers.widgets::Wizard: a task in steps - a step indicator,WizardPages,
each with an optionalFormthat Next checks, Back, Next, Cancel and
Finish, pages skipped by the app's data;WizardEvent::PageChanged,
Finished,CancelledandUpdated.widgets::Popover: an interactive card beside a widget, with an arrow
pointing at it, following the widget as the layout moves; it closes on
Escape, a press outside and its close button (PopoverEvent::Closed),
andwith_modalkeeps the focus inside.widgets::Drawer: a panel sliding in from an edge (Side::Start,End,
Top,Bottom) beside the content, or over it above a scrim
(with_modal);DrawerEvent::Closed.Ui::anchored_popup(anchor, placement, style): a popup placed beside a
widget by every layout - aPlacementof aSide, an alignment along it
and a gap - flipped and shifted to stay in the window, with an optional
arrow;Ui::popup_side.Side::StartandEndfollow the UI's
direction.Ui::focus_first(container): the first widget in it that
takes the focus gets it at the next layout.- Title bars drawn by the app (ADR 0068): widgets ask their window for
something with aWindowRequest(Drag,Resize(ResizeEdge),
Minimize,ToggleMaximize,Close,ShowMenu), queued by
Ui::request_windowand taken by the backend with
Ui::take_window_requests.Style::window_drag()moves the window from
a widget (a double click maximizes, the secondary button shows the
window's menu),Style::window_resize(edge)is a grip, and
Ui::set_resize_bordera band along the window's edges;
Ui::set_window_maximizedandwindow_maximized. widgets::TitleBar: the title, a row for the app's widgets and the
window's buttons where the platform has them (ButtonLayout::platform,
parsefor GNOME'sbutton-layout), drawn as macOS's traffic lights or
as flat buttons; it reports theWindowButtonpressed, and gives way
without overlapping as the window narrows.- Touch, pens and gestures (ADR 0069):
InputEvent::Touch(aTouchwith
aTouchPhaseand a force), andPinchandRotatefrom a trackpad.
One finger is the pointer: a tap clicks, and a drag scrolls what it is
on, or drags what follows the pointer - sliders, splitters, scrollbars,
custom widgets, the parts of forma's widgets that are dragged, and a
widget withStyle::touch_drag().Style::gestures(): two fingers, or a
pinch or turn over the widget, reportEvent::Gesture(and
WidgetInput::Gestureto a custom widget).Ui::pointer_force()and
InputCx::force(). - Several carets in a text area: an Alt+click adds one, and Ctrl+D selects
the word and then each next place it occurs; typing, deleting, pasting
and the editing keys work at each, undo takes them back as one step, and
Escape goes back to one.Selection,Ui::selections,
primary_selection,set_selections,add_caretand
keep_primary_selection. - Code folding:
Style::folding()folds a text area's regions by
indentation, with a mark beside each line that starts one
(Event::FoldToggled);Command::FoldandUnfold(Ctrl+Shift+[ and
], Cmd+Alt+[ and ] on macOS);Ui::set_fold_ranges,
clear_fold_ranges,fold,unfold,fold_all,unfold_alland
folded_lines. - Syntax highlighting, behind the new
syntaxfeature:
forma::syntax::Highlightercolors a text area by one of syntect's
grammars, in a theme that follows the UI's, each color held to 7:1
against the background, highlighting again only what an edit changed.
Ui::text_spansreads a widget's color spans. - Spelling marks:
Ui::set_misspellingsmarks the words an app's checker
found with a squiggle (Misspelling), and the marks follow the text;
Ui::misspelling_at_point,Misspelling::menu_itemsand
Ui::correct_misspellingoffer a right click's suggestions;
misspellings,misspelling_atandclear_misspellings. - Right-to-left and mixed text in text inputs: the arrows move the caret
the way they point through text of either direction, where the two meet
every place between two letters has a caret position, a click lands on
the nearest, a selection covers the stretches its letters take, and a
single-line field's right-to-left text starts at its right edge. Screen
readers find right-to-left runs where their glyphs are. - Right-to-left layout:
Locale::direction(Direction::LeftToRightor
RightToLeft;Direction::of_language) mirrors the UI - rows, grids,
padding, borders and corners, labels, checkboxes, switches, sliders,
scrollbars, submenus, splitters, tables, drawers and the kanban board -
and tells screen readers;Ui::set_localewith a new direction mirrors
what is built. Left and Right reach widgets as each other's command, a
custom widget gets them as they are. FieldText, the text of a text input (len,as_str,chunks,
slice,line_count,line_of,line_start), andUi::field_text.Style::pasted_tabs(PastedTabs): what a text input does with the tabs
in text put into it - pasted, inserted withUi::insert_stror
replace_range, committed by an input method, or in a text area the
indentation the Tab key inserts:PastedTabs::Keep,SpaceorDrop.
Without it a text area and a password field keep them, and another
single-line field turns each into a space.Theme::high_contrast_darkandhigh_contrast_light: text at 7:1 or
more against everything it sits on, outlines and marks at 3:1 or more,
2 px outlines and a 3 px focus ring.Metrics::state_outlinedraws
hover, press, focus and invalid states in shape,Metrics::selection_bar
marks a selected row with a bar at its start, and
Metrics::HIGH_CONTRASTholds those themes' metrics.Palette
implementsDefault.Ui::announce(text, Politeness): screen readers saytext, after what
they are saying or at once.- The
lucidefeature:forma::lucide, Lucide 1.52.0's 1866 icons as
Iconconstants (lucide::ARROW_LEFT), withby_name,ALLand
VERSION; an app carries only the icons it uses. - The
bundled-emojifeature: Twemoji in Mozilla's COLR build (about
1.5 MB) as the emoji family of everyFonts::new(), so emoji draw in
color without system fonts. - Motion:
Style::animate_layout(transition)slides a container's children
from where they were to where a relayout puts them, a keyed child carried
across to another container that animates too.
Style::enter,leave,children_enterandchildren_leavefade or
slide a widget in as a build adds it and out as a rebuild orUi::remove
drops it (anim::Presence).Ui::draw_offsetanddraw_opacity.
Ui::set_reduced_motionandreduced_motionmake every change
immediate. - Editable cells:
TableModel::edit_textsays whether a cell can be edited
and with what text, andTableModel::set_celltakes the text or refuses
it with a message. Enter, a double click or typing starts an edit, Escape
drops it, and Enter, Tab and the arrows keep it and move on
(TableEvent::Edited).Table::edit_cell,editing,edit_error,
cancel_edit;VecModel::set_editable(with_editable,
is_editable) andVecModel::set_text. Table::set_rulesandwith_rules: what is typed into a column is
checked byform::Rules, the message shown beside the cell;
form::first_errorchecks text taken in another way.- Cell selection (ADR 0086): with
Table::with_cell_selection()the
keyboard moves over cells, the current one outlined (current_column,
set_current_column), and what is selected is a block of cells - the
selected rows by a range of columns - of which only the cells are
filled, so what is highlighted is what Copy takes. A click selects a
cell, and Shift with a click or the arrows stretches the block; with
with_multi_select(), Ctrl (Cmd on macOS) and a click add or remove a
row, keeping the columns, and Ctrl+A selects every row and every column
shown. Screen readers hear the block's cells selected, not the rows. Style::takes_typing()andEvent::TextTyped: text typed while a widget
that is no text field has the keyboard is reported to it, and the keys
typed trigger no shortcuts; an input method composes there too.- Columns arranged:
column_order,set_column_orderandmove_column
(TableEvent::ColumnsChanged);with_movable_columns(headers dragged,
or Ctrl+Shift and an arrow);with_column_chooser,set_column_visible,
is_column_visibleandvisible_columns;with_header_menu, a header's
context menu; frozen columns (with_frozen,set_frozen,frozen). - Grouped rows and totals:
TableModel::group_by,row_kind
(RowKind::Group),set_expanded,groupableandtotal; group rows
open and close (TableEvent::Toggled,ChildrenNeeded,
GroupRequested) and a row of totals sits under the rows.
Table::group_by,set_grouping,groupingandset_expanded.
VecModelgroups, totals columns with anAggregate(Sum,Count,
Average,Min,Max) and aligns them (set_alignment). A grouped
table is a tree grid to screen readers. - Copy and paste in a table: Ctrl+C copies the selected rows (with cell
selection, the block) as tab-separated text, and Ctrl+V pastes
such text into the cells that can be edited, every value checked
(TableEvent::Pasted,CellChange);Table::selection_textand
Table::pastefor menus.Table::to_csvandwrite_csv. - Tree tables: a
TreeModelofTreeItems (leaf,branch,lazy),
shown by aTablewith its header, sorting, editing and copying, the
first column indented by depth;RowKind::Node,RowKind::Busyand
RowKind::has_cells. plot::PieChart: pie and donut charts (Slice,donut,with_center),
labels inside the slices or beside them (SliceLabels), a legend that
toggles slices (PieEvent::Toggled), a hover readout andset_values.- Histograms (
Series::histogram,SeriesKind::Histogram,Bins,
plot::histogram) and box plots (Series::boxes_of,Series::boxes,
SeriesKind::Box,BoxStats). plot::Heatmap: a grid of values as colors on aColorScale
(viridis,blues,diverging), with a color bar, labels, a readout
andHeatmapEvent::Clicked; a million cells stay cheap.plot::Sparkline: a tiny chart for table cells, tiles and status bars -
a line, an area or bars (SparkKind,with_kind), marks on the lowest,
highest and last value (with_min_mark,with_max_mark,
with_last_mark) and a shared range (with_range).- A second y axis (
Plot::with_right_axis,Series::on(YAxis::Right),
Plot::right_view) and annotations (Plot::with_annotation:
Annotation::hline,vline,hband,vbandandpoint;
Plot::set_annotations). - Chart export (ADR 0049):
picture(&ui, size, scale)onPlot,
PieChart,HeatmapandSparklinedraws the chart on its own as a
plot::Picture, which writes SVG (to_svg) or is drawn to PNG by
forma-wgpu'sexport;Plot::as_shownkeeps the view the user zoomed
to. Each chart'stable_modelgives its data as rows. - The
planningfeature,forma::planning: aKanbanBoardofLanes of
Cards, moved by dragging or from the keyboard and reported as
KanbanEvent::Moved(CardMove)andLaneMoved(LaneMove)for the app to
apply, with work-in-progress limits; aCalendarwith month, week, day
and agenda views ofCalendarEntrys at aCalendarSpan, moved and
resized by dragging or from the keyboard (CalendarEvent); aTimeline
ofTimelineRows ofTimelineBars on a date axis, a Gantt chart with
milestones andDependencyarrows, zoomed from days to years
(TimelineEvent). Each is one focus stop with a current item, speaks to
screen readers and announces every change. - Pictures and HTML on the clipboard:
Clipboardgainsget_image,
set_image,get_htmlandset_html, whose defaults fail with
ClipboardUnsupported, so a clipboard of text only keeps compiling.
Ui::copy_image,copy_html,clipboard_text,clipboard_imageand
clipboard_htmlreturn the clipboard's error;ClipboardErrorKind
(Unsupported,Failed) andClipboardErrorKind::of(&error).
MemoryClipboardholds text, a picture or HTML. RgbaImage, forma's one picture in pixels, checked against its size
(RgbaImageError) and shared rather than copied: the clipboard's, a
window's and a tray's icon, forma-wgpu's screenshots and exports, a
PDF's pictures.RgbaImage::pixel.ImageViewer::with_pixels(andset_pixels,pixels): the viewer
copies its picture with Copy (ImageViewerEvent::Copied), and
ImageViewer::copyfor a menu.ImageViewer::set_center.- PDF files, behind the new
pdffeature (ADR 0065): apdf::Document
written page by page, each with aPageSetup(PageSize,Margins).
ui_pagelays aUiout on a page,pagedraws pieces of aUi, a
draw list or a chart,ui_pagesflows a longUiover pages, breaking
between lines and rows,ui_pages_withadds a header or footer with its
PageNumber, andpicture_pageputs a chart on a page. Text stays text,
in subsets of the fonts forma shaped it with; shapes, icons and clips are
vector.PdfErrorsays what failed. inspector::Inspector: a panel drawn over a running UI with its widget
tree, the selected widget's rect, state, colors, font and layout, and the
last frame's work (Ui::stats); a password's text is never shown.forma::Instant, the time forma's clock, animations and wakeups use:
web-time'sInstant, which is the standard library's on every target
but the web, wherestd::time::Instant::now()panics.- New phrases (
Phrase) for the new widgets: the table'sColumns,
HideColumn,FreezeColumns,UnfreezeColumns,GroupBy,Ungroup,
GroupRow,EmptyValueandTotal; the command palette's
SearchCommandsandNoMatchingCommands; the search field's
SearchPlaceholder,ClearSearchandSearchResults; the password
field'sShowPasswordandHidePassword; the tag input'sRemoveTag;
the breadcrumb'sBreadcrumbandHiddenItems; the wizard'sBack,
Next,Finish,Cancel,WizardStepandCompleted; the title
bar'sMinimize,MaximizeandRestore; chords' and the key binding
editor'sChordPending,FilterShortcuts,CommandColumn,
ShortcutColumn,NoShortcut,PressShortcut,PressSecondKey,
ChangeShortcut,RemoveShortcut,ResetShortcutand
ShortcutConflict; the kanban board'sBoard,LaneCards,
LaneOverLimit,CardPickedUp,CardAt,CardDropped,
CardMoveCancelledandLaneMoved; the calendar'sCalendar,
PreviousPeriod,NextPeriod,Today,MonthView,WeekView,
DayView,AgendaView,AllDay,MoreEntries,NoEntries,
MonthTitle,DayTitle,FromTo,EntryPickedUp,EntryAt,
EntryMoved,EntryResizedandEntryMoveCancelled; the timeline's
Timeline,ZoomIn,ZoomOut,Quarter,BarPickedUp,BarAt,
BarMoved,BarResizedandBarMoveCancelled.
Fixed
- A
TableorTreeViewevent that came after the app changed the model
or the nodes between a build and offering that frame's events - a
worker's answer that came in first - acted on the row now in the clicked
row's place. The table and the tree find the row by its id now, and a
row that went takes no click. - Shift+Tab in a text area moved the caret back a whole indent unit
whatever it took off, so on a line with less indentation the caret could
land inside a character (in日本語, say) and the next key panicked. It
moves back by what was taken off now. Shift+Tab with nothing to take off,
and Tab over blank lines only, reportedEvent::TextChanged. - A click in the same frame as an edit, before the next layout, could put
the caret inside a character of a text input, and the next key panicked. - A line holding a paragraph separator (U+2029, U+0085 or U+001C to
U+001E) between text of the two directions made cosmic-text panic. - A legend click after the app replaced a plot's series with fewer could
index past them, andTreeView::set_childrenpanicked for a node a
set_rootsremoved while its children were loading; such a late answer
is dropped now. - A popup that is not modal shown above a modal one (a menu or a select's
list in a dialog) switched off the modal popup's keyboard trap: Tab
walked the window behind it, and the window's global shortcuts fired. - A modal popup taking the focus as it opened selected the whole text of a
text area it landed on, so the next key replaced it; it enters the field
as Tab does. - A popup opened during a rebuild - a
MenuBar's open menu, built before
the window's content - took the place of the root's next unkeyed child,
so the content was built again into other widgets, losing their focus,
scroll offsets and undo history. - The find bar's Replace and Replace all could not be undone, and broke
the text area's history. - In a text area whose edit profile indents with tabs (
Indent::Tab), the
Tab key inserted nothing at the caret, and opening a line below or above
(Ctrl+Enter, Ctrl+Shift+Enter) andUi::replace_linesdropped the tabs
of the indentation they carried. - Clicking the open title of a
MenuBar, or aDatePicker's calendar
button while the calendar was open, closed it and opened it again at
once; it closes now. Escape did not close aSelect's or a
SplitButton's menu while the pointer rested on its button. - A range
DatePickerreportedRangeChanged(None)as soon as a new start
was picked, and closing the calendar then lost the old range; the range
changes once both ends are picked. - Under a
TreeViewfilter, the arrows, the expanded state told to screen
readers, and Left and Right went by the expanded nodes rather than by
what the filter opened. Tableasked for a row withTableEvent::RowsNeededonly when its first
cell wasCell::Pending; a row pending in any column is asked for now.Locale::format_datewrote a year before 1 as "-005", which
parse_datecould not read, andparse_numberdid not read a number
grouped with another of the three spaces (U+0020, U+00A0, U+202F) than
the locale's.- A button, text field or
progress_barwhose style chose square corners
(corner_radius(0.0)) was drawn with the theme's radius. - An icon button (
Style::icon_button) stretched by its row showed its
icon at its top-left corner; it centers its content, unless its style
aligns it. - A text with emoji ending in a newline had no empty last line, so the
caret after that newline was drawn on the line before. - A text area with a gutter sized by its text was measured without the
gutter's strip, so its text wrapped into what the strip left, and a
placeholder wrapped at its field's whole width rather than its content
box's. - A centered right-to-left line (a
text_centerlabel, centered rich text)
was pushed off to the right, and a link in right-to-left rich text was
found under the wrong stretch. - An input method's composition in an empty field was drawn over the
field's placeholder. - A shortcut pressed with Alt in a text field typed its letter as well on
Linux (and one with Ctrl+Alt, AltGr, on Windows): the text that comes
with a key a shortcut takes is dropped. - Commands claimed in the style handed to
VirtualList::buildwere
dropped. - Screen readers heard a right-aligned or centered table cell without a
name, and abusy_barnot as a progress indicator. - A plot's readout of the values under the pointer was worked out only as
the pointer moved, so afterset_data, a series hidden from the legend
or a box zoom it showed what was there before. It and
Plot::table_modelwrote a value that is not a number as "NaN" (a dash
now, and ∞ for an infinite one). Bars on a logarithmic x axis were sized
in values, fitting an axis of hundreds of decades; they take even room in
decades. - Hiding a revealed password field, editing or removing it while revealed,
and ending a composition in it freed the lines cosmic-text had shaped
from the real text, and forma's cached copy of it, without wiping them. - Work in a long text area that grew with the whole text, on each key or
each frame: one emoji in it made every keystroke shape the whole text;
the gutter drew and walked every line number on every frame, and shaped
them all again when the line count changed; measuring walked every visual
line, and a text area sized by its text laid every line out twice; one
that does not wrap walked every line several times a frame; each
decoration and the selection walked the lines from the top when drawn,
and color spans found every line start again on each paint; and each
bracket or quote typed with auto-pairing read the whole text before the
caret. TreeViewworked out its whole row list again for every event it was
offered, andUi::removeand a rebuild's removals took time in the
square of how much they removed.
forma-wgpu
Changed (breaking)
- The variants of
ImageErrorwith fields (Empty,WrongLength,
TooLarge) are#[non_exhaustive]: a pattern naming their fields ends
in... - A lost surface is reported as
FrameStatus::SurfaceLostinstead of
Retry: make a new surface for the window and hand it to
WindowSurface::replace_surfaceorSurfaceRenderer::replace_surface.
It was configured again, which fails (wgpu's default error handler
panics) or left every frame retrying a dead surface;resizeleaves it
alone now. Renderer::remove_targeton the main target resets it - its buffers,
its draws and the atlas pages they held are freed, andprepareand
renderstill draw into it - and returnstrue, where it refused.
Changed
- An icon whose SVG does not parse draws nothing in a debug build too,
where it panicked: an icon can come from a file the app was handed. - When no target is left with anything to draw (the last window closed),
the glyph atlas starts over small, without a color page, and large
images' tiles leave the GPU. The color glyph page grows to 4096 px at
most (64 MiB), not 8192.
Added
- Screenshot tests, behind the new
screenshotfeature (ADR 0042):
screenshot::Offscreenrenders aUi, a draw list (render_list) or a
Harness(capture) into a texture the way a window draws it, and reads
it back as aforma::RgbaImage;comparechecks it against another
within aTolerance(Mismatch);Referenceskeeps reference PNGs,
writes<name>.new.pngand<name>.diff.pngwhen a check fails, and
updates them withFORMA_UPDATE_SCREENSHOTS=1;read_pngand
write_png. - The
exportfeature:export::renderandexport::pngdraw a chart's
forma::plot::Pictureoffscreen, andexport::encode_pngencodes any
RgbaImage; a picture larger than the GPU's textures is
ExportError::TooLarge. It turns onscreenshotand forma'splot. - Large images (ADR 0066):
LargeImage::new(image)keeps anRgbaImage
with smaller copies, made once on any thread, and
Renderer::register_large_imageregisters it under anImageHandlelike
any other, drawn from tiles of the level the zoom needs, uploaded as they
come into view and dropped beyond 256 MiB. Pictures larger than the
device's largest texture work this way. FrameStatus::SurfaceLost, withWindowSurface::replace_surfaceand
SurfaceRenderer::replace_surface.
Fixed
- The glyph atlas doubled whenever it filled up, even with the glyphs of
long-gone frames, and never shrank; a full color page doubled the
coverage page too, and the color page was as large as the coverage page
(256 MiB for the first emoji after a long session). A page that fills up
long after it was made starts over at 1024 px, one grown for a single
large frame starts over once it is old and little used, and the two
pages have sizes of their own. - Glyph bitmaps were kept twice, once in swash's cache, which nothing
emptied, and parsed icon SVGs were kept forever, one per icon an app ever
drew. - Targets sharing the atlas could draw another target's glyphs, even
prepared and rendered in the order the docs called safe. Each target
draws from the atlas pages it was prepared with now, in any order. - Every mesh after the first in a frame was drawn with a base vertex,
which OpenGL ES below 3.2 cannot do; mesh indices are rebased on the CPU. - The first window's GPU resources were never freed when it closed while
other windows stayed: its surface draws into the main target, which
remove_targetrefused. - Color emoji from COLR fonts, such as Segoe UI Emoji, had dark fringes:
swash composites their layers premultiplied, and they were blended as
straight alpha. - A registered image's mipmaps averaged the colors of clear pixels in as
they were, so transparent edges came out dark when drawn small; colors
are weighted by their alpha now. - An icon's SVG read any file an
<image href>named -/dev/zero
exhausted the memory, a FIFO blocked the UI thread. Pictures inside an
icon come only fromdata:URLs. - An icon in a box of another shape (an icon without a size of its own in
a stretched row) was stretched to fill it; it is scaled to fit and
centered.
forma-winit
Changed (breaking)
SettingsandWindowSettingsare#[non_exhaustive]: built withnew
and setters that chain, one per field
(Settings::new("Editor").size(1200.0, 800.0).menu(menus())); their
fields stay public to read and assign, but a struct literal no longer
compiles, not even with..Settings::new(..).Frame::open_window,close_window,exit,wakerandset_menu
moved to the newContext, whichFramederefs to: calls through a
frame compile as they were, and a path namesContext.- The free function
notifyis gone: notifications are shown with
Context::notify, which does not block and gets answers. The
notificationsfeature needsrun, and turns it on.NotifyErroris an
enum (Unsupported,Service) instead of a struct, andNotification
is#[non_exhaustive], with public fields to read. - The light or dark appearance on Linux and the BSDs comes from the
system-settingsclient, which reads the portal'scolor-schemewith
the other settings, on one connection:system-themeis now the same
feature assystem-settings, so a 0.2 feature list still builds. With
it, as with the default features, the defaultApp::theme_changed
appliesTheme::high_contrast_light()orhigh_contrast_dark()while
the system asks for high contrast, and the new
App::system_settings_changedapplies the system's reduced motion to
each window'sUi.
Changed
runkeeps one OS clipboard for its event loop, opened for its display,
and gives every window'sUia handle to it: on Wayland the Wayland
clipboard - the data-control protocol where the compositor offers it,
text over the core protocol where it does not (GNOME) - instead of X11's
through Xwayland.smithay-clipboardis held below 0.7.3, which would
bring a second smithay-client-toolkit, until winit 0.31.- On Linux and the BSDs notifications go through forma's own client of
org.freedesktop.Notifications, one connection and one listener for all
of an app's notifications, instead of notify-rust, which macOS and
Windows keep. Context::set_menuchanges the platform's menu bar in place when only
labels, shortcuts and enabled or checked states changed: an open menu
stays open and nothing flickers.
Added
- Running without a window (ADR 0045):
Context, what an app reaches with
or without a window open -open_window,open_main_window,
close_window,has_window,exit,set_exit_when,is_exiting,
waker,request_wakeup,next_wakeup,rebuild_windows,set_menu,
activation_token.App::launched(cx)runs before any window opens, and
App::woken(cx)for a wake or a time asked for, a window open or not (a
wake with no window open was lost).ExitWhen::ExitCalledkeeps the app
running after its last window closes,Settings::open_main(false)
starts it without the main window, andContext::open_main_windowopens
that again where it was (App::window_openedruns for it). On macOS a
choice from the menu bar with no window open opens the main window.
Context::detachedandContext::dispatchtest the windowless
callbacks. - App identity and the window icon (ADR 0047):
Settings::app_id, the
Wayland app id and X11 class of every window and the notifications'
desktop entry, andSettings::iconandWindowSettings::icon, a
WindowIconmade from aforma::RgbaImage(WindowIcon::from_image),
shown on X11 and Windows. - Notifications with actions (ADR 0046):
Notification::actionand
timeout(NotificationTimeout);Context::notifyreturns a
NotificationId,Context::replaceandwithdrawchange it, and the
answer comes inApp::notification_answered(NotificationAnswer: an
action, a click, dismissed, expired), a failure in
App::notification_failed, a window open or not;
Context::supports_notification_actions. The first window opened while
a click's answer is handled takes the focus with its activation token.
MemoryNotificationsfor tests. - The system tray (ADR 0048), the
trayfeature:Context::set_tray(Tray)
shows an icon (TrayIcon::from_icon,from_image) with a tooltip and a
menu ofMenuItems, changed in place when called again; choices arrive
inApp::tray_chosen, clicks on the icon inApp::tray_activated, a
tray that went away inApp::tray_failed.Context::supports_tray,
remove_trayandtray;TrayError. A StatusNotifierItem on Linux and
the BSDs, tray-icon's on macOS and Windows.MemoryTrayfor tests. - Global hotkeys (ADR 0064), the
hotkeysfeature:Context::set_hotkeys
asks forHotkeys that reach the app while another app has the focus;
presses arrive inApp::hotkey_pressedandhotkey_released, the keys
the system bound inApp::hotkeys_bound(BoundHotkey), and a loss in
App::hotkeys_failed.Context::hotkey_support(HotkeySupport::Direct,
Desktop,Unsupported) andHotkeyError. X11 grabs, and the desktop
portal's GlobalShortcuts on Wayland; global-hotkey on macOS and Windows.
MemoryHotkeysfor tests. - Printing (ADR 0065), the
printfeature:Context::print_pdf(pdf, title)
hands a PDF to the system - the desktop portal's print dialog on Linux
and the BSDs, the PDF viewer elsewhere - andApp::print_finished
(PrintOutcome) orApp::print_failed(PrintError) says what came of
it.MemoryPrinterfor tests. - Recent documents, the
recent-documentsfeature:
Context::add_recent_documentputs a file among the system's recent
documents - the freedesktoprecently-used.xbelunless the user turned
remembering recent files off, the shell's on Windows, the document
controller's on macOS - andContext::recent_documentsand
RecentDocumentsread the app's own back;RecentError. - System settings, the
system-settingsfeature, on by default:
App::system_settings_changed(frame, SystemSettings)-reduced_motion,
high_contrastandaccent_color- as a window opens and when they
change;Context::system_settings. The desktop portal's settings on
Linux and the BSDs, the system's own on macOS and Windows. - One instance at a time (ADR 0071), the
single-instancefeature:
Settings::single_instance(true)makes a second start hand its
arguments, working directory and activation token to the first, and end.
The first hears of it inApp::open_requested(cx, OpenRequest), whose
default brings the main window to the front. Over the session bus
(org.freedesktop.Application, soDBusActivatabledesktop entries work)
on Linux and the BSDs, a socket on macOS and Windows;
RunError::SingleInstanceandInstanceError.Context::hand_overfor
tests. - Activation:
startup_token(), the launcher's activation token, which
rungives to the first window it opens;WindowSettings::activation_token;
Context::activate(window, token)raises an open window on X11, macOS
and Windows (ActivateError). - Title bars of the app's own:
runcarries out every window's
WindowRequests, aCloseaskingApp::close_requested, and tells each
Uiwhether its window is maximized.Settings::decorations(false)and
Frame::set_decorations; a window without decorations gets a resize
border (WindowSettings::resize_border, 5 logical pixels, not on macOS).
apply_window_requests(window, ui)for an event loop of the app's own,
andcursor_iconmaps the diagonal resize cursors. WinitInputpasses winit's touches on, with their pressure, and macOS's
trackpad pinch and rotation.Task<T, M>: work on a thread of its own with its result handed to the
UI thread through theWaker, without an async runtime - progress
(TaskCx::set_progress), messages (TaskCx::send), cancelling
(Task::cancel,TaskCx::is_cancelled,checkandCancelled), and a
panic as aTaskError.Task::spawn_with_wakefor an event loop of the
app's own.- The
settings-filefeature:SettingsFile<T>keeps the app's settings,
one serde struct, as JSON in the platform's settings directory
(settings_path), written beside the file and renamed over it;
SettingsFileError. - The
openfeature:open_urlopenshttp,httpsandmailtolinks
in the browser or mail program, andopen_patha file or folder with its
default app;OpenError. Settings::inspector(key): a key that shows forma's inspector over every
window ofrun.OsClipboardholds pictures and HTML too, and
OsClipboard::for_display(event_loop.owned_display_handle())opens it
the way the event loop's display reaches it (ADR 0078);OsClipboardis
Clone, its clones handles to one clipboard.- The platform's menu bar shows no accelerator for a chord; on Windows the
chord follows the item's label. - The
planning,lucide,bundled-emoji,pdfandsyntaxfeatures,
which turn on forma's. - The
fluentexample: an app in English and German whose own texts,
forma's phrases and the locale's month and weekday names come from
Fluent resources.
Fixed
- Animations in
runmoved only when something else woke the app - input,
a task, a timer: a window that was animating never seemed due, so
transitions and spinners ran at a few frames a second. The runner asks
the windows first now, and paces animation frames to the display
(pre_present_notify). runasked the desktop portal for the system theme on the main thread
before the first window opened, so a portal that had to start first held
the window back for about 25 seconds; the window waits a quarter of a
second at most now, and a later answer arrives as a switch. The thread
that watched the theme outlivedrun.- The light or dark appearance was taken from any program on the session
bus, through dark-light's watcher; it comes from the portal only now. - On Wayland a paste could get forma's own earlier copy, or old text,
instead of what another app had just copied: the clipboard was X11's,
through Xwayland, which the compositor updates only while an X11 window
has the focus. - A native menu bar that could not be shown on Windows was ignored in
release builds whileFrame::native_menusaid it showed; it is reported
on stderr andnative_menuis false.native_menuwas true in every
window on Windows, where only the main window has the menu bar, and
Frame::set_menu(vec![])left the old menu bar up. - A password field might not be flagged to the input method: what each
window's input method was told was kept by window id and never cleaned,
and Wayland gives a new window the id of one that closed.runkeeps it
with each window, andsync_windowtells the input method on every
frame while a password field has the keyboard.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- forma-ui:
-
released this
2026-10-03 22:23:40 +02:00 | 556 commits to main since this release0.2.1 - unreleased
What a password manager needs: a secret text field, secret clipboard
writes, and the time of the last user input. All additive; nothing changes
behavior unless a field is made withpassword_input.New dependency:
zeroizein forma-ui (MIT/Apache-2.0, no dependencies of
its own), to wipe password text.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
Source code (ZIP)
-
released this
2026-10-03 20:33:21 +02:00 | 557 commits to main since this release0.2.0 - 2026-10-03
0.2 takes forma from one window to a desktop application: several windows,
the platform around them, and forms, data tables, dates, documents and
charts. It breaks the API; the migration guide
shows every break with code before and after. The minimum Rust version
stays 1.90.0.2 was tested by hand on Linux only. It compiles for macOS and Windows,
but the native menu, file dialogs, notifications, window positions and the
system theme have not been run there yet.New optional dependencies, each behind a feature:
pulldown-cmark
(markdown),lyon_tessellation(plot) andjiff(jiff) in
forma-ui;rfd(dialogs),muda(native-menu, on by default, macOS
and Windows only),notify-rust(notifications) anddark-light
(system-theme, on by default, Linux and the BSDs only) in forma-winit.forma-ui
Changed (breaking)
Style::scrolltakes the axes to scroll on:.scroll()becomes
.scroll(Axes::Y)for the old behavior, orAxes::X/Axes::BOTH.
Style::scrollandWidget::scrollareAxesinstead ofbool.Ui::scroll_offset,set_scroll_offsetandmax_scroll_offsettake
and returnVec2instead off32: use.yfor the old value, and
Vec2::new(0.0, y)to set it.- The wheel chains: a nested scroll container at its end hands the wheel to
the one around it, instead of swallowing it. MenuItem::shortcuttakes aShortcutinstead of text:
.shortcut("Ctrl+S")becomes.shortcut(Shortcut::primary(Key::Char('s'))),
shown as "Ctrl+S" or "⌘S" depending on the platform.FindBar::statustakes theUi, to speak its language:
bar.status()becomesbar.status(&ui).DrawCommandhas a newMeshvariant, andDrawCommand::Imagehas two
new fields,uvandfilter: a renderer of its own handles both, and
code building anImagecommand addsuv: Rect::UNIT, filter: ImageFilter::Linearfor the old behavior.- The caret, the arrow keys, Backspace and Delete move by grapheme
cluster instead of by code point. Fontsis a handle whose clones share one set of fonts.
Ui::font_system_mut()andFonts::system_mut()return a guard instead
of&mut FontSystem: pass&mut ui.font_system_mut()to a renderer, and
drop the guard before laying out again.CursorHinthas a newPointervariant (a hand, over links): a backend
of its own maps it, as forma-winit does toCursorIcon::Pointer.Tableshows aTableModeland its selection holds row ids:
TableEvent::SelectedandActivatedcarry the row's id instead of its
index, andselected(),set_selectedandselection()take and
return ids (selected_row()gives the index).Tableis
Table<M = VecModel>;set_rowsstill fills itsVecModel,rows()
is gone (readmodel()), and a header click sorts aVecModelitself,
reportingSortRequestedonly for models that cannot sort.
TableEventis no longerCopy.TreeNodehas a new field,lazy, andTreeRowa new field,busy:
aTreeNode { .. }orTreeRow { .. }literal adds them.- taffy 0.14 instead of 0.12. forma re-exports taffy (
forma::taffy,
forma::prelude,Style::layout), so code using taffy's types through
forma follows taffy's changes:min_sizeandmax_sizeare now
Size<LengthPercentageAuto>. The upgrade fixes layout time that doubled
with every level of nested containers. MenuItem::Action'sshortcutfield is anOption<Shortcut>instead of
anOption<String>, asMenuItem::shortcutabove.Stylehas new public fields (accepts_files,image_fit,invalid):
aStyle { .. }literal adds them, or starts fromStyle::new().icons::ALLholds 12 icons instead of 11 (the newCALENDAR), so its
array type changed.Statsis#[non_exhaustive]: it gains counters, so it is read, not
built.MenuPartshas a private field: it is made by the menu, or with
Default.
Added
- Horizontal and two-axis scrolling, with a horizontal scrollbar. Shift and
the wheel scroll sideways. - A text area that does not wrap scrolls sideways with the wheel and a
scrollbar, and keeps its scroll while unfocused. Tablescrolls sideways when its columns are wider than it, the header
with the rows.VirtualList::with_row_widthmakes rows at least that
wide.- App shortcuts:
Ui::shortcuts_mut()binds aShortcutto an id, scoped
to a widget subtree or global; pressing it reportsEvent::Shortcut(id)
after the widgets had their turn.Actions::on_shortcutmaps it to a
message,Shortcuts::bind_menubinds a menu's shortcuts, and
Shortcut::labelwrites it the platform's way.Modifiers::PRIMARYis
Cmd on macOS and Ctrl elsewhere. - Localization hooks:
Ui::set_localetakes aLocale(separators, date
names and order, 12- or 24-hour time, the language for font fallback),
andUi::set_stringstakes aStringsthat translates forma's own
phrases (Phrase).NumberFieldandMask::Decimaluse the locale's
decimal separator;Locale::format_number,format_groupedand
parse_numberare there for the app. - An invalid state:
Style::invalid(message)/invalid_if, or
Ui::set_invalidafter build. The control is outlined in the danger
color, a screen reader hears it as invalid with the message, and the
message is its tooltip unless it has one. Ui::set_enabledandUi::is_enabled: enable or disable a widget and
its subtree after build.- Triangle meshes with a color per vertex (
DrawCommand::Mesh,
PaintCx::mesh), and image regions with nearest or linear sampling
(PaintCx::image_region,ImageFilter), drawn by forma-wgpu.
Rect::UNITis the whole texture. - Color emoji, from a color emoji font: emoji are shaped in
Fonts::emoji_family(found byload_system_fonts, or set with
set_emoji_family) even where the text's font has a black-and-white
glyph. widgets::SplitPane: fixed and shared panes with draggable, focusable
dividers; collapsible panes come back at their old size
(SplitPaneEvent,SplitPaneState).widgets::SplitButton: a button with a menu of alternatives under an
arrow beside it (SplitEvent::Primary,Chosen(id)).Phrasehas a new
MoreOptions.- Dates and times:
Date,TimeandDateTimeas plain civil values
without time zones (forma::date), with calendar arithmetic
(add_days,add_months,weekday,from_unix_days). Dates run from
the year -9999 to 9999 (Date::MIN,Date::MAX), and the arithmetic
stops there. Withserde, only real dates and times deserialize. The new
jifffeature converts them to and from jiff's civil types and adds
Date::today(). widgets::DatePicker: a date field with a calendar popup (month grid,
today marked, keys for days, weeks, months and years, min/max and a
closure for allowed days), and a range mode with two fields
(DateEvent).widgets::TimePicker: hour, minute and optional second
fields that step and wrap, with AM/PM on a 12-hour clock (TimeEvent).
widgets::DateTimePickerputs the two side by side (DateTimeEvent).- Table models: the
TableModeltrait (row_count,row_id,cell,
optionalsortandrow_of),Cell(text withCellAlignand a
color, orPending), andVecModel, which sorts naturally and filters
by text or a predicate.Table::with_model,update_modeland
set_model; pending rows on screen are asked for with
TableEvent::RowsNeeded(range), once while they stay pending.
compare_naturalsorts the way a person does: empty cells, then numbers
by value, then text with its digits by value.VecModelfinds rows by id
in constant time and refuses two rows with one id. - Image fit modes:
Style::fit(ImageFit::Contain | Cover | None | Fill),
andUi::set_image_size, which also makes an image widget measure to
its picture.widgets::ImageViewerzooms toward the pointer, pans by
dragging, draws pixels sharp above 100 % over a checkerboard, and
reportsViewerEvent::Zoomed. - Charts, with the new
plotfeature (lyon_tessellation; forma-winit has
aplotfeature that turns it on):forma::plot::Plotdraws line,
scatter, area, step and bar series (grouped or stacked) on linear, log
or time axes with locale-written ticks, a legend that toggles series, a
hover readout, wheel zoom, drag pan, box zoom and double-click reset.
set_datatakes live data with a redraw only;decimatethins long
series per pixel column andfillturns an outline into a mesh with a
soft edge.InputCx::modifierstells a custom widget the held keys. - Markdown, with the new
markdownfeature (pulldown-cmark; forma-winit
has amarkdownfeature that turns it on):widgets::MarkdownViewshows
CommonMark with tables, task lists and strikethrough as widgets, with
links, task boxes that edit the source, an image resolver and a code
highlighter hook (MarkdownEvent,MarkdownStyle).
widgets::MarkdownEditorcolors Markdown source, continues lists on
Enter, wraps the selection with Ctrl+B and Ctrl+I, and can show a
preview that follows it in a split pane (EditorEvent). - Lazy trees:
TreeNode::lazynodes reportTreeEvent::ChildrenNeeded
when first expanded and show a busy row untilTreeView::set_children;
children_needed()covers nodes expanded from a saved state. Tree
filters (filter_text,filter) show matches and their ancestors,
opened;sort_byorders every level.Phrase::Loadingnames the busy
row. form::Form<K>: fields kept by the app's own keys, with rules
(Rule::required,min_len,range, and closures withRule::check)
checked on change, on blur or on submit (ValidateOn). It builds each
field with a message line beneath, andvalidatemarks the failing
fields and focuses the first.set_value(ui, ..)andreset(ui)show on
screen at once. Numbers are plain decimals in the locale: no "NaN" or
exponents. New phrasesRequired,TooShort,
NotANumberandOutOfRange.Locale::format_date,parse_dateandformat_time, and a new
Locale::am_pm.Mask::Date(separator)for typed dates, the
icons::CALENDARicon, and new phrases for the pickers (ChooseDate,
PreviousMonth,NextMonth,PreviousYear,NextYear,Hour,
Minute,Second).Toasts::push_messagetakes aToastMessagewith a title and an action
button, reported asToastEvent::Action(id);ToastPartshas a new
actionsfield.- Files dropped from outside the app:
Style::accepts_filesmarks a
target, highlighted while files hover over it;Event::FilesHovering
andEvent::FilesDropped { target, paths }report them, one event for a
whole drop. NewInputEventsFileHovered,FileHoverCancelledand
FileDropped, which forma-winit sends. Table::state/restore(TableState) andTreeView::expanded/
restore_expandedkeep column widths, the sort and the expanded nodes.- Rich text:
Ui::rich_textshows aRichTextbuilt from runs, each with
aRunStyle(family, weight, italic, size, color, underline,
strikethrough, background, link). Links report
Event::LinkActivated, take the keyboard one at a time with Tab, and are
links of their own to screen readers. Ui::record_edit: an app's own change to a text field (a toolbar's
Bold button) as one undo step.FontSystemGuard, the typeUi::font_system_mutreturns, is exported.Ui::copy_text: put text on the clipboard from app code (a "Copy key"
button), reportingEvent::ClipboardFailedon failure like Copy does.Dialog::with_danger(index)draws a destructive answer ("Delete") in the
danger color.Harness::try_find_text(Nonewhen nothing shows the text) and
Harness::find_all_text(every match, in tree order).
Fixed
- A focused widget's own
Style::claimscome before its built-in keys: a
text area that claims Enter getsEvent::Commandinstead of a newline.
An open date picker calendar takes Escape. - The first pointer move onto a custom widget reaches it, and a custom
widget that consumes input is redrawn. - A rebuild that passes a different icon to
Ui::iconnow draws the new
icon; the widget used to keep its first icon for good. - A wrapping label is drawn wrapped at the width its rect got after a
relayout that only a sibling caused. Before, a label next to a button
in a growing column could be drawn on one line, past its box. The same
goes for rich text. - A caption one glyph too wide for its box is no longer shaped again on
every measure at the same width. - Tab (or Enter in a single-line field) into a text area no longer selects
its whole text, where the next keystroke would replace the document. The
caret goes back where it was, or to the end. Single-line fields are still
selected whole. - A menu no longer panics when two inputs for a submenu arrive in one frame
(two quick keys, or a key and a pointer move); keys go to the deepest
menu open by then. - Clicking a
Select's button while its list is open closes the list
instead of opening it again. Toasts::with_duration(Duration::MAX)no longer panics: such toasts stay
until closed.- A wheel delta that is not a finite number is ignored instead of leaving
a text area's scroll broken for good.
forma-wgpu
Added
- Color glyphs drawn from an RGBA atlas page, for color emoji.
- Meshes (
DrawCommand::Mesh) and image regions with nearest or linear
sampling. - One
Rendererdraws into several targets
(add_target,prepare_target,render_target,remove_target),
sharing its glyph atlas and images;WindowSurfaceis one window's
surface drawn through a shared renderer.SurfaceRendererkeeps its API.
Gpu::for_surfaceacquires a GPU for a surface, blocking.
Fixed
- A translucent polyline is blended once where its segments meet, instead
of darker at every join. - A glyph, emoji or icon wider than the glyph atlas no longer panics in
wgpu: the atlas grows to fit it, and what does not fit even the largest
atlas (8192 px) is skipped, with a debug assertion. Gpu::requestasks for the adapter's largest texture size instead of
wgpu's default 8192, so larger images can be registered, and window
surfaces are clamped to the device's limit instead of panicking.update_image_rgbaafterupdate_imageon the same image shows the new
pixels.
forma-winit
Changed (breaking)
App::buildtakes the window it builds:
fn build(&mut self, ui: &mut Ui)becomes
fn build(&mut self, _window: WindowKey, ui: &mut Ui).Frame::exit
closes every window.Settingshas new fields:system_fonts(on by default:runloads
the machine's fonts),exit_when,theme_mode,stateandmenu. A
Settings { .. }literal without..Settings::new(..)adds them.
Added
- Several windows in
run:Frame::open_window(WindowSettings)returns
aWindowKeyand opens the window after the callback;
Frame::close_window,Frame::window_key,App::window_openedand
App::window_closed(window, state), which hears about every window that
goes, on every path, with its finalWindowState. The windows share the GPU, renderer, images and
fonts; screen readers see each.Settings::exit_when(ExitWhen) ends
runwith the last window (the default) or with the main one. - Native menus on macOS and Windows, with forma-winit's new default
native-menufeature (muda):Settings::menubecomes the platform's
menu bar - on macOS with an app menu (About, Hide, Quit) and a Window
menu - andApp::menu_chosen(frame, id)reports choices.
Frame::native_menusays whether it is shown (not on Linux, which keeps
the in-windowMenuBar);Frame::set_menureplaces it.
App::quit_requesteddecides about the Quit of that app menu and its
Cmd+Q. Quit in the Dock, logging out, and Cmd+Q in an app without
Settings::menucannot be refused; the windows still get
window_closed, andApp::exitingruns. - System notifications, with the new
notificationsfeature
(notify-rust):notify(&Notification).Frame::request_attention
(Attention) flashes a window's taskbar entry or bounces its dock icon. - Native file dialogs, with forma-winit's new
dialogsfeature (rfd):
Frame::file_dialog(FileDialog)shows an open, open-many, save or folder
dialog over the window without blocking it, and
App::dialog_closed(frame, DialogId, paths)hands back what was picked. - Dropped files are passed on to forma (not on Wayland, where winit has
no drag and drop). - Windows open where they were:
Frame::window_stateandwindow_closed
give aWindowState(the last normal size and position, and whether it
was maximized or fullscreen), andSettings::restore/
WindowSettings::restoreopen a window that way again. A position is
restored with the scale of its screen, and dropped unless enough of the
window's top edge lands on a screen; an unusable size falls back to the
default.WindowSettingshas
maximizedandfullscreen. The newserdefeature serializes
WindowStateand turns on forma's. runfollows the system's light or dark appearance:
App::theme_changed(frame, SystemTheme)runs when a window opens and
when the system switches, and appliesTheme::light()ordark()by
default.Settings::theme_mode(ThemeMode) can fix it instead. On
Linux and the BSDs the new defaultsystem-themefeature asks the
desktop portal (dark-light); "no preference", GNOME's default, counts
as light.App::close_requesteddecides whether a window the user closes closes
(CloseResponse::Close, the default) or stays open (Keep), to ask
first;App::exitingruns once as the app ends, after every window got
window_closed.markdownandplotfeatures, which turn on forma's.forma_winit::winitre-exports winit, so matching window events in
App::unhandledneeds no direct winit dependency of the same version.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
released this
2026-10-02 21:25:14 +02:00 | 601 commits to main since this releaseforma-winit
Added
Frame::waker,WakerandClosed: wake an app driven byrunfrom
another thread, so a worker's results reach the UI without input or
polling.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
released this
2026-10-02 19:51:39 +02:00 | 604 commits to main since this releaseforma-ui
- A retained widget tree with
WidgetIdhandles, editable in place
(remove,move_widget,set_style) or rebuilt from app state with
Ui::rebuild, which matches widgets by key or position and keeps their
state;key(..).memo(..)skips unchanged subtrees. - Layout with taffy (flexbox and grid) through a
Stylebuilder; lengths
are written at 1x and scaled to the display. - Text with cosmic-text: font registration, families and fallback, bundled
DejaVu fonts; text fields and areas with selection, undo and redo, input
masks, placeholders, IME composition, and code-editor features (gutter,
highlighting spans, decorations, find and replace, completion list, edit
profiles). Editing a long text reshapes only the changed lines. - Input in forma's own vocabulary, with a rebindable
Keymap, a
Clipboardtrait, keyboard focus on every control, popups and modal
dialogs. - Events,
Actionsfor turning them into app messages, andComponent
andCustomWidgetfor app-defined widgets. - Ready-made widgets: menus and context menus, menu bar, select, combo box,
dialog, radio group, number field, range slider, collapsible section,
toasts, progress and busy indicators, virtual list, tree view, table,
docking, tab bar, color picker. - Themes (palette, metrics, text defaults) with dark and light built in.
- Transitions, tweens and springs on a UI clock; the UI says when it needs
a frame, so an idle app does no work. - Accessibility through AccessKit (the
accesskitfeature): roles, names,
states, text with caret and selection, actions from assistive technology. forma::testing::Harnessand snapshot helpers for headless tests.
forma-wgpu
SurfaceRendererfor a window, andRendererfor drawing into an app's
own render pass on its device.- Rounded rectangles, borders, shadows, lines, a growing glyph atlas, SVG
icons, images by handle, clipping.
forma-winit
- Input translation from winit, cursor icons, input method placement, the
OS clipboard and AccessKit. run: a whole app from theApptrait, drawing only when needed.
Known limitations
See the README.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- A retained widget tree with