Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Word-wrap functionality for MultilineTextBox #984

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 109 additions & 8 deletions Blish HUD/Controls/MultilineTextBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,88 @@ public bool HideBackground {
set => SetProperty(ref _hideBackground, value);
}

protected int[] _displayNewLineIndices = Array.Empty<int>();

/// <summary>
/// The indices of the added new line characters in the processed
/// <see cref="TextInputBase.DisplayText"/>.
/// </summary>
public int[] DisplayNewLineIndices => _displayNewLineIndices;

private bool _disableWordWrap;

/// <summary>
/// Determines whether the automatic word-wrap will be disabled.
/// </summary>
public bool DisableWordWrap {
get => _disableWordWrap;
set {
if (SetProperty(ref _disableWordWrap, value)) {
RecalculateLayout();
}
}
}

private char[] _wrapCharacters;

/// <summary>
/// The characters, that are used to wrap a word, if it does not fit the current line
/// it's on.
/// </summary>
public char[] WrapCharacters {
get => _wrapCharacters ?? Array.Empty<char>();
set {
if (SetProperty(ref _wrapCharacters, value)) {
RecalculateLayout();
}
}
}

public MultilineTextBox() {
_multiline = true;
_maxLength = 524288;
}

/// <summary>
/// Calculates the actual cursor index (in reference to
/// <see cref="TextInputBase.Text"/>), if the <paramref name="displayIndex"/>
/// was calculated using the <see cref="TextInputBase.DisplayText"/>.
/// </summary>
protected int GetCursorIndexFromDisplayIndex(int displayIndex) {
int cursorIndex = displayIndex;
foreach (int displayNewLineIndex in _displayNewLineIndices) {
if (displayNewLineIndex > displayIndex) break;
cursorIndex--;
}

return cursorIndex;
}

/// <summary>
/// Calculates the display cursor index (in reference to
/// <see cref="TextInputBase.DisplayText"/>), if the <paramref name="cursorIndex"/>
/// was calculated using the <see cref="TextInputBase.Text"/>.
/// </summary>
protected int GetDisplayIndexFromCursorIndex(int cursorIndex) {
int displayIndex = cursorIndex;
foreach (int displayNewLineIndex in _displayNewLineIndices) {
if (displayNewLineIndex > displayIndex) break;
displayIndex++;
}
return displayIndex;
}

protected override void MoveLine(int delta) {
int newIndex = 0; // if targetLine is < 0, we set cursor index to 0

string[] lines = _text.Split(NEWLINE);
string[] lines = _displayText.Split(NEWLINE);

var cursor = GetSplitIndex(_cursorIndex);

int targetLine = cursor.Line + delta;

if (targetLine >= lines.Length) {
newIndex = _text.Length;
newIndex = _displayText.Length;
} else if (targetLine >= 0) {
float cursorLeft = MeasureStringWidth(lines[cursor.Line].Substring(0, cursor.Character));
float minOffset = cursorLeft;
Expand All @@ -53,20 +119,42 @@ protected override void MoveLine(int delta) {
}
}

newIndex = GetCursorIndexFromDisplayIndex(newIndex);

UserSetCursorIndex(newIndex);
UpdateSelectionIfShiftDown();
}

/// <inheritdoc/>
protected override string ProcessDisplayText(string value) {
return ApplyWordWrap(value);
}

/// <summary>
/// Applies word-wrap to the <paramref name="value"/>.
/// </summary>
protected string ApplyWordWrap(string value) {
if (DisableWordWrap) {
_displayNewLineIndices = Array.Empty<int>();
return value;
}

string displayText = DrawUtil.WrapText(_font, value, this._textRegion.Width, WrapCharacters, out int[] newLineIndices);
_displayNewLineIndices = newLineIndices;

return displayText;
}

public override int GetCursorIndexFromPosition(int x, int y) {
x -= TEXT_LEFTPADDING;
y -= TEXT_TOPPADDING;

string[] lines = _text.Split(NEWLINE);
string[] lines = _displayText.Split(NEWLINE);

int predictedLine = y / _font.LineHeight;

if (predictedLine > lines.Length - 1) {
return _text.Length;
return GetCursorIndexFromDisplayIndex(_displayText.Length);
}

var glyphs = _font.GetGlyphs(lines[predictedLine]);
Expand All @@ -85,21 +173,28 @@ public override int GetCursorIndexFromPosition(int x, int y) {
charIndex += lines[i].Length + 1;
}

return charIndex;
return GetCursorIndexFromDisplayIndex(charIndex);
}

private Rectangle _textRegion = Rectangle.Empty;
private Rectangle[] _highlightRegions = Array.Empty<Rectangle>();
private Rectangle _cursorRegion = Rectangle.Empty;

/// <remarks>
/// The <paramref name="index"/> refers to the cursorIndex (in reference to
/// <see cref="TextInputBase.Text"/>), while the return value is based on
/// the <see cref="TextInputBase.DisplayText"/>.
/// </remarks>
private (int Line, int Character) GetSplitIndex(int index) {
int lineIndex = 0;
int charIndex = 0;

index = GetDisplayIndexFromCursorIndex(index);

for (int i = 0; i < index; i++) {
charIndex++;

if (_text[i] == NEWLINE) {
if (_displayText[i] == NEWLINE) {
lineIndex++;
charIndex = 0;
}
Expand All @@ -114,7 +209,7 @@ private Rectangle[] CalculateHighlightRegions() {

if (selectionLength <= 0 || selectionStart + selectionLength > _text.Length) return Array.Empty<Rectangle>();

string[] lines = _text.Split(NEWLINE);
string[] lines = _displayText.Split(NEWLINE);

var startIndex = GetSplitIndex(selectionStart);
var endIndex = GetSplitIndex(selectionStart + selectionLength);
Expand Down Expand Up @@ -173,7 +268,7 @@ private Rectangle CalculateTextRegion() {
private Rectangle CalculateCursorRegion() {
var cursor = GetSplitIndex(_cursorIndex);

string[] lines = _text.Split(NEWLINE);
string[] lines = _displayText.Split(NEWLINE);

float cursorLeft = MeasureStringWidth(lines[cursor.Line].Substring(0, cursor.Character));

Expand All @@ -191,11 +286,17 @@ private Rectangle CalculateCursorRegion() {
}

public override void RecalculateLayout() {
_displayText = ProcessDisplayText(_text);
_textRegion = CalculateTextRegion();
_highlightRegions = CalculateHighlightRegions();
_cursorRegion = CalculateCursorRegion();
}

protected override void HandleDelete() {
base.HandleDelete();
RecalculateLayout();
}

protected override void UpdateScrolling() { /* NOOP */ }

protected override void Paint(SpriteBatch spriteBatch, Rectangle bounds) {
Expand Down
20 changes: 19 additions & 1 deletion Blish HUD/Controls/TextInputBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ public string Text {
set => SetText(value, false);
}

protected string _displayText = string.Empty;

/// <summary>
/// The displayed text. Inheriting classes may process the
/// <see cref="Text"/> via <see cref="ProcessDisplayText(string)"/>.
/// </summary>
public string DisplayText => _displayText;

protected int _maxLength = int.MaxValue;

/// <summary>
Expand Down Expand Up @@ -485,11 +493,21 @@ protected bool SetText(string value, bool byUser) {
_redoStack.Reset();
}

_displayText = ProcessDisplayText(value);

OnTextChanged(new ValueChangedEventArgs<string>(prevText, value));

return true;
}

/// <summary>
/// Processes the <see cref="Text"/> before it is displayed. Result will be
/// used to set <see cref="DisplayText"/>.
/// </summary>
protected virtual string ProcessDisplayText(string value) {
return value;
}

public override void UnsetFocus() {
this.Focused = false;
GameService.Input.Keyboard.FocusedControl = null;
Expand Down Expand Up @@ -785,7 +803,7 @@ protected void PaintText(SpriteBatch spriteBatch, Rectangle textRegion, Horizont
}

// Draw the text
spriteBatch.DrawStringOnCtrl(this, _text, _font, textRegion, _foreColor, false, false, 0, horizontalAlignment, VerticalAlignment.Top);
spriteBatch.DrawStringOnCtrl(this, _displayText, _font, textRegion, _foreColor, false, false, 0, horizontalAlignment, VerticalAlignment.Top);
}

protected void PaintHighlight(SpriteBatch spriteBatch, Rectangle highlightRegion) {
Expand Down
Loading
Loading