diff --git a/Sources/Backend/GTK4/Rendering/GTKViewHost.swift b/Sources/Backend/GTK4/Rendering/GTKViewHost.swift index 2a7f15c..8d0164a 100644 --- a/Sources/Backend/GTK4/Rendering/GTKViewHost.swift +++ b/Sources/Backend/GTK4/Rendering/GTKViewHost.swift @@ -236,19 +236,46 @@ public class GTKViewHost: AnyViewHost, DependencyTrackingHost { observationDidFire = false lock.unlock() - // --- Narrow mutation path: try text/color in-place update --- - // Skipped when withObservationTracking's onChange fired — the narrow - // path returns without re-running body under withObservationTracking, - // which would leave @Observable subscriptions dead after the first - // change. Fall through to the full rebuild so observation re-registers. - if !fromObservation, - let describeBody = describeBody, + // --- Narrow mutation path: try text/color/canvas in-place update --- + // Applied for both @State- and @Observable-driven changes. A full + // rebuild tears down the widget tree, which cancels any in-flight + // gesture (e.g. a drag on a Canvas knob); the narrow path mutates in + // place and preserves it. For an @Observable change the describe pass + // below is run under `withObservationTracking`, so re-reading the + // observed properties re-registers the one-shot subscription — the same + // re-registration the full rebuild gets from `buildBodyWithTracking`, + // without the teardown. If the change is not narrow-applicable we fall + // through to the full rebuild, which re-registers observation as before. + if let describeBody = describeBody, let oldRetained = lastRetainedDescriptor, let oldExecutor = retainedExecutor { let previousEnv = getCurrentEnvironment() installRebuildEnvironment() - let described = gtkDescribeCapturingCanvasPayloads(describeBody) + let described: (descriptor: GTK4DescriptorNode, canvasPayloads: [GTK4CanvasPayload]) + if fromObservation { + #if canImport(Observation) + if #available(macOS 14.0, iOS 17.0, *) { + var captured: (descriptor: GTK4DescriptorNode, canvasPayloads: [GTK4CanvasPayload])! + withObservationTracking { + captured = gtkDescribeCapturingCanvasPayloads(describeBody) + } onChange: { [weak self] in + guard let self else { return } + self.lock.lock() + self.observationDidFire = true + self.lock.unlock() + self.scheduleRebuild() + } + described = captured + } else { + described = gtkDescribeCapturingCanvasPayloads(describeBody) + } + #else + described = gtkDescribeCapturingCanvasPayloads(describeBody) + #endif + } else { + described = gtkDescribeCapturingCanvasPayloads(describeBody) + } setCurrentEnvironment(previousEnv) let newIdentified = gtkIdentifyDescriptorTree(described.descriptor)