Repository navigation
frust 0.6.0
Type erasure moved from the call site to the API boundary: public builders take generic views and
erase internally, and views are composed with fluent builders, child sequences (ViewSeq<State, M>)
and Either<L, R> instead of any() everywhere; helpers return impl View<S> instead of
AnyView<S> wherever the result is not stored erased. Debug builds hot-patch a saved edit into the
running app on the desktop, an Android device and the iOS simulator, keeping the process and every
component's State.
Breaking
Component::buildreturnsimpl View<Self::State>instead ofAnyView<Self::State>. An impl
that keeps-> AnyView<..>warns (refining_impl_trait) and fails under-D warnings.Componentis no longer dyn-compatible:dyn ComponentandBox<dyn Component>do not
compile. Use generics, or store theAnyViews a component produces.- Driver closures must erase the build result with
AnyView::new(root.build(state)); the opaque
return captures the&selfand&mut Stateborrows and cannot escape the closure otherwise.
frust::run,frust::app!and the templates already do this. - Public parameters that were typed
AnyVieware generic (impl View<S>,Option<V>,
impl IntoIterator<Item = impl View<S>>,impl Fn(..) -> V,ViewSeq<S, M>), with one exception
that keeps erasure because it lazily produces views: the deprecatedfrust_material::list_view
item builder (impl Fn(usize) -> AnyView<State>;frust::list_viewis its generic replacement).
The material button-group icon family (button_group_icon_action,ButtonGroupAction::iconand
checked_icon,ButtonGroupView::overflow_icon),SplitButtonView::leading_icon, beui's
ChatConversationView::extra_rowsand clean-signals-frust'sasync_viewtake generic views too.
Callers passingany(..)(or a closure returning it) still compile, but a bareNone, an empty
vec![], an explicit turbofish call, or a.collect()straight into a list parameter now needs a
type: a turbofish caller of a plugin list function writes::<State, _>, and one of
button_group_icon_actionneeds one more_. - Wrapper views with
type Element = Box<dyn Widget>that forward to an innerAnyViewstill hide
inner type swaps from focus bookkeeping (seefocus-wrapper-erasure-swap-blindin
docs/LIMITATIONS.md). - Keyed flex lists are all-or-nothing, and the check is a
debug_assert: a mixed list silently
falls back to positional reconciliation in release builds (see
keyed-list-all-or-nothing-debug-onlyindocs/LIMITATIONS.md).
Added
- Hot patching (debug builds):
frust run --watch(the desktop preview),
frust run -d <Android device> --watchandfrust run -d <booted iOS simulator> --watchpatch
each settled save into the running app. The process, its PID and every component's State are
kept; the CLI printspatched in N msand relaunches only onrestart required: <reason>; a
compile error leaves the app untouched;--no-hotkeeps the kill-and-relaunch loop. It works on
a stockfrust createapp. - Hot-patch targets: macOS, Linux and Windows x64 (MSVC) desktop, Android arm64-v8a and the arm64
iOS simulator. Not supported: a physical iOS device, the web, Windows on ARM64 and other Android
ABIs. - Hot patching in the TUI: debug desktop, Android-device and iOS-simulator sessions run hot by
default;rhot-patches now,Wtoggles the watcher,Ris a full rebuild and relaunch, and
oopens the run dialog with 'Run hot' and 'Auto-apply on save' (both on by default). - Hot-patch restart fallbacks, each printed as
restart required: <reason>and answered with a
fresh build and launch: a replayed type's layout change, a component State type change, a
manifest, build script, cargo config, toolchain orfrust.tomledit, a path-dependency edit on
Android or the iOS simulator, a proc macro or an uncaptured package, the patch budget (64
patches or 96 MiB per process by default, set by[hotpatch]infrust.toml) and a lost
apply reply. Seeno-hot-reload-restart-is-a-rebuildindocs/LIMITATIONS.md. - New crate
frust-hotpatch, the in-app hot-patch runtime (a port of Dioxus subsecond 0.7.10),
and the opt-infrust/hotpatchfeature, which a hot run switches on for its own build only. ScrollControllerforScrollViewandListView: programmaticjump_to, physics-driven
animate_to,scroll_to_itemwith settle-and-correct, offset publishing andon_change; a
command ends a live drag. shadcn'sscroll_areathumb can be dragged.- Drag and drop: a
DragCoordinator,draggablesources anddrag_targetdrop targets with
keyboard and accessibility parity, auto-scroll zones,reorderable_list, and desktop file drop
(InputEvent::FileDropwithFileDropEvent/FileDropPhase). frust::ViewSeq<State, M>— the child-sequence trait every list parameter now takes (Mis an
inference marker callers never name): implemented for a single view, tuples of 1–12 views (nest
tuples beyond 12),Vec<V>, arrays[V; N],Option<V>(Nonedrops its slot, like
.when_some()), and iterators wrapped inviews(iter); erasure happens once, inside the
container.frust::Either<L, R>— two-arm typed view composition witheither(cond, || l, || r)factory.
Swapping an arm rebuilds; both arms stay typed.frust::views(iter)— wraps an iterator as a child sequence. A bare.collect()passed to a
sequence API needs.collect::<Vec<_>>().- Generic view parameters for the icon and row APIs that still took erased closures:
button_group_icon_action,ButtonGroupAction::icon/checked_icon,
ButtonGroupView::overflow_iconandSplitButtonView::leading_icontakeFn() -> Vwith
V: View<State>; beui'sChatConversationView::extra_rowstakesFn() -> Seqwith
Seq: ViewSeq<State, M>; clean-signals-frust'sasync_viewtakes loading, data and error
closures returning any view and erases once inside. AnyView::newandany()are idempotent: erasing anAnyViewreturns it unchanged.- Fluent builders:
column()androw()withchild,flex,keyed,children,push,
whenandwhen_some;stack()withchild,children,whenandwhen_some. scripts/codemod/frust_any_codemod.pymigrates an app to the new idiom, including the opt-in
rule--t5that dropsany()from homogeneousvec![..]list arguments, and rules T6 and T7
(opt-in flags, enforced by the tripwire): T6 rewritesvec![..]arguments of sequence APIs to
tuples when all elements are erasure calls (1–12 of them); T7 converts helpers ending in one
erasure call to returnimpl View<S>instead ofAnyView<S>.scripts/ci/erasure-check.shguards the repository against redundant erasure (rules T1–T7 in
strict mode) and runs as a CI hygiene step; a site that must keepany()carries a
// erasure: keep <why>comment the tool honours.docs/PERFORMANCE_BASELINES.mdcollects the size and build-time measurements.
Changed
frust create's default template is a singlesrc/lib.rsholding the whole counter app: one
Componentwhosebuildis one declarative widget tree, helpers as associated functions called
Self::name(), andfrust::app!default-constructing the root;src/home_page.rsis gone. The
clean-signals template's pages pass genericasync_viewclosures with noany().Column,Row,Stackfluent builders and all layout/plugin list parameters (frust-material,
frust-beui,frust-shadcn,frust-glyph) accept any child sequence;Vec<_>callers compile
unchanged; iterator callers wrap inviews(..).- Helpers across the examples, benchmarks and the app template return
-> impl View<S>where the
result is not stored erased (-> AnyViewsignatures 1,159 → 592 tree-wide in the first pass);
the sites that must stay erased — stored view tables and page registries, accumulators into
Vec<AnyView>, three-plus-arm bodies, helpers feeding a hand-written view'sChildPod— keep
AnyViewand carry// erasure: keep <why>. - The example apps, benchmarks and templates carry no redundant erasure: every remaining
any()
inexamples/andbenchmarks/is marked// erasure: keep <why>, and the tripwire enforces it
(rule U, the codemod's--unmarkedcheck). - Templates, README and rustdoc present erasure at the API boundary and the fluent builders;
rustdoc examples and the crates.io READMEs are on the 0.6 idiom with 0.6 version pins. - frust-mcp's
restart_appdescription no longer claims Frust has no hot reload. - frust-engine bounds flattening cost for every finite input: huge viewport-straddling curves are
split at the viewport before flattening (the pre-flatten viewport split).
Fixed
- frust-mcp: rmcp 1.8 → 2.2, fixing the Streamable-HTTP session leak.
Measured
- Hot patch, save to patched frame against a full restart (medians): macOS desktop 1,397 ms vs
5,218 ms (1b1ef31); Pixel 5, stock app, 2,883 ms vs 13,458 ms (4c7e25b); Windows x64 1,468 ms
vs 10,861 ms (9011350); arm64 iOS simulator 1,694 ms vs 4,395 ms (6b82685). material3-demo on
a Pixel 5 patches in 3.5 s (median over four body-only edits), down from 58-72 s (80663b2). - Cost of erasure at the API boundary (medians of 3 on one host, main 79686b6 → f79b739; details
indocs/PERFORMANCE_BASELINES.md): stripped arm64.sohuddle +0.12 %, material3-demo
+0.34 %; clean release rebuild frust-gallery +1.6 % (an earlier run at the pull-request head
measured +3.4 %), material3-demo −2.9 %. Build-time deltas of this size sit inside the host's
run-to-run noise; the size figures are exact. - Cost of child sequences and helper return types (i5-12600 Linux, main 721ab1e →
feature/erasure-free-apps b2a4133; seedocs/PERFORMANCE_BASELINES.md): stripped arm64.so
(material3-demo) 14,643,024 B → 14,658,576 B (+0.11 %; bar ≤ 2 %); clean release rebuild
frust-gallery 14.42 s → 13.74 s (−4.7 %; bar ≤ 10 %), material3demo 129.02 s → 125.77 s
(−2.5 %; bar ≤ 10 %).
Migration
- Change every
impl ComponentwhosebuildreturnsAnyView<..>to-> impl View<Self::State>;
the body may keepany(..)for now. - Where the app writes its own driver closure, erase the build result there:
move |s| AnyView::new(root.build(s))(frust::run,frust::app!and the templates already
do). - Run
python3 -I scripts/codemod/frust_any_codemod.py --write --t5 --t6 --t7 <src dirs>, then
cargo fmtandcargo check. - Fix what the compiler reports: give a type to any bare
None, emptyvec![], turbofish call or
.collect()it rejects (a.collect()passed to a sequence API gets::<Vec<_>>(), a turbofish
caller of a plugin list function writes::<State, _>, one ofbutton_group_icon_actionadds a
_); a caller whose result now lands in aVec<AnyView>or amatcharm is re-erased with
any(..)(or its list becomes a tuple); two-arm bodies useeither(cond, || a, || b). Only a
site that must stay erased (a stored view table, an accumulator intoVec<AnyView>, a
three-plus-arm body, aChildPod-fed helper, a same-head list whose elements differ in type)
keepsany(). Re-run the codemod after each fix until--check --t6 --t7is quiet. - Replace
dyn Componentwith generics or storedAnyViews; seedocs/LIMITATIONS.mdfor the
two behaviours listed under Breaking. - Run
scripts/ci/erasure-check.sh; mark required erasure with// erasure: keep <why>.