[css-anchor-position] Support basic position-visibility: anchors-visible

This patch implements basic support for position-visibility:
anchors-visible with a single anchor. There is active discussion about
whether to track the visibility of multiple anchors at:
https://github.com/w3c/csswg-drafts/issues/7758#issuecomment-2026137829.
The high-level approach in this patch is to use a post-layout
intersection observer for the tracked anchor. On visibility changes, the
anchored element's paint layer is updated via
`PaintLayer::SetInvisibleForPositionVisibility`.

Spec:
https://github.com/w3c/csswg-drafts/issues/7758#issuecomment-1965540529

Bug: 329703412
Change-Id: Icedcb43510a0c6a491cf463e7dc8a114ab7abf5f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5410539
Commit-Queue: Xianzhu Wang <wangxianzhu@chromium.org>
Auto-Submit: Philip Rogers <pdr@chromium.org>
Reviewed-by: Xianzhu Wang <wangxianzhu@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1281911}
diff --git a/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-in-ref.html b/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-in-ref.html
new file mode 100644
index 0000000..10f74d4
--- /dev/null
+++ b/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-in-ref.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+  #scroll-container {
+    overflow: hidden scroll;
+    width: 300px;
+    height: 100px;
+  }
+
+  #anchor {
+    width: 100px;
+    height: 100px;
+    background: orange;
+    margin-bottom: 100px;
+  }
+
+  #target {
+    width: 100px;
+    height: 100px;
+    background: green;
+  }
+</style>
+
+<div id="scroll-container">
+  <div id="anchor">anchor</div>
+</div>
+<div id="target">target</div>
+
+<script>
+  const scroller = document.getElementById('scroll-container');
+  scroller.scrollTop = 0;
+</script>
diff --git a/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-in.tentative.html b/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-in.tentative.html
new file mode 100644
index 0000000..cea439c
--- /dev/null
+++ b/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-in.tentative.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<meta charset="utf-8">
+<meta name="assert" content="Scrolling an anchor in to view should cause a position-visibility: anchors-visible element to appear." />
+<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="match" href="position-visibility-anchors-visible-after-scroll-in-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="/common/rendering-utils.js"></script>
+<style>
+  #scroll-container {
+    overflow: hidden scroll;
+    width: 300px;
+    height: 100px;
+  }
+
+  #anchor {
+    anchor-name: --a1;
+    width: 100px;
+    height: 100px;
+    background: orange;
+  }
+
+  #spacer {
+    height: 100px;
+  }
+
+  #target {
+    position-anchor: --a1;
+    position-visibility: anchors-visible;
+    inset-area: block-end;
+    width: 100px;
+    height: 100px;
+    background: green;
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+</style>
+
+<div id="scroll-container">
+  <div id="anchor">anchor</div>
+  <div id="spacer"></div>
+  <div id="target">target</div>
+</div>
+
+<script>
+  // #target should be initially visible because it is anchored to #anchor,
+  // which is visible.
+  waitForAtLeastOneFrame().then(() => {
+    // Scroll #anchor out of view.
+    const scroller = document.getElementById('scroll-container');
+    scroller.scrollTop = 100;
+    // #target should now be invisible.
+
+    waitForAtLeastOneFrame().then(() => {
+      // Scroll #anchor back into view.
+      scroller.scrollTop = 0;
+
+      // #target should now be visible again.
+      takeScreenshot();
+    });
+  });
+</script>
diff --git a/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-out-ref.html b/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-out-ref.html
new file mode 100644
index 0000000..bd4fe1f
--- /dev/null
+++ b/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-out-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+  #scroll-container {
+    overflow: hidden scroll;
+    width: 300px;
+    height: 100px;
+  }
+
+  #spacer {
+    height: 200px;
+  }
+</style>
+
+<div id="scroll-container">
+  <div id="spacer"><div>
+</div>
+
+<script>
+  const scroller = document.getElementById('scroll-container');
+  scroller.scrollTop = 100;
+</script>
diff --git a/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-out.tentative.html b/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-out.tentative.html
new file mode 100644
index 0000000..b2e3643
--- /dev/null
+++ b/css/css-anchor-position/position-visibility-anchors-visible-after-scroll-out.tentative.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<meta charset="utf-8">
+<meta name="assert" content="Scrolling an anchor out of view should cause a position-visibility: anchors-visible element to disappear." />
+<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="match" href="position-visibility-anchors-visible-after-scroll-out-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="/common/rendering-utils.js"></script>
+<style>
+  #scroll-container {
+    overflow: hidden scroll;
+    width: 300px;
+    height: 100px;
+  }
+
+  #anchor {
+    anchor-name: --a1;
+    width: 100px;
+    height: 100px;
+    background: orange;
+  }
+
+  #spacer {
+    height: 100px;
+  }
+
+  #target {
+    position-anchor: --a1;
+    position-visibility: anchors-visible;
+    inset-area: bottom;
+    width: 100px;
+    height: 100px;
+    background: red;
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+</style>
+
+<div id="scroll-container">
+  <div id="anchor">anchor</div>
+  <div id="spacer"></div>
+  <div id="target">target</div>
+</div>
+
+<script>
+  // #target should be initially visible because it is anchored to #anchor,
+  // which is visible.
+
+  waitForAtLeastOneFrame().then(() => {
+    // Scroll #anchor so that it is out of view.
+    const scroller = document.getElementById('scroll-container');
+    scroller.scrollTop = 100;
+
+    // #target should now be invisible.
+    takeScreenshot();
+  });
+</script>
diff --git a/css/css-anchor-position/position-visibility-anchors-visible-change-anchor-ref.html b/css/css-anchor-position/position-visibility-anchors-visible-change-anchor-ref.html
new file mode 100644
index 0000000..cc35e4c
--- /dev/null
+++ b/css/css-anchor-position/position-visibility-anchors-visible-change-anchor-ref.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+  #scroll-container {
+    overflow: hidden scroll;
+    width: 300px;
+    height: 100px;
+  }
+  #anchor {
+    width: 100px;
+    height: 200px;
+    background: orange;
+  }
+  #target {
+    width: 100px;
+    height: 100px;
+    background: green;
+  }
+</style>
+
+<div id="scroll-container">
+  <div id="anchor"></div>
+</div>
+<div id="target">target</div>
+
+<script>
+  const scroller = document.getElementById('scroll-container');
+  scroller.scrollTop = 100;
+</script>
diff --git a/css/css-anchor-position/position-visibility-anchors-visible-change-anchor.tentative.html b/css/css-anchor-position/position-visibility-anchors-visible-change-anchor.tentative.html
new file mode 100644
index 0000000..f8b1cc6
--- /dev/null
+++ b/css/css-anchor-position/position-visibility-anchors-visible-change-anchor.tentative.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<meta charset="utf-8">
+<meta name="assert" content="Position-visibility should not be affected by the visibility of a previous anchor." />
+<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="match" href="position-visibility-anchors-visible-change-anchor-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="/common/rendering-utils.js"></script>
+<style>
+  #scroll-container {
+    overflow: hidden scroll;
+    width: 300px;
+    height: 100px;
+  }
+
+  .anchor {
+    width: 100px;
+    height: 100px;
+    background: orange;
+    display: inline-block;
+  }
+
+  #anchor1 {
+    height: 200px;
+    anchor-name: --a1;
+  }
+
+  #anchor2 {
+    anchor-name: --a2;
+  }
+
+  #target {
+    position-anchor: --a2;
+    position-visibility: anchors-visible;
+    inset-area: bottom;
+    width: 100px;
+    height: 100px;
+    background: green;
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+</style>
+
+<div id="scroll-container">
+  <div id="anchor1" class="anchor">anchor1</div>
+  <div id="anchor2" class="anchor">anchor2</div>
+  <div id="target">target</div>
+</div>
+
+<script>
+  // #target should be initially visible because it is anchored to #anchor2,
+  // which is visible.
+  waitForAtLeastOneFrame().then(() => {
+    // Change #target to be anchored to #anchor1.
+    target.style.positionAnchor = '--a1';
+    // #target should be still be visible because #anchor1 is also visible.
+    waitForAtLeastOneFrame().then(() => {
+      // Scroll #anchor2 out of view, with #anchor1 still in view.
+      const scroller = document.getElementById('scroll-container');
+      scroller.scrollTop = 100;
+      // #target should still be visible because it is anchored to #anchor1,
+      // which is still visible.
+      takeScreenshot();
+    });
+  });
+</script>
diff --git a/css/css-anchor-position/position-visibility-anchors-visible-non-intervening-container-ref.html b/css/css-anchor-position/position-visibility-anchors-visible-non-intervening-container-ref.html
new file mode 100644
index 0000000..3b6532e
--- /dev/null
+++ b/css/css-anchor-position/position-visibility-anchors-visible-non-intervening-container-ref.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+  #target {
+    width: 100px;
+    height: 100px;
+    background: green;
+  }
+</style>
+<div id="target">target</div>
diff --git a/css/css-anchor-position/position-visibility-anchors-visible-non-intervening-container.tentative.html b/css/css-anchor-position/position-visibility-anchors-visible-non-intervening-container.tentative.html
new file mode 100644
index 0000000..7b84976
--- /dev/null
+++ b/css/css-anchor-position/position-visibility-anchors-visible-non-intervening-container.tentative.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="assert" content="position-visibility: anchors-visible should consider the visibility of the anchor relative the containing scroller, ignoring visibility in other scrollers." />
+<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="match" href="position-visibility-anchors-visible-non-intervening-container-ref.html">
+<style>
+  #non-intervening-scroll-container {
+    overflow: hidden;
+    width: 200px;
+    height: 200px;
+    position: relative;
+  }
+
+  #position-container {
+    position: relative;
+  }
+
+  #scroll-container {
+    overflow: hidden scroll;
+    width: 400px;
+    height: 100px;
+  }
+
+  #anchor {
+    anchor-name: --a1;
+    width: 100px;
+    height: 100px;
+    background: orange;
+  }
+
+  #spacer {
+    height: 100px;
+  }
+
+  #target {
+    position-anchor: --a1;
+    position-visibility: anchors-visible;
+    inset-area: right;
+    width: 100px;
+    height: 100px;
+    background: green;
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+</style>
+
+<div id="non-intervening-scroll-container">
+  <div id="position-container">
+    <div id="scroll-container">
+      <!-- The anchor is not visible to the screen, but it is visible in the -->
+      <!-- containing block of anchor1 and target1, so the target should not -->
+      <!-- be hidden due to position-visibility: anchors-visible. -->
+      <div id="anchor">anchor</div>
+      <div id="spacer"></div>
+      <div id="target">target</div>
+    </div>
+  </div>
+</div>
+
+<script>
+  const non_intervening_scroller = document.getElementById('non-intervening-scroll-container');
+  non_intervening_scroller.scrollLeft = 100;
+</script>
diff --git a/css/css-anchor-position/position-visibility-anchors-visible-ref.html b/css/css-anchor-position/position-visibility-anchors-visible-ref.html
index 6f8d3cb..1779817 100644
--- a/css/css-anchor-position/position-visibility-anchors-visible-ref.html
+++ b/css/css-anchor-position/position-visibility-anchors-visible-ref.html
@@ -3,30 +3,17 @@
 <style>
   #scroll-container {
     overflow: hidden scroll;
-    width: 400px;
+    width: 300px;
     height: 100px;
   }
 
-  #contents-container {
-    height: 400px;
-  }
-
-  .anchor {
-    width: 100px;
-    height: 100px;
-    background: orange;
-    display: inline-block;
+  #spacer {
+    height: 200px;
   }
 </style>
 
 <div id="scroll-container">
-  <div id="contents-container">
-    <div class="anchor">anchor1</div>
-
-    <div class="anchor" style="height: 150px;">anchor2</div>
-
-    <div class="anchor" style="height: 150px;">anchor3</div>
-  </div>
+  <div id="spacer"></div>
 </div>
 
 <script>
diff --git a/css/css-anchor-position/position-visibility-anchors-visible-with-position.tentative.html b/css/css-anchor-position/position-visibility-anchors-visible-with-position.tentative.html
new file mode 100644
index 0000000..82eed0b
--- /dev/null
+++ b/css/css-anchor-position/position-visibility-anchors-visible-with-position.tentative.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="assert" content="Position-visibility: anchors-visible should hide an element with an out-of-view anchor and a relpos scroller." />
+<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="match" href="position-visibility-anchors-visible-ref.html">
+<style>
+  #scroll-container {
+    overflow: hidden scroll;
+    width: 300px;
+    height: 100px;
+    /* Same as position-visibility-anchors-visible.html, but with relpos here */
+    position: relative;
+  }
+
+  #anchor {
+    anchor-name: --a1;
+    width: 100px;
+    height: 100px;
+    background: orange;
+  }
+
+  #spacer {
+    height: 100px;
+  }
+
+  #target {
+    position-anchor: --a1;
+    position-visibility: anchors-visible;
+    inset-area: bottom right;
+    width: 100px;
+    height: 100px;
+    background: red;
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+</style>
+
+<div id="scroll-container">
+  <div id="anchor">anchor</div>
+  <div id="spacer"></div>
+  <div id="target">target</div>
+</div>
+
+<script>
+  const scroller = document.getElementById('scroll-container');
+  scroller.scrollTop = 100;
+  // #target should not be visible because #anchor is scrolled out of view.
+</script>
diff --git a/css/css-anchor-position/position-visibility-anchors-visible.tentative.html b/css/css-anchor-position/position-visibility-anchors-visible.tentative.html
index 6605bbc..85b8d89 100644
--- a/css/css-anchor-position/position-visibility-anchors-visible.tentative.html
+++ b/css/css-anchor-position/position-visibility-anchors-visible.tentative.html
@@ -1,55 +1,48 @@
 <!DOCTYPE html>
 <meta charset="utf-8">
+<meta name="assert" content="Position-visibility: anchors-visible should hide an element with an out-of-view anchor." />
 <title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
 <link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
 <link rel="match" href="position-visibility-anchors-visible-ref.html">
 <style>
   #scroll-container {
     overflow: hidden scroll;
-    width: 400px;
+    width: 300px;
     height: 100px;
   }
 
-  #contents-container {
-    height: 400px;
-  }
-
-  .anchor {
+  #anchor {
+    anchor-name: --a1;
     width: 100px;
     height: 100px;
     background: orange;
-    display: inline-block;
   }
 
-  .target {
-    position: absolute;
+  #spacer {
+    height: 100px;
+  }
+
+  #target {
+    position-anchor: --a1;
     position-visibility: anchors-visible;
-    inset-area: block-end;
+    inset-area: bottom right;
     width: 100px;
     height: 100px;
     background: red;
+    position: absolute;
     top: 0;
     left: 0;
   }
 </style>
 
 <div id="scroll-container">
-  <div id="contents-container">
-    <!-- #target1 should not be visible because anchor is scrolled to not be visible. -->
-    <div class="anchor" style="anchor-name: --a1;">anchor1</div>
-    <div id="target1" class="target" style="position-anchor: --a1;">target1</div>
-
-    <!-- #target2 should not be visible because referenced name in anchor() is not visible. -->
-    <div class="anchor" style="anchor-name: --a2; height: 150px;">anchor2</div>
-    <div id="target2" class="target" style="position-anchor: --a2; top: anchor(--a1 bottom);">target2</div>
-
-    <!-- #target3 should not be visible because referenced name in anchor-size() is not visible. -->
-    <div class="anchor" style="anchor-name: --a3; height: 150px;">anchor3</div>
-    <div id="target3" class="target" style="position-anchor: --a3; min-width: anchor-width(--a1 width);">target3</div>
-  </div>
+  <div id="anchor">anchor</div>
+  <div id="spacer"></div>
+  <div id="target">target</div>
 </div>
 
 <script>
   const scroller = document.getElementById('scroll-container');
   scroller.scrollTop = 100;
+  // #target should not be visible because #anchor is scrolled out of view.
 </script>
diff --git a/css/css-anchor-position/position-visibility-remove-anchors-visible-ref.html b/css/css-anchor-position/position-visibility-remove-anchors-visible-ref.html
new file mode 100644
index 0000000..135763b
--- /dev/null
+++ b/css/css-anchor-position/position-visibility-remove-anchors-visible-ref.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+  #scroll-container {
+    overflow: hidden scroll;
+    width: 300px;
+    height: 100px;
+  }
+
+  #target {
+    width: 100px;
+    height: 100px;
+    margin-top: 100px;
+    background: green;
+  }
+</style>
+
+<div id="scroll-container">
+  <div id="target">target</div>
+</div>
+
+<script>
+  const scroller = document.getElementById('scroll-container');
+  scroller.scrollTop = 100;
+</script>
diff --git a/css/css-anchor-position/position-visibility-remove-anchors-visible.tentative.html b/css/css-anchor-position/position-visibility-remove-anchors-visible.tentative.html
new file mode 100644
index 0000000..c6649e5
--- /dev/null
+++ b/css/css-anchor-position/position-visibility-remove-anchors-visible.tentative.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<meta charset="utf-8">
+<meta name="assert" content="Removing position-visibility: anchors-visible from an invisible anchored element should cause it to become visible." />
+<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title>
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758">
+<link rel="match" href="position-visibility-remove-anchors-visible-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<script src="/common/rendering-utils.js"></script>
+<style>
+  #scroll-container {
+    overflow: hidden scroll;
+    width: 300px;
+    height: 100px;
+  }
+
+  #anchor {
+    anchor-name: --a1;
+    width: 100px;
+    height: 100px;
+    background: orange;
+  }
+
+  #spacer {
+    height: 100px;
+  }
+
+  #target {
+    position-anchor: --a1;
+    position-visibility: anchors-visible;
+    inset-area: bottom;
+    width: 100px;
+    height: 100px;
+    background: green;
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+</style>
+
+<div id="scroll-container">
+  <div id="anchor">anchor</div>
+  <div id="spacer"></div>
+  <div id="target">target</div>
+</div>
+
+<script>
+  // #target should be initially visible because it is anchored to #anchor,
+  // which is visible.
+
+  // Scroll #anchor so that it is no longer visible.
+  const scroller = document.getElementById('scroll-container');
+  scroller.scrollTop = 100;
+
+  waitForAtLeastOneFrame().then(() => {
+    // Remove position-visibility: anchors-visible. #target should become
+    // visible again.
+    target.style.positionVisibility = 'initial';
+    takeScreenshot();
+  });
+</script>