MarkdownViewer supports document-wide text selection via a transparent DocumentSelectionLayer overlay. The layer covers the entire rendered document and uses TextBlock.TextLayout hit-testing to map pointer positions to character offsets.
| Gesture / Key | Action |
|---|---|
| Click + drag | Select a text range |
Ctrl+A |
Select all text in the document |
Ctrl+C |
Copy the current selection to the clipboard |
| Right-click or the menu key | Open the context menu |
// Select all text
viewer.SelectAll();
// Clear the selection
viewer.ClearSelection();
// Read the current selection (lines separated by '\n')
string text = viewer.SelectedText; // or viewer.GetSelectedText()
// Copy to clipboard (with the platform's line endings, "\r\n" on Windows), with HTML next to the text
await viewer.CopyToClipboardAsync();
// Read or copy the selection as Markdown
string markdown = viewer.GetSelectedMarkdown();
await viewer.CopyAsMarkdownAsync();
GetSelectedMarkdown() returns the selection as Markdown, and CopyAsMarkdownAsync() writes it to the clipboard as plain text. Lines are separated by \n in the first, by the platform's line endings in the second.
Ctrl+A) gives back the Markdown it was rendered from, as written, task toggles included.**bold**, *italic*, - bullets, inline HTML for subscript, superscript, marked and inserted text), not with the source's own characters. Text that Markdown would read as syntax is escaped.MarkdownSelection.CopyText, which is their source.A link written with a reference copied as part of a whole block keeps its [text][ref] form; the definition is only included when it is selected too.
When the selection is copied (Ctrl+C or CopyToClipboardAsync()), the viewer also writes it as HTML, next to the plain text, so that pasting into a word processor, a chat or a web editor keeps headings, lists, emphasis, links, code and tables. The HTML is rendered by Markdig, with the viewer's pipeline, from what GetSelectedMarkdown() returns, so it holds exactly what was selected. Relative addresses are resolved against BaseUri (or the folder of Source).
Avalonia has no cross-platform HTML clipboard format, so the viewer writes HTML Format on Windows, public.html on macOS and text/html on Linux. In the browser and on mobile platforms, only the plain text is written.
Set IsHtmlCopyEnabled="False" to write the plain text alone. CopyAsMarkdownAsync() and a code block's copy button never write HTML.
The viewer has a context menu, opened by a right-click or, when the viewer or a link has the focus, by the menu key (the platform's OpenContextMenu hotkeys):
| Item | Action |
|---|---|
| Copy | Copies the selection, as Ctrl+C does. Disabled when nothing is selected. |
| Copy as Markdown | Copies the selection as Markdown. Disabled when nothing is selected. |
| Copy link | Copies the address of the link the menu was opened on, as written in the document. Shown only on a link, under the pointer or focused with Tab. |
| Copy link to heading | Copies a link to the heading the menu was opened on. Shown only on a heading, under the pointer. |
| Copy table | Copies the whole table the menu was opened on, whatever is selected, as tab-separated values and an HTML table. Shown only on a table, under the pointer or holding the focused link. |
| Copy table as Markdown | Copies that table as Markdown. Shown with Copy table. |
| Save image as… | Saves the image or Mermaid diagram the menu was opened on to a file, see Saving images and diagrams. Shown only on one, under the pointer or focused with Tab, while IsImageSaveEnabled is set. |
| Select all | Selects the whole document, as Ctrl+A does. |
A right-click on text outside the selection clears it first, as in a text editor; a right-click inside it keeps it, so it can be copied.
The menu is the viewer's default ContextFlyout, used only when the application sets neither ContextFlyout nor ContextMenu. Set either one, locally or in a style, to show your own menu, or set ContextFlyout to null to have none:
<mv:MarkdownViewer ContextFlyout="{x:Null}" />
The labels are resources, read each time the menu opens, so they can be translated or reworded like any other resource:
| Resource key | Default |
|---|---|
StringTextFlyoutCopyText |
Copy (the key a TextBox's own menu uses, so a translation of it applies to both) |
MarkdownStringCopyAsMarkdown |
Copy as Markdown |
MarkdownStringCopyLink |
Copy link |
MarkdownStringCopyHeadingLink |
Copy link to heading |
MarkdownStringCopyTable |
Copy table |
MarkdownStringCopyTableAsMarkdown |
Copy table as Markdown |
MarkdownStringSaveImage |
Save image as… |
MarkdownStringSelectAll |
Select all |
<Application.Resources>
<x:String x:Key="StringTextFlyoutCopyText">Copier</x:String>
<x:String x:Key="MarkdownStringCopyAsMarkdown">Copier en Markdown</x:String>
<x:String x:Key="MarkdownStringCopyLink">Copier le lien</x:String>
<x:String x:Key="MarkdownStringCopyHeadingLink">Copier le lien vers le titre</x:String>
<x:String x:Key="MarkdownStringCopyTable">Copier le tableau</x:String>
<x:String x:Key="MarkdownStringCopyTableAsMarkdown">Copier le tableau en Markdown</x:String>
<x:String x:Key="MarkdownStringSaveImage">Enregistrer l'image sous…</x:String>
<x:String x:Key="MarkdownStringSelectAll">Tout sélectionner</x:String>
</Application.Resources>
Each item has a style class (markdown-context-copy, markdown-context-copy-markdown, markdown-context-copy-link, markdown-context-copy-heading-link, markdown-context-copy-table, markdown-context-copy-table-markdown, markdown-context-save-image, markdown-context-select-all), for instance to give it an icon or hide it:
<Style Selector="MenuItem.markdown-context-copy-markdown">
<Setter Property="IsVisible" Value="False" />
</Style>
Copy link to heading writes the address of the document with the heading's anchor as its fragment, such as https://example.com/guide.md#installation, when the document was loaded through Source from the web or a file: the link opens the document at that heading in a viewer given it as its Source, and on a site that makes the same GitHub-style anchors. Any fragment Source had is replaced, and its query is kept. A document set through Markdown, or loaded from the application's resources (avares://), gives the anchor alone, such as #installation, which links to the heading from the same document.
Copy table writes the table as tab-separated values, one line per row, which a spreadsheet such as Excel, LibreOffice Calc or Google Sheets pastes as cells. Each cell's text is the one selecting it would copy (an image gives its alt text); a tab or line break in a cell becomes a space, and a cell spanning several columns is followed by empty ones. While IsHtmlCopyEnabled is on, an HTML <table> is written next to it, with the cells' emphasis, code and links, for word processors and web editors (see HTML on the clipboard).
Copy table as Markdown writes the table's source text when it is a top-level block of the document, exactly as written. A table inside a list, a quote or an alert is rebuilt from its cells as a normalised Markdown table, without the list or quote around it.
Both copy the whole table, whatever is selected, and leave the selection as it is.
SelectionChanged is raised whenever the selected range changes: at each step of a drag that moves its end to another character, on SelectAll() and ClearSelection(), on a click that clears it, when a new document replaces the one it was in, and when a streaming update shortens or clears it. It is not raised when the range stays the same, so pressing Ctrl+A twice raises it once.
When a drag that changed the selection ends, SelectionChanged is raised once more. e.IsSelecting (MarkdownSelectionChangedEventArgs) is true for the events raised during the drag and false for that last one and every other change, so a handler that only needs the final selection skips the rest:
viewer.SelectionChanged += (_, e) =>
{
if (!e.IsSelecting) preview.Text = viewer.SelectedText;
};
HasSelection is a bindable read-only property, true while some text is selected. It only changes when the selection starts or stops being empty, so it suits a Copy button's IsEnabled:
<Button Content="Copy" IsEnabled="{Binding #Viewer.HasSelection}" Click="OnCopy" />
viewer.SelectionChanged += (_, _) => status.Text = $"{viewer.SelectedText.Length} characters selected";
The event args carry no text, so raising the event allocates no string. SelectedText is built on the first read after the selection changes, and later reads return the same string until it changes again (or an alert in it is folded or unfolded). It is not a bindable property.
SelectionChanged is a bubbling routed event (MarkdownViewer.SelectionChangedEvent), so a container holding several viewers, such as a chat list, can handle it once for all of them; e.Source is the viewer.
CopyingToClipboard is raised before the viewer writes anything to the clipboard, when the selection is copied (Ctrl+C, CopyToClipboardAsync() or CopyAsMarkdownAsync()), when the copy button of a code block or a display formula is clicked, and when the context menu copies a link or a table. It is not raised when there is nothing to copy.
| Property | Meaning |
|---|---|
Origin |
What is copied: MarkdownCopyOrigin.Selection, MarkdownCopyOrigin.SelectionAsMarkdown, MarkdownCopyOrigin.CodeBlock, MarkdownCopyOrigin.MathBlock (a display formula's TeX source), MarkdownCopyOrigin.Link, MarkdownCopyOrigin.HeadingLink, MarkdownCopyOrigin.Table (tab-separated values) or MarkdownCopyOrigin.TableAsMarkdown |
Text |
The text to write, lines separated by \n. Set it to change what is written; it is written with the platform's line endings. Set to null or "", nothing is written. |
Html |
The HTML written next to the text, for Selection and Table only; null otherwise, and when IsHtmlCopyEnabled is off or the platform has no HTML format. Set it to change or drop the HTML. |
Cancel |
Set to true to write nothing, for instance when the application writes the clipboard itself. A copy button then shows no confirmation. |
viewer.CopyingToClipboard += (_, e) =>
{
if (e.Origin == MarkdownCopyOrigin.Selection)
e.Text += "\n\nCopied from the docs";
};
Like SelectionChanged, it is a bubbling routed event (MarkdownViewer.CopyingToClipboardEvent).
All text-bearing block types are registered with the selection layer:
•, 1., or the ☑/☐ task-list glyph — for a clickable task-list checkbox, its current state) and any code blocks or tables nested inside them<div>Non-text content is copied as a text equivalent:
| Content | Highlighted | Copied as |
|---|---|---|
Image (including linked images, HTML <img> and YouTube thumbnails) |
With the surrounding text | Its alt text; nothing when it has none |
HTML <kbd> key (HTML subset) |
As a whole, with the surrounding text | Its text |
Inline math (MarkView.Avalonia.Math) |
With the surrounding text | Its source between its own delimiters: $...$, \(...\), or \[...\] within a line |
Display math (MarkView.Avalonia.Math) |
As a whole block | Its source between $$ lines, or \[ and \] lines |
Mermaid diagram (MarkView.Avalonia.Mermaid) |
As a whole block | Its source in a ```mermaid fence |
| Control of a fence renderer | As a whole block | Its source in its fence, or the control's own MarkdownSelection.CopyText |
Hidden content is not selectable: the body of a collapsed foldable alert (> [!NOTE]-) details block (<details>) or heading section is skipped by hit-testing, highlighting and copy (Ctrl+A then Ctrl+C copies the text around it) until it is expanded. The same applies to any block a custom renderer hides with IsVisible. An alert's header and a details block's summary are not selectable; clicking one toggles its block instead of starting a selection.
Content scrolled out of view inside the document is treated the same way while it is out of view: the lines of a code block taller than CodeBlockMaxHeight that lie above or below its scroll viewport are neither hit-tested nor highlighted, so pointing at the text right below such a block selects that text, not the block's hidden lines. They are still copied when the selection spans them.
Custom renderers opt their own non-text controls in through the MarkdownSelection.CopyText attached property — see Content that is not text.
Finding text searches the same text blocks and draws its matches on the same overlay, under the selection; the two are independent, so finding never changes the selection and selecting never clears the matches.
DocumentSelectionLayer is a single transparent Control rendered in a single-cell Grid on top of the document. It tracks a start and end character offset across the entire flattened character index built by the renderer.
On PointerMoved, the layer calls TextBlock.TextLayout.HitTestPoint + TranslatePoint for each registered text block to find the nearest character offset, then redraws the selection highlight rectangles using HitTestTextRange. Both are limited to the part of each text block that its clipping ancestors inside the document (such as a code block's ScrollViewer) leave visible.
On copy, GetSelectedText() extracts the substring from each text block's registered text and joins them with newlines (tabs between table cells, a space after a list marker). Every line ending in the result is \n, including those inside code blocks and copy texts. The clipboard, both from Ctrl+C and from a code block's copy button, gets the platform's line endings instead (Environment.NewLine, so \r\n on Windows). A list item's marker is registered as its own text block, so it is highlighted with the item and selection offsets inside the item text line up with what is rendered.
A control embedded in a text block (InlineUIContainer) occupies one character position in its TextLayout, so it is registered as a U+FFFC placeholder that keeps selection offsets aligned with the layout. A block control with copy text is registered as an entry of its own, one placeholder long, whose bounds are hit-tested and highlighted as a whole. On copy, each placeholder is replaced by the control's MarkdownSelection.CopyText.