Commit f2c6fed5 authored by Sahir Vellani's avatar Sahir Vellani Committed by Commit Bot

Resize Observer: Observe Device Pixel Content Box

In order to correctly snap box sizes to pixels using SnapSizeToPixel,
the function FragmentData::PaintOffset() needs to be used. This function
provides an accurate location/offset of the box in question. However,
this the paint offset is determined in the PrePaint lifecycle phase. To
accommodate this, LocalFrameView::NotifyResizeObservers is moved to
LocalFrameView::UpdateLifecyclePhasesInternal. In addition,
UpdateLifecyclePhasesInternal is refactored so that all lifecycle
steps prior to paint are run in a loop. The loop is broken when one of
the following 3 conditions are met:

1) the target state < kPaintClean
2) No changes to layout are pending
3) Resize observer loop limit is reached

Resize Observer can now observe changes to the device pixel content box.
ResizeObserverEntry now reports the dimensions of the pixel snapped
content box.

Bug: 1055579
Change-Id: Ibc7b2e4e0a8468f9af74c8cfaadd02e4f0bcfc1f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2071575Reviewed-by: default avatarPhilip Rogers <pdr@chromium.org>
Reviewed-by: default avatarDaniel Libby <dlibby@microsoft.com>
Reviewed-by: default avatarStefan Zager <szager@chromium.org>
Commit-Queue: Sahir Vellani <sahir.vellani@microsoft.com>
Cr-Commit-Position: refs/heads/master@{#751159}
parent 8a1c766c
......@@ -2166,42 +2166,50 @@ void LocalFrameView::ScheduleVisualUpdateForPaintInvalidationIfNeeded() {
// phase of this cycle.
}
void LocalFrameView::NotifyResizeObservers() {
bool LocalFrameView::NotifyResizeObservers(
DocumentLifecycle::LifecycleState target_state) {
// Return true if lifecycles need to be re-run
TRACE_EVENT0("blink,benchmark", "LocalFrameView::NotifyResizeObservers");
if (target_state < DocumentLifecycle::kPaintClean)
return false;
// Controller exists only if ResizeObserver was created.
if (!GetFrame().GetDocument()->GetResizeObserverController())
return;
return false;
ResizeObserverController& resize_controller =
frame_->GetDocument()->EnsureResizeObserverController();
DCHECK(Lifecycle().GetState() >= DocumentLifecycle::kLayoutClean);
DCHECK(Lifecycle().GetState() >= DocumentLifecycle::kPrePaintClean);
size_t min_depth = 0;
for (min_depth = resize_controller.GatherObservations(0);
min_depth != ResizeObserverController::kDepthBottom;
min_depth = resize_controller.GatherObservations(min_depth)) {
resize_controller.DeliverObservations();
GetFrame().GetDocument()->UpdateStyleAndLayout(
DocumentUpdateReason::kSizeChange);
}
size_t min_depth = resize_controller.GatherObservations();
if (resize_controller.SkippedObservations()) {
resize_controller.ClearObservations();
ErrorEvent* error = ErrorEvent::Create(
"ResizeObserver loop limit exceeded",
SourceLocation::Capture(frame_->GetDocument()->ToExecutionContext()),
nullptr);
// We're using |SanitizeScriptErrors::kDoNotSanitize| as the error is made
// by blink itself.
// TODO(yhirano): Reconsider this.
frame_->GetDocument()->ToExecutionContext()->DispatchErrorEvent(
error, SanitizeScriptErrors::kDoNotSanitize);
// Ensure notifications will get delivered in next cycle.
ScheduleAnimation();
if (min_depth != ResizeObserverController::kDepthBottom) {
resize_controller.DeliverObservations();
} else {
// Observation depth limit reached
if (resize_controller.SkippedObservations()) {
resize_controller.ClearObservations();
ErrorEvent* error = ErrorEvent::Create(
"ResizeObserver loop limit exceeded",
SourceLocation::Capture(frame_->GetDocument()->ToExecutionContext()),
nullptr);
// We're using |SanitizeScriptErrors::kDoNotSanitize| as the error is made
// by blink itself.
// TODO(yhirano): Reconsider this.
frame_->GetDocument()->ToExecutionContext()->DispatchErrorEvent(
error, SanitizeScriptErrors::kDoNotSanitize);
// Ensure notifications will get delivered in next cycle.
ScheduleAnimation();
DCHECK(Lifecycle().GetState() >= DocumentLifecycle::kPrePaintClean);
}
if (Lifecycle().GetState() >= DocumentLifecycle::kPrePaintClean)
return false;
}
DCHECK(!GetLayoutView()->NeedsLayout());
// Lifecycle needs to be run again because Resize Observer affected layout
return true;
}
// TODO(leviw): We don't assert lifecycle information from documents in child
......@@ -2309,39 +2317,56 @@ bool LocalFrameView::UpdateLifecyclePhases(
void LocalFrameView::UpdateLifecyclePhasesInternal(
DocumentLifecycle::LifecycleState target_state) {
bool run_more_lifecycle_phases =
RunStyleAndLayoutLifecyclePhases(target_state);
if (!run_more_lifecycle_phases)
return;
DCHECK(Lifecycle().GetState() >= DocumentLifecycle::kLayoutClean);
// Run style, layout, compositing and prepaint lifecycle phases and deliver
// resize observations if required. Resize observer callbacks/delegates have
// the potential to dirty layout (until loop limit is reached) and therefore
// the above lifecycle phases need to be re-run until the limit is reached
// or no layout is pending.
while (true) {
bool run_more_lifecycle_phases =
RunStyleAndLayoutLifecyclePhases(target_state);
if (!run_more_lifecycle_phases)
return;
DCHECK(Lifecycle().GetState() >= DocumentLifecycle::kLayoutClean);
if (!GetLayoutView())
return;
if (!GetLayoutView())
return;
{
// We need scoping braces here because this
// DisallowLayoutInvalidationScope is meant to be in effect during
// pre-paint, but not during ResizeObserver.
#if DCHECK_IS_ON()
DisallowLayoutInvalidationScope disallow_layout_invalidation(this);
DisallowLayoutInvalidationScope disallow_layout_invalidation(this);
#endif
TRACE_EVENT_INSTANT1(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"),
"SetLayerTreeId", TRACE_EVENT_SCOPE_THREAD, "data",
inspector_set_layer_tree_id::Data(frame_.Get()));
TRACE_EVENT1("devtools.timeline", "UpdateLayerTree", "data",
inspector_update_layer_tree_event::Data(frame_.Get()));
run_more_lifecycle_phases = RunCompositingLifecyclePhase(target_state);
if (!run_more_lifecycle_phases)
return;
// TODO(pdr): PrePaint should be under the "Paint" devtools timeline step
// when CompositeAfterPaint is enabled.
run_more_lifecycle_phases = RunPrePaintLifecyclePhase(target_state);
DCHECK(ShouldThrottleRendering() ||
Lifecycle().GetState() >= DocumentLifecycle::kPrePaintClean);
if (!run_more_lifecycle_phases)
return;
}
{
TRACE_EVENT_INSTANT1(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"),
"SetLayerTreeId", TRACE_EVENT_SCOPE_THREAD, "data",
inspector_set_layer_tree_id::Data(frame_.Get()));
TRACE_EVENT1("devtools.timeline", "UpdateLayerTree", "data",
inspector_update_layer_tree_event::Data(frame_.Get()));
run_more_lifecycle_phases = RunCompositingLifecyclePhase(target_state);
if (!run_more_lifecycle_phases)
return;
// TODO(pdr): PrePaint should be under the "Paint" devtools timeline step
// when CompositeAfterPaint is enabled.
run_more_lifecycle_phases = RunPrePaintLifecyclePhase(target_state);
DCHECK(ShouldThrottleRendering() ||
Lifecycle().GetState() >= DocumentLifecycle::kPrePaintClean);
run_more_lifecycle_phases = RunResizeObserverSteps(target_state);
if (!run_more_lifecycle_phases)
return;
break;
}
// Layout invalidation scope was disabled for resize observer
// re-enable it for subsequent steps
#if DCHECK_IS_ON()
DisallowLayoutInvalidationScope disallow_layout_invalidation(this);
#endif
// Now that we have run the lifecycle up to paint, we can reset
// |need_paint_phase_after_throttling_| so that the paint phase will
// properly see us as being throttled (if that was the only reason we remained
......@@ -2358,6 +2383,27 @@ void LocalFrameView::UpdateLifecyclePhasesInternal(
[](RemoteFrameView& frame_view) { frame_view.UpdateCompositingRect(); });
}
bool LocalFrameView::RunResizeObserverSteps(
DocumentLifecycle::LifecycleState target_state) {
bool re_run_lifecycles = false;
if (target_state == DocumentLifecycle::kPaintClean) {
ForAllNonThrottledLocalFrameViews(
[&re_run_lifecycles](LocalFrameView& frame_view) {
bool result =
frame_view.NotifyResizeObservers(DocumentLifecycle::kPaintClean);
re_run_lifecycles = re_run_lifecycles || result;
});
}
if (!re_run_lifecycles) {
ForAllNonThrottledLocalFrameViews([](LocalFrameView& frame_view) {
ResizeObserverController& resize_controller =
frame_view.frame_->GetDocument()->EnsureResizeObserverController();
resize_controller.ClearMinDepth();
});
}
return re_run_lifecycles;
}
bool LocalFrameView::RunStyleAndLayoutLifecyclePhases(
DocumentLifecycle::LifecycleState target_state) {
TRACE_EVENT0("blink,benchmark",
......@@ -2409,18 +2455,11 @@ bool LocalFrameView::RunStyleAndLayoutLifecyclePhases(
frame_->GetPage()->GetValidationMessageClient().LayoutOverlay();
if (target_state == DocumentLifecycle::kPaintClean) {
ForAllNonThrottledLocalFrameViews(
[](LocalFrameView& frame_view) { frame_view.NotifyResizeObservers(); });
ForAllNonThrottledLocalFrameViews([](LocalFrameView& frame_view) {
frame_view.NotifyFrameRectsChangedIfNeeded();
});
}
// If we exceed the number of re-layouts during ResizeObserver notifications,
// then we shouldn't continue with the lifecycle updates. At that time, we
// have scheduled an animation and we'll try again.
DCHECK(Lifecycle().GetState() >= DocumentLifecycle::kLayoutClean ||
Lifecycle().GetState() == DocumentLifecycle::kVisualUpdatePending);
return Lifecycle().GetState() >= DocumentLifecycle::kLayoutClean;
}
......
......@@ -824,7 +824,8 @@ class CORE_EXPORT LocalFrameView final
bool UpdateViewportIntersectionsForSubtree(unsigned parent_flags) override;
void DeliverSynchronousIntersectionObservations();
void NotifyResizeObservers();
bool NotifyResizeObservers(DocumentLifecycle::LifecycleState target_state);
bool RunResizeObserverSteps(DocumentLifecycle::LifecycleState target_state);
bool CheckLayoutInvalidationIsAllowed() const;
......
......@@ -4,11 +4,13 @@
#include "third_party/blink/renderer/core/resize_observer/resize_observation.h"
#include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h"
#include "third_party/blink/renderer/core/layout/adjust_for_absolute_zoom.h"
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/core/resize_observer/resize_observer.h"
#include "third_party/blink/renderer/core/resize_observer/resize_observer_box_options.h"
#include "third_party/blink/renderer/core/svg/svg_element.h"
#include "third_party/blink/renderer/core/svg/svg_graphics_element.h"
#include "third_party/blink/renderer/platform/geometry/layout_unit.h"
namespace blink {
......@@ -60,13 +62,31 @@ LayoutSize ResizeObservation::ComputeTargetSize() const {
return LayoutSize();
if (LayoutBox* layout_box = ToLayoutBox(layout_object)) {
const ComputedStyle& style = layout_object->StyleRef();
switch (observed_box_) {
case ResizeObserverBoxOptions::BorderBox:
return LayoutSize(layout_box->LogicalWidth(),
layout_box->LogicalHeight());
return LayoutSize(AdjustForAbsoluteZoom::AdjustLayoutUnit(
layout_box->LogicalWidth(), style),
AdjustForAbsoluteZoom::AdjustLayoutUnit(
layout_box->LogicalHeight(), style));
case ResizeObserverBoxOptions::ContentBox:
return LayoutSize(layout_box->ContentLogicalWidth(),
layout_box->ContentLogicalHeight());
return LayoutSize(AdjustForAbsoluteZoom::AdjustLayoutUnit(
layout_box->ContentLogicalWidth(), style),
AdjustForAbsoluteZoom::AdjustLayoutUnit(
layout_box->ContentLogicalHeight(), style));
case ResizeObserverBoxOptions::DevicePixelContentBox: {
LayoutSize paint_offset =
layout_object->FirstFragment().PaintOffset().ToLayoutSize();
return LayoutSize(
SnapSizeToPixel(layout_box->ContentLogicalWidth(),
style.IsHorizontalWritingMode()
? paint_offset.Width()
: paint_offset.Height()),
SnapSizeToPixel(layout_box->ContentLogicalHeight(),
style.IsHorizontalWritingMode()
? paint_offset.Height()
: paint_offset.Width()));
}
default:
NOTREACHED();
}
......
......@@ -18,6 +18,8 @@ namespace blink {
constexpr const char* kBoxOptionBorderBox = "border-box";
constexpr const char* kBoxOptionContentBox = "content-box";
constexpr const char* kBoxOptionDevicePixelContentBox =
"device-pixel-content-box";
ResizeObserver* ResizeObserver::Create(Document& document,
V8ResizeObserverCallback* callback) {
......@@ -53,6 +55,8 @@ ResizeObserverBoxOptions ResizeObserver::ParseBoxOptions(
return ResizeObserverBoxOptions::BorderBox;
if (box_options == kBoxOptionContentBox)
return ResizeObserverBoxOptions::ContentBox;
if (box_options == kBoxOptionDevicePixelContentBox)
return ResizeObserverBoxOptions::DevicePixelContentBox;
return ResizeObserverBoxOptions::ContentBox;
}
......
......@@ -5,6 +5,7 @@ namespace blink {
enum class ResizeObserverBoxOptions {
BorderBox,
ContentBox,
DevicePixelContentBox
};
}
......
......@@ -14,15 +14,16 @@ void ResizeObserverController::AddObserver(ResizeObserver& observer) {
observers_.insert(&observer);
}
size_t ResizeObserverController::GatherObservations(size_t deeper_than) {
size_t ResizeObserverController::GatherObservations() {
size_t shallowest = ResizeObserverController::kDepthBottom;
for (auto& observer : observers_) {
size_t depth = observer->GatherObservations(deeper_than);
size_t depth = observer->GatherObservations(min_depth_);
if (depth < shallowest)
shallowest = depth;
}
return shallowest;
min_depth_ = shallowest;
return min_depth_;
}
bool ResizeObserverController::SkippedObservations() {
......
......@@ -28,14 +28,16 @@ class ResizeObserverController final
void AddObserver(ResizeObserver&);
// observation API
// Returns depth of shallowest observed node, kDepthLimit if none.
size_t GatherObservations(size_t deeper_than);
// Returns min depth of shallowest observed node, kDepthLimit if none.
size_t GatherObservations();
// Returns true if gatherObservations has skipped observations
// because they were too shallow.
bool SkippedObservations();
void DeliverObservations();
void ClearObservations();
void ClearMinDepth() { min_depth_ = 0; }
void Trace(Visitor*);
// For testing only.
......@@ -46,6 +48,8 @@ class ResizeObserverController final
private:
// Active observers
HeapLinkedHashSet<WeakMember<ResizeObserver>> observers_;
// Minimum depth for observations to be active
size_t min_depth_ = 0;
};
} // namespace blink
......
......@@ -13,6 +13,7 @@
#include "third_party/blink/renderer/core/svg/svg_graphics_element.h"
#include "third_party/blink/renderer/platform/geometry/layout_rect.h"
#include "third_party/blink/renderer/platform/geometry/layout_size.h"
#include "third_party/blink/renderer/platform/geometry/layout_unit.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
namespace blink {
......@@ -48,11 +49,13 @@ ResizeObserverEntry::ResizeObserverEntry(Element* target) : target_(target) {
if (auto* svg_graphics_element = DynamicTo<SVGGraphicsElement>(target)) {
LayoutSize bounding_box_size =
LayoutSize(svg_graphics_element->GetBBox().Size());
LayoutRect content_rect(LayoutPoint(), bounding_box_size);
content_rect_ = ZoomAdjustedLayoutRect(content_rect, style);
content_rect_ = DOMRectReadOnly::FromFloatRect(
FloatRect(FloatPoint(), FloatSize(bounding_box_size)));
if (RuntimeEnabledFeatures::ResizeObserverUpdatesEnabled()) {
content_box_size_ = ZoomAdjustedSize(bounding_box_size, style);
border_box_size_ = ZoomAdjustedSize(bounding_box_size, style);
content_box_size_ = ResizeObserverSize::Create(
bounding_box_size.Width(), bounding_box_size.Height());
border_box_size_ = content_box_size_;
device_pixel_content_box_size_ = content_box_size_;
}
} else if (layout_object->IsBox()) {
LayoutBox* layout_box = target->GetLayoutBox();
......@@ -70,6 +73,18 @@ ResizeObserverEntry::ResizeObserverEntry(Element* target) : target_(target) {
content_box_size_ = ZoomAdjustedSize(content_box_size, style);
border_box_size_ = ZoomAdjustedSize(border_box_size, style);
LayoutSize paint_offset =
layout_object->FirstFragment().PaintOffset().ToLayoutSize();
device_pixel_content_box_size_ = ResizeObserverSize::Create(
SnapSizeToPixel(layout_box->ContentLogicalWidth(),
style.IsHorizontalWritingMode()
? paint_offset.Width()
: paint_offset.Height()),
SnapSizeToPixel(layout_box->ContentLogicalHeight(),
style.IsHorizontalWritingMode()
? paint_offset.Height()
: paint_offset.Width()));
}
}
}
......@@ -81,6 +96,8 @@ ResizeObserverEntry::ResizeObserverEntry(Element* target) : target_(target) {
content_box_size_ = ResizeObserverSize::Create(0, 0);
if (!border_box_size_)
border_box_size_ = ResizeObserverSize::Create(0, 0);
if (!device_pixel_content_box_size_)
device_pixel_content_box_size_ = ResizeObserverSize::Create(0, 0);
}
}
......@@ -89,6 +106,7 @@ void ResizeObserverEntry::Trace(Visitor* visitor) {
visitor->Trace(content_rect_);
visitor->Trace(content_box_size_);
visitor->Trace(border_box_size_);
visitor->Trace(device_pixel_content_box_size_);
ScriptWrappable::Trace(visitor);
}
......
......@@ -28,6 +28,9 @@ class CORE_EXPORT ResizeObserverEntry final : public ScriptWrappable {
DOMRectReadOnly* contentRect() const { return content_rect_; }
ResizeObserverSize* contentBoxSize() const { return content_box_size_; }
ResizeObserverSize* borderBoxSize() const { return border_box_size_; }
ResizeObserverSize* devicePixelContentBoxSize() const {
return device_pixel_content_box_size_;
}
void Trace(Visitor*) override;
......@@ -36,6 +39,7 @@ class CORE_EXPORT ResizeObserverEntry final : public ScriptWrappable {
Member<DOMRectReadOnly> content_rect_;
Member<ResizeObserverSize> content_box_size_;
Member<ResizeObserverSize> border_box_size_;
Member<ResizeObserverSize> device_pixel_content_box_size_;
static DOMRectReadOnly* ZoomAdjustedLayoutRect(LayoutRect content_rect,
const ComputedStyle& style);
......
......@@ -10,4 +10,5 @@ interface ResizeObserverEntry {
readonly attribute DOMRectReadOnly contentRect;
[RuntimeEnabled=ResizeObserverUpdates] readonly attribute ResizeObserverSize contentBoxSize;
[RuntimeEnabled=ResizeObserverUpdates] readonly attribute ResizeObserverSize borderBoxSize;
[RuntimeEnabled=ResizeObserverUpdates] readonly attribute ResizeObserverSize devicePixelContentBoxSize;
};
......@@ -5,7 +5,7 @@
// https://drafts.csswg.org/resize-observer/#enumdef-resizeobserverboxoptions
enum ResizeObserverBoxOptions {
"border-box", "content-box"
"border-box", "content-box", "device-pixel-content-box"
};
// https://drafts.csswg.org/resize-observer/#dictdef-resizeobserveroptions
......
......@@ -60,13 +60,12 @@ class TestResizeObserverDelegate : public ResizeObserver::Delegate {
*/
class ResizeObserverUnitTest : public SimTest {};
TEST_F(ResizeObserverUnitTest, ResizeObservationSize) {
TEST_F(ResizeObserverUnitTest, ResizeObserverDOMContentBoxAndSVG) {
SimRequest main_resource("https://example.com/", "text/html");
LoadURL("https://example.com/");
main_resource.Write(R"HTML(
<div id='domTarget' style='width:100px;height:100px'>yo</div>
<div id='domBorderTarget' style='width:100px;height:100px;padding:5px'>yoyo</div>
<svg height='200' width='200'>
<circle id='svgTarget' cx='100' cy='100' r='100'/>
</svg>
......@@ -77,19 +76,14 @@ TEST_F(ResizeObserverUnitTest, ResizeObservationSize) {
MakeGarbageCollected<TestResizeObserverDelegate>(GetDocument());
ResizeObserver* observer = ResizeObserver::Create(GetDocument(), delegate);
Element* dom_target = GetDocument().getElementById("domTarget");
Element* dom_border_target = GetDocument().getElementById("domBorderTarget");
Element* svg_target = GetDocument().getElementById("svgTarget");
ResizeObservation* dom_observation = MakeGarbageCollected<ResizeObservation>(
dom_target, observer, ResizeObserverBoxOptions::ContentBox);
ResizeObservation* dom_border_observation =
MakeGarbageCollected<ResizeObservation>(
dom_border_target, observer, ResizeObserverBoxOptions::BorderBox);
ResizeObservation* svg_observation = MakeGarbageCollected<ResizeObservation>(
svg_target, observer, ResizeObserverBoxOptions::ContentBox);
// Initial observation is out of sync
ASSERT_TRUE(dom_observation->ObservationSizeOutOfSync());
ASSERT_TRUE(dom_border_observation->ObservationSizeOutOfSync());
ASSERT_TRUE(svg_observation->ObservationSizeOutOfSync());
// Target size is correct
......@@ -98,11 +92,6 @@ TEST_F(ResizeObserverUnitTest, ResizeObservationSize) {
ASSERT_EQ(size.Height(), 100);
dom_observation->SetObservationSize(size);
size = dom_border_observation->ComputeTargetSize();
ASSERT_EQ(size.Width(), 110);
ASSERT_EQ(size.Height(), 110);
dom_border_observation->SetObservationSize(size);
size = svg_observation->ComputeTargetSize();
ASSERT_EQ(size.Width(), 200);
ASSERT_EQ(size.Height(), 200);
......@@ -110,13 +99,94 @@ TEST_F(ResizeObserverUnitTest, ResizeObservationSize) {
// Target size is in sync
ASSERT_FALSE(dom_observation->ObservationSizeOutOfSync());
ASSERT_FALSE(dom_border_observation->ObservationSizeOutOfSync());
ASSERT_FALSE(svg_observation->ObservationSizeOutOfSync());
// Target depths
ASSERT_EQ(svg_observation->TargetDepth() - dom_observation->TargetDepth(),
(size_t)1);
}
TEST_F(ResizeObserverUnitTest, ResizeObserverDOMBorderBox) {
SimRequest main_resource("https://example.com/", "text/html");
LoadURL("https://example.com/");
main_resource.Write(R"HTML(
<div id='domBorderTarget' style='width:100px;height:100px;padding:5px'>
yoyo
</div>
)HTML");
main_resource.Finish();
ResizeObserver::Delegate* delegate =
MakeGarbageCollected<TestResizeObserverDelegate>(GetDocument());
ResizeObserver* observer = ResizeObserver::Create(GetDocument(), delegate);
Element* dom_border_target = GetDocument().getElementById("domBorderTarget");
ResizeObservation* dom_border_observation =
MakeGarbageCollected<ResizeObservation>(
dom_border_target, observer, ResizeObserverBoxOptions::BorderBox);
// Initial observation is out of sync
ASSERT_TRUE(dom_border_observation->ObservationSizeOutOfSync());
// Target size is correct
LayoutSize size = dom_border_observation->ComputeTargetSize();
ASSERT_EQ(size.Width(), 110);
ASSERT_EQ(size.Height(), 110);
dom_border_observation->SetObservationSize(size);
// Target size is in sync
ASSERT_FALSE(dom_border_observation->ObservationSizeOutOfSync());
}
TEST_F(ResizeObserverUnitTest, ResizeObserverDOMDevicePixelContentBox) {
SimRequest main_resource("https://example.com/", "text/html");
LoadURL("https://example.com/");
main_resource.Write(R"HTML(
<div id='domTarget' style='width:100px;height:100px'>yo</div>
<svg height='200' width='200'>
<div style='zoom:3;'>
<div id='domDPTarget' style='width:50px;height:30px'></div>
</div>
</svg>
)HTML");
main_resource.Finish();
ResizeObserver::Delegate* delegate =
MakeGarbageCollected<TestResizeObserverDelegate>(GetDocument());
ResizeObserver* observer = ResizeObserver::Create(GetDocument(), delegate);
Element* dom_target = GetDocument().getElementById("domTarget");
Element* dom_dp_target = GetDocument().getElementById("domDPTarget");
ResizeObservation* dom_dp_nested_observation =
MakeGarbageCollected<ResizeObservation>(
dom_dp_target, observer,
ResizeObserverBoxOptions::DevicePixelContentBox);
ResizeObservation* dom_dp_observation =
MakeGarbageCollected<ResizeObservation>(
dom_target, observer,
ResizeObserverBoxOptions::DevicePixelContentBox);
// Initial observation is out of sync
ASSERT_TRUE(dom_dp_observation->ObservationSizeOutOfSync());
ASSERT_TRUE(dom_dp_nested_observation->ObservationSizeOutOfSync());
// Target size is correct
LayoutSize size = dom_dp_observation->ComputeTargetSize();
ASSERT_EQ(size.Width(), 100);
ASSERT_EQ(size.Height(), 100);
dom_dp_observation->SetObservationSize(size);
size = dom_dp_nested_observation->ComputeTargetSize();
ASSERT_EQ(size.Width(), 150);
ASSERT_EQ(size.Height(), 90);
dom_dp_nested_observation->SetObservationSize(size);
// Target size is in sync
ASSERT_FALSE(dom_dp_observation->ObservationSizeOutOfSync());
ASSERT_FALSE(dom_dp_nested_observation->ObservationSizeOutOfSync());
}
// Test whether a new observation is created when an observation's
// observed box is changed
TEST_F(ResizeObserverUnitTest, TestBoxOverwrite) {
......@@ -175,6 +245,8 @@ TEST_F(ResizeObserverUnitTest, TestNonBoxTarget) {
EXPECT_EQ(entry->contentBoxSize()->blockSize(), 0);
EXPECT_EQ(entry->borderBoxSize()->inlineSize(), 0);
EXPECT_EQ(entry->borderBoxSize()->blockSize(), 0);
EXPECT_EQ(entry->devicePixelContentBoxSize()->inlineSize(), 0);
EXPECT_EQ(entry->devicePixelContentBoxSize()->blockSize(), 0);
}
TEST_F(ResizeObserverUnitTest, TestMemoryLeaks) {
......
......@@ -96,7 +96,6 @@
"http/tests/serviceworker/webexposed",
"inspector-protocol/dom-snapshot",
"media/stable",
"resize-observer",
"webexposed"],
"args": ["--stable-release-mode"]
},
......
<!doctype html>
<style>
#greenrect {
width: 100%;
height: 100%;
border: 1px solid green;
box-sizing: border-box;
}
#outer {
padding-top: 1.7px;
width: 300.8px;
height: 250.1px;
}
#outer2 {
padding-top: 1.4px;
width: 300.8px;
height: 250.1px;
}
</style>
<body>
<div id="outer">
<div id="greenrect"></div>
</div>
<div id="outer2">
<div id="greenrect"></div>
</div>
</body>
<!doctype html>
<link rel="match" href="devicepixel-ref.html">
<meta name="assert" content="Device pixel content box sizes and pixel snapping are correct in Resize Observer callback">
<!--
This test verifies that the device pixel content box sizes available
in a resize observer callback are correct. Resize observer notifications
are delivered as the element is loaded. A box is then drawn using the
available dimensions in device pixels. The result is compared to the reference
which uses div and border to draw a box.
-->
<style>
#canvas2D {
width: 100%;
height: 100%;
}
#canvas2DPadding14 {
width: 100%;
height: 100%;
}
#outer {
padding-top: 1.7px;
width: 300.8px;
height: 250.1px;
}
#outer2 {
padding-top: 1.4px;
width: 300.8px;
height: 250.1px;
}
</style>
<body>
<div id="outer">
<canvas id="canvas2D"></canvas>
</div>
<div id="outer2">
<canvas id="canvas2DPadding14"></canvas>
</div>
</body>
<script>
// Create a box using device pixel content box dimensions
function paint2D(ctx, snappedSize) {
ctx.beginPath();
// Use a linewidth of 2. Because the rectangle is drawn at 0,0 with
// its dimensions being the same as canvas dimensions, linewidth as it
// is drawn on the canvas will be 1.
ctx.lineWidth = "2";
ctx.strokeStyle = "green";
ctx.rect(0, 0, snappedSize.inlineSize, snappedSize.blockSize);
ctx.stroke();
}
function updateCanvasSize2D(canvas2D, ctx, snappedSize) {
canvas2D.width = snappedSize.inlineSize;
canvas2D.height = snappedSize.blockSize;
paint2D(ctx, snappedSize);
}
(function() {
let canvas2D = document.querySelector("#canvas2D");
let canvas2DPadding14 = document.querySelector("#canvas2DPadding14");
function observeSizes() {
let ro = new ResizeObserver( entries => {
for (entry of entries) {
let size = entry.devicePixelContentBoxSize;
if (entry.target == canvas2D) {
let canvas2D = document.querySelector("#canvas2D");
let ctx = canvas2D.getContext("2d");
updateCanvasSize2D(canvas2D, ctx, size);
} else if (entry.target == canvas2DPadding14){
let canvas2DPadding14 = document.querySelector("#canvas2DPadding14");
let ctx = canvas2DPadding14.getContext("2d");
updateCanvasSize2D(canvas2DPadding14, ctx, size);
}
}
});
ro.observe(canvas2D, {box: "device-pixel-content-box"});
ro.observe(canvas2DPadding14, {box: "device-pixel-content-box"});
}
observeSizes();
})();
</script>
......@@ -716,7 +716,6 @@ function test16() {
}
function test17() {
// <div id="outer">
// <div id="nested">
......@@ -847,6 +846,66 @@ function test17() {
return helper.start(() => nested.remove());
}
function test18() {
let t = createAndAppendElement("div");
t.style.height = "100px";
t.style.width = "50px";
let helper = new ResizeTestHelper(
"test18: an observation is fired when device-pixel-content-box is being " +
"observed",
[
{
setup: observer => {
observer.observe(t, {box: "device-pixel-content-box"});
},
notify: entries => {
assert_equals(entries.length, 1, "1 pending notification");
assert_equals(entries[0].target, t, "target is t");
assert_equals(entries[0].contentRect.width, 50, "target width");
assert_equals(entries[0].contentRect.height, 100, "target height");
assert_equals(entries[0].contentBoxSize.inlineSize, 50,
"target content-box inline size");
assert_equals(entries[0].contentBoxSize.blockSize, 100,
"target content-box block size");
assert_equals(entries[0].borderBoxSize.inlineSize, 50,
"target border-box inline size");
assert_equals(entries[0].borderBoxSize.blockSize, 100,
"target border-box block size");
assert_equals(entries[0].devicePixelContentBoxSize.inlineSize, 50,
"target device-pixel-content-box inline size");
assert_equals(entries[0].devicePixelContentBoxSize.blockSize, 100,
"target device-pixel-content-box block size");
}
},
{
setup: observer => {
document.body.style.zoom = 3;
},
notify: entries => {
assert_equals(entries.length, 1, "1 pending notification");
assert_equals(entries[0].target, t, "target is t");
assert_equals(entries[0].contentRect.width, 50, "target width");
assert_equals(entries[0].contentRect.height, 100, "target height");
assert_equals(entries[0].contentBoxSize.inlineSize, 50,
"target content-box inline size");
assert_equals(entries[0].contentBoxSize.blockSize, 100,
"target content-box block size");
assert_equals(entries[0].borderBoxSize.inlineSize, 50,
"target border-box inline size");
assert_equals(entries[0].borderBoxSize.blockSize, 100,
"target border-box block size");
assert_equals(entries[0].devicePixelContentBoxSize.inlineSize, 150,
"target device-pixel-content-box inline size");
assert_equals(entries[0].devicePixelContentBoxSize.blockSize, 300,
"target device-pixel-content-box block size");
}
}
]);
return helper.start(() => t.remove());
}
let guard;
test(_ => {
assert_own_property(window, "ResizeObserver");
......@@ -871,6 +930,7 @@ test0()
.then(() => test15())
.then(() => test16())
.then(() => test17())
.then(() => test18())
.then(() => guard.done());
</script>
......@@ -9,7 +9,7 @@
Features:
- can queue multiple notification steps in a test
- handles timeouts
- returns Promise that is fullfilled when test completes.
- returns Promise that is fulfilled when test completes.
Use to chain tests (since parallel async ResizeObserver tests
would conflict if reusing same DOM elements).
......
......@@ -496,6 +496,46 @@ function test15() {
return helper.start();
}
function test16() {
let target = document.querySelector('#g_rect');
let helper = new ResizeTestHelper(
"test16: observe g:rect content, border and device-pixel-content boxes",
[
{
setup: observer => {
observer.observe(target, {box: "device-pixel-content-box"});
target.setAttribute('width', 50);
document.body.style.zoom = 0.1;
},
notify: (entries, observer) => {
// Ensure zoom does not impact the sizes
assert_equals(entries.length, 1);
assert_equals(entries[0].contentRect.width, 50);
assert_equals(entries[0].contentRect.height, 20);
assert_equals(entries[0].contentBoxSize.inlineSize, 50);
assert_equals(entries[0].contentBoxSize.blockSize, 20);
assert_equals(entries[0].borderBoxSize.inlineSize, 50);
assert_equals(entries[0].borderBoxSize.blockSize, 20);
assert_equals(entries[0].devicePixelContentBoxSize.inlineSize, 50);
assert_equals(entries[0].devicePixelContentBoxSize.blockSize, 20);
return true; // Delay next step
}
},
{
setup: observer => {
document.body.style.zoom = 10;
},
notify: (entries, observer) => {
},
timeout: () => {
// SVG computed size is always bounding box inline and block lengths
}
}
]);
return helper.start();
}
let guard;
test(_ => {
assert_own_property(window, "ResizeObserver");
......@@ -518,6 +558,7 @@ test0()
.then(() => { return test13(); })
.then(() => { return test14(); })
.then(() => { return test15(); })
.then(() => { return test16(); })
.then(() => { guard.done(); });
</script>
<!doctype HTML>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="./resources/resizeTestHelper.js"></script>
<body></body>
<script>
'use strict';
// allow uncaught exception because ResizeObserver posts exceptions
// to window error handler when limit is exceeded.
setup({allow_uncaught_exception: true});
function test() {
let t = document.createElement("div");
document.body.appendChild(t);
t.style.height = "25.25px";
t.style.width = "55.5px";
document.body.style.zoom = 1;
let helper = new ResizeTestHelper(
"An observation is fired when device-pixel-content-box is being " +
"observed and sub pixel values are used",
[
{
setup: observer => {
observer.observe(t, {box: "device-pixel-content-box"});
},
notify: entries => {
assert_equals(entries.length, 1, "1 pending notification");
assert_equals(entries[0].target, t, "target is t");
assert_equals(entries[0].contentRect.width, 55.5, "target width");
assert_equals(entries[0].contentRect.height, 25.25, "target height");
assert_equals(entries[0].contentBoxSize.inlineSize, 55.5,
"target content-box inline size");
assert_equals(entries[0].contentBoxSize.blockSize, 25.25,
"target content-box block size");
assert_equals(entries[0].borderBoxSize.inlineSize, 55.5,
"target border-box inline size");
assert_equals(entries[0].borderBoxSize.blockSize, 25.25,
"target border-box block size");
assert_equals(entries[0].devicePixelContentBoxSize.inlineSize, 56,
"target device-pixel-content-box inline size");
assert_equals(entries[0].devicePixelContentBoxSize.blockSize, 25,
"target device-pixel-content-box block size");
}
},
{
setup: observer => {
t.style.marginLeft = "10.5px"
},
notify: entries => {
assert_equals(entries.length, 1, "1 pending notification");
assert_equals(entries[0].target, t, "target is t");
assert_equals(entries[0].contentRect.width, 55.5, "target width");
assert_equals(entries[0].contentRect.height, 25.25, "target height");
assert_equals(entries[0].contentBoxSize.inlineSize, 55.5,
"target content-box inline size");
assert_equals(entries[0].contentBoxSize.blockSize, 25.25,
"target content-box block size");
assert_equals(entries[0].borderBoxSize.inlineSize, 55.5,
"target border-box inline size");
assert_equals(entries[0].borderBoxSize.blockSize, 25.25,
"target border-box block size");
assert_equals(entries[0].devicePixelContentBoxSize.inlineSize, 55,
"target device-pixel-content-box inline size");
assert_equals(entries[0].devicePixelContentBoxSize.blockSize, 25,
"target device-pixel-content-box block size");
}
},
{
setup: observer => {
document.body.style.zoom = 3;
},
notify: entries => {
assert_equals(entries.length, 1, "1 pending notification");
assert_equals(entries[0].target, t, "target is t");
assert_equals(entries[0].contentRect.width, 55.5, "target width");
assert_equals(entries[0].contentRect.height, 25.25, "target height");
assert_equals(entries[0].contentBoxSize.inlineSize, 55.5,
"target content-box inline size");
assert_equals(entries[0].contentBoxSize.blockSize, 25.25,
"target content-box block size");
assert_equals(entries[0].borderBoxSize.inlineSize, 55.5,
"target content-box inline size");
assert_equals(entries[0].borderBoxSize.blockSize, 25.25,
"target content-box block size");
assert_equals(entries[0].devicePixelContentBoxSize.inlineSize, 166,
"target content-box inline size");
assert_equals(entries[0].devicePixelContentBoxSize.blockSize, 76,
"target content-box block size");
}
}
]);
return helper.start(() => t.remove());
}
function test2() {
// Ensure a resize observer callback that triggers only layout does not crash
// the browser
let c = document.createElement('canvas');
let a = document.createElement('div');
a.style.height = '10px';
a.style.width = '20px';
c.width = "500";
c.height = "500";
document.body.appendChild(c);
document.body.appendChild(a);
let helper = new ResizeTestHelper(
"Resize observer callback triggers layout without style change",
[
{
setup: observer => {
observer.observe(a, { box: "border-box" });
},
notify: entries => {
}
},
{
setup: observer => {
a.style.width = '50px';
},
notify: entries => {
c.height = "100";
assert_equals(entries.length, 1, "1 pending notification");
assert_equals(entries[0].target, a, "target is t");
assert_equals(entries[0].contentRect.width, 50, "target width");
assert_equals(entries[0].contentRect.height, 10, "target height");
assert_equals(entries[0].borderBoxSize.inlineSize, 50,
"target border-box inline size");
assert_equals(entries[0].borderBoxSize.blockSize, 10,
"target border-box block size");
}
},
]);
return helper.start(() => assert_equals(c.height, 100,
"canvas height"));
}
test();
test2();
</script>
'use strict';
/**
ResizeTestHelper is a framework to test ResizeObserver
notifications. Use it to make assertions about ResizeObserverEntries.
This framework is needed because ResizeObservations are
delivered asynchronously inside the event loop.
Features:
- can queue multiple notification steps in a test
- handles timeouts
- returns Promise that is fulfilled when test completes.
Use to chain tests (since parallel async ResizeObserver tests
would conflict if reusing same DOM elements).
Usage:
create ResizeTestHelper for every test.
Make assertions inside notify, timeout callbacks.
Start tests with helper.start()
Chain tests with Promises.
Counts animation frames, see startCountingRaf
*/
/*
@param name: test name
@param steps:
{
setup: function(ResizeObserver) {
// called at the beginning of the test step
// your observe/resize code goes here
},
notify: function(entries, observer) {
// ResizeObserver callback.
// Make assertions here.
// Return true if next step should start on the next event loop.
},
timeout: function() {
// Define this if your test expects to time out.
// If undefined, timeout is assert_unreached.
}
}
*/
function ResizeTestHelper(name, steps)
{
this._name = name;
this._steps = steps || [];
this._stepIdx = -1;
this._harnessTest = null;
this._observer = new ResizeObserver(this._handleNotification.bind(this));
this._timeoutBind = this._handleTimeout.bind(this);
this._nextStepBind = this._nextStep.bind(this);
}
ResizeTestHelper.TIMEOUT = 100;
ResizeTestHelper.prototype = {
get _currentStep() {
return this._steps[this._stepIdx];
},
_nextStep: function() {
if (++this._stepIdx == this._steps.length)
return this._done();
this._timeoutId = this._harnessTest.step_timeout(
this._timeoutBind, ResizeTestHelper.TIMEOUT);
try {
this._steps[this._stepIdx].setup(this._observer);
}
catch(err) {
this._harnessTest.step(() => {
assert_unreached("Caught a throw, possible syntax error");
});
}
},
_handleNotification: function(entries) {
if (this._timeoutId) {
window.clearTimeout(this._timeoutId);
delete this._timeoutId;
}
this._harnessTest.step(() => {
try {
let rafDelay = this._currentStep.notify(entries, this._observer);
if (rafDelay)
window.requestAnimationFrame(this._nextStepBind);
else
this._nextStep();
}
catch(err) {
this._harnessTest.step(() => {
throw err;
});
// Force to _done() the current test.
this._done();
}
});
},
_handleTimeout: function() {
delete this._timeoutId;
this._harnessTest.step(() => {
if (this._currentStep.timeout) {
this._currentStep.timeout();
}
else {
this._harnessTest.step(() => {
assert_unreached("Timed out waiting for notification. (" + ResizeTestHelper.TIMEOUT + "ms)");
});
}
this._nextStep();
});
},
_done: function() {
this._observer.disconnect();
delete this._observer;
this._harnessTest.done();
if (this._rafCountRequest) {
window.cancelAnimationFrame(this._rafCountRequest);
delete this._rafCountRequest;
}
window.requestAnimationFrame(() => { this._resolvePromise(); });
},
start: function(cleanup) {
this._harnessTest = async_test(this._name);
if (cleanup) {
this._harnessTest.add_cleanup(cleanup);
}
this._harnessTest.step(() => {
assert_equals(this._stepIdx, -1, "start can only be called once");
this._nextStep();
});
return new Promise( (resolve, reject) => {
this._resolvePromise = resolve;
this._rejectPromise = reject;
});
},
get rafCount() {
if (!this._rafCountRequest)
throw "rAF count is not active";
return this._rafCount;
},
get test() {
if (!this._harnessTest) {
throw "_harnessTest is not initialized";
}
return this._harnessTest;
},
_incrementRaf: function() {
if (this._rafCountRequest) {
this._rafCount++;
this._rafCountRequest = window.requestAnimationFrame(this._incrementRafBind);
}
},
startCountingRaf: function() {
if (this._rafCountRequest)
window.cancelAnimationFrame(this._rafCountRequest);
if (!this._incrementRafBind)
this._incrementRafBind = this._incrementRaf.bind(this);
this._rafCount = 0;
this._rafCountRequest = window.requestAnimationFrame(this._incrementRafBind);
}
}
function createAndAppendElement(tagName, parent) {
if (!parent) {
parent = document.body;
}
const element = document.createElement(tagName);
parent.appendChild(element);
return element;
}
......@@ -6666,6 +6666,7 @@ interface ResizeObserverEntry
getter borderBoxSize
getter contentBoxSize
getter contentRect
getter devicePixelContentBoxSize
getter target
method constructor
interface ResizeObserverSize
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment