From 7d1201d76d4e98be00cc682bedffbae867eac674 Mon Sep 17 00:00:00 2001 From: phranck Date: Fri, 30 Jan 2026 15:34:38 +0100 Subject: [PATCH] fix: Clear preference callbacks per render pass to prevent accumulation (H.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- Sources/TUIKit/App/RenderLoop.swift | 3 ++- Sources/TUIKit/Core/Preferences.swift | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Sources/TUIKit/App/RenderLoop.swift b/Sources/TUIKit/App/RenderLoop.swift index f836662..3f274ee 100644 --- a/Sources/TUIKit/App/RenderLoop.swift +++ b/Sources/TUIKit/App/RenderLoop.swift @@ -50,8 +50,9 @@ internal struct RenderLoop { /// 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 diff --git a/Sources/TUIKit/Core/Preferences.swift b/Sources/TUIKit/Core/Preferences.swift index 011b5b5..7f763d6 100644 --- a/Sources/TUIKit/Core/Preferences.swift +++ b/Sources/TUIKit/Core/Preferences.swift @@ -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()