commit 6ef3d44ec645cc393300806684d11c6de0b302b0
parent 485430290992d12505284a1ea8c0d01cbeaf571b
Author: Masayuki Nakano <masayuki@d-toybox.com>
Date: Wed, 3 Dec 2025 04:58:35 +0000
Bug 2003127 - Make the layout module paint normal selection when selection is styled as "attention" r=emilio,layout-reviewers
The "Find in Page" result appears as a normal selection range and the
selection's display is "attention". And that may select characters in
unselectable elements including `::before`/`::after` pseudo elements.
At least while the selection's display is "attention", we should paint
the normal selection in any nodes.
Note that once the selection's display is set to "on", the selection
becomes invisible since the user not selectable and won't appear as
visible text when you copy the selection to the clipboard. If this is
a bad UI, we should change the behavior in a follow up bug.
Differential Revision: https://phabricator.services.mozilla.com/D274550
Diffstat:
8 files changed, 98 insertions(+), 9 deletions(-)
diff --git a/layout/generic/nsContainerFrame.cpp b/layout/generic/nsContainerFrame.cpp
@@ -496,8 +496,9 @@ void nsContainerFrame::DisplaySelectionOverlay(nsDisplayListBuilder* aBuilder,
// look up to see what selection(s) are on this frame
UniquePtr<SelectionDetails> details = frameSelection->LookUpSelection(
newContent, offset, 1,
- IsSelectable() ? nsFrameSelection::IgnoreNormalSelection::No
- : nsFrameSelection::IgnoreNormalSelection::Yes);
+ ShouldPaintNormalSelection()
+ ? nsFrameSelection::IgnoreNormalSelection::No
+ : nsFrameSelection::IgnoreNormalSelection::Yes);
if (!details) {
return;
}
diff --git a/layout/generic/nsIFrame.cpp b/layout/generic/nsIFrame.cpp
@@ -4903,6 +4903,24 @@ bool nsIFrame::IsSelectable(StyleUserSelect* aSelectStyle) const {
return style != StyleUserSelect::None;
}
+bool nsIFrame::ShouldPaintNormalSelection() const {
+ if (IsSelectable()) {
+ // NOTE: Ideally, we should return false if display selection is "OFF".
+ // However, here is a hot path at painting. Therefore, it should be checked
+ // before and we shouldn't need to check it.
+ return true;
+ }
+ // If we're not selectable by user, we should paint selection only while the
+ // normal selection is styled as "attention" by "Find in Page" or something.
+ nsCOMPtr<nsISelectionController> selCon;
+ GetSelectionController(PresContext(), getter_AddRefs(selCon));
+ int16_t displaySelection = nsISelectionController::SELECTION_OFF;
+ if (selCon) {
+ selCon->GetDisplaySelection(&displaySelection);
+ }
+ return displaySelection == nsISelectionController::SELECTION_ATTENTION;
+}
+
bool nsIFrame::ShouldHaveLineIfEmpty() const {
switch (Style()->GetPseudoType()) {
case PseudoStyleType::NotPseudo:
@@ -9175,8 +9193,8 @@ bool nsIFrame::IsSelfEmpty() {
return IsHiddenByContentVisibilityOfInFlowParentForLayout();
}
-nsresult nsIFrame::GetSelectionController(nsPresContext* aPresContext,
- nsISelectionController** aSelCon) {
+nsresult nsIFrame::GetSelectionController(
+ nsPresContext* aPresContext, nsISelectionController** aSelCon) const {
if (!aPresContext || !aSelCon) {
return NS_ERROR_INVALID_ARG;
}
diff --git a/layout/generic/nsIFrame.h b/layout/generic/nsIFrame.h
@@ -4047,6 +4047,16 @@ class nsIFrame : public nsQueryFrame {
mozilla::StyleUserSelect* aSelectStyle = nullptr) const;
/**
+ * Return true if the frame should paint normal selection. This may return
+ * true even if IsSelectable() in some cases. E.g., when the normal selection
+ * is the result of "Find in Page".
+ * NOTE: This returns true even if the display selection is OFF since it
+ * should've already been checked before this is called and this should be
+ * cheaper as far as possible because of a part of painting.
+ */
+ [[nodiscard]] bool ShouldPaintNormalSelection() const;
+
+ /**
* Returns whether this frame should have the content-block-size of a line,
* even if empty.
*/
@@ -4059,7 +4069,7 @@ class nsIFrame : public nsQueryFrame {
* the frame.
*/
nsresult GetSelectionController(nsPresContext* aPresContext,
- nsISelectionController** aSelCon);
+ nsISelectionController** aSelCon) const;
/**
* Call to get nsFrameSelection for this frame.
diff --git a/layout/generic/nsTextFrame.cpp b/layout/generic/nsTextFrame.cpp
@@ -5237,8 +5237,9 @@ UniquePtr<SelectionDetails> nsTextFrame::GetSelectionDetails() {
// if `user-select` is specified to `none` so that we never stop painting
// selections when there is IME composition which may need normal
// selection as a part of it.
- IsSelectable() ? nsFrameSelection::IgnoreNormalSelection::No
- : nsFrameSelection::IgnoreNormalSelection::Yes);
+ ShouldPaintNormalSelection()
+ ? nsFrameSelection::IgnoreNormalSelection::No
+ : nsFrameSelection::IgnoreNormalSelection::Yes);
for (SelectionDetails* sd = details.get(); sd; sd = sd->mNext.get()) {
sd->mStart += mContentOffset;
sd->mEnd += mContentOffset;
diff --git a/layout/mathml/nsMathMLmoFrame.cpp b/layout/mathml/nsMathMLmoFrame.cpp
@@ -57,8 +57,9 @@ bool nsMathMLmoFrame::IsFrameInSelection(nsIFrame* aFrame) {
const nsFrameSelection* frameSelection = aFrame->GetConstFrameSelection();
UniquePtr<SelectionDetails> details = frameSelection->LookUpSelection(
aFrame->GetContent(), 0, 1,
- aFrame->IsSelectable() ? nsFrameSelection::IgnoreNormalSelection::No
- : nsFrameSelection::IgnoreNormalSelection::Yes);
+ aFrame->ShouldPaintNormalSelection()
+ ? nsFrameSelection::IgnoreNormalSelection::No
+ : nsFrameSelection::IgnoreNormalSelection::Yes);
return details != nullptr;
}
diff --git a/layout/reftests/selection/reftest.list b/layout/reftests/selection/reftest.list
@@ -56,5 +56,6 @@ skip-if(Android&&emulator) needs-focus fuzzy(0-5,0-1) == 1478604.html 1478604-re
skip-if(Android&&emulator) needs-focus fuzzy(0-3,0-13) == disabled-1.html disabled-1-ref.html # Bug 1912571 for failures in Android emulator
needs-focus != disabled-2.html disabled-2-notref.html
+needs-focus == selection-attention-in-user-select-none.html selection-attention-in-user-select-none-ref.html
== shadow-tree-order-1.html shadow-tree-order-1-ref.html
!= shadow-tree-order-1.html shadow-tree-order-1-notref.html
diff --git a/layout/reftests/selection/selection-attention-in-user-select-none-ref.html b/layout/reftests/selection/selection-attention-in-user-select-none-ref.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Normal selection for the "Find in Page" result should be painted even if `user-select:none`</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<style></style>
+<script>
+"use strict";
+
+document.querySelector("style").textContent = `
+ ::selection {
+ color: ${SpecialPowers.getStringPref("ui.textSelectAttentionForeground")};
+ background-color: ${SpecialPowers.getStringPref("ui.textSelectAttentionBackground")};
+ }
+`;
+
+addEventListener("DOMContentLoaded", () => {
+ // Select "THIS".
+ getSelection().setBaseAndExtent(
+ document.body.firstChild,
+ "Look for ".length,
+ document.body.firstChild,
+ "Look for THIS".length
+ );
+}, {once: true});
+</script>
+</head>
+<body>Look for THIS.</body>
+</html>
diff --git a/layout/reftests/selection/selection-attention-in-user-select-none.html b/layout/reftests/selection/selection-attention-in-user-select-none.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Normal selection for the "Find in Page" result should be painted even if `user-select:none`</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script>
+"use strict";
+
+addEventListener("DOMContentLoaded", () => {
+ // Select "THIS".
+ getSelection().setBaseAndExtent(
+ document.body.firstChild,
+ "Look for ".length,
+ document.body.firstChild,
+ "Look for THIS".length
+ );
+ const selCon = SpecialPowers.wrap(window).docShell
+ .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsISelectionDisplay)
+ .QueryInterface(SpecialPowers.Ci.nsISelectionController);
+ selCon.setDisplaySelection(SpecialPowers.Ci.nsISelectionController.SELECTION_ATTENTION);
+}, {once: true});
+</script>
+</head>
+<body style="user-select:none">Look for THIS.</body>
+</html>