Skip to content

Fix impersistent highlight visibility - #21535

Open
LoukasPap wants to merge 4 commits into
mozilla:masterfrom
LoukasPap:fix/false-enable-of-highlights
Open

LoukasPap wants to merge 4 commits into
mozilla:masterfrom
LoukasPap:fix/false-enable-of-highlights

Conversation

@LoukasPap

@LoukasPap LoukasPap commented Jul 3, 2026 •

Copy link
Copy Markdown

Fixes #21498

When the "Show all" toggle is off, the highlights must stay hidden: while scrolling, after leaving editing mode, and when entering again.

Why hiding the editors isn't enough

A highlight can be shown by three different surfaces:

  1. the draw layer, where the editor draws its own svg.highlight while editing
  2. the annotation layer, which only provides hit-testing (double-click to edit)
  3. the page canvas, where the annotation is painted as pixels once the editing mode is off

Hiding an editor only takes care of the first one. As soon as the editing mode is left, or a page is rendered for the first time while scrolling, the annotation is painted into the canvas again and the highlight reappears.

Keeping them out of the canvas

Whether an annotation is painted is decided by mustBeViewedWhenEditing, which skips the ids it's given. So the highlights to hide must be listed by their annotation id, and that list can't be built from the existing editors: a page rendered after exiting editing mode, has no editor at all. It has to come from the document itself, through getAnnotationsByType.

That call takes annotation types, which is where the two kinds of highlight diverge. A highlight on a text is saved as a /Highlight, but a free (hand-drawn) one is saved as an /Ink with /IT /InkHighlight (see AnnotationFactory). Asking for highlights alone silently misses every free highlight, so both types are fetched and the ink ones are kept only when their intent says they're highlights.

Getting the ids to the renderer

AnnotationStorage already exposes modifiedIds, the annotations whose edited version is drawn instead of the original. The highlights to hide need the same treatment but for the opposite reason — they're unmodified — so rather than widening the meaning of modifiedIds, they're kept apart:

  • setHiddenIds(ids) / get hiddenIds hold them, with their own hash, computed lazily and cached exactly like modifiedIds;
  • both getters build their {ids, hash} pair through a shared #makeIdsWithHash, which is the only change made to modifiedIds itself;
  • getRenderingIntent unions the two sets and feeds both hashes into the rendering cache key so either concern invalidates on its own.

PrintAnnotationStorage overrides hiddenIds to an empty set, as it already does for modifiedIds: hiding a highlight in the viewer must not remove it from the printed output.

Why the hiddenIds aren't merged into modifiedIds

I first pushed them straight into modifiedIds, which is shorter and works. Two things made me keep them apart instead.

  • The getter answered two unrelated questions violating SRP — what the user changed, and what the UI decided to hide — under a name covering only the first.
  • Every toggle re-serialized every editor. With the merge living in that getter, setHiddenIds had to invalidate its cache, so switching "Show all" threw away the modified ids and the next render rebuilt them by calling serialize() on each editor.

Separating them means a toggle recomputes only what it actually changed, and modifiedIds keeps its original meaning.

The union is still handed to the worker under the name modifiedIds, since that's what the whole render path calls it. Renaming it there would make the intent clearer, but it reaches into src/core/ for what is otherwise a display-layer fix. I can change that in a follow-up, or here, if you'd prefer.

Consequences of the fetch being asynchronous

Collecting the annotations walks the whole document, which brings two things to handle:

  • the toggle can be switched back on while the fetch is in flight, so the state is checked again once it resolves and an outdated update is dropped;
  • the promise is cached rather than the ids it resolves with, so the callers arriving during that first fetch share one round trip instead of each starting their own.

Tests

test/integration/highlight_editor_spec.mjs

  1. must preserve the status when closing and opening any editor — the toggle stays off, and the highlight stays hidden, after a detour through another editing mode.
  2. must preserve the status when highlighting from edit toolbar and re-opening the editor — highlighting a text from the floating toolbar shows the highlights back, and that state survives closing and re-opening the editor.
  3. must preserve the hidden state after scrolling away and re-entering highlight mode — a highlight hidden on the first page is still hidden after highlighting on the fourteenth one and coming back to it.
  4. must keep the highlights hidden when the editor is closed — compares the coloured canvas pixels between the hidden and the shown state on a rendered page, on a page never rendered in editing mode, and on the page holding a free highlight.
  5. must show the highlights back when a text is highlighted — highlighting a text while they're hidden paints them back in the canvas, and closing the editor doesn't hide them again.

test/integration/freetext_editor_spec.mjs

  1. must keep a modified annotation visible when re-entering editing mode — an edited FreeText is still displayed after the editing mode has been left and entered again; the editor is looked up by its text, since the ids are regenerated across a round-trip.

The fourth and fifth ones read the canvas because that's the surface which had the bug: an earlier version of this patch passed a DOM-based assertion while the highlights were still fully painted. Counting the saturated pixels of a page is shared as countHighlightPixels in test_utils.mjs, next to isCanvasMonochrome.

test/pdfs/comments.pdf gains a free highlight on page 4, a page which isn't rendered when the document is opened.

Comment thread test/integration/highlight_editor_spec.mjs Outdated
@codecov-commenter

codecov-commenter commented Jul 4, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 19.64286% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.26%. Comparing base (f4f90c2) to head (5343e31).
⚠️ Report is 138 commits behind head on master.

Files with missing lines Patch % Lines
src/display/editor/tools.js 0.00% 28 Missing ⚠️
src/display/annotation_storage.js 52.63% 9 Missing ⚠️
src/display/editor/annotation_editor_layer.js 0.00% 7 Missing ⚠️
src/display/editor/editor.js 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #21535      +/-   ##
==========================================
- Coverage   90.31%   90.26%   -0.06%     
==========================================
  Files         269      269              
  Lines       67534    67580      +46     
==========================================
+ Hits        60994    61000       +6     
- Misses       6540     6580      +40     
Flag Coverage Δ
browsertest 66.10% <19.64%> (-0.08%) ⬇️
fonttest 8.94% <ø> (ø)
unittest 59.48% <19.64%> (-0.03%) ⬇️
unittestcli 57.97% <19.64%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@LoukasPap

Copy link
Copy Markdown
Author

@calixteman I applied the changes discussed

Comment thread src/display/editor/tools.js
@LoukasPap
LoukasPap marked this pull request as draft July 22, 2026 13:48
@LoukasPap
LoukasPap force-pushed the fix/false-enable-of-highlights branch 2 times, most recently from 9ab860e to 4d5fca8 Compare July 25, 2026 22:01
@LoukasPap
LoukasPap marked this pull request as ready for review July 25, 2026 22:02
Comment thread src/display/editor/annotation_editor_layer.js Outdated
@calixteman

Copy link
Copy Markdown
Contributor

It doesn't work unfortunately, here's a STR:

  • open freetexts.pdf;
  • switch to freetext mode;
  • modify an existing one;
  • switch to reading mode;
  • switch to freetext mode.

The modified freetext is no more visible.

An other problem, here's a STR:

  • open comments.pdf at page 1;
  • switch to highlight mode and hide all highlights;
  • scroll to page 6.

A highlight is visible.

@LoukasPap

Copy link
Copy Markdown
Author

Many cases are covered now. Changes are a little big but I believe the bug is now completely fixed.

Comment thread test/integration/test_utils.mjs Outdated
Comment thread test/test_manifest.json Outdated

This branch was successfully deployed

1 active (outdated) deployment
code-coverage — e8ad0278 Deployed Sep 3, 2026 by LoukasPap via windows-latest / chrome #5594
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Opening editor toolbar falsely enables hidden highlights

5 participants