Skip to content

1.5.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 21:22
· 37 commits to master since this release
1c9b9a9

Changed

  • gap-* on a flex container goes to Flex.spacing instead of injecting a SizedBox between every pair of children. Spacing lands in the render object, so the gap costs no widget. Measured driving a real app, SizedBox was the most numerous wrapper in it at 1.06 per WDiv built and is now 0.23. The swap is deliberately NOT applied under justify-around or justify-evenly, which divide free space by the number of children: there the injected gaps are load-bearing, and handing them to spacing would move every real child (measured 138.7px apart against 118px on a three-child row). Those two keep the old path, and gap_spacing_equivalence_test.dart pins child DISTANCES under all six alignments, since a symmetry check passes under both mechanisms and would prove nothing. (lib/src/widgets/w_div.dart)

  • WindFlexOverflowScope is published only when its value differs from what the context already answers. Both readers resolve it as maybeOf(context)?.skipExpanded ?? false, so a scope repeating the inherited value is indistinguishable from no scope at all, and there were 0.62 of them per WDiv on screens with no main-axis-scrollable flex anywhere. A comparison rather than a delete: a false published under an ancestor true is a genuine shadow, and dropping it would leak the ancestor's true into a subtree that has to read false. (lib/src/widgets/w_div.dart)

  • Two tests that asserted the gap MECHANISM now assert the outcome. They checked for a SizedBox in the Row's children list and that it was not wrapped in Flexible; both now assert the 16px distance gap-4 actually 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.parse runs on every build of every W-widget, and until now the only thing anyone could say about its cache was a guess. WindPerfCounters counts 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 a baseStyle. A bypass is a property of a CALLER writing style:, not of using WDiv or WText: both pass baseStyle: style, and style is 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. WDiv and WText build counts sit alongside them. Everything is off by default: WindPerfCounters.enabled is false, every increment is behind if (!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 on WindDebugRegistry, where a debug tool reads it without importing wind. The recording methods are @internal: only enabled, the getters and installPerfResolver() 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 WDynamic node carrying an id and no onChange never wrote to state, so the form-state example in this project's own documentation could not work. The class doc says "widgets with an id prop automatically bind to WDynamicState" and .claude/rules/dynamic.md repeats it, but the write lived inside the callback parseValueAction returns, and that returns null unless props.onChange is a Map carrying a String action. So the binding was a side effect of having an action rather than of having an id. Measured on the worked example printed in doc/core-concepts/dynamic-rendering.md and demoed on the gallery's Form State Management section, an input with 'id': 'username' marked // Auto-tracked next to a button whose handler reads state.get('username'): typing a name and pressing the button has always answered Hello, Guest!. WDatePicker was the only one of the four that honoured the contract, because it calls state.set(id, date) directly instead of routing through the parsed action. WInput, WCheckbox and WSelect now resolve their callback through one _valueBinding<T> helper: the parsed action when the node carries a usable one (which already writes state first), a plain state.set(id, value) when it carries only an id, 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; WDatePicker keeps its direct write, since its onChange goes through parseAction and 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 reading state.get(id), and _WDynamicState.build() called renderer.build(json) once and never listened, so notifyListeners() reached nobody. The only subscriber anywhere was WDynamicController, which is host-facing. The visible result on {"type": "WCheckbox", "props": {"id": "agree"}}: the box wrote true on its first tap, kept rendering unchecked, and could never be unticked, because the second tap sent !false = true again and WDynamicState.set early-returns on an unchanged value. WInput hid it by owning a TextEditingController. An id + onChange checkbox was broken the same way and had been since before the write half existed, since parseValueAction also writes to a store nothing was reading. WDynamic now 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.md have promised all along; both were also wrong about the MECHANISM, naming addIdListener, which is the host-facing per-id watch behind WDynamicController.addListener and was never on the way to a build. The listener comes off in dispose before the _ownsState check, because a state arriving through a controller outlives the widget that borrowed it. Expect a rebuild of the JSON tree per keystroke on an id-bound WInput; the cursor does not move, because the value written to state is the text the controller already holds and didUpdateWidget's widget.value != _controller.text guard short-circuits. (lib/src/dynamic/w_dynamic.dart, lib/src/dynamic/w_dynamic_renderer.dart, .claude/rules/dynamic.md)