From 3273e62b374afa7ceca76347c61418e3ec102326 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:30:55 +0100 Subject: [PATCH] feat: improve word wrapping in ChatLine --- src/EchoHub.Client/UI/Chat/ChatLine.cs | 102 +++++++++++------- .../UI/Chat/ChatMessageManager.cs | 3 + src/EchoHub.Client/UI/MainWindow.cs | 2 +- 3 files changed, 70 insertions(+), 37 deletions(-) diff --git a/src/EchoHub.Client/UI/Chat/ChatLine.cs b/src/EchoHub.Client/UI/Chat/ChatLine.cs index cf230cc..087d16e 100644 --- a/src/EchoHub.Client/UI/Chat/ChatLine.cs +++ b/src/EchoHub.Client/UI/Chat/ChatLine.cs @@ -1,3 +1,4 @@ +using System.Text; using System.Text.RegularExpressions; using EchoHub.Core.Models; using Terminal.Gui.Drawing; @@ -18,6 +19,8 @@ public partial class ChatLine public string? AttachmentUrl { get; set; } public string? AttachmentFileName { get; set; } public MessageType? Type { get; set; } + /// Number of spaces to prepend on continuation lines when this line is word-wrapped. + public int ContinuationIndent { get; set; } public ChatLine(string plainText) { @@ -42,51 +45,78 @@ public partial class ChatLine if (width <= 0 || TextLength <= width) return [this]; - var results = new List(); - var currentSegments = new List(); - int col = 0; - + var tokens = new List<(string grapheme, Attribute? color)>(); foreach (var segment in Segments) + foreach (var g in GraphemeHelper.GetGraphemes(segment.Text)) + tokens.Add((g, segment.Color)); + + var results = new List(); + int pos = 0; + bool firstLine = true; + + while (pos < tokens.Count) { - var text = segment.Text; - int chunkStart = 0; - int charPos = 0; + // First line uses the full width + int col = firstLine ? 0 : continuationIndent; + 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); - - if (col + graphemeCols > width) - { - 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; - } - + var graphemeCols = Math.Max(tokens[i].grapheme.GetColumns(), 1); + if (col + graphemeCols > width) break; + if (tokens[i].grapheme == " ") lastSpaceIdx = i; col += graphemeCols; - charPos += grapheme.Length; } - if (chunkStart < text.Length) - currentSegments.Add(new ChatSegment(text[chunkStart..], segment.Color)); + 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 + { + // No space found, break word. + lineEnd = Math.Max(i, pos + 1); + nextPos = lineEnd; + } + + var segments = new List(); + 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) - results.Add(new ChatLine(currentSegments)); + if (results.Count == 0) + return [this]; // Propagate attachment/type metadata to all wrapped lines so they remain clickable foreach (var wrapped in results) diff --git a/src/EchoHub.Client/UI/Chat/ChatMessageManager.cs b/src/EchoHub.Client/UI/Chat/ChatMessageManager.cs index 533a4a1..7e921ed 100644 --- a/src/EchoHub.Client/UI/Chat/ChatMessageManager.cs +++ b/src/EchoHub.Client/UI/Chat/ChatMessageManager.cs @@ -259,6 +259,9 @@ public sealed class ChatMessageManager lines.Add(new ChatLine(ChatColors.SplitMentions(contText))); } + foreach (var l in lines) + l.ContinuationIndent = indent.Length; + if (message.Embeds is { Count: > 0 }) { var chatWidth = _chatWidth > 0 ? _chatWidth : 80; diff --git a/src/EchoHub.Client/UI/MainWindow.cs b/src/EchoHub.Client/UI/MainWindow.cs index 8f0929c..45a6a18 100644 --- a/src/EchoHub.Client/UI/MainWindow.cs +++ b/src/EchoHub.Client/UI/MainWindow.cs @@ -772,7 +772,7 @@ public sealed class MainWindow : Runnable if (width > 0) { foreach (var line in messages) - source.AddRange(line.Wrap(width)); + source.AddRange(line.Wrap(width, line.ContinuationIndent)); } else {