-
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
-