Skip to content

Fix: Several Wordwrap issues - Indent, HangingIndent, international character widths #7714

Merged
vadi2 merged 47 commits intoMudlet:developmentfrom
Harrison-Teeg:wrap-overhaul
Jun 26, 2025
Merged

Fix: Several Wordwrap issues - Indent, HangingIndent, international character widths #7714
vadi2 merged 47 commits intoMudlet:developmentfrom
Harrison-Teeg:wrap-overhaul

Conversation

@Harrison-Teeg
Copy link
Copy Markdown
Contributor

@Harrison-Teeg Harrison-Teeg commented Feb 14, 2025

Brief overview of PR changes/additions

  • Created TTextProperties.h so TBuffer and TTextEdit both share the same functions for checking the width of graphemes.
  • Added the functions necessary to set hanging indent for (mini-)consoles (exposes Lua function: setWindowWrapHangingIndent(str:window, int:hangingIndent))
  • Deduplicated the process of appending and wrapping text in TBuffer to follow, generally:
    • append calls appendLine and wrapLine and handles buffer shrinking.
    • appendLine adds text to buffer.
    • wrapLine calls getWrapInfo then applies wrap, also logs the wrapped lines.
      • getWrapInfo checks the width of each grapheme for determining wrap points instead of the previous method of treating each QChar as width one.
More specifically

Created TTextProperties.h for one shared source of grapheme width for both TTextEdit and TBuffer:

  • graphemeInfo::getBaseCharacter copied over from TTextEdit::getGraphemeBaseCharacter
  • graphemeInfo::getWidth(unicode, mWideAmbigousWidthGlyphs) copied from TTextEdit::getGraphemeWidth(unicode) (with some modification)

Added TLuaInterpreter::setWindowWrapHangingIndent / TConsole::setHangingIndentCount / TBuffer::setWrapHangingIndent to allow setting hanging indents for any (mini)console

Added WrapInfo(bool isNewline, bool needsIndent, int firstChar, int lastChar) class to TBuffer.h for storing information on whether to indent / where to snip a line to properly wrap it.

Added the inline method TBuffer::getWrapInfo(const QString& lineText, bool isNewline, const int maxWidth, const int indent, const int hangingIndent) which takes information on the current line of text and returns a QList<WrapInfo> indicating the start / end indices and indentation to wrap the text (empty -> no wrap).
I think the code speaks for itself, but hand-wavily the algorithm for wrapping looks like this (glossing over how it handles edge cases and indentation):

  • Use QTextBoundaryFinder to identify each grapheme
  • Use QTextBoundaryFinder to identify linebreaks
  • loop through the line and track the width of each grapheme -> current x position
  • if the x position plus the expected indentation exceed the maxwidth:
    • check if the current character is a space or valid linebreak
    • if not, use the boundaryFinder to search backwards from the current position for a valid linebreak
    • if none found, choose current position for linebreak
    • record wrap point indices and indentation by adding a new WrapInfo to the list
  • return list of WrapInfos

Removed TBuffer::wrap (this is replaced by wrapLine)

Changed TBuffer::wrapLine(int startLine, int screenWidth, int indentSize, TChar& format) to TBuffer::wrapLine(int startLine, int maxWidth, int indentSize, int hangingIndentSize)
TBuffer::wrapLine still loops through each line in the buffer from the startline to the lastline, now calling getWrapInfo for each line and applying the wrap/indentation.

  • screenWidth -> maxWidth for a less misleading var name
  • removed TChar& format as this serves no purpose now
  • added hangingIndentSize for... hanging indents
  • Note: wrapline only records a timestamp for the first line of a multiline-wrap lines and also uses the resulting blank timestamps to implicitly indicate that the line was previously wrapped (and has its hanging indent already), i.e.: const bool isNewline = (time != mudlet::smBlankTimeStamp);.
  • finally, the startline to the penultimate line are logged (consequently, if no wrap action occurs, no logging occurs), as all previously wrapped lines will no longer be touched by append actions.

TBuffer::appendLine: simplified this method to just handle appending text to the buffer (by removing handling of shrink buffer / console overflow).

Added appendEmptyLine for adding a blank line to the buffer and reduce duplicated code.

Motivation for adding to Mudlet

Adds functioning hanging indents to all consoles.

Fixes wrapping for 2-width characters (emojis, asian characters, etc.).

Moving all wrapping logic into one method should be much easier to maintain/optimize, as well as hopefully cutting down on the bugs arising from having three different implementations being used in parallel.

Other info (issues closed, discussion etc)

Fixes #3674
Fixes #5564
Fixes #5936
Fixes #6390
Fixes #6432
Fixes #6970
Fixes #7846
Fixes #7892

/claim #5936
/claim #5564

Before benchmark (Mudlet 4.19.1):
image

After benchmark (commit e6c516e):
image

Miniconsole + main console showing first line indentation & hanging indentation for single and double width chars:
447297497-10e5f35e-2ff7-43a2-a128-9dbf029af83d

Script for generating a log to compare old wrapping vs. new
function testWrap()
  local echoText = {
    "12x4567x90",
    "12x 567x 0",
    "字❤️包裝🙏😇",
    "字 ❤️ 包 裝 🙏 😇",
    "字a❤️🙏b😇",
    "字 ❤️ test 🙏 😇"
  }

  setWindowWrap(200)
  local success, message, filename, code = startLogging(true)
  if code == 1 or code == -1 then print(f"Started logging to {filename}") end

  if setWindowWrapHangingIndent then
    print("Updated wrapping")
    setWindowWrapHangingIndent("main",0)
    else
    print("Legacy wrapping")
  end 
  
  for _,indent in ipairs({0,2}) do
    
    local wrapwidth = 5
    setWindowWrapIndent("main",indent)
    
    setWindowWrap(200)
    cecho(f "Wrap width: {wrapwidth}, indent: {indent}\n")
    
    for _,str in ipairs(echoText) do
    
      for _,action in ipairs({"feedTriggers","echo","cecho","decho","hecho","print","display"}) do
        setWindowWrap(200)
        cecho(f "\n<green>{action}<reset>:\n")
        setWindowWrap(wrapwidth)
        _G[action](f "\n{str}\n")
      end
      
      
      setWindowWrap(200)
      cecho(f "\n\n<green>Echoed commands<reset>:\n")
      setWindowWrap(wrapwidth)
    
      send(str, true)
      print("")
      send(str..str, true)
    end
  end
  
  setWindowWrap(200)
  cecho("<green>End of log!\n")
  
  startLogging(false)
end

@add-deployment-links
Copy link
Copy Markdown

add-deployment-links bot commented Feb 14, 2025

Hey there! Thanks for helping Mudlet improve. 🌟

Test versions

You can directly test the changes here:

No need to install anything - just unzip and run.
Let us know if it works well, and if it doesn't, please give details.

Harrison-Teeg and others added 22 commits February 14, 2025 14:36
…ndent = 0 explicit with bool firstLineOfParagraph
…e where append is called on an empty buffer.
…pended text if the append is just going on a newline.
…s widths and linebreaks, make TBuffer::wrap to use that.
…ne cases & accept wrap points in the middle of a string of spaces)
…dth and move TTextProperties functions into the graphemeInfo namespace.
…wrapLine and adjust all external calls to use the unified wrapLine method.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2025

Warnings
⚠️ PR makes changes to 11 source files. Double check the scope hasn't gotten out of hand

Generated by 🚫 dangerJS against 0df93a2

@vadi2
Copy link
Copy Markdown
Member

vadi2 commented Jun 7, 2025

Perfect! Let's get this reviewed...

@ZookaOnGit
Copy link
Copy Markdown
Contributor

Great work!

Tested all of the related issues except the foreign language ones. The only one I couldn't get working is #5936 using the provided use case. It doesn't seem to wrap at all in a miniconsole? Can someone test/prove me wrong?

@Harrison-Teeg
Copy link
Copy Markdown
Contributor Author

Great work!

Tested all of the related issues except the foreign language ones. The only one I couldn't get working is #5936 using the provided use case. It doesn't seem to wrap at all in a miniconsole? Can someone test/prove me wrong?

You are correct, AFAIK miniconsoles don't automatically wrap based on their width. This PR fixes the underlying issue, but the code to demonstrate it needs to be modified to (first line, add wrapAt = 100):

Geyser.MiniConsole:new({name = "indent-test", wrapAt = 100})
setWindowWrapIndent("indent-test", 10)
echo("indent-test", "This is the song that doesn't end, yes it goes on and on my friend, some people started singing it not knowing what it was, and they'll continue singing it forever just because this is the song that doesn't end...")

Copy link
Copy Markdown
Member

@vadi2 vadi2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me, thanks so much for your work!

Just had one trivial point of feedback.

const TChar styling(fgColor, bgColor,
(mEchoingText ? (TChar::Echo | (flags & TChar::TestMask))
: (flags & TChar::TestMask)));
buffer.back().push_back(styling);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied this styling push in from the original append method. I think this should cause errors as the buffer will have a value whereas the lineBuffer is empty but I've copied it over anyway out of fear of deleting things I don't understand.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SlySven could you comment?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I'm not entirely sure what is going on here - but I would point out that normally there is a one-to-one relationship between the TChars in the buffer and the QChars in the lineBuffer except for empty lines which do have a TChar but don't have any QChars for them - I can't recall the exact details but I suspect that this is so that the formatting persists from one line to the next unless it gets changed by an <SCR> sequence (I think most MUDs do a <SGR>0m at the start of each line to do that!).

@vadi2 vadi2 merged commit 09054e5 into Mudlet:development Jun 26, 2025
13 checks passed
vadi2 pushed a commit that referenced this pull request Jul 2, 2025
…7923)

<!-- Keep the title short & concise so anyone non-technical can
understand it,
     the title appears in PTB changelogs -->
#### Brief overview of PR changes/additions
Added a check for whether new lines were added from trigger
processing/wrapping and if the last line in the buffer is blank. If new
lines were added and the last line in the buffer is empty,
TBuffer::translateToPlainText will skip appending an additional blank
line.

#### Motivation for adding to Mudlet
A discord user mentioned the wrap-overhaul causing their echoes to
behave differently.

#### Other info (issues closed, discussion etc)
4.19.1:

![image](https://github.com/user-attachments/assets/51be1432-b534-4f10-991c-1f57a89714a7)

After PR #7714:

![image](https://github.com/user-attachments/assets/27a74309-8d48-4933-8194-788b7ced77ac)

After this PR:

![image](https://github.com/user-attachments/assets/b36c8182-7267-414a-b396-e6e68fa71ff3)
vadi2 added a commit that referenced this pull request Feb 5, 2026
In trigger mode, appendLine() now skips newline-to-new-line
conversion to restore pre-#7714 behavior where \n was treated
as a literal character that could be replaced by creplaceLine.

Fixes #8824
vadi2 added a commit to vadi2/Mudlet that referenced this pull request Mar 7, 2026
getWrapInfo discarded all wrap info when totalWidth <= mWrapAt,
including newline-triggered splits. Track hasNewline and skip
the early return when newlines are present.

Add C++ functional tests for insertText with newlines and echo
with newlines in trigger mode. Add Lua tests for the same.

Fixes Mudlet#8945
vadi2 added a commit that referenced this pull request Mar 25, 2026
Use insertInLine instead of appendLine in TConsole::echo()
trigger mode so newlines are embedded in the line text rather
than creating new buffer lines. This fixes cecho/creplaceLine
regression from #7714 where subsequent echo calls would append
to wrong lines.

Also adds regression tests for #8945 and #8824.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment