Skip to content

Instantly share code, notes, and snippets.

@TimPaterson
Created May 8, 2023 21:28
Show Gist options
  • Save TimPaterson/3194bd3db1f34b2e1bfc412cc28b17d7 to your computer and use it in GitHub Desktop.
Save TimPaterson/3194bd3db1f34b2e1bfc412cc28b17d7 to your computer and use it in GitHub Desktop.
SelectableTextBlock: A WPF TextBlock control with selectable text
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
namespace S100debugger;
static class TextBlockExtension
{
const int LinebreakLength = 2;
public static TextPointer GetPositionFromIndex(this TextBlock textBlock, int index)
{
int curIndex = 0;
int length;
TextPointer pointer;
pointer = textBlock.ContentStart;
while (curIndex < index && pointer.CompareTo(textBlock.ContentEnd) < 0)
{
TextPointerContext context = pointer.GetPointerContext(LogicalDirection.Forward);
switch (context)
{
case TextPointerContext.Text:
length = pointer.GetTextRunLength(LogicalDirection.Forward);
if (length + curIndex > index)
{
// found it in this run
return pointer.GetPositionAtOffset(index - curIndex);
}
curIndex += length;
break;
case TextPointerContext.ElementStart:
DependencyObject element = pointer.GetAdjacentElement(LogicalDirection.Forward);
if (element is LineBreak)
curIndex += LinebreakLength;
break;
}
pointer = pointer.GetNextContextPosition(LogicalDirection.Forward);
}
return pointer;
}
public static int GetIndexFromPosition(this TextBlock textBlock, TextPointer position)
{
return new TextRange(textBlock.ContentStart, position).Text.Length;
}
}
class SelectableTextBlock : TextBlock
{
#region Constructor
public SelectableTextBlock() : base()
{
m_selectionStart = ContentStart;
m_selectionEnd = ContentStart;
m_selectedRange = new(ContentStart, ContentStart);
}
#endregion
#region Private Fields & Properties
readonly TextRange m_selectedRange;
TextPointer m_selectionStart;
TextPointer m_selectionEnd;
Point m_startPoint;
bool m_isSelecting = false;
#endregion
#region Public Properties & Events
public string SelectedText
{
get => m_selectedRange.Text;
set
{
m_selectedRange.Text = value;
m_selectionStart = m_selectedRange.Start;
m_selectionEnd = m_selectedRange.End;
SetSelected(m_selectedRange);
}
}
public TextRange SelectedRange => m_selectedRange;
public int SelectionStart
{
get => new TextRange(ContentStart, m_selectedRange.Start).Text.Length;
set
{
int length = SelectionLength;
SetNormal(m_selectedRange);
m_selectionStart = this.GetPositionFromIndex(value);
m_selectionEnd = this.GetPositionFromIndex(value + length);
m_selectedRange.Select(m_selectionStart, m_selectionEnd);
SetSelected(m_selectedRange);
}
}
public int SelectionLength
{
get => m_selectedRange.Text.Length;
set
{
int index = SelectionStart;
SetNormal(m_selectedRange);
m_selectionEnd = this.GetPositionFromIndex(value + index);
m_selectionStart = m_selectedRange.Start;
m_selectedRange.Select(m_selectionStart, m_selectionEnd);
SetSelected(m_selectedRange);
}
}
// Set this property to 1/2 the character width (obviously simplest
// if using a fixed-pitch font). It ensures the first character
// remains in the selection.
public double CharWidthAdjustment { get; set; } = 0;
public Brush SelectedBackground { get; set; } = SystemColors.HighlightBrush;
public Brush SelectedForeground { get; set; } = SystemColors.HighlightTextBrush;
public event EventHandler? SelectionChanging;
public event EventHandler? SelectionChanged;
#endregion
#region Private Methods
private void SetSelected(TextRange range)
{
range.ApplyPropertyValue(ForegroundProperty, SelectedForeground);
range.ApplyPropertyValue(BackgroundProperty, SelectedBackground);
}
private void SetNormal(TextRange range)
{
range.ApplyPropertyValue(ForegroundProperty, Foreground);
range.ApplyPropertyValue(BackgroundProperty, Background);
}
private void EndSelection()
{
if (m_isSelecting)
{
m_isSelecting = false;
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}
#endregion
#region Protected Method Overrides
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
SetNormal(m_selectedRange);
m_isSelecting = true;
m_startPoint = e.GetPosition(this);
m_selectionStart = GetPositionFromPoint(m_startPoint, true);
m_selectionEnd = m_selectionStart;
m_selectedRange.Select(m_selectionStart, m_selectionEnd);
SelectionChanging?.Invoke(this, EventArgs.Empty);
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (!m_isSelecting)
return;
if (e.LeftButton != MouseButtonState.Pressed)
{
EndSelection();
return;
}
TextPointer end = GetPositionFromPoint(e.GetPosition(this), true);
if (m_selectionEnd.CompareTo(end) == 0)
return;
SetNormal(m_selectedRange);
// Adjust start depending on selection direction
Point start = m_startPoint;
if (m_selectionStart.CompareTo(end) > 0)
start.X += CharWidthAdjustment;
else
start.X -= CharWidthAdjustment;
m_selectionStart = GetPositionFromPoint(start, true);
m_selectionEnd = end;
m_selectedRange.Select(m_selectionStart, m_selectionEnd);
SetSelected(m_selectedRange);
SelectionChanging?.Invoke(this, EventArgs.Empty);
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
EndSelection();
}
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
EndSelection();
}
#endregion
}
class TextBlockRunBuilder
{
readonly List<Inline> m_range = new();
readonly StringBuilder m_sb = new();
public void Append(string text) => m_sb.Append(text);
public void Append(char ch) => m_sb.Append(ch);
public void Append(char ch, int count) => m_sb.Append(ch, count);
public void AppendLine() => m_sb.AppendLine();
public void Append(char ch, Brush foreground, Brush? background = null)
{
Append(ch.ToString(), foreground, background);
}
public void Append(string text, Brush foreground, Brush? background = null)
{
if (m_sb.Length > 0)
{
m_range.Add(new Run(m_sb.ToString()));
m_sb.Clear();
}
Run run = new(text);
run.Foreground = foreground;
if (background != null)
run.Background = background;
m_range.Add(run);
}
public void SetTextBlock(TextBlock block)
{
if (m_sb.Length > 0)
{
m_range.Add(new Run(m_sb.ToString()));
m_sb.Clear();
}
block.Inlines.Clear();
block.Inlines.AddRange(m_range);
m_range.Clear();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment