Bug 1921146 - Make SelectionMovementUtils::GetFrameForNodeOffset use last frame when the loop ended with invisible content r=emilio a=RyanVM

The editor in ChatGPT has only invisible `<br>` (`display: none`) and text
content as `::after`. `SelectionMovementUtils::GetFrameForNodeOffset` assumes
that at least one child has a primary frame.  Therefore, it may return `nullptr`
and `nsCaret` fails to paint the caret due to no frame.  So, it should use a
parent frame in such case.

However, doing it may cause an assertion failure [1] when
`IMEContentObserver::UpdateSelectionCache()` is called while an editor gets
focus and the editor does not have visible children.  Therefore, this patch
makes it allow to flush pending notifications when it gets current selection
range in the flattened text.  Fortunately, this change does not make damage
to the performance.  I guess the reason is, sending focus notification
synchronously will calling `ContentCacheInChild::CacheAll` soon.  Then,
it collects all information which should be cached in the parent process with
flushing the pending layout.  Therefore, this change should just do that a
little bit faster than before.

1. https://searchfox.org/mozilla-central/rev/7e0ae4372c52b8183d1178132dd6493edb576738/layout/generic/nsIFrame.cpp#2039

Differential Revision: https://phabricator.services.mozilla.com/D224561
This commit is contained in:
Masayuki Nakano 2024-10-04 22:41:07 +00:00
parent 93fa74e203
commit 4d7835983e
4 changed files with 78 additions and 12 deletions

View File

@ -1953,16 +1953,13 @@ void IMEContentObserver::IMENotificationSender::SendFocusSet() {
}
observer->mIMEHasFocus = true;
// Initialize selection cache with the first selection data.
#ifdef XP_MACOSX
// We need to flush layout only on macOS because character coordinates are
// cached by cocoa with this call, but we don't have a way to update them
// after that. Therefore, we need the latest layout information right now.
// Initialize selection cache with the first selection data. However, this
// may be handled synchronously when the editor gets focus. In that case,
// some frames may be dirty and they may be required to get caret frame in
// ContentEventHandler::Init() to get the nearest widget from the selection.
// Therefore, we need to update selection cache with flushing the pending
// notifications.
observer->UpdateSelectionCache(true);
#else
// We avoid flushing for focus in the general case.
observer->UpdateSelectionCache(false);
#endif // #ifdef XP_MACOSX #else
MOZ_LOG(sIMECOLog, LogLevel::Info,
("0x%p IMENotificationSender::SendFocusSet(), sending "
"NOTIFY_IME_OF_FOCUS...",

View File

@ -206,11 +206,15 @@ nsIFrame* SelectionMovementUtils::GetFrameForNodeOffset(
return nullptr;
}
nsIFrame* returnFrame = nullptr;
nsIFrame *returnFrame = nullptr, *lastFrame = aNode->GetPrimaryFrame();
nsCOMPtr<nsIContent> theNode;
uint32_t offsetInFrameContent;
uint32_t offsetInFrameContent, offsetInLastFrameContent = 0;
while (true) {
if (returnFrame) {
lastFrame = returnFrame;
offsetInLastFrameContent = offsetInFrameContent;
}
offsetInFrameContent = aOffset;
theNode = aNode;
@ -322,7 +326,11 @@ nsIFrame* SelectionMovementUtils::GetFrameForNodeOffset(
} // end while
if (!returnFrame) {
return nullptr;
if (!lastFrame) {
return nullptr;
}
returnFrame = lastFrame;
offsetInFrameContent = offsetInLastFrameContent;
}
// If we ended up here and were asked to position the caret after a visible

View File

@ -0,0 +1,30 @@
<!doctype html>
<html class="reftest-wait">
<head>
<meta charset="utf-8">
<title>Caret should be rendered in the editing host which has no visible non-anonymous content</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
div[contenteditable]::after {
content: "placeholder text";
}
</style>
<script>
"use strict";
addEventListener("load", async () => {
const editingHost = document.querySelector("div[contenteditable]");
document.activeElement?.blur();
const waitForFocus = new Promise(resolve =>
editingHost.addEventListener("focus", resolve, {once: true})
);
editingHost.focus();
await waitForFocus;
document.documentElement.removeAttribute("class");
}, {once: true});
</script>
</head>
<body>
<div contenteditable></div>
</body>
</html>

View File

@ -0,0 +1,31 @@
<!doctype html>
<html class="reftest-wait">
<head>
<meta charset="utf-8">
<title>Caret should be rendered in the editing host which has no visible non-anonymous content</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="match" href="editing-host-has-only-invisible-br-ref.html">
<style>
div[contenteditable]::after {
content: "placeholder text";
}
</style>
<script>
"use strict";
addEventListener("load", async () => {
const editingHost = document.querySelector("div[contenteditable]");
document.activeElement?.blur();
const waitForFocus = new Promise(resolve =>
editingHost.addEventListener("focus", resolve, {once: true})
);
editingHost.focus();
await waitForFocus;
document.documentElement.removeAttribute("class");
}, {once: true});
</script>
</head>
<body>
<div contenteditable><br style="display:none"></div>
</body>
</html>