• v0.3.1 9c7152f95b

    v0.3.1
    All checks were successful
    ci / check (push) Successful in 58m17s
    ci / cross (push) Successful in 8m55s
    Stable

    gntx released this 2026-10-06 09:07:43 +02:00 | 1 commits to main since this release

    0.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 new graph feature. 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, and rustix (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 reports Event::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_text and find_all_text match a
      RichText widget too, so a test with a label and a rich text showing
      the same text now finds two: find_text and try_find_text panic with
      "2 widgets show ...". Tell them apart with find_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::count is the same label, inside the
      field now.

    • Enter in a single-line text input now reports Event::Entered(id) just
      before its Event::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_shortcuts replaces the bindings of the set's ids that
      are not scoped - its own, everywhere or through modals, and an app's
      Shortcuts::bind on those ids - and keeps an app's Shortcuts::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::unbind still removes every binding of an id,
      scoped ones included, as it now says.

    • Toolbar::with_style is 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 as Style::new() has it stays
      the toolbar's (with_plain leaves 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, and PaletteParts::rows still 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
        ListBox node then.
      • Wheel scrolling past the rows built is reported as
        PaletteEvent::Updated from Component::poll, which
        Mounted::handle_all (and Frame::offer) call; an app that calls
        only handle for each event shows unbuilt rows after a scroll until
        it rebuilds for another reason.
      • matches() and highlighted_action() are empty while the palette is
        closed; they were the last search's.
    • Mounted::build_keyed's documentation, and the guide, name the command
      palette, the chooser and completion beside Popover as 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
      new Palette::series instead 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 as Ui::focus does; 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(): what Ui::announce was asked to say since the
      last call, each with its Politeness, with or without the accesskit
      feature. A Ui taken back with Harness::into_ui keeps no record.

    • Harness::text reads a RichText widget's text (and the finders match
      it, see Changed).

    • ClaimIf is in forma::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_text does: 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 the Ui (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
      Popover keys its card itself). Two pub(crate) helpers in
      ui/rebuild.rs (key_pending, set_key_giver) and a field of Ui
      serve the checks.

    • NumberField::integer(value, min, max): whole numbers, stepping by 1,
      no decimals; the value stays an f64. Panics beyond 2^53.

    • NumberField::with_label_width(width), so the fields of a column line
      up, and NumberField::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 a Style, because a Style
      is not Send and NumberField is (and must stay so).

    • Event::Entered(id): Enter was pressed in a single-line text input. It
      comes just before the Submitted of 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 no Entered. Actions::on_enter(id, |text| ..)
      builds on it.

    • TagInput grows:

      • with_normalize(|tag| ..) makes each tag the user adds what the
        function returns (None refuses it); duplicates are compared after
        normalizing, and suggestions are offered as they normalize. The tags of
        new and set_tags are 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) and clear_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 as TagEvent::Updated after 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 next handle_all reports SearchEvent::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) makes ActionSet::bind_shortcuts bind an
      action's shortcut that way.

    • ActionSet::sync_shortcuts(&mut Shortcuts): binds as bind_shortcuts
      does, but only when the set changed since it was bound there (the
      Shortcuts remember the revision per set, by its first id), so it can be
      called in every build - where a test's Harness, 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), as Action::label_with_shortcut.

    • Action and Entry are re-exported at the crate root (forma::Action,
      forma::Entry).

    • Entry::Spacer: free space that pushes the entries after it to the end
      of a Toolbar. 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) and Emphasis (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) and Tone (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, SheetEvent and SheetParts: 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: unbind and bind_in beside an action set, binding in a
      test with sync_shortcuts in build, shortcuts through modals, the
      palette's descriptions, spacers and emphasis on toolbars, tones and
      fill in the status bar, and a section on Sheet.

    • Clipboard for Arc<Mutex<C>> of any clipboard C: a test keeps one
      handle and gives the other to a Ui, a Harness or 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) with detail(..),
      shown muted after the title, and icon(..); 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 }, rows pairing 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, as Ui::popup takes 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.
    • CommandPalette::with_placeholder and with_empty_text replace "Type a
      command" and "No matching commands" for one palette.
      PaletteParts::list is 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); range covers the trigger and the query, in
      bytes, for the app to replace, in one undo step with Ui::record_edit
      around Ui::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, unless with_empty_text gives
        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> and Completion<K> are Send for a Send key, checked in
      send.rs.

    • MarkdownEditor::with_monospace(bool): the source stays monospace by
      default; false shows 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 takes with_style; the area style is
      not used then.

    • MarkdownEditor::with_preview_view(view) shows the app's own
      MarkdownView as the preview - with its highlighter, images, look,
      hard breaks and style - in the split with_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
      as MarkdownEditorEvent::Changed by the editor's next poll (the next
      Mounted::handle_all), as typing is; typing in the same round makes it
      one Changed, not two. The range is in bytes and taken
      as Ui::replace_range takes 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 the Ui. set_text keeps 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,
      so TaskToggled and scroll_to_source work as before. An editor's
      preview gets it through with_preview_view.
      MarkdownView::set_hard_breaks(bool) changes it on a view held
      elsewhere, as through MarkdownEditor::preview_mut.

    • With the syntax feature, syntax::Highlighter::highlight_str(language, code, background) -> Vec<(Range<usize>, Color)> colors a string: the
      grammar by name or extension, as Highlighter::new takes it, and the
      theme picked by background as update picks it by the UI's (dark or
      light), every color held to the same contrast against background. An
      unknown or empty language colors nothing.

    • syntax::Highlighter::for_markdown(&theme) returns the closure
      MarkdownView::with_highlighter takes, coloring each code block by the
      language after its fence (up to a comma, as in rust,ignore) for a view
      in theme, 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; None for a removed widget.

    • Harness::find_tooltip(text), try_find_tooltip and find_all_tooltip:
      the widgets whose tooltip says text (each one's own tooltip, or else
      why its value is invalid). With the accesskit feature, Harness::find_role(role),
      try_find_role and find_all_role: the widgets assistive technology
      knows as that accesskit::Role, as their style names them or as forma
      names their kind. Both panic as find_text does.

    • Ui::command_modifiers(scope, command): the modifier keys held when a
      claimed command was routed to scope, kept while the app handles the
      events it came with, so a widget that claims Enter tells Shift+Enter
      (which the keymap turns into Command::Enter) from Enter, even when
      Shift was let go before the events were read. A command run_command
      ran comes with none. Ui::command_modifiers_each gives 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 its TreeRowState (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 with mask (the app's own draggable
      widgets, or another tree's rows), highlighted while hovered, and report
      a drop as TreeEvent::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 over with_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
      its PointerPressed and 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() and LayoutCx::reduced_motion(): a custom widget that
      moves by itself works out where it has got to in layout, on the UI's
      clock (which a test's Harness::advance moves), and shows where it
      would come to rest when the user asked for less motion.

    • ForceGraph<K>, behind the new graph feature (forma-winit's graph
      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), and set_data(ui, &parts, nodes, edges), which keeps the
        nodes it had where they were.
      • Layout: ForceLayout (repulsion, link_distance, link_strength,
        gravity, cooling; Default and with_* 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 and 0
        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 with with_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 series colors 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.
    • ClipboardErrorKind::Stale and ClipboardStale (ClipboardStale::new(),
      non-exhaustive), the error it is told by: a copy asked about after a
      newer one took the clipboard's place. ClipboardErrorKind::of returns it
      for a ClipboardStale.

    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, Popover and the new Sheet give the
      focus back with Ui::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. Select and SplitButton do the same, so a pick
      with the pointer draws no ring on the button.

    • CommandPalette closed 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 TagInput chip 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_style was 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-settings feature on Linux and the BSDs, run waits for
      the desktop portal's first answer before App::launched rather than as
      the first window opens - the same quarter of a second at most from the
      start - so Context::system_theme() and Context::system_settings()
      say the system's in launched already, 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's set_text and set_secret_text fail 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::Failed saying 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. Only run
      tells the clipboard the focus; with an event loop of the app's own,
      nothing is refused.

    • TrayError::BadIcon for 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:
      PreparePrint shows the desktop's dialog, and Print prints 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 hears PrintOutcome::Shown, with the reason on
      stderr. A desktop that does not allow printing (PreparePrint refused
      with NotAllowed, as under a lockdown, or AccessDenied) is
      PrintError::Service saying "the desktop does not allow printing", and
      no viewer opens. A print that fails after the dialog is
      PrintError::Service saying "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 what App::resized asks 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
      offers x-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 through OsClipboard on the UI thread wait so,
      also a window's Copy.

    Added

    • Context::clipboard(): a handle to the OS clipboard every window's
      Ui has, 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 the clipboard feature.

    • Context::with_clipboard(OsClipboard) and
      OsClipboard::from_clipboard(clipboard), to hand a detached context a
      clipboard of the test's, such as an Arc<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.

    • OsClipboard is Debug.

    • Context::system_theme(): the system's light or dark appearance
      without a window - the desktop portal's on Linux and the BSDs (the
      system-settings feature), known in App::launched already; elsewhere
      what a window last reported, None until one opened.

    • The svg-icons feature, which tray turns on: IconExt::rasterize
      (icon.rasterize(size, color)) draws a forma Icon into an RgbaImage
      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 new IconError (InvalidSvg, BadSize).

    • WindowSettings::always_on_top(bool) and its field: a window that stays
      above the others (winit's WindowLevel::AlwaysOnTop). Wayland ignores
      it.

    • WindowSettings::app_id and its setter: the desktop id of one window,
      overriding Settings::app_id for 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 (after theme_changed, before start,
      window_opened and its first build) and whenever it changes after
      (before the next update), 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_pdf with the print
      dialog over the frame's window - on X11 the portal is given the window
      as x11:<id>; on Wayland the dialog still belongs to no window. It
      shadows Context::print_pdf for calls through a frame, with the same
      signature; Context::print_pdf passes no window, as before.

    • Watching the clipboard, with the clipboard and run features:
      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. A ClipboardChange says
      which types the copy offers (formats, in the order offered), whether
      another app marked it secret (secret, the x-kde-passwordManagerHint
      type), whether this app made it (own) and which clipboard changed
      (selection, ClipboardSelection::Clipboard or
      ClipboardSelection::Primary). Its offer (ClipboardOffer::text,
      html, image) hands out what was read of the copy as it came: the
      formats the ClipboardWatch names (ClipboardFormat), up to its size
      limit (ClipboardTooLarge beyond it;
      ClipboardWatch::DEFAULT_SIZE_LIMIT is 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 Windows watch_clipboard
      fails with ClipboardErrorKind::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.

    • MemoryClipboardWatch and Context::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 at Context::dispatch.

    • On Linux and the BSDs the clipboard feature depends on the image
      crate (0.25, PNG only, as arboard has it there) and on rustix (1.1,
      the version wl-clipboard-rs has, for poll); both were built already.
      x11rb's xfixes feature 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::clear over 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
      PreparePrint and refused the Print forma sent alone, so nothing
      printed.
    • OsClipboard::get_text over 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 - every text/*
      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 answers None
      where none is offered. Over data-control a byte order mark before UTF-8
      text is dropped. A watched copy's ClipboardOffer::text takes the
      plain-text types in other spellings too.
    Downloads