Repository navigation
1.5.0
Changed
-
gap-*on a flex container goes toFlex.spacinginstead of injecting aSizedBoxbetween every pair of children. Spacing lands in the render object, so the gap costs no widget. Measured driving a real app,SizedBoxwas the most numerous wrapper in it at 1.06 perWDivbuilt and is now 0.23. The swap is deliberately NOT applied underjustify-aroundorjustify-evenly, which divide free space by the number of children: there the injected gaps are load-bearing, and handing them tospacingwould move every real child (measured 138.7px apart against 118px on a three-child row). Those two keep the old path, andgap_spacing_equivalence_test.dartpins child DISTANCES under all six alignments, since a symmetry check passes under both mechanisms and would prove nothing. (lib/src/widgets/w_div.dart) -
WindFlexOverflowScopeis published only when its value differs from what the context already answers. Both readers resolve it asmaybeOf(context)?.skipExpanded ?? false, so a scope repeating the inherited value is indistinguishable from no scope at all, and there were 0.62 of them perWDivon screens with no main-axis-scrollable flex anywhere. A comparison rather than a delete: afalsepublished under an ancestortrueis a genuine shadow, and dropping it would leak the ancestor'strueinto a subtree that has to readfalse. (lib/src/widgets/w_div.dart) -
Two tests that asserted the gap MECHANISM now assert the outcome. They checked for a
SizedBoxin theRow's children list and that it was not wrapped inFlexible; both now assert the 16px distancegap-4actually promises, which survives the next change to how the gap is produced. (test/widgets/w_div_test.dart,test/widgets/w_div/flex_shrink_test.dart)
Added
- Opt-in counters for the parse path, published through the diagnostics contract, so the cost of Wind's hottest code is measurable rather than argued about.
WindParser.parseruns on every build of every W-widget, and until now the only thing anyone could say about its cache was a guess.WindPerfCounterscounts THREE outcomes, not the usual two, and the third is the point: a HIT is a keyed lookup that landed, a MISS is a keyed lookup that did not and then filled the slot, and a BYPASS is a call that never consulted the cache at all because it carried abaseStyle. A bypass is a property of a CALLER writingstyle:, not of usingWDivorWText: both passbaseStyle: style, andstyleis the widget's own nullable property, null in ordinary use, so those calls take the cached path. Driven against a real app, bypasses measured zero across 1613 W-widget builds and the hit rate was 99% or better; counting the three outcomes separately is what turned that from an argument into a number.WDivandWTextbuild counts sit alongside them. Everything is off by default:WindPerfCounters.enabledisfalse, every increment is behindif (!enabled) return;, and a disabled counter costs one static bool load.WindParser.clearCache()now resets the counters too, so a hit rate is always reported against the cache it was measured on, and no existing test needed a line changed to stay isolated.Wind.installPerfResolver()publishes the six-key aggregate (cacheHits,cacheMisses,cacheBypasses,cacheSize,wDivBuilds,wTextBuilds) into the new perf slot onWindDebugRegistry, where a debug tool reads it without importing wind. The recording methods are@internal: onlyenabled, the getters andinstallPerfResolver()are consumer surface, and semver should not pin the rest. (lib/src/utils/wind_perf_counters.dart,lib/src/parser/wind_parser.dart,lib/src/wind_facade.dart,lib/src/widgets/w_div.dart,lib/src/widgets/w_text.dart,lib/fluttersdk_wind.dart,doc/core-concepts/debugging.md,llms.txt,skills/wind-ui/)
Fixed
-
A
WDynamicnode carrying anidand noonChangenever wrote to state, so the form-state example in this project's own documentation could not work. The class doc says "widgets with anidprop automatically bind toWDynamicState" and.claude/rules/dynamic.mdrepeats it, but the write lived inside the callbackparseValueActionreturns, and that returnsnullunlessprops.onChangeis a Map carrying a Stringaction. So the binding was a side effect of having an action rather than of having an id. Measured on the worked example printed indoc/core-concepts/dynamic-rendering.mdand demoed on the gallery's Form State Management section, an input with'id': 'username'marked// Auto-trackednext to a button whose handler readsstate.get('username'): typing a name and pressing the button has always answeredHello, Guest!.WDatePickerwas the only one of the four that honoured the contract, because it callsstate.set(id, date)directly instead of routing through the parsed action.WInput,WCheckboxandWSelectnow resolve their callback through one_valueBinding<T>helper: the parsed action when the node carries a usable one (which already writes state first), a plainstate.set(id, value)when it carries only anid, and null when it carries neither, so a node with nowhere to put a value stays display-only rather than becoming interactive. The three call sites are what earned the helper;WDatePickerkeeps its direct write, since itsonChangegoes throughparseActionand delivers no_value. (lib/src/dynamic/w_dynamic_renderer.dart,doc/core-concepts/dynamic-rendering.md,skills/wind-ui/references/dynamic.md) -
A state write never reached the screen, because nothing in the render path was subscribed to
WDynamicState. The write half above landed without its read half: every value-bearing builder decides what to display by readingstate.get(id), and_WDynamicState.build()calledrenderer.build(json)once and never listened, sonotifyListeners()reached nobody. The only subscriber anywhere wasWDynamicController, which is host-facing. The visible result on{"type": "WCheckbox", "props": {"id": "agree"}}: the box wrotetrueon its first tap, kept rendering unchecked, and could never be unticked, because the second tap sent!false=trueagain andWDynamicState.setearly-returns on an unchanged value.WInputhid it by owning aTextEditingController. Anid+onChangecheckbox was broken the same way and had been since before the write half existed, sinceparseValueActionalso writes to a store nothing was reading.WDynamicnow listens to the state and rebuilds the whole rendered tree on any write, which is what the renderer's class doc and.claude/rules/dynamic.mdhave promised all along; both were also wrong about the MECHANISM, namingaddIdListener, which is the host-facing per-idwatch behindWDynamicController.addListenerand was never on the way to a build. The listener comes off indisposebefore the_ownsStatecheck, because a state arriving through a controller outlives the widget that borrowed it. Expect a rebuild of the JSON tree per keystroke on anid-boundWInput; the cursor does not move, because the value written to state is the text the controller already holds anddidUpdateWidget'swidget.value != _controller.textguard short-circuits. (lib/src/dynamic/w_dynamic.dart,lib/src/dynamic/w_dynamic_renderer.dart,.claude/rules/dynamic.md)