From 2d9bfb81dde9bc1d6a8123009702181e9e9906b9 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 9 Jun 2026 07:24:17 +0300 Subject: [PATCH] TextArea: grow by content reliably while editing (#4854) A growByContent multi-line TextArea was clipping the previous row while being edited: text would wrap onto a new visual line but the field would not grow to follow it until focus moved away. The during-editing grow gate (#4741) decided whether to revalidate using estimateLineCount(), a character-column prediction (string length vs getColumns()). The real renderer wraps using font metrics against getWidth() - horizontalPadding, so with a proportional font text wraps a row earlier than the character count predicts. The estimate undercounted, revalidateLater() did not fire, and the field stayed one row short. Leaving the field ran a normal layout that used the accurate getLines(), which is why the size corrected itself. Fix: drop the wrap prediction entirely. Any growth in text length can push content onto an extra wrapped row, so textMightGrowByContent() now triggers on a length increase (or an added hard newline) and lets the layout's getLines() determine the actual size. revalidateLater() coalesces into a single layout per paint, so erring toward an extra revalidate is cheap and, crucially, never under-grows. The gate stays width- and EDT-independent and does not call getLines() from inside setText(), avoiding any layout re-entrancy. The bogus estimateLineCount() helper is removed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/com/codename1/ui/TextArea.java | 40 ++++++------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/CodenameOne/src/com/codename1/ui/TextArea.java b/CodenameOne/src/com/codename1/ui/TextArea.java index 6ff51b351c..b1e3695e99 100644 --- a/CodenameOne/src/com/codename1/ui/TextArea.java +++ b/CodenameOne/src/com/codename1/ui/TextArea.java @@ -618,19 +618,21 @@ && textMightGrowByContent(old, text)) { } private boolean textMightGrowByContent(String oldText, String newText) { - int oldNewLines = countNewLines(oldText); - int newNewLines = countNewLines(newText); - if (newNewLines > oldNewLines) { + if (newText == null || oldText == null) { return true; } - if (newText == null || oldText == null || newText.length() <= oldText.length()) { - return false; - } - int cols = getColumns(); - if (cols > 1) { - return estimateLineCount(newText, cols) > estimateLineCount(oldText, cols); + // An added hard newline always adds a row. + if (countNewLines(newText) > countNewLines(oldText)) { + return true; } - return false; + // Any growth in length can push the content onto an extra wrapped row. + // We deliberately do NOT try to predict the wrap point here: the previous + // character-column estimate ignored the font and padding, so proportional + // text wrapped a row earlier than predicted and the field clipped the prior + // line while editing (#4854). revalidateLater() coalesces and the layout's + // getLines() (real font metrics, real width) determines the actual size, so + // erring toward an extra revalidate is cheap and never under-grows. + return newText.length() > oldText.length(); } private int countNewLines(String value) { @@ -646,24 +648,6 @@ private int countNewLines(String value) { return count; } - private int estimateLineCount(String value, int cols) { - if (value == null || value.length() == 0) { - return 1; - } - int lines = 0; - int segmentLength = 0; - for (int i = 0; i < value.length(); i++) { - if (value.charAt(i) == '\n') { - lines += Math.max(1, (segmentLength + cols - 1) / cols); - segmentLength = 0; - } else { - segmentLength++; - } - } - lines += Math.max(1, (segmentLength + cols - 1) / cols); - return lines; - } - /// Convenience method for numeric text fields, returns the value as a number or invalid if the value in the /// text field isn't a number ///