Skip to content

ChangeHighlightAdorner

317jamtay317 edited this page Jul 26, 2026 · 1 revision

ChangeHighlightAdorner

The ChangeHighlightAdorner draws the Change Highlight over the MarkdownRichEditor's Visual Document after a live reload: a shade behind every Block another writer added or altered, and a thin tick at the seam of every run they deleted. It is what makes "another user or an AI just edited your file" something the reader can see rather than something that lands invisibly (INV-060).

  • Class: UI.Controls.ChangeHighlightAdorner (derives from System.Windows.Documents.Adorner)
  • Attached by: MarkdownRichEditor on Loaded, into the editor's AdornerLayerfirst of the editor's overlays, so its shade sits beneath the Code Shading, the spelling squiggles, and the Find highlights rather than washing them out.
  • Driven by: MarkdownRichEditor.ChangedRegions, bound to EditorSessionViewModel.ChangeHighlight.

Division of labour

The Change Highlight is split three ways, so only the editor Control ever touches the document:

  1. The Domain compares. ReloadDifference.Compute maps the Markdown Document as the session held it and the contents that replaced it to the ordered Changed Regions — runs of the reloaded text that were added or altered, and the empty seams where content was deleted. It is pure: no state, no I/O, no UI.
  2. The scanner resolves. ChangeHighlightScanner.Scan turns those line numbers into Blocks, using the Source Line Range that SourceLines records on each Block at projection time. A Changed region shades every Block it intersects; a Removed seam anchors to the first Block that starts at or after it, or below the last Block when the deletion fell at the end.
  3. The adorner draws. Show resolves the targets and starts the hold-then-fade; OnRender paints each on-screen target from the live palette brushes.

How it looks

  • A changed Block gets a soft rounded panel over its whole extent in ChangeHighlightBrush, with a 3px bar down its leading edge in ChangeHighlightMarkerBrush so the change still reads at a glance on a busy page.
  • A deletion seam gets a short 2px tick in ChangeHighlightMarkerBrush, between the Blocks that closed over the deleted content. There is nothing left to shade, so the mark says "something was here" without inventing content.
  • Both brushes come from the active palette, so the highlight follows the light/dark theme — and, like Code Shading, recoloring an overlay that owns no text can never reflow the document.

How it behaves

  • Hold, then fade. The overlay holds at full strength for 2.2s and then fades over 0.9s, animated on Opacity with FillBehavior.Stop. Nothing has to be dismissed, and it can never be mistaken for permanent document state.
  • It moves nothing. No caret, no selection, no scrolling — deliberately. The reload is another writer's action, and taking the reader's place away mid-read is not something they asked for. A change that lands off-screen simply is not seen, which is the accepted cost of not stealing focus.
  • Never stale (INV-060). The Editor Session clears ChangeHighlight on an edit, a load, or a save, which pushes an empty set and takes the overlay down early. A highlight therefore always refers to a change that is actually on screen.
  • Painted after layout. The regions arrive immediately after the reloaded source, when the new document has been projected but not yet laid out — every character rectangle would still be empty. Show therefore queues one repaint at DispatcherPriority.Loaded, which is dispatched after layout has run at Render.
  • Repaint (cheap). Scrolling and resizing repaint from the targets already resolved; they never re-resolve them. Off-screen targets are skipped.
  • View-only (INV-060). The adorner is presentation-only (IsHitTestVisible = false) and only ever draws. Nothing it holds feeds back into Capture, so seeing what changed never changes the Markdown Document.
  • Stale pointers. A repaint that races a document replacement swallows the resulting InvalidOperationException: a highlight only ever describes the document it was resolved against, and the next reload resolves afresh.

Why not for a Conflict

A Conflict is resolved through the Conflict Difference (INV-021), which shows both sides so the user can judge them. A Change Highlight can only ever show the side that won, which is the right thing for a reload — where there was no decision to make — and the wrong thing for a Conflict. Choosing Reload from disk to resolve a Conflict does raise the highlight, because at that point the decision has been made and the disk side is what is now on screen.

Clone this wiki locally