• 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
  • v0.3.0 6c1aa36389

    v0.3.0
    All checks were successful
    ci / check (push) Successful in 57m42s
    ci / cross (push) Successful in 9m15s
    Stable

    gntx released this 2026-10-05 13:15:35 +02:00 | 127 commits to main since this release

    0.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 license field 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 was MIT OR Apache-2.0, which did not cover
    the fonts, and the package carries DejaVu's license text now. An app with
    bundled-emoji credits 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-bidi and web-time, in the tree before, are direct
      dependencies now. pdf: krilla 0.7 and the crates it brings, among
      them zlib-rs (Zlib) and yoke, yoke-derive, zerofrom and
      zerofrom-derive (Unicode-3.0). syntax: syntect 5.3, with
      fancy-regex, bincode 1.3 (MIT; no longer maintained upstream) and
      aho-corasick (Unlicense or MIT). lucide and bundled-emoji add no
      crates: they carry Lucide's icons (ISC) and the Twemoji font (CC-BY-4.0).
    • forma-wgpu: screenshot uses png, and swash is a direct dependency;
      both were in the tree already.
    • forma-winit: on Linux and the BSDs the default clipboard brings
      wl-clipboard-rs and smithay-clipboard, with foldhash (Zlib) among
      theirs. settings-file: dirs 7 and serde_json, with option-ext
      (MPL-2.0). open and print: opener 0.9, which on Linux and Windows
      brings url, idna and eighteen ICU4X crates (Unicode-3.0). tray:
      ksni (Unlicense) on Linux and the BSDs, tray-icon on macOS and
      Windows. hotkeys: global-hotkey on macOS and Windows. The system
      settings, notifications, recent documents, single instance and run use
      zbus, x11rb, roxmltree, objc2 and windows-sys, all in the tree
      already. dark-light and winreg are gone, and notify-rust is 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; and run in 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, Metrics and TextStyle are #[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. Style has new fields - folding,
      pasted_tabs, takes_typing, layout_transition, enter, leave,
      children_enter, children_leave, window_area, gestures,
      touch_drag, keep_focus - and Metrics two, state_outline and
      selection_bar (0 in the standard themes; with serde, a theme saved by
      0.2 still reads).
    • Style::corner_radii is an Option<[f32; 4]>: None, the default,
      leaves the corners to the widget, and Some([0.0; 4]) is square. The
      builders corner_radius, round_top and corner_radii are unchanged.
    • Shortcut's fields are private, since a shortcut can be a chord of two
      keys: make one with Shortcut::new, primary, parse or then, and
      read it with first_key, first_modifiers and second. A character is
      kept lowercase, so Key::Char('S') is Ctrl+S; written out uppercase, it
      showed as "Ctrl+S" and never fired.
    • Shift no longer counts in a Shortcut on a digit or symbol, which a
      layout types with or without Shift: Shortcut::new drops 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 Table claims 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::Action has a new field, icon, and it and
      MenuItem::Submenu are #[non_exhaustive]: build menu items with
      MenuItem::action, submenu and their builders, and match the variants
      with ...
    • plot::Series is #[non_exhaustive] and has new fields, boxes and
      axis: make one with Series::new (or line, bar, histogram,
      boxes) and color.
    • WidgetKind::TextInput holds a FieldText instead of a String: an
      edited field's text is kept in a rope. FieldText derefs to str and
      compares with str and String.
    • TableState is #[non_exhaustive] and keeps the column order, the
      hidden and frozen columns and the grouping (order, hidden, frozen,
      grouping): make one with TableState::default(). With serde, a state
      saved by 0.2 still reads.
    • Every component's parts (each *Parts, and MenuLevel, 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's card, DateTimeParts::row, DialogParts::scrim,
      DockParts::area, MarkdownParts::view, MenuBarParts::bar,
      NumberParts::row, RadioParts::group, the split button's row,
      SplitPaneParts::container, TableParts::table, TimeParts::row,
      ToastParts::stack and VirtualParts::view are each root now.
      TableParts has new fields, and its headers and handles are in the
      order the columns are shown, without hidden ones (columns says which
      column each heads).
    • form::Rule::check takes a closure that is Send + Sync, so rules no
      longer keep a Form, or a component that checks text by them, from
      being Send.
    • Ui::copy_text returns the clipboard's error, Result<(), ClipboardError>, instead of reporting it as Event::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: ClipboardErrorKind beside message.
    • Ui::run_command goes where the command's key would go: to the topmost
      popup or the focused widget that claims it (an Event::Command for 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 one run_command(Command::Copy)
      for a text field, a Table or an ImageViewer. The events it gives ask
      for the next frame at once.
    • A press on a MenuBar or a Menu (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
      reported Event::Submitted.
    • CursorHint has two new variants, ResizeDiagonalDown and
      ResizeDiagonalUp, for the corners of a window's resize border: a
      backend of its own maps them.
    • Ui::set_locale panics for a Locale whose grouping is its decimal
      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's widgets::CalendarParts is
      DateCalendarParts, Markdown's EditorEvent and EditorParts are
      MarkdownEditorEvent and MarkdownEditorParts, ViewerEvent and
      ViewerParts are ImageViewerEvent and ImageViewerParts, SplitEvent
      and SplitParts are SplitButtonEvent and SplitButtonParts, and
      RangeEvent and RangeParts are RangeSliderEvent and
      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_of still gives the whole text - for an edited
      field a copy, made on the first call after each edit - and
      Ui::field_text reads 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 and Ui::ime_cursor_area sit 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 as 36 London Ada, not
      36LondonAda. A mask checks the text as it is then: 12, a tab and 34
      pasted into a Mask::Digits field is refused, where it gave 1234.
      Style::pasted_tabs(PastedTabs::Drop) drops them as before.

    Added

    • ActionSet<A> (forma::action_set, with Action and Entry): 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. menu and menus build menu items from a layout of Entrys,
      bind_shortcuts binds their shortcuts (switched off for disabled
      actions), and chosen and triggered turn menu choices and shortcut
      events back into the app's ids. Setters, revision, and cheap copies.
    • CommandPalette<A> (widgets::palette): every action of an ActionSet,
      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,
      reporting PaletteEvent::Chosen with the keyboard back where it was.
    • Toolbar<A> (widgets::toolbar): buttons for a layout of Entrys -
      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. Reports ToolbarEvent::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 a StatusProgress between 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_undo and next_undo for a menu, and
      mark_saved and is_modified for unsaved changes.
    • MenuItem::icon and MenuItem::is_enabled; Menu::set_items and
      MenuBar::set_menus (and MenuBar::menus) show new items in place,
      keeping an open menu open.
    • Chords (ADR 0070): Shortcut::then makes a shortcut of two keys (Ctrl+K
      Ctrl+S), with is_chord, first and second; 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_chord and Ui::cancel_chord.
    • A shortcut's text form: Display writes "Ctrl+K Ctrl+S" on every
      platform, and Shortcut::parse and FromStr read it back
      (ParseShortcutError); with serde a shortcut is saved as that text.
      Shortcuts::bindings lists the bindings, and Shortcuts::set_enabled
      and is_enabled switch 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; KeyBindings and KeyBinding keep, save
      (serde) and apply what the user changed. Ui::set_key_capture and
      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, reporting SearchEvent::Changed on each edit and
      SearchEvent::SearchRequested once typing pauses (with_delay, 300 ms),
      on Enter and on clearing; set_result_count shows 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) and Revealed;
      with_mask for a PIN, with_label.
    • MaskedInput and MaskPattern (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's Rules (with_rules); keys over the chips;
      TagEvent::Changed and Updated.
    • 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 optional Form that Next checks, Back, Next, Cancel and
      Finish, pages skipped by the app's data; WizardEvent::PageChanged,
      Finished, Cancelled and Updated.
    • 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),
      and with_modal keeps 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 - a Placement of a Side, an alignment along it
      and a gap - flipped and shifted to stay in the window, with an optional
      arrow; Ui::popup_side. Side::Start and End follow 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 a WindowRequest (Drag, Resize(ResizeEdge),
      Minimize, ToggleMaximize, Close, ShowMenu), queued by
      Ui::request_window and 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_border a band along the window's edges;
      Ui::set_window_maximized and window_maximized.
    • widgets::TitleBar: the title, a row for the app's widgets and the
      window's buttons where the platform has them (ButtonLayout::platform,
      parse for GNOME's button-layout), drawn as macOS's traffic lights or
      as flat buttons; it reports the WindowButton pressed, and gives way
      without overlapping as the window narrows.
    • Touch, pens and gestures (ADR 0069): InputEvent::Touch (a Touch with
      a TouchPhase and a force), and Pinch and Rotate from 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 with Style::touch_drag(). Style::gestures(): two fingers, or a
      pinch or turn over the widget, report Event::Gesture (and
      WidgetInput::Gesture to 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_caret and
      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::Fold and Unfold (Ctrl+Shift+[ and
      ], Cmd+Alt+[ and ] on macOS); Ui::set_fold_ranges,
      clear_fold_ranges, fold, unfold, fold_all, unfold_all and
      folded_lines.
    • Syntax highlighting, behind the new syntax feature:
      forma::syntax::Highlighter colors 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_spans reads a widget's color spans.
    • Spelling marks: Ui::set_misspellings marks the words an app's checker
      found with a squiggle (Misspelling), and the marks follow the text;
      Ui::misspelling_at_point, Misspelling::menu_items and
      Ui::correct_misspelling offer a right click's suggestions;
      misspellings, misspelling_at and clear_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::LeftToRight or
      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_locale with 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), and Ui::field_text.
    • Style::pasted_tabs(PastedTabs): what a text input does with the tabs
      in text put into it - pasted, inserted with Ui::insert_str or
      replace_range, committed by an input method, or in a text area the
      indentation the Tab key inserts: PastedTabs::Keep, Space or Drop.
      Without it a text area and a password field keep them, and another
      single-line field turns each into a space.
    • Theme::high_contrast_dark and high_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_outline draws
      hover, press, focus and invalid states in shape, Metrics::selection_bar
      marks a selected row with a bar at its start, and
      Metrics::HIGH_CONTRAST holds those themes' metrics. Palette
      implements Default.
    • Ui::announce(text, Politeness): screen readers say text, after what
      they are saying or at once.
    • The lucide feature: forma::lucide, Lucide 1.52.0's 1866 icons as
      Icon constants (lucide::ARROW_LEFT), with by_name, ALL and
      VERSION; an app carries only the icons it uses.
    • The bundled-emoji feature: Twemoji in Mozilla's COLR build (about
      1.5 MB) as the emoji family of every Fonts::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_enter and children_leave fade or
      slide a widget in as a build adds it and out as a rebuild or Ui::remove
      drops it (anim::Presence). Ui::draw_offset and draw_opacity.
      Ui::set_reduced_motion and reduced_motion make every change
      immediate.
    • Editable cells: TableModel::edit_text says whether a cell can be edited
      and with what text, and TableModel::set_cell takes 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) and VecModel::set_text.
    • Table::set_rules and with_rules: what is typed into a column is
      checked by form::Rules, the message shown beside the cell;
      form::first_error checks 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() and Event::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_order and move_column
      (TableEvent::ColumnsChanged); with_movable_columns (headers dragged,
      or Ctrl+Shift and an arrow); with_column_chooser, set_column_visible,
      is_column_visible and visible_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, groupable and total; group rows
      open and close (TableEvent::Toggled, ChildrenNeeded,
      GroupRequested) and a row of totals sits under the rows.
      Table::group_by, set_grouping, grouping and set_expanded.
      VecModel groups, totals columns with an Aggregate (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_text and
      Table::paste for menus. Table::to_csv and write_csv.
    • Tree tables: a TreeModel of TreeItems (leaf, branch, lazy),
      shown by a Table with its header, sorting, editing and copying, the
      first column indented by depth; RowKind::Node, RowKind::Busy and
      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 and set_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 a ColorScale
      (viridis, blues, diverging), with a color bar, labels, a readout
      and HeatmapEvent::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, vband and point;
      Plot::set_annotations).
    • Chart export (ADR 0049): picture(&ui, size, scale) on Plot,
      PieChart, Heatmap and Sparkline draws the chart on its own as a
      plot::Picture, which writes SVG (to_svg) or is drawn to PNG by
      forma-wgpu's export; Plot::as_shown keeps the view the user zoomed
      to. Each chart's table_model gives its data as rows.
    • The planning feature, forma::planning: a KanbanBoard of Lanes of
      Cards, moved by dragging or from the keyboard and reported as
      KanbanEvent::Moved(CardMove) and LaneMoved(LaneMove) for the app to
      apply, with work-in-progress limits; a Calendar with month, week, day
      and agenda views of CalendarEntrys at a CalendarSpan, moved and
      resized by dragging or from the keyboard (CalendarEvent); a Timeline
      of TimelineRows of TimelineBars on a date axis, a Gantt chart with
      milestones and Dependency arrows, 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: Clipboard gains get_image,
      set_image, get_html and set_html, whose defaults fail with
      ClipboardUnsupported, so a clipboard of text only keeps compiling.
      Ui::copy_image, copy_html, clipboard_text, clipboard_image and
      clipboard_html return the clipboard's error; ClipboardErrorKind
      (Unsupported, Failed) and ClipboardErrorKind::of(&error).
      MemoryClipboard holds 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 (and set_pixels, pixels): the viewer
      copies its picture with Copy (ImageViewerEvent::Copied), and
      ImageViewer::copy for a menu. ImageViewer::set_center.
    • PDF files, behind the new pdf feature (ADR 0065): a pdf::Document
      written page by page, each with a PageSetup (PageSize, Margins).
      ui_page lays a Ui out on a page, page draws pieces of a Ui, a
      draw list or a chart, ui_pages flows a long Ui over pages, breaking
      between lines and rows, ui_pages_with adds a header or footer with its
      PageNumber, and picture_page puts a chart on a page. Text stays text,
      in subsets of the fonts forma shaped it with; shapes, icons and clips are
      vector. PdfError says 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's Instant, which is the standard library's on every target
      but the web, where std::time::Instant::now() panics.
    • New phrases (Phrase) for the new widgets: the table's Columns,
      HideColumn, FreezeColumns, UnfreezeColumns, GroupBy, Ungroup,
      GroupRow, EmptyValue and Total; the command palette's
      SearchCommands and NoMatchingCommands; the search field's
      SearchPlaceholder, ClearSearch and SearchResults; the password
      field's ShowPassword and HidePassword; the tag input's RemoveTag;
      the breadcrumb's Breadcrumb and HiddenItems; the wizard's Back,
      Next, Finish, Cancel, WizardStep and Completed; the title
      bar's Minimize, Maximize and Restore; chords' and the key binding
      editor's ChordPending, FilterShortcuts, CommandColumn,
      ShortcutColumn, NoShortcut, PressShortcut, PressSecondKey,
      ChangeShortcut, RemoveShortcut, ResetShortcut and
      ShortcutConflict; the kanban board's Board, LaneCards,
      LaneOverLimit, CardPickedUp, CardAt, CardDropped,
      CardMoveCancelled and LaneMoved; the calendar's Calendar,
      PreviousPeriod, NextPeriod, Today, MonthView, WeekView,
      DayView, AgendaView, AllDay, MoreEntries, NoEntries,
      MonthTitle, DayTitle, FromTo, EntryPickedUp, EntryAt,
      EntryMoved, EntryResized and EntryMoveCancelled; the timeline's
      Timeline, ZoomIn, ZoomOut, Quarter, BarPickedUp, BarAt,
      BarMoved, BarResized and BarMoveCancelled.

    Fixed

    • A Table or TreeView event 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, reported Event::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, and TreeView::set_children panicked for a node a
      set_roots removed 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) and Ui::replace_lines dropped the tabs
      of the indentation they carried.
    • Clicking the open title of a MenuBar, or a DatePicker's calendar
      button while the calendar was open, closed it and opened it again at
      once; it closes now. Escape did not close a Select's or a
      SplitButton's menu while the pointer rested on its button.
    • A range DatePicker reported RangeChanged(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 TreeView filter, 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.
    • Table asked for a row with TableEvent::RowsNeeded only when its first
      cell was Cell::Pending; a row pending in any column is asked for now.
    • Locale::format_date wrote a year before 1 as "-005", which
      parse_date could not read, and parse_number did 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_bar whose 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_center label, 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::build were
      dropped.
    • Screen readers heard a right-aligned or centered table cell without a
      name, and a busy_bar not as a progress indicator.
    • A plot's readout of the values under the pointer was worked out only as
      the pointer moved, so after set_data, a series hidden from the legend
      or a box zoom it showed what was there before. It and
      Plot::table_model wrote 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.
    • TreeView worked out its whole row list again for every event it was
      offered, and Ui::remove and a rebuild's removals took time in the
      square of how much they removed.

    forma-wgpu

    Changed (breaking)

    • The variants of ImageError with fields (Empty, WrongLength,
      TooLarge) are #[non_exhaustive]: a pattern naming their fields ends
      in ...
    • A lost surface is reported as FrameStatus::SurfaceLost instead of
      Retry: make a new surface for the window and hand it to
      WindowSurface::replace_surface or SurfaceRenderer::replace_surface.
      It was configured again, which fails (wgpu's default error handler
      panics) or left every frame retrying a dead surface; resize leaves it
      alone now.
    • Renderer::remove_target on the main target resets it - its buffers,
      its draws and the atlas pages they held are freed, and prepare and
      render still draw into it - and returns true, 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 screenshot feature (ADR 0042):
      screenshot::Offscreen renders a Ui, a draw list (render_list) or a
      Harness (capture) into a texture the way a window draws it, and reads
      it back as a forma::RgbaImage; compare checks it against another
      within a Tolerance (Mismatch); References keeps reference PNGs,
      writes <name>.new.png and <name>.diff.png when a check fails, and
      updates them with FORMA_UPDATE_SCREENSHOTS=1; read_png and
      write_png.
    • The export feature: export::render and export::png draw a chart's
      forma::plot::Picture offscreen, and export::encode_png encodes any
      RgbaImage; a picture larger than the GPU's textures is
      ExportError::TooLarge. It turns on screenshot and forma's plot.
    • Large images (ADR 0066): LargeImage::new(image) keeps an RgbaImage
      with smaller copies, made once on any thread, and
      Renderer::register_large_image registers it under an ImageHandle like
      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, with WindowSurface::replace_surface and
      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_target refused.
    • 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 from data: 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)

    • Settings and WindowSettings are #[non_exhaustive]: built with new
      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, waker and set_menu
      moved to the new Context, which Frame derefs to: calls through a
      frame compile as they were, and a path names Context.
    • The free function notify is gone: notifications are shown with
      Context::notify, which does not block and gets answers. The
      notifications feature needs run, and turns it on. NotifyError is an
      enum (Unsupported, Service) instead of a struct, and Notification
      is #[non_exhaustive], with public fields to read.
    • The light or dark appearance on Linux and the BSDs comes from the
      system-settings client, which reads the portal's color-scheme with
      the other settings, on one connection: system-theme is now the same
      feature as system-settings, so a 0.2 feature list still builds. With
      it, as with the default features, the default App::theme_changed
      applies Theme::high_contrast_light() or high_contrast_dark() while
      the system asks for high contrast, and the new
      App::system_settings_changed applies the system's reduced motion to
      each window's Ui.

    Changed

    • run keeps one OS clipboard for its event loop, opened for its display,
      and gives every window's Ui a 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-clipboard is 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_menu changes 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::ExitCalled keeps the app
      running after its last window closes, Settings::open_main(false)
      starts it without the main window, and Context::open_main_window opens
      that again where it was (App::window_opened runs for it). On macOS a
      choice from the menu bar with no window open opens the main window.
      Context::detached and Context::dispatch test 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, and Settings::icon and WindowSettings::icon, a
      WindowIcon made from a forma::RgbaImage (WindowIcon::from_image),
      shown on X11 and Windows.
    • Notifications with actions (ADR 0046): Notification::action and
      timeout (NotificationTimeout); Context::notify returns a
      NotificationId, Context::replace and withdraw change it, and the
      answer comes in App::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.
      MemoryNotifications for tests.
    • The system tray (ADR 0048), the tray feature: Context::set_tray(Tray)
      shows an icon (TrayIcon::from_icon, from_image) with a tooltip and a
      menu of MenuItems, changed in place when called again; choices arrive
      in App::tray_chosen, clicks on the icon in App::tray_activated, a
      tray that went away in App::tray_failed. Context::supports_tray,
      remove_tray and tray; TrayError. A StatusNotifierItem on Linux and
      the BSDs, tray-icon's on macOS and Windows. MemoryTray for tests.
    • Global hotkeys (ADR 0064), the hotkeys feature: Context::set_hotkeys
      asks for Hotkeys that reach the app while another app has the focus;
      presses arrive in App::hotkey_pressed and hotkey_released, the keys
      the system bound in App::hotkeys_bound (BoundHotkey), and a loss in
      App::hotkeys_failed. Context::hotkey_support (HotkeySupport::Direct,
      Desktop, Unsupported) and HotkeyError. X11 grabs, and the desktop
      portal's GlobalShortcuts on Wayland; global-hotkey on macOS and Windows.
      MemoryHotkeys for tests.
    • Printing (ADR 0065), the print feature: 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 - and App::print_finished
      (PrintOutcome) or App::print_failed (PrintError) says what came of
      it. MemoryPrinter for tests.
    • Recent documents, the recent-documents feature:
      Context::add_recent_document puts a file among the system's recent
      documents - the freedesktop recently-used.xbel unless the user turned
      remembering recent files off, the shell's on Windows, the document
      controller's on macOS - and Context::recent_documents and
      RecentDocuments read the app's own back; RecentError.
    • System settings, the system-settings feature, on by default:
      App::system_settings_changed(frame, SystemSettings) - reduced_motion,
      high_contrast and accent_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-instance feature:
      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 in App::open_requested(cx, OpenRequest), whose
      default brings the main window to the front. Over the session bus
      (org.freedesktop.Application, so DBusActivatable desktop entries work)
      on Linux and the BSDs, a socket on macOS and Windows;
      RunError::SingleInstance and InstanceError. Context::hand_over for
      tests.
    • Activation: startup_token(), the launcher's activation token, which
      run gives 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: run carries out every window's
      WindowRequests, a Close asking App::close_requested, and tells each
      Ui whether 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,
      and cursor_icon maps the diagonal resize cursors.
    • WinitInput passes 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 the Waker, without an async runtime - progress
      (TaskCx::set_progress), messages (TaskCx::send), cancelling
      (Task::cancel, TaskCx::is_cancelled, check and Cancelled), and a
      panic as a TaskError. Task::spawn_with_wake for an event loop of the
      app's own.
    • The settings-file feature: 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 open feature: open_url opens http, https and mailto links
      in the browser or mail program, and open_path a file or folder with its
      default app; OpenError.
    • Settings::inspector(key): a key that shows forma's inspector over every
      window of run.
    • OsClipboard holds 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); OsClipboard is
      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, pdf and syntax features,
      which turn on forma's.
    • The fluent example: 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 run moved 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).
    • run asked 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 outlived run.
    • 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 while Frame::native_menu said it showed; it is reported
      on stderr and native_menu is false. native_menu was 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. run keeps it
      with each window, and sync_window tells the input method on every
      frame while a password field has the keyboard.
    Downloads
  • v0.2.1 b92cfdb164

    v0.2.1
    All checks were successful
    ci / check (push) Successful in 29m16s
    Stable

    gntx released this 2026-10-03 22:23:40 +02:00 | 556 commits to main since this release

    0.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 with password_input.

    New dependency: zeroize in forma-ui (MIT/Apache-2.0, no dependencies of
    its own), to wipe password text.

    Downloads
  • v0.2.0 37f233ac91

    v0.2.0
    All checks were successful
    ci / check (push) Successful in 30m1s
    Stable

    gntx released this 2026-10-03 20:33:21 +02:00 | 557 commits to main since this release

    0.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) and jiff (jiff) in
    forma-ui; rfd (dialogs), muda (native-menu, on by default, macOS
    and Windows only), notify-rust (notifications) and dark-light
    (system-theme, on by default, Linux and the BSDs only) in forma-winit.

    forma-ui

    Changed (breaking)

    • Style::scroll takes the axes to scroll on: .scroll() becomes
      .scroll(Axes::Y) for the old behavior, or Axes::X / Axes::BOTH.
      Style::scroll and Widget::scroll are Axes instead of bool.
    • Ui::scroll_offset, set_scroll_offset and max_scroll_offset take
      and return Vec2 instead of f32: use .y for 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::shortcut takes a Shortcut instead of text:
      .shortcut("Ctrl+S") becomes .shortcut(Shortcut::primary(Key::Char('s'))),
      shown as "Ctrl+S" or "⌘S" depending on the platform.
    • FindBar::status takes the Ui, to speak its language:
      bar.status() becomes bar.status(&ui).
    • DrawCommand has a new Mesh variant, and DrawCommand::Image has two
      new fields, uv and filter: a renderer of its own handles both, and
      code building an Image command adds uv: Rect::UNIT, filter: ImageFilter::Linear for the old behavior.
    • The caret, the arrow keys, Backspace and Delete move by grapheme
      cluster instead of by code point.
    • Fonts is a handle whose clones share one set of fonts.
      Ui::font_system_mut() and Fonts::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.
    • CursorHint has a new Pointer variant (a hand, over links): a backend
      of its own maps it, as forma-winit does to CursorIcon::Pointer.
    • Table shows a TableModel and its selection holds row ids:
      TableEvent::Selected and Activated carry the row's id instead of its
      index, and selected(), set_selected and selection() take and
      return ids (selected_row() gives the index). Table is
      Table<M = VecModel>; set_rows still fills its VecModel, rows()
      is gone (read model()), and a header click sorts a VecModel itself,
      reporting SortRequested only for models that cannot sort.
      TableEvent is no longer Copy.
    • TreeNode has a new field, lazy, and TreeRow a new field, busy:
      a TreeNode { .. } or TreeRow { .. } 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_size and max_size are now
      Size<LengthPercentageAuto>. The upgrade fixes layout time that doubled
      with every level of nested containers.
    • MenuItem::Action's shortcut field is an Option<Shortcut> instead of
      an Option<String>, as MenuItem::shortcut above.
    • Style has new public fields (accepts_files, image_fit, invalid):
      a Style { .. } literal adds them, or starts from Style::new().
    • icons::ALL holds 12 icons instead of 11 (the new CALENDAR), so its
      array type changed.
    • Stats is #[non_exhaustive]: it gains counters, so it is read, not
      built.
    • MenuParts has 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.
    • Table scrolls sideways when its columns are wider than it, the header
      with the rows. VirtualList::with_row_width makes rows at least that
      wide.
    • App shortcuts: Ui::shortcuts_mut() binds a Shortcut to an id, scoped
      to a widget subtree or global; pressing it reports Event::Shortcut(id)
      after the widgets had their turn. Actions::on_shortcut maps it to a
      message, Shortcuts::bind_menu binds a menu's shortcuts, and
      Shortcut::label writes it the platform's way. Modifiers::PRIMARY is
      Cmd on macOS and Ctrl elsewhere.
    • Localization hooks: Ui::set_locale takes a Locale (separators, date
      names and order, 12- or 24-hour time, the language for font fallback),
      and Ui::set_strings takes a Strings that translates forma's own
      phrases (Phrase). NumberField and Mask::Decimal use the locale's
      decimal separator; Locale::format_number, format_grouped and
      parse_number are there for the app.
    • An invalid state: Style::invalid(message) / invalid_if, or
      Ui::set_invalid after 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_enabled and Ui::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::UNIT is the whole texture.
    • Color emoji, from a color emoji font: emoji are shaped in
      Fonts::emoji_family (found by load_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)). Phrase has a new
      MoreOptions.
    • Dates and times: Date, Time and DateTime as 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. With serde, only real dates and times deserialize. The new
      jiff feature 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::DateTimePicker puts the two side by side (DateTimeEvent).
    • Table models: the TableModel trait (row_count, row_id, cell,
      optional sort and row_of), Cell (text with CellAlign and a
      color, or Pending), and VecModel, which sorts naturally and filters
      by text or a predicate. Table::with_model, update_model and
      set_model; pending rows on screen are asked for with
      TableEvent::RowsNeeded(range), once while they stay pending.
      compare_natural sorts the way a person does: empty cells, then numbers
      by value, then text with its digits by value. VecModel finds rows by id
      in constant time and refuses two rows with one id.
    • Image fit modes: Style::fit(ImageFit::Contain | Cover | None | Fill),
      and Ui::set_image_size, which also makes an image widget measure to
      its picture. widgets::ImageViewer zooms toward the pointer, pans by
      dragging, draws pixels sharp above 100 % over a checkerboard, and
      reports ViewerEvent::Zoomed.
    • Charts, with the new plot feature (lyon_tessellation; forma-winit has
      a plot feature that turns it on): forma::plot::Plot draws 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_data takes live data with a redraw only; decimate thins long
      series per pixel column and fill turns an outline into a mesh with a
      soft edge. InputCx::modifiers tells a custom widget the held keys.
    • Markdown, with the new markdown feature (pulldown-cmark; forma-winit
      has a markdown feature that turns it on): widgets::MarkdownView shows
      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::MarkdownEditor colors 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::lazy nodes report TreeEvent::ChildrenNeeded
      when first expanded and show a busy row until TreeView::set_children;
      children_needed() covers nodes expanded from a saved state. Tree
      filters (filter_text, filter) show matches and their ancestors,
      opened; sort_by orders every level. Phrase::Loading names the busy
      row.
    • form::Form<K>: fields kept by the app's own keys, with rules
      (Rule::required, min_len, range, and closures with Rule::check)
      checked on change, on blur or on submit (ValidateOn). It builds each
      field with a message line beneath, and validate marks the failing
      fields and focuses the first. set_value(ui, ..) and reset(ui) show on
      screen at once. Numbers are plain decimals in the locale: no "NaN" or
      exponents. New phrases Required, TooShort,
      NotANumber and OutOfRange.
    • Locale::format_date, parse_date and format_time, and a new
      Locale::am_pm. Mask::Date(separator) for typed dates, the
      icons::CALENDAR icon, and new phrases for the pickers (ChooseDate,
      PreviousMonth, NextMonth, PreviousYear, NextYear, Hour,
      Minute, Second).
    • Toasts::push_message takes a ToastMessage with a title and an action
      button, reported as ToastEvent::Action(id); ToastParts has a new
      actions field.
    • Files dropped from outside the app: Style::accepts_files marks a
      target, highlighted while files hover over it; Event::FilesHovering
      and Event::FilesDropped { target, paths } report them, one event for a
      whole drop. New InputEvents FileHovered, FileHoverCancelled and
      FileDropped, which forma-winit sends.
    • Table::state / restore (TableState) and TreeView::expanded /
      restore_expanded keep column widths, the sort and the expanded nodes.
    • Rich text: Ui::rich_text shows a RichText built from runs, each with
      a RunStyle (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 type Ui::font_system_mut returns, is exported.
    • Ui::copy_text: put text on the clipboard from app code (a "Copy key"
      button), reporting Event::ClipboardFailed on failure like Copy does.
    • Dialog::with_danger(index) draws a destructive answer ("Delete") in the
      danger color.
    • Harness::try_find_text (None when nothing shows the text) and
      Harness::find_all_text (every match, in tree order).

    Fixed

    • A focused widget's own Style::claims come before its built-in keys: a
      text area that claims Enter gets Event::Command instead 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::icon now 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 Renderer draws into several targets
      (add_target, prepare_target, render_target, remove_target),
      sharing its glyph atlas and images; WindowSurface is one window's
      surface drawn through a shared renderer. SurfaceRenderer keeps its API.
      Gpu::for_surface acquires 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::request asks 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_rgba after update_image on the same image shows the new
      pixels.

    forma-winit

    Changed (breaking)

    • App::build takes 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.
    • Settings has new fields: system_fonts (on by default: run loads
      the machine's fonts), exit_when, theme_mode, state and menu. A
      Settings { .. } literal without ..Settings::new(..) adds them.

    Added

    • Several windows in run: Frame::open_window(WindowSettings) returns
      a WindowKey and opens the window after the callback;
      Frame::close_window, Frame::window_key, App::window_opened and
      App::window_closed(window, state), which hears about every window that
      goes, on every path, with its final WindowState. The windows share the GPU, renderer, images and
      fonts; screen readers see each. Settings::exit_when (ExitWhen) ends
      run with the last window (the default) or with the main one.
    • Native menus on macOS and Windows, with forma-winit's new default
      native-menu feature (muda): Settings::menu becomes the platform's
      menu bar - on macOS with an app menu (About, Hide, Quit) and a Window
      menu - and App::menu_chosen(frame, id) reports choices.
      Frame::native_menu says whether it is shown (not on Linux, which keeps
      the in-window MenuBar); Frame::set_menu replaces it.
      App::quit_requested decides 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::menu cannot be refused; the windows still get
      window_closed, and App::exiting runs.
    • System notifications, with the new notifications feature
      (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 dialogs feature (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_state and window_closed
      give a WindowState (the last normal size and position, and whether it
      was maximized or fullscreen), and Settings::restore /
      WindowSettings::restore open 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. WindowSettings has
      maximized and fullscreen. The new serde feature serializes
      WindowState and turns on forma's.
    • run follows the system's light or dark appearance:
      App::theme_changed(frame, SystemTheme) runs when a window opens and
      when the system switches, and applies Theme::light() or dark() by
      default. Settings::theme_mode (ThemeMode) can fix it instead. On
      Linux and the BSDs the new default system-theme feature asks the
      desktop portal (dark-light); "no preference", GNOME's default, counts
      as light.
    • App::close_requested decides whether a window the user closes closes
      (CloseResponse::Close, the default) or stays open (Keep), to ask
      first; App::exiting runs once as the app ends, after every window got
      window_closed.
    • markdown and plot features, which turn on forma's.
    • forma_winit::winit re-exports winit, so matching window events in
      App::unhandled needs no direct winit dependency of the same version.
    Downloads
  • v0.1.1 687b87b9ed

    v0.1.1
    All checks were successful
    ci / check (push) Successful in 27m46s
    Stable

    gntx released this 2026-10-02 21:25:14 +02:00 | 601 commits to main since this release

    forma-winit

    Added

    • Frame::waker, Waker and Closed: wake an app driven by run from
      another thread, so a worker's results reach the UI without input or
      polling.
    Downloads
  • v0.1.0 3bc43114d7

    v0.1.0
    All checks were successful
    ci / check (push) Successful in 27m57s
    Stable

    gntx released this 2026-10-02 19:51:39 +02:00 | 604 commits to main since this release

    forma-ui

    • A retained widget tree with WidgetId handles, 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 Style builder; 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
      Clipboard trait, keyboard focus on every control, popups and modal
      dialogs.
    • Events, Actions for turning them into app messages, and Component
      and CustomWidget for 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 accesskit feature): roles, names,
      states, text with caret and selection, actions from assistive technology.
    • forma::testing::Harness and snapshot helpers for headless tests.

    forma-wgpu

    • SurfaceRenderer for a window, and Renderer for 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 the App trait, drawing only when needed.

    Known limitations

    See the README.

    Downloads