fix: Clear preference callbacks per render pass to prevent accumulation (H.4)

Add PreferenceStorage.beginRenderPass() that clears callbacks and resets
the value stack at the start of each frame. Without this, every render
pass appended duplicate callbacks via OnPreferenceChangeModifier, causing
N callbacks after N renders — each fired on every setValue() call.

Called from RenderLoop.render() alongside the existing clearHandlers()
and focusManager.clear() calls. Follows the same per-frame reset pattern.

Remove unused clearCallbacks() method (dead code, never called).
This commit is contained in:
phranck
2026-01-30 15:34:38 +01:00
parent 241fb9e755
commit 7d1201d76d
2 changed files with 11 additions and 3 deletions
+2 -1
View File
@@ -50,8 +50,9 @@ internal struct RenderLoop<A: App> {
/// 4. Ends lifecycle tracking (triggers `onDisappear` for removed views)
/// 5. Renders the status bar at the bottom
func render() {
// Clear event handlers before re-rendering
// Clear per-frame state before re-rendering
tuiContext.keyEventDispatcher.clearHandlers()
tuiContext.preferences.beginRenderPass()
focusManager.clear()
// Begin lifecycle tracking for this render pass
+9 -2
View File
@@ -174,12 +174,19 @@ public final class PreferenceStorage: @unchecked Sendable {
callbacks[keyId]?.append(wrappedCallback)
}
/// Clears all callbacks.
public func clearCallbacks() {
/// Prepares preference storage for a new render pass.
///
/// Clears all accumulated callbacks and resets the value stack
/// to a single empty context. Called at the start of each frame
/// by ``RenderLoop/render()`` to prevent callback accumulation.
public func beginRenderPass() {
callbacks.removeAll()
stack = [PreferenceValues()]
}
/// Resets all preference state.
///
/// Called once during app shutdown by ``TUIContext/reset()``.
public func reset() {
stack = [PreferenceValues()]
callbacks.removeAll()