• 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