Merge pull request #25 from HueByte/dev_improve_word_wrapping

feat: improve word wrapping in ChatLine
This commit is contained in:
Stone_Red
2026-02-24 13:54:36 +01:00
committed by GitHub
3 changed files with 70 additions and 37 deletions
+60 -30
View File
@@ -1,3 +1,4 @@
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using EchoHub.Core.Models; using EchoHub.Core.Models;
using Terminal.Gui.Drawing; using Terminal.Gui.Drawing;
@@ -18,6 +19,8 @@ public partial class ChatLine
public string? AttachmentUrl { get; set; } public string? AttachmentUrl { get; set; }
public string? AttachmentFileName { get; set; } public string? AttachmentFileName { get; set; }
public MessageType? Type { get; set; } public MessageType? Type { get; set; }
/// <summary>Number of spaces to prepend on continuation lines when this line is word-wrapped.</summary>
public int ContinuationIndent { get; set; }
public ChatLine(string plainText) public ChatLine(string plainText)
{ {
@@ -42,51 +45,78 @@ public partial class ChatLine
if (width <= 0 || TextLength <= width) if (width <= 0 || TextLength <= width)
return [this]; return [this];
var results = new List<ChatLine>(); var tokens = new List<(string grapheme, Attribute? color)>();
var currentSegments = new List<ChatSegment>();
int col = 0;
foreach (var segment in Segments) foreach (var segment in Segments)
{ foreach (var g in GraphemeHelper.GetGraphemes(segment.Text))
var text = segment.Text; tokens.Add((g, segment.Color));
int chunkStart = 0;
int charPos = 0;
foreach (var grapheme in GraphemeHelper.GetGraphemes(text)) var results = new List<ChatLine>();
{ int pos = 0;
var graphemeCols = Math.Max(grapheme.GetColumns(), 1); bool firstLine = true;
if (col + graphemeCols > width) while (pos < tokens.Count)
{ {
if (charPos > chunkStart) // First line uses the full width
currentSegments.Add(new ChatSegment(text[chunkStart..charPos], segment.Color)); int col = firstLine ? 0 : continuationIndent;
int lastSpaceIdx = -1;
results.Add(new ChatLine(currentSegments)); int i = pos;
currentSegments = []; for (; i < tokens.Count; i++)
if (continuationIndent > 0)
{ {
currentSegments.Add(new ChatSegment(new string(' ', continuationIndent), null)); var graphemeCols = Math.Max(tokens[i].grapheme.GetColumns(), 1);
col = continuationIndent; if (col + graphemeCols > width) break;
if (tokens[i].grapheme == " ") lastSpaceIdx = i;
col += graphemeCols;
}
int lineEnd, nextPos;
if (i == tokens.Count)
{
// All remaining tokens fit on this line.
lineEnd = tokens.Count;
nextPos = tokens.Count;
}
else if (lastSpaceIdx >= pos)
{
// Break at the last space that fit, skip the space itself.
lineEnd = lastSpaceIdx;
nextPos = lastSpaceIdx + 1;
} }
else else
{ {
col = 0; // No space found, break word.
lineEnd = Math.Max(i, pos + 1);
nextPos = lineEnd;
} }
chunkStart = charPos; var segments = new List<ChatSegment>();
if (!firstLine && continuationIndent > 0)
segments.Add(new ChatSegment(new string(' ', continuationIndent), null));
// Rebuild segments by grouping consecutive same-color tokens.
var sb = new StringBuilder();
int groupStart = pos;
while (groupStart < lineEnd)
{
var color = tokens[groupStart].color;
sb.Clear();
int groupEnd = groupStart;
while (groupEnd < lineEnd && tokens[groupEnd].color == color)
{
sb.Append(tokens[groupEnd].grapheme);
groupEnd++;
}
segments.Add(new ChatSegment(sb.ToString(), color));
groupStart = groupEnd;
} }
col += graphemeCols; results.Add(new ChatLine(segments));
charPos += grapheme.Length; pos = nextPos;
firstLine = false;
} }
if (chunkStart < text.Length) if (results.Count == 0)
currentSegments.Add(new ChatSegment(text[chunkStart..], segment.Color)); return [this];
}
if (currentSegments.Count > 0)
results.Add(new ChatLine(currentSegments));
// Propagate attachment/type metadata to all wrapped lines so they remain clickable // Propagate attachment/type metadata to all wrapped lines so they remain clickable
foreach (var wrapped in results) foreach (var wrapped in results)
@@ -259,6 +259,9 @@ public sealed class ChatMessageManager
lines.Add(new ChatLine(ChatColors.SplitMentions(contText))); lines.Add(new ChatLine(ChatColors.SplitMentions(contText)));
} }
foreach (var l in lines)
l.ContinuationIndent = indent.Length;
if (message.Embeds is { Count: > 0 }) if (message.Embeds is { Count: > 0 })
{ {
var chatWidth = _chatWidth > 0 ? _chatWidth : 80; var chatWidth = _chatWidth > 0 ? _chatWidth : 80;
+1 -1
View File
@@ -772,7 +772,7 @@ public sealed class MainWindow : Runnable
if (width > 0) if (width > 0)
{ {
foreach (var line in messages) foreach (var line in messages)
source.AddRange(line.Wrap(width)); source.AddRange(line.Wrap(width, line.ContinuationIndent));
} }
else else
{ {