feat: improve word wrapping in ChatLine

This commit is contained in:
Stone_Red
2026-02-24 13:53:40 +01:00
parent cf5fca5772
commit 3273e62b37
3 changed files with 70 additions and 37 deletions
+66 -36
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))
tokens.Add((g, segment.Color));
var results = new List<ChatLine>();
int pos = 0;
bool firstLine = true;
while (pos < tokens.Count)
{ {
var text = segment.Text; // First line uses the full width
int chunkStart = 0; int col = firstLine ? 0 : continuationIndent;
int charPos = 0; int lastSpaceIdx = -1;
foreach (var grapheme in GraphemeHelper.GetGraphemes(text)) int i = pos;
for (; i < tokens.Count; i++)
{ {
var graphemeCols = Math.Max(grapheme.GetColumns(), 1); var graphemeCols = Math.Max(tokens[i].grapheme.GetColumns(), 1);
if (col + graphemeCols > width) break;
if (col + graphemeCols > width) if (tokens[i].grapheme == " ") lastSpaceIdx = i;
{
if (charPos > chunkStart)
currentSegments.Add(new ChatSegment(text[chunkStart..charPos], segment.Color));
results.Add(new ChatLine(currentSegments));
currentSegments = [];
if (continuationIndent > 0)
{
currentSegments.Add(new ChatSegment(new string(' ', continuationIndent), null));
col = continuationIndent;
}
else
{
col = 0;
}
chunkStart = charPos;
}
col += graphemeCols; col += graphemeCols;
charPos += grapheme.Length;
} }
if (chunkStart < text.Length) int lineEnd, nextPos;
currentSegments.Add(new ChatSegment(text[chunkStart..], segment.Color)); 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
{
// No space found, break word.
lineEnd = Math.Max(i, pos + 1);
nextPos = lineEnd;
}
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;
}
results.Add(new ChatLine(segments));
pos = nextPos;
firstLine = false;
} }
if (currentSegments.Count > 0) if (results.Count == 0)
results.Add(new ChatLine(currentSegments)); return [this];
// 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
{ {