• 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