Skip to content

Commit 36e2b12

Browse files
committed
fixed scroll offset trouble (viewport)
1 parent 61ab7c0 commit 36e2b12

File tree

5 files changed

+94
-72
lines changed

5 files changed

+94
-72
lines changed

Examples/Basic/Sources/App/Views.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ struct App {
6868
}
6969
}
7070
}
71-
div(.style(["display": "flex", "flex-direction": "column"])) {
71+
div(.style(["display": "flex", "flex-direction": "column", "border": "1px solid red"])) {
7272
ForEach(counters, key: { String($0) }) { counter in
7373
div(.style(["display": "flex", "flex-direction": "column"])) {
7474
h3 { "Counter \(counter)" }

Sources/ElementaryDOM/DOM/DOMInteractor.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public enum DOM {
3030
func getOffsetParent(_ node: Node) -> Node?
3131

3232
// Scroll offset API for FLIP animations
33-
func getScrollOffset() -> (x: Float, y: Float)
33+
func getScrollOffset() -> (x: Double, y: Double)
3434

3535
// Low-level DOM-like event listener APIs
3636
func addEventListener(_ node: Node, event: String, sink: EventSink)

Sources/ElementaryDOM/FLIP/FLIPLayoutObserver.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
final class FLIPLayoutObserver: DOMLayoutObserver {
22
private var childNodes: [DOM.Node] = []
3-
private var containerNode: DOM.Node?
43
private var animateContainerSize: Bool
54

65
init(animateContainerSize: Bool) {
@@ -44,7 +43,10 @@ final class FLIPLayoutObserver: DOMLayoutObserver {
4443
}
4544

4645
func unmount(_ context: inout _CommitContext) {
47-
// TODO: figure this out
46+
for node in childNodes {
47+
context.scheduler.flip.markAsRemoved(node)
48+
}
49+
childNodes = []
4850
}
4951
}
5052

Sources/ElementaryDOM/FLIP/FLIPScheduler.swift

Lines changed: 83 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ final class FLIPScheduler {
44
// NOTE: extend this to support css properties as well - for now it is always the bounding rect stuff
55
private var scheduledAnimations: [DOM.Node: ScheduledNode] = [:]
66
private var runningAnimations: [DOM.Node: GeometryAnimation] = [:]
7+
private var firstWindowScrollOffset: (x: Double, y: Double)? = nil
78

89
init(dom: any DOM.Interactor) {
910
self.dom = dom
1011
}
1112

1213
func scheduleAnimationOf(_ nodes: [DOM.Node], inParent parentNode: DOM.Node, context: inout _RenderContext) {
13-
logTrace("scheduling FLIP animations for \(nodes.count) nodes, animation: \(context.transaction.animation != nil)")
14+
if firstWindowScrollOffset == nil {
15+
storeWindowScrollOffset()
16+
}
17+
1418
let parentRect = dom.getBoundingClientRect(parentNode)
1519
for node in nodes {
1620
guard !scheduledAnimations.keys.contains(node) else {
@@ -27,6 +31,10 @@ final class FLIPScheduler {
2731
}
2832

2933
func scheduleAnimationOf(_ node: DOM.Node, context: inout _RenderContext) {
34+
if firstWindowScrollOffset == nil {
35+
storeWindowScrollOffset()
36+
}
37+
3038
scheduledAnimations[node] = ScheduledNode(
3139
transaction: context.transaction,
3240
geometry: getNodeGeometry(node),
@@ -50,14 +58,26 @@ final class FLIPScheduler {
5058
}
5159

5260
func commitScheduledAnimations(context: inout _CommitContext) {
61+
let scroll = dom.getScrollOffset()
62+
let firstWindowScroll = firstWindowScrollOffset ?? (x: Double(scroll.x), y: Double(scroll.y))
63+
self.firstWindowScrollOffset = nil
5364

5465
commitPreMeasurementChanges(context: &context)
5566

56-
measureLastAndCreateAnimations(context: &context)
67+
let lastScroll = dom.getScrollOffset()
68+
let lastWindowScroll = (x: Double(lastScroll.x), y: Double(lastScroll.y))
69+
let windowScrollDelta = (x: lastWindowScroll.x - firstWindowScroll.x, y: lastWindowScroll.y - firstWindowScroll.y)
70+
71+
measureLastAndCreateAnimations(windowScrollDelta: windowScrollDelta, context: &context)
5772

5873
progressAllAnimations(context: &context)
5974
}
6075

76+
private func storeWindowScrollOffset() {
77+
let scroll = dom.getScrollOffset()
78+
self.firstWindowScrollOffset = (x: Double(scroll.x), y: Double(scroll.y))
79+
}
80+
6181
private func commitPreMeasurementChanges(context: inout _CommitContext) {
6282

6383
for (node, animation) in scheduledAnimations {
@@ -70,38 +90,14 @@ final class FLIPScheduler {
7090
case .none:
7191
continue
7292
case .moveAbsolute(let rect):
73-
// Extract current style values before setting new ones
74-
let stylePosition = context.dom.makeStyleAccessor(node, cssName: "position")
75-
let styleLeft = context.dom.makeStyleAccessor(node, cssName: "left")
76-
let styleTop = context.dom.makeStyleAccessor(node, cssName: "top")
77-
let styleWidth = context.dom.makeStyleAccessor(node, cssName: "width")
78-
let styleHeight = context.dom.makeStyleAccessor(node, cssName: "height")
79-
80-
// Extract previous style values for later reversal
81-
let previousValues = PreviousStyleValues(
82-
position: stylePosition.get(),
83-
left: styleLeft.get(),
84-
top: styleTop.get(),
85-
width: styleWidth.get(),
86-
height: styleHeight.get()
87-
)
93+
let previousValues = context.dom.fixAbsolutePosition(node, toRect: rect)
8894
// TODO: store previousValues for reversal when animation completes
8995
_ = previousValues
90-
91-
logTrace(
92-
"setting position of node \(node) to absolute, left: \(rect.x)px, top: \(rect.y)px, width: \(rect.width)px, height: \(rect.height)px"
93-
)
94-
95-
stylePosition.set("absolute")
96-
styleLeft.set("\(rect.x)px")
97-
styleTop.set("\(rect.y)px")
98-
styleWidth.set("\(rect.width)px")
99-
styleHeight.set("\(rect.height)px")
10096
}
10197
}
10298
}
10399

104-
private func measureLastAndCreateAnimations(context: inout _CommitContext) {
100+
private func measureLastAndCreateAnimations(windowScrollDelta: (x: Double, y: Double), context: inout _CommitContext) {
105101
// measures all last states and calculates all new animations
106102

107103
// parent rect cache
@@ -130,6 +126,7 @@ final class FLIPScheduler {
130126
node: node,
131127
first: animation.geometry,
132128
last: lastGeometry,
129+
windowScrollDelta: windowScrollDelta,
133130
transaction: animation.transaction,
134131
frameTime: context.currentFrameTime
135132
)
@@ -138,7 +135,7 @@ final class FLIPScheduler {
138135
scheduledAnimations.removeAll()
139136
}
140137

141-
func progressAllAnimations(context: inout _CommitContext) {
138+
private func progressAllAnimations(context: inout _CommitContext) {
142139
// applies all changes of dirty animations and removes completed ones
143140
// TODO: optimize
144141
var removedNodes: [DOM.Node] = []
@@ -186,11 +183,24 @@ private extension FLIPScheduler {
186183
var width: Double
187184
var height: Double
188185

189-
var scopedCoordinates: (x: Double, y: Double) {
190-
if let parentRect = parentRect {
191-
return (x: boundingClientRect.x - parentRect.x, y: boundingClientRect.y - parentRect.y)
186+
func difference(
187+
from other: NodeGeometry,
188+
scrollDelta: (x: Double, y: Double)
189+
) -> (x: Double, y: Double, width: Double, height: Double) {
190+
if let parentRect = parentRect, let otherParentRect = other.parentRect {
191+
return (
192+
x: self.boundingClientRect.x - other.boundingClientRect.x - parentRect.x + otherParentRect.x,
193+
y: self.boundingClientRect.y - other.boundingClientRect.y - parentRect.y + otherParentRect.y,
194+
width: width - other.width,
195+
height: height - other.height
196+
)
192197
} else {
193-
return (x: boundingClientRect.x, y: boundingClientRect.y)
198+
return (
199+
x: self.boundingClientRect.x - other.boundingClientRect.x - scrollDelta.x,
200+
y: self.boundingClientRect.y - other.boundingClientRect.y - scrollDelta.y,
201+
width: width - other.width,
202+
height: height - other.height
203+
)
194204
}
195205
}
196206
// NOTE: extend with transform/rotate or other stuff
@@ -205,13 +215,15 @@ private extension FLIPScheduler {
205215
translation == nil && width == nil && height == nil
206216
}
207217

208-
init(node: DOM.Node, first: NodeGeometry, last: NodeGeometry, transaction: Transaction, frameTime: Double) {
209-
let firstCoordinates = first.scopedCoordinates
210-
let lastCoordinates = last.scopedCoordinates
211-
let dx = firstCoordinates.x - lastCoordinates.x
212-
let dy = firstCoordinates.y - lastCoordinates.y
213-
let dw = last.width - first.width
214-
let dh = last.height - first.height
218+
init(
219+
node: DOM.Node,
220+
first: NodeGeometry,
221+
last: NodeGeometry,
222+
windowScrollDelta: (x: Double, y: Double),
223+
transaction: Transaction,
224+
frameTime: Double
225+
) {
226+
let (dx, dy, dw, dh) = first.difference(from: last, scrollDelta: windowScrollDelta)
215227

216228
// TODO: implement scale animation as option
217229

@@ -280,18 +292,6 @@ fileprivate extension FLIPScheduler {
280292
func getNodeGeometry(_ node: DOM.Node, scopedTo parentRect: DOM.Rect? = nil) -> NodeGeometry {
281293
let rect = dom.getBoundingClientRect(node)
282294

283-
// TODO: figure out how to do this in embedded
284-
// let computedStyle = dom.makeComputedStyleAccessor(node)
285-
286-
// // Parse width and height from computed styles (e.g., "100px" -> 100.0)
287-
// let widthString = computedStyle.get("width")
288-
// let heightString = computedStyle.get("height")
289-
290-
// logTrace("width: \(widthString), height: \(heightString)")
291-
292-
// let width = parseCSSLength(widthString) ?? rect.width
293-
// let height = parseCSSLength(heightString) ?? rect.height
294-
295295
let width = rect.width
296296
let height = rect.height
297297

@@ -302,18 +302,6 @@ fileprivate extension FLIPScheduler {
302302
height: height
303303
)
304304
}
305-
306-
func parseCSSLength(_ value: String) -> Double? {
307-
nil
308-
309-
// guard !value.isEmpty, value != "auto" else { return nil }
310-
// // Remove "px" suffix and parse as Double
311-
// if value.hasSuffix("px") {
312-
// let numberString = String(value.dropLast(2))
313-
// return Double(numberString)
314-
// }
315-
// return nil
316-
}
317305
}
318306

319307
private func shouldAnimateSizeDelta(_ ds: Double) -> Bool {
@@ -344,3 +332,34 @@ extension DOM.Interactor {
344332
return nodeRect
345333
}
346334
}
335+
336+
private extension DOM.Interactor {
337+
func fixAbsolutePosition(_ node: DOM.Node, toRect rect: DOM.Rect) -> FLIPScheduler.PreviousStyleValues {
338+
let stylePosition = makeStyleAccessor(node, cssName: "position")
339+
let styleLeft = makeStyleAccessor(node, cssName: "left")
340+
let styleTop = makeStyleAccessor(node, cssName: "top")
341+
let styleWidth = makeStyleAccessor(node, cssName: "width")
342+
let styleHeight = makeStyleAccessor(node, cssName: "height")
343+
344+
// Extract previous style values for later reversal
345+
let previousValues = FLIPScheduler.PreviousStyleValues(
346+
position: stylePosition.get(),
347+
left: styleLeft.get(),
348+
top: styleTop.get(),
349+
width: styleWidth.get(),
350+
height: styleHeight.get()
351+
)
352+
353+
logTrace(
354+
"setting position of node \(node) to absolute, left: \(rect.x)px, top: \(rect.y)px, width: \(rect.width)px, height: \(rect.height)px"
355+
)
356+
357+
stylePosition.set("absolute")
358+
styleLeft.set("\(rect.x)px")
359+
styleTop.set("\(rect.y)px")
360+
styleWidth.set("\(rect.width)px")
361+
styleHeight.set("\(rect.height)px")
362+
363+
return previousValues
364+
}
365+
}

Sources/ElementaryDOM/Interop/Elementary+JavaScriptKit.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,11 +270,12 @@ final class JSKitDOMInteractor: DOM.Interactor {
270270
jsPerformance.now!().number! / 1000
271271
}
272272

273-
func getScrollOffset() -> (x: Float, y: Float) {
273+
func getScrollOffset() -> (x: Double, y: Double) {
274274
let window = JSObject.global.window.object!
275-
let scrollX = window.scrollX.number ?? 0
276-
let scrollY = window.scrollY.number ?? 0
277-
return (x: Float(scrollX), y: Float(scrollY))
275+
return (
276+
x: window.scrollX.number ?? 0,
277+
y: window.scrollY.number ?? 0
278+
)
278279
}
279280
}
280281

0 commit comments

Comments
 (0)