-
v0.3.0
Stablereleased this
2026-10-05 13:15:35 +02:00 | 127 commits to main since this release0.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
licensefield 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 wasMIT OR Apache-2.0, which did not cover
the fonts, and the package carries DejaVu's license text now. An app with
bundled-emojicredits 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-bidiandweb-time, in the tree before, are direct
dependencies now.pdf:krilla0.7 and the crates it brings, among
themzlib-rs(Zlib) andyoke,yoke-derive,zerofromand
zerofrom-derive(Unicode-3.0).syntax:syntect5.3, with
fancy-regex,bincode1.3 (MIT; no longer maintained upstream) and
aho-corasick(Unlicense or MIT).lucideandbundled-emojiadd no
crates: they carry Lucide's icons (ISC) and the Twemoji font (CC-BY-4.0). - forma-wgpu:
screenshotusespng, andswashis a direct dependency;
both were in the tree already. - forma-winit: on Linux and the BSDs the default
clipboardbrings
wl-clipboard-rsandsmithay-clipboard, withfoldhash(Zlib) among
theirs.settings-file:dirs7 andserde_json, withoption-ext
(MPL-2.0).openandprint:opener0.9, which on Linux and Windows
bringsurl,idnaand eighteen ICU4X crates (Unicode-3.0).tray:
ksni(Unlicense) on Linux and the BSDs,tray-iconon macOS and
Windows.hotkeys:global-hotkeyon macOS and Windows. The system
settings, notifications, recent documents, single instance andrunuse
zbus,x11rb,roxmltree,objc2andwindows-sys, all in the tree
already.dark-lightandwinregare gone, andnotify-rustis 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; andrunin 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,MetricsandTextStyleare#[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.Stylehas new fields -folding,
pasted_tabs,takes_typing,layout_transition,enter,leave,
children_enter,children_leave,window_area,gestures,
touch_drag,keep_focus- andMetricstwo,state_outlineand
selection_bar(0 in the standard themes; withserde, a theme saved by
0.2 still reads).Style::corner_radiiis anOption<[f32; 4]>:None, the default,
leaves the corners to the widget, andSome([0.0; 4])is square. The
builderscorner_radius,round_topandcorner_radiiare unchanged.Shortcut's fields are private, since a shortcut can be a chord of two
keys: make one withShortcut::new,primary,parseorthen, and
read it withfirst_key,first_modifiersandsecond. A character is
kept lowercase, soKey::Char('S')is Ctrl+S; written out uppercase, it
showed as "Ctrl+S" and never fired.- Shift no longer counts in a
Shortcuton a digit or symbol, which a
layout types with or without Shift:Shortcut::newdrops 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
Tableclaims 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::Actionhas a new field,icon, and it and
MenuItem::Submenuare#[non_exhaustive]: build menu items with
MenuItem::action,submenuand their builders, and match the variants
with...plot::Seriesis#[non_exhaustive]and has new fields,boxesand
axis: make one withSeries::new(orline,bar,histogram,
boxes) andcolor.WidgetKind::TextInputholds aFieldTextinstead of aString: an
edited field's text is kept in a rope.FieldTextderefs tostrand
compares withstrandString.TableStateis#[non_exhaustive]and keeps the column order, the
hidden and frozen columns and the grouping (order,hidden,frozen,
grouping): make one withTableState::default(). Withserde, a state
saved by 0.2 still reads.- Every component's parts (each
*Parts, andMenuLevel,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'scard,DateTimeParts::row,DialogParts::scrim,
DockParts::area,MarkdownParts::view,MenuBarParts::bar,
NumberParts::row,RadioParts::group, the split button'srow,
SplitPaneParts::container,TableParts::table,TimeParts::row,
ToastParts::stackandVirtualParts::vieware eachrootnow.
TablePartshas new fields, and itsheadersandhandlesare in the
order the columns are shown, without hidden ones (columnssays which
column each heads). form::Rule::checktakes a closure that isSend + Sync, so rules no
longer keep aForm, or a component that checks text by them, from
beingSend.Ui::copy_textreturns the clipboard's error,Result<(), ClipboardError>, instead of reporting it asEvent::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: ClipboardErrorKindbesidemessage.Ui::run_commandgoes where the command's key would go: to the topmost
popup or the focused widget that claims it (anEvent::Commandfor 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 onerun_command(Command::Copy)
for a text field, aTableor anImageViewer. The events it gives ask
for the next frame at once.- A press on a
MenuBaror aMenu(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
reportedEvent::Submitted. CursorHinthas two new variants,ResizeDiagonalDownand
ResizeDiagonalUp, for the corners of a window's resize border: a
backend of its own maps them.Ui::set_localepanics for aLocalewhosegroupingis itsdecimal
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'swidgets::CalendarPartsis
DateCalendarParts, Markdown'sEditorEventandEditorPartsare
MarkdownEditorEventandMarkdownEditorParts,ViewerEventand
ViewerPartsareImageViewerEventandImageViewerParts,SplitEvent
andSplitPartsareSplitButtonEventandSplitButtonParts, and
RangeEventandRangePartsareRangeSliderEventand
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_ofstill gives the whole text - for an edited
field a copy, made on the first call after each edit - and
Ui::field_textreads 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 andUi::ime_cursor_areasit 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 as36 London Ada, not
36LondonAda. A mask checks the text as it is then:12, a tab and34
pasted into aMask::Digitsfield is refused, where it gave1234.
Style::pasted_tabs(PastedTabs::Drop)drops them as before.
Added
ActionSet<A>(forma::action_set, withActionandEntry): 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.menuandmenusbuild menu items from a layout ofEntrys,
bind_shortcutsbinds their shortcuts (switched off for disabled
actions), andchosenandtriggeredturn menu choices and shortcut
events back into the app's ids. Setters,revision, and cheap copies.CommandPalette<A>(widgets::palette): every action of anActionSet,
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,
reportingPaletteEvent::Chosenwith the keyboard back where it was.Toolbar<A>(widgets::toolbar): buttons for a layout ofEntrys -
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. ReportsToolbarEvent::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 aStatusProgressbetween 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_undoandnext_undofor a menu, and
mark_savedandis_modifiedfor unsaved changes.MenuItem::iconandMenuItem::is_enabled;Menu::set_itemsand
MenuBar::set_menus(andMenuBar::menus) show new items in place,
keeping an open menu open.- Chords (ADR 0070):
Shortcut::thenmakes a shortcut of two keys (Ctrl+K
Ctrl+S), withis_chord,firstandsecond; 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_chordandUi::cancel_chord. - A shortcut's text form:
Displaywrites "Ctrl+K Ctrl+S" on every
platform, andShortcut::parseandFromStrread it back
(ParseShortcutError); withserdea shortcut is saved as that text.
Shortcuts::bindingslists the bindings, andShortcuts::set_enabled
andis_enabledswitch 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;KeyBindingsandKeyBindingkeep, save
(serde) andapplywhat the user changed.Ui::set_key_captureand
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, reportingSearchEvent::Changedon each edit and
SearchEvent::SearchRequestedonce typing pauses (with_delay, 300 ms),
on Enter and on clearing;set_result_countshows 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) andRevealed;
with_maskfor a PIN,with_label.MaskedInputandMaskPattern(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'sRules (with_rules); keys over the chips;
TagEvent::ChangedandUpdated.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 optionalFormthat Next checks, Back, Next, Cancel and
Finish, pages skipped by the app's data;WizardEvent::PageChanged,
Finished,CancelledandUpdated.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),
andwith_modalkeeps 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 - aPlacementof aSide, an alignment along it
and a gap - flipped and shifted to stay in the window, with an optional
arrow;Ui::popup_side.Side::StartandEndfollow 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 aWindowRequest(Drag,Resize(ResizeEdge),
Minimize,ToggleMaximize,Close,ShowMenu), queued by
Ui::request_windowand 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_bordera band along the window's edges;
Ui::set_window_maximizedandwindow_maximized. widgets::TitleBar: the title, a row for the app's widgets and the
window's buttons where the platform has them (ButtonLayout::platform,
parsefor GNOME'sbutton-layout), drawn as macOS's traffic lights or
as flat buttons; it reports theWindowButtonpressed, and gives way
without overlapping as the window narrows.- Touch, pens and gestures (ADR 0069):
InputEvent::Touch(aTouchwith
aTouchPhaseand a force), andPinchandRotatefrom 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 withStyle::touch_drag().Style::gestures(): two fingers, or a
pinch or turn over the widget, reportEvent::Gesture(and
WidgetInput::Gestureto 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_caretand
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::FoldandUnfold(Ctrl+Shift+[ and
], Cmd+Alt+[ and ] on macOS);Ui::set_fold_ranges,
clear_fold_ranges,fold,unfold,fold_all,unfold_alland
folded_lines. - Syntax highlighting, behind the new
syntaxfeature:
forma::syntax::Highlightercolors 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_spansreads a widget's color spans. - Spelling marks:
Ui::set_misspellingsmarks the words an app's checker
found with a squiggle (Misspelling), and the marks follow the text;
Ui::misspelling_at_point,Misspelling::menu_itemsand
Ui::correct_misspellingoffer a right click's suggestions;
misspellings,misspelling_atandclear_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::LeftToRightor
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_localewith 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), andUi::field_text.Style::pasted_tabs(PastedTabs): what a text input does with the tabs
in text put into it - pasted, inserted withUi::insert_stror
replace_range, committed by an input method, or in a text area the
indentation the Tab key inserts:PastedTabs::Keep,SpaceorDrop.
Without it a text area and a password field keep them, and another
single-line field turns each into a space.Theme::high_contrast_darkandhigh_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_outlinedraws
hover, press, focus and invalid states in shape,Metrics::selection_bar
marks a selected row with a bar at its start, and
Metrics::HIGH_CONTRASTholds those themes' metrics.Palette
implementsDefault.Ui::announce(text, Politeness): screen readers saytext, after what
they are saying or at once.- The
lucidefeature:forma::lucide, Lucide 1.52.0's 1866 icons as
Iconconstants (lucide::ARROW_LEFT), withby_name,ALLand
VERSION; an app carries only the icons it uses. - The
bundled-emojifeature: Twemoji in Mozilla's COLR build (about
1.5 MB) as the emoji family of everyFonts::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_enterandchildren_leavefade or
slide a widget in as a build adds it and out as a rebuild orUi::remove
drops it (anim::Presence).Ui::draw_offsetanddraw_opacity.
Ui::set_reduced_motionandreduced_motionmake every change
immediate. - Editable cells:
TableModel::edit_textsays whether a cell can be edited
and with what text, andTableModel::set_celltakes 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) andVecModel::set_text. Table::set_rulesandwith_rules: what is typed into a column is
checked byform::Rules, the message shown beside the cell;
form::first_errorchecks 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()andEvent::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_orderandmove_column
(TableEvent::ColumnsChanged);with_movable_columns(headers dragged,
or Ctrl+Shift and an arrow);with_column_chooser,set_column_visible,
is_column_visibleandvisible_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,groupableandtotal; group rows
open and close (TableEvent::Toggled,ChildrenNeeded,
GroupRequested) and a row of totals sits under the rows.
Table::group_by,set_grouping,groupingandset_expanded.
VecModelgroups, totals columns with anAggregate(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_textand
Table::pastefor menus.Table::to_csvandwrite_csv. - Tree tables: a
TreeModelofTreeItems (leaf,branch,lazy),
shown by aTablewith its header, sorting, editing and copying, the
first column indented by depth;RowKind::Node,RowKind::Busyand
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 andset_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 aColorScale
(viridis,blues,diverging), with a color bar, labels, a readout
andHeatmapEvent::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,vbandandpoint;
Plot::set_annotations). - Chart export (ADR 0049):
picture(&ui, size, scale)onPlot,
PieChart,HeatmapandSparklinedraws the chart on its own as a
plot::Picture, which writes SVG (to_svg) or is drawn to PNG by
forma-wgpu'sexport;Plot::as_shownkeeps the view the user zoomed
to. Each chart'stable_modelgives its data as rows. - The
planningfeature,forma::planning: aKanbanBoardofLanes of
Cards, moved by dragging or from the keyboard and reported as
KanbanEvent::Moved(CardMove)andLaneMoved(LaneMove)for the app to
apply, with work-in-progress limits; aCalendarwith month, week, day
and agenda views ofCalendarEntrys at aCalendarSpan, moved and
resized by dragging or from the keyboard (CalendarEvent); aTimeline
ofTimelineRows ofTimelineBars on a date axis, a Gantt chart with
milestones andDependencyarrows, 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:
Clipboardgainsget_image,
set_image,get_htmlandset_html, whose defaults fail with
ClipboardUnsupported, so a clipboard of text only keeps compiling.
Ui::copy_image,copy_html,clipboard_text,clipboard_imageand
clipboard_htmlreturn the clipboard's error;ClipboardErrorKind
(Unsupported,Failed) andClipboardErrorKind::of(&error).
MemoryClipboardholds 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(andset_pixels,pixels): the viewer
copies its picture with Copy (ImageViewerEvent::Copied), and
ImageViewer::copyfor a menu.ImageViewer::set_center.- PDF files, behind the new
pdffeature (ADR 0065): apdf::Document
written page by page, each with aPageSetup(PageSize,Margins).
ui_pagelays aUiout on a page,pagedraws pieces of aUi, a
draw list or a chart,ui_pagesflows a longUiover pages, breaking
between lines and rows,ui_pages_withadds a header or footer with its
PageNumber, andpicture_pageputs a chart on a page. Text stays text,
in subsets of the fonts forma shaped it with; shapes, icons and clips are
vector.PdfErrorsays 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'sInstant, which is the standard library's on every target
but the web, wherestd::time::Instant::now()panics.- New phrases (
Phrase) for the new widgets: the table'sColumns,
HideColumn,FreezeColumns,UnfreezeColumns,GroupBy,Ungroup,
GroupRow,EmptyValueandTotal; the command palette's
SearchCommandsandNoMatchingCommands; the search field's
SearchPlaceholder,ClearSearchandSearchResults; the password
field'sShowPasswordandHidePassword; the tag input'sRemoveTag;
the breadcrumb'sBreadcrumbandHiddenItems; the wizard'sBack,
Next,Finish,Cancel,WizardStepandCompleted; the title
bar'sMinimize,MaximizeandRestore; chords' and the key binding
editor'sChordPending,FilterShortcuts,CommandColumn,
ShortcutColumn,NoShortcut,PressShortcut,PressSecondKey,
ChangeShortcut,RemoveShortcut,ResetShortcutand
ShortcutConflict; the kanban board'sBoard,LaneCards,
LaneOverLimit,CardPickedUp,CardAt,CardDropped,
CardMoveCancelledandLaneMoved; the calendar'sCalendar,
PreviousPeriod,NextPeriod,Today,MonthView,WeekView,
DayView,AgendaView,AllDay,MoreEntries,NoEntries,
MonthTitle,DayTitle,FromTo,EntryPickedUp,EntryAt,
EntryMoved,EntryResizedandEntryMoveCancelled; the timeline's
Timeline,ZoomIn,ZoomOut,Quarter,BarPickedUp,BarAt,
BarMoved,BarResizedandBarMoveCancelled.
Fixed
- A
TableorTreeViewevent 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, reportedEvent::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, andTreeView::set_childrenpanicked for a node a
set_rootsremoved 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) andUi::replace_linesdropped the tabs
of the indentation they carried. - Clicking the open title of a
MenuBar, or aDatePicker's calendar
button while the calendar was open, closed it and opened it again at
once; it closes now. Escape did not close aSelect's or a
SplitButton's menu while the pointer rested on its button. - A range
DatePickerreportedRangeChanged(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
TreeViewfilter, 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. Tableasked for a row withTableEvent::RowsNeededonly when its first
cell wasCell::Pending; a row pending in any column is asked for now.Locale::format_datewrote a year before 1 as "-005", which
parse_datecould not read, andparse_numberdid 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_barwhose 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_centerlabel, 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::buildwere
dropped. - Screen readers heard a right-aligned or centered table cell without a
name, and abusy_barnot as a progress indicator. - A plot's readout of the values under the pointer was worked out only as
the pointer moved, so afterset_data, a series hidden from the legend
or a box zoom it showed what was there before. It and
Plot::table_modelwrote 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. TreeViewworked out its whole row list again for every event it was
offered, andUi::removeand a rebuild's removals took time in the
square of how much they removed.
forma-wgpu
Changed (breaking)
- The variants of
ImageErrorwith fields (Empty,WrongLength,
TooLarge) are#[non_exhaustive]: a pattern naming their fields ends
in... - A lost surface is reported as
FrameStatus::SurfaceLostinstead of
Retry: make a new surface for the window and hand it to
WindowSurface::replace_surfaceorSurfaceRenderer::replace_surface.
It was configured again, which fails (wgpu's default error handler
panics) or left every frame retrying a dead surface;resizeleaves it
alone now. Renderer::remove_targeton the main target resets it - its buffers,
its draws and the atlas pages they held are freed, andprepareand
renderstill draw into it - and returnstrue, 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
screenshotfeature (ADR 0042):
screenshot::Offscreenrenders aUi, a draw list (render_list) or a
Harness(capture) into a texture the way a window draws it, and reads
it back as aforma::RgbaImage;comparechecks it against another
within aTolerance(Mismatch);Referenceskeeps reference PNGs,
writes<name>.new.pngand<name>.diff.pngwhen a check fails, and
updates them withFORMA_UPDATE_SCREENSHOTS=1;read_pngand
write_png. - The
exportfeature:export::renderandexport::pngdraw a chart's
forma::plot::Pictureoffscreen, andexport::encode_pngencodes any
RgbaImage; a picture larger than the GPU's textures is
ExportError::TooLarge. It turns onscreenshotand forma'splot. - Large images (ADR 0066):
LargeImage::new(image)keeps anRgbaImage
with smaller copies, made once on any thread, and
Renderer::register_large_imageregisters it under anImageHandlelike
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, withWindowSurface::replace_surfaceand
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_targetrefused. - 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 fromdata: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)
SettingsandWindowSettingsare#[non_exhaustive]: built withnew
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,wakerandset_menu
moved to the newContext, whichFramederefs to: calls through a
frame compile as they were, and a path namesContext.- The free function
notifyis gone: notifications are shown with
Context::notify, which does not block and gets answers. The
notificationsfeature needsrun, and turns it on.NotifyErroris an
enum (Unsupported,Service) instead of a struct, andNotification
is#[non_exhaustive], with public fields to read. - The light or dark appearance on Linux and the BSDs comes from the
system-settingsclient, which reads the portal'scolor-schemewith
the other settings, on one connection:system-themeis now the same
feature assystem-settings, so a 0.2 feature list still builds. With
it, as with the default features, the defaultApp::theme_changed
appliesTheme::high_contrast_light()orhigh_contrast_dark()while
the system asks for high contrast, and the new
App::system_settings_changedapplies the system's reduced motion to
each window'sUi.
Changed
runkeeps one OS clipboard for its event loop, opened for its display,
and gives every window'sUia 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-clipboardis 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_menuchanges 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::ExitCalledkeeps the app
running after its last window closes,Settings::open_main(false)
starts it without the main window, andContext::open_main_windowopens
that again where it was (App::window_openedruns for it). On macOS a
choice from the menu bar with no window open opens the main window.
Context::detachedandContext::dispatchtest 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, andSettings::iconandWindowSettings::icon, a
WindowIconmade from aforma::RgbaImage(WindowIcon::from_image),
shown on X11 and Windows. - Notifications with actions (ADR 0046):
Notification::actionand
timeout(NotificationTimeout);Context::notifyreturns a
NotificationId,Context::replaceandwithdrawchange it, and the
answer comes inApp::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.
MemoryNotificationsfor tests. - The system tray (ADR 0048), the
trayfeature:Context::set_tray(Tray)
shows an icon (TrayIcon::from_icon,from_image) with a tooltip and a
menu ofMenuItems, changed in place when called again; choices arrive
inApp::tray_chosen, clicks on the icon inApp::tray_activated, a
tray that went away inApp::tray_failed.Context::supports_tray,
remove_trayandtray;TrayError. A StatusNotifierItem on Linux and
the BSDs, tray-icon's on macOS and Windows.MemoryTrayfor tests. - Global hotkeys (ADR 0064), the
hotkeysfeature:Context::set_hotkeys
asks forHotkeys that reach the app while another app has the focus;
presses arrive inApp::hotkey_pressedandhotkey_released, the keys
the system bound inApp::hotkeys_bound(BoundHotkey), and a loss in
App::hotkeys_failed.Context::hotkey_support(HotkeySupport::Direct,
Desktop,Unsupported) andHotkeyError. X11 grabs, and the desktop
portal's GlobalShortcuts on Wayland; global-hotkey on macOS and Windows.
MemoryHotkeysfor tests. - Printing (ADR 0065), the
printfeature: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 - andApp::print_finished
(PrintOutcome) orApp::print_failed(PrintError) says what came of
it.MemoryPrinterfor tests. - Recent documents, the
recent-documentsfeature:
Context::add_recent_documentputs a file among the system's recent
documents - the freedesktoprecently-used.xbelunless the user turned
remembering recent files off, the shell's on Windows, the document
controller's on macOS - andContext::recent_documentsand
RecentDocumentsread the app's own back;RecentError. - System settings, the
system-settingsfeature, on by default:
App::system_settings_changed(frame, SystemSettings)-reduced_motion,
high_contrastandaccent_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-instancefeature:
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 inApp::open_requested(cx, OpenRequest), whose
default brings the main window to the front. Over the session bus
(org.freedesktop.Application, soDBusActivatabledesktop entries work)
on Linux and the BSDs, a socket on macOS and Windows;
RunError::SingleInstanceandInstanceError.Context::hand_overfor
tests. - Activation:
startup_token(), the launcher's activation token, which
rungives 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:
runcarries out every window's
WindowRequests, aCloseaskingApp::close_requested, and tells each
Uiwhether 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,
andcursor_iconmaps the diagonal resize cursors. WinitInputpasses 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 theWaker, without an async runtime - progress
(TaskCx::set_progress), messages (TaskCx::send), cancelling
(Task::cancel,TaskCx::is_cancelled,checkandCancelled), and a
panic as aTaskError.Task::spawn_with_wakefor an event loop of the
app's own.- The
settings-filefeature: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
openfeature:open_urlopenshttp,httpsandmailtolinks
in the browser or mail program, andopen_patha file or folder with its
default app;OpenError. Settings::inspector(key): a key that shows forma's inspector over every
window ofrun.OsClipboardholds 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);OsClipboardis
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,pdfandsyntaxfeatures,
which turn on forma's. - The
fluentexample: 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
runmoved 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). runasked 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 outlivedrun.- 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 whileFrame::native_menusaid it showed; it is reported
on stderr andnative_menuis false.native_menuwas 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.runkeeps it
with each window, andsync_windowtells the input method on every
frame while a password field has the keyboard.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- forma-ui: