Skip to content

Commit

Permalink
fix: fix coordinate conversion related to position fixed.
Browse files Browse the repository at this point in the history
  • Loading branch information
andycall committed Nov 12, 2024
1 parent d03f421 commit aeaec89
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 12 deletions.
3 changes: 2 additions & 1 deletion bridge/core/binding_call_methods.json5
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
"dir",
"pageXOffset",
"pageYOffset",
"title"
"title",
"__test_global_to_local__"
]
}
12 changes: 12 additions & 0 deletions bridge/core/dom/element.cc
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,18 @@ ScriptPromise Element::toBlob(double device_pixel_ratio, ExceptionState& excepti
return resolver->Promise();
}

ScriptValue Element::___testGlobalToLocal__(double x, double y, webf::ExceptionState& exception_state) {
const NativeValue args[] = {
NativeValueConverter<NativeTypeDouble>::ToNativeValue(x),
NativeValueConverter<NativeTypeDouble>::ToNativeValue(y),
};

NativeValue result = InvokeBindingMethod(binding_call_methods::k__test_global_to_local__, 2, args,
FlushUICommandReason::kDependentsOnElement | FlushUICommandReason::kDependentsOnLayout, exception_state);

return ScriptValue(ctx(), result);
}

void Element::DidAddAttribute(const AtomicString& name, const AtomicString& value) {}

void Element::WillModifyAttribute(const AtomicString& name,
Expand Down
2 changes: 2 additions & 0 deletions bridge/core/dom/element.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,7 @@ interface Element extends Node, ParentNode, ChildNode {
// WebF special API.
toBlob(devicePixelRatioValue?: double): Promise<ArrayBuffer>;

__testGlobalToLocal__(x: number, y: number): any;

new(): void;
}
2 changes: 2 additions & 0 deletions bridge/core/dom/element.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ class Element : public ContainerNode {
ScriptPromise toBlob(double device_pixel_ratio, ExceptionState& exception_state);
ScriptPromise toBlob(ExceptionState& exception_state);

ScriptValue ___testGlobalToLocal__(double x, double y, ExceptionState& exception_state);

void DidAddAttribute(const AtomicString&, const AtomicString&);
void WillModifyAttribute(const AtomicString&, const AtomicString& old_value, const AtomicString& new_value);
void DidModifyAttribute(const AtomicString&,
Expand Down
100 changes: 93 additions & 7 deletions integration_tests/specs/dom/nodes/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,96 @@ describe('DOM Element API', () => {

});

it('should work with scroll with fixed elements', async () => {
const style = document.createElement('style');
style.innerHTML = `.container {
margin: 64px 0 32px;
text-align: center;
padding-top: 1000px;
padding-bottom: 300px;
background: linear-gradient(to right, #ff7e5f, #feb47b);
}`;
document.head.appendChild(style);

const container = createElement('div', {
className: 'container',
}, [
createElement('div', {
style: {
position: 'fixed',
top: '300px',
left: 0,
}
}, [
createElement('div', {
id: 'box'
}, [
createText('click me')
])
])
]);

BODY.append(container);

const clickBox = document.querySelector('#box');

const rect1 = clickBox?.getBoundingClientRect();

window.scrollTo(0, 200);

const rect2 = clickBox?.getBoundingClientRect();

expect(JSON.stringify(rect1)).toEqual(JSON.stringify(rect2));
});

it('should works with globalToLocal transform with position fixed layout', async () => {
const style = document.createElement('style');
style.innerHTML = `.container {
margin: 64px 0 32px;
text-align: center;
padding-top: 1000px;
padding-bottom: 300px;
background: linear-gradient(to right, #ff7e5f, #feb47b);
}`;
document.head.appendChild(style);

const container = createElement('div', {
className: 'container',
}, [
createElement('div', {
style: {
position: 'fixed',
top: '300px',
left: 0,
}
}, [
createElement('div', {
id: 'box'
}, [
createText('click me')
])
])
]);

BODY.append(container);

const clickBox = document.querySelector('#box');
const rect = clickBox?.getBoundingClientRect();

// @ts-ignore
const offset1 = clickBox?.___testGlobalToLocal__(rect.x, rect.y + 10);

window.scrollTo(0, 200);

// @ts-ignore
const offset2 = clickBox?.___testGlobalToLocal__(rect?.x, rect.y + 10);
expect(JSON.stringify(offset1)).toEqual(JSON.stringify(offset2));
});

it('should works when getting multiple zero rects', () => {
const div = document.createElement('div');
expect(JSON.parse(JSON.stringify(div.getBoundingClientRect()))).toEqual({bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0});
expect(JSON.parse(JSON.stringify(div.getBoundingClientRect()))).toEqual({bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0});
expect(JSON.parse(JSON.stringify(div.getBoundingClientRect()))).toEqual({ bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0 });
expect(JSON.parse(JSON.stringify(div.getBoundingClientRect()))).toEqual({ bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0 });
});

it('children should only contain elements', () => {
Expand Down Expand Up @@ -120,7 +206,7 @@ describe('DOM Element API', () => {

el.appendChild(document.createTextNode('text'));
el.appendChild(document.createComment('comment'));
for (let i = 0; i < 20; i ++) {
for (let i = 0; i < 20; i++) {
el.appendChild(document.createElement('span'));
}

Expand All @@ -132,7 +218,7 @@ describe('DOM Element API', () => {
const el = document.createElement('div');
document.body.appendChild(el);

for (let i = 0; i < 20; i ++) {
for (let i = 0; i < 20; i++) {
el.appendChild(document.createElement('span'));
}
el.appendChild(document.createTextNode('text'));
Expand Down Expand Up @@ -174,16 +260,16 @@ describe('DOM Element API', () => {
el.appendChild(document.createElement('span'));
var target = el.lastElementChild?.parentElement;
expect(target.tagName).toEqual('DIV');

let childDiv = document.createDocumentFragment().appendChild(document.createElement('div'));
expect(childDiv.parentElement).toEqual(null);

expect(document.documentElement.parentElement).toEqual(null);
});
});

describe('children', () => {
test(function() {
test(function () {
var container = document.createElement('div');
container.innerHTML = '<img id=foo><img id=foo><img name="bar">';
var list = container.children;
Expand Down
41 changes: 41 additions & 0 deletions integration_tests/specs/dom/nodes/event-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,45 @@ describe('DOM EventTarget', () => {

});

it('should work with scroll with fixed elements', async (done) => {
const style = document.createElement('style');
style.innerHTML = `.container {
margin: 64px 0 32px;
text-align: center;
padding-top: 1000px;
padding-bottom: 300px;
background: linear-gradient(to right, #ff7e5f, #feb47b);
}`;
document.head.appendChild(style);

const container = createElement('div', {
className: 'container',
}, [
createElement('div', {
style: {
position: 'fixed',
top: '300px',
left: 0,
}
}, [
createElement('div', {
id: 'box'
}, [
createText('click me')
])
])
]);

BODY.append(container);

window.scrollTo(0, 200);

const clickBox = document.querySelector('#box');
clickBox?.addEventListener('click', () => {
done();
});

await simulateClick(10, 300);
});

});
14 changes: 14 additions & 0 deletions webf/lib/src/dom/element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,10 @@ abstract class Element extends ContainerNode with ElementBase, ElementEventMixin
methods['querySelector'] = BindingObjectMethodSync(call: (args) => querySelector(args));
methods['matches'] = BindingObjectMethodSync(call: (args) => matches(args));
methods['closest'] = BindingObjectMethodSync(call: (args) => closest(args));

if (kDebugMode || kProfileMode) {
methods['__test_global_to_local__'] = BindingObjectMethodSync(call: (args) => testGlobalToLocal(args[0], args[1]));
}
}

dynamic getElementsByClassName(List<dynamic> args) {
Expand Down Expand Up @@ -1821,6 +1825,16 @@ abstract class Element extends ContainerNode with ElementBase, ElementEventMixin
return offset;
}

dynamic testGlobalToLocal(double x, double y) {
if (!isRendererAttached) {
return { 'x': 0, 'y': 0 };
}

Offset offset = Offset(x, y);
Offset result = renderBoxModel!.globalToLocal(offset);
return {'x': result.dx, 'y': result.dy};
}

// The HTMLElement.offsetTop read-only property returns the distance of the outer border
// of the current element relative to the inner border of the top of the offsetParent node.
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsettop
Expand Down
16 changes: 12 additions & 4 deletions webf/lib/src/rendering/box_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,34 @@ Offset getLayoutTransformTo(RenderObject current, RenderObject ancestor, {bool e
assert(renderer.parent != null);
}
renderers.add(ancestor);
Offset offset = Offset.zero;
List<Offset> stackOffsets = [];
final Matrix4 transform = Matrix4.identity();

for (int index = renderers.length - 1; index > 0; index -= 1) {
RenderObject parentRenderer = renderers[index];
RenderObject childRenderer = renderers[index - 1];
// Apply the layout transform for renderBoxModel and fallback to paint transform for other renderObject type.
if (parentRenderer is RenderBoxModel) {
offset += parentRenderer.obtainLayoutTransform(childRenderer, excludeScrollOffset);
// If the next renderBox has a fixed position,
// the outside scroll offset won't affect the actual results because of its fixed positioning.
if (childRenderer is RenderBoxModel && childRenderer.renderStyle.position == CSSPositionType.fixed) {
stackOffsets.clear();
}

stackOffsets.add(parentRenderer.obtainLayoutTransform(childRenderer, excludeScrollOffset));
} else if (parentRenderer is RenderSliverRepaintProxy) {
parentRenderer.applyLayoutTransform(childRenderer, transform, excludeScrollOffset);
} else if (parentRenderer is RenderBox) {
assert(childRenderer.parent == parentRenderer);
if (childRenderer.parentData is BoxParentData) {
offset += (childRenderer.parentData as BoxParentData).offset;
stackOffsets.add((childRenderer.parentData as BoxParentData).offset);
}
}
}

return offset;
if (stackOffsets.isEmpty) return Offset.zero;

return stackOffsets.reduce((prev, next) => prev + next);
}

/// Modified from Flutter rendering/box.dart.
Expand Down
12 changes: 12 additions & 0 deletions webf/lib/src/rendering/overflow.dart
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,20 @@ mixin RenderOverflowMixin on RenderBoxModelBase {
return result;
}


// For position fixed render box, should reduce the outer scroll offsets.
void applyPositionFixedPaintTransform(RenderBoxModel child, Matrix4 transform) {
Offset totalScrollOffset = child.getTotalScrollOffset();
transform.translate(totalScrollOffset.dx, totalScrollOffset.dy);
}

void applyOverflowPaintTransform(RenderBox child, Matrix4 transform) {
final Offset paintOffset = Offset(_paintOffsetX, _paintOffsetY);

if (child is RenderBoxModel && child.renderStyle.position == CSSPositionType.fixed) {
applyPositionFixedPaintTransform(child, transform);
}

transform.translate(paintOffset.dx, paintOffset.dy);
}

Expand Down

0 comments on commit aeaec89

Please sign in to comment.