Skip to content

Fix infinite Text redraw for SWT.SEARCH with custom background on macOS#7

Open
vogella wants to merge 3 commits intomasterfrom
fix-text-infinite-redraw
Open

Fix infinite Text redraw for SWT.SEARCH with custom background on macOS#7
vogella wants to merge 3 commits intomasterfrom
fix-text-infinite-redraw

Conversation

@vogella
Copy link
Copy Markdown
Owner

@vogella vogella commented Apr 22, 2026

Summary

  • Adds the JUnit reproducer from upstream PR Text control should not redraw indefinitely eclipse-platform/eclipse.platform.swt#3260 (test_finiteRedraw), which spins readAndDispatch() for 1s against a Text styled SWT.SEARCH | SWT.ICON_CANCEL with a custom background and fails if it never returns false.
  • Switches the text lookup inside Text.drawInteriorWithFrame_inView_searchfield from NSControl.stringValue() to NSCell.stringValue(), avoiding the re-entrant setNeedsDisplay: that drove the infinite paint loop.
  • Adds a stringValue() wrapper to NSCell.java to support the call.

Root cause

NSControl.stringValue() (called on the NSSearchField) posts setNeedsDisplay: on the view from inside the paint pass. Widget.setNeedsDisplay enqueues the view in display.needsDisplay because isPainting contains it, that queue is flushed after the paint, the view is marked dirty again, and the next readAndDispatch() iteration paints it once more. See eclipse-platform/eclipse.platform.ui#3920 for the captured stack.

NSCell.stringValue() returns the same string without touching the control's editing/binding machinery, so the cancel-icon visibility contract is preserved.

Test plan

  • CI on vogella master runs the new test_finiteRedraw and passes on macOS (it is the only platform where the buggy draw path exists).
  • CI on Linux and Windows stays green (the test constructs a SWT.SEARCH Text, which is valid everywhere; the loop only reproduces on cocoa).
  • Manually verify on macOS that Eclipse preferences dialog filter no longer hangs at ~100% CPU when typing "avail".
  • Manually verify the cancel icon still appears/disappears correctly as the search field text is typed and cleared.

🤖 Generated with Claude Code

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request addresses an infinite redraw loop in the Cocoa implementation of the Text widget by fetching the string value from NSCell instead of NSControl. It adds the necessary stringValue method to NSCell and includes a regression test to ensure the UI stops refreshing. A review comment suggests optimizing the paint loop by caching the cancelButtonCell to reduce redundant native calls and object creation.

Comment on lines 757 to 760
NSString cellValue = _cell.stringValue();
if (cell.cancelButtonCell() != null && cellValue != null && cellValue.length() > 0) {
cell.cancelButtonCell().drawInteriorWithFrame(cell.cancelButtonRectForBounds(cellFrame), view);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In the paint loop, it is more efficient to store the result of cell.cancelButtonCell() in a local variable to avoid redundant native calls and the creation of extra Java wrapper objects.

NSString cellValue = _cell.stringValue();
	NSButtonCell cancelButtonCell = cell.cancelButtonCell();
	if (cancelButtonCell != null && cellValue != null && cellValue.length() > 0) {
		cancelButtonCell.drawInteriorWithFrame(cell.cancelButtonRectForBounds(cellFrame), view);
	}

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

Test Results

  182 files  ±0    182 suites  ±0   25m 41s ⏱️ +46s
4 724 tests +1  4 701 ✅ +1   23 💤 ±0  0 ❌ ±0 
6 818 runs  +6  6 653 ✅ +6  165 💤 ±0  0 ❌ ±0 

Results for commit d6ec708. ± Comparison against base commit 81a811a.

♻️ This comment has been updated with latest results.

@basilevs
Copy link
Copy Markdown

basilevs commented Apr 22, 2026

I've fixed the test to fail more reliably and it fails even after this change.

I've merged latest SWT master for testing. This branch seems to be quite old.

@vogella vogella force-pushed the fix-text-infinite-redraw branch from a3de8a7 to 6ff2c49 Compare April 22, 2026 19:10
@basilevs
Copy link
Copy Markdown

Please merge #8 too. This revision of test fails on my workstation reliably again.

…gger

Picks up the more reliable reproducer from PR eclipse-platform#3260
(shell.setLayout + requestLayout + shell.open) and adds temporary
instrumentation so CI logs reveal which native call in
drawInteriorWithFrame_inView_searchfield posts the re-entrant
setNeedsDisplay that drives the loop.

- Text.drawInteriorWithFrame_inView / _searchfield: print a stable
  step marker to stderr before and after every significant native
  call; update Text.DEBUG_PAINT_STEP so Widget.setNeedsDisplay can
  attribute the trigger.
- Widget.setNeedsDisplay / setNeedsDisplayInRect: when called
  re-entrantly during paint (isPainting contains view) and a paint
  step is armed, log the offending step, view id and widget class,
  plus a stack trace for the first 3 occurrences.

Temporary: to be removed once the trigger is identified.

Refs eclipse-platform/eclipse.platform.ui#3920

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vogella vogella force-pushed the fix-text-infinite-redraw branch from 6ff2c49 to 9badc5a Compare April 22, 2026 19:55
@vogella vogella closed this Apr 22, 2026
@vogella vogella reopened this Apr 22, 2026
@vogella vogella closed this Apr 22, 2026
@vogella vogella reopened this Apr 22, 2026
@vogella
Copy link
Copy Markdown
Owner Author

vogella commented Apr 22, 2026

Please merge #8 too. This revision of test fails on my workstation reliably again.

I added some tracing code (I don't have a Mac myself) and see if I can spot what is wrong.

@basilevs
Copy link
Copy Markdown

Traces on my workstation
test_output.txt.zip

@basilevs
Copy link
Copy Markdown

Can you now reintroduce the failed fix? I assume its traces would be useful too.
Note, that official SWT CI still can't reproduce the problem.

@vogella
Copy link
Copy Markdown
Owner Author

vogella commented Apr 22, 2026

Can you now reintroduce the failed fix? I assume its traces would be useful too. Note, that official SWT CI still can't reproduce the problem.

I don't have a Mac hence I can only test on the CI

Trace zip from basilevs's workstation confirms the re-entrant
setNeedsDisplay originates from NSControl.stringValue() inside
drawInteriorWithFrame_inView_searchfield (~800 paints in 1s, every
one hitting the same step). Stack:

  Widget.setNeedsDisplay
  Display.windowProc                  <- AppKit callback
  OS.objc_msgSend (native)
  NSControl.stringValue (line 122)    <- our caller
  Text.drawInteriorWithFrame_inView_searchfield

Switch the length check to NSCell.stringValue() (read the stored
cell value directly, no NSControl editing-path side effect) and
add the NSCell.stringValue wrapper this requires.

Instrumentation is kept so the next CI run (and basilevs on a real
macOS workstation) can confirm the [SWT-DEBUG-SND] markers disappear
once the fix is applied.

Refs eclipse-platform/eclipse.platform.ui#3920

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vogella
Copy link
Copy Markdown
Owner Author

vogella commented Apr 23, 2026

@basilevs thanks for the trace zip — it was conclusive.

What the trace showed

All ~800 paints in your 1-second window hit the same re-entrant setNeedsDisplay at the exact same step:

[SWT-DEBUG-PAINT N] step=before NSControl.stringValue()
[SWT-DEBUG-SND N]  re-entrant setNeedsDisplay during step=before NSControl.stringValue()

Stack (every time):

Widget.setNeedsDisplay (Widget.java:2152)
Display.windowProc (Display.java:6476)      ← AppKit callback
OS.objc_msgSend (native)
NSControl.stringValue (NSControl.java:122)
Text.drawInteriorWithFrame_inView_searchfield

So your original hypothesis was exactly right: NSControl.stringValue() on the NSSearchField, called during paint, synchronously posts setNeedsDisplay: on the same view. Nothing else in the search-field draw path fires it.

The GitHub Actions macOS runners still can't reproduce the loop (only paint 1 ever happens — presumably headless/offscreen skips the repaint flush), so CI won't settle this. I need your help to verify.

What I just pushed

5fb3b67 re-applies the fix (read the text via NSCell.stringValue() instead of NSControl.stringValue() — a wrapper added to NSCell.java) and keeps the instrumentation, so the same markers will print.

How to verify on your workstation

git fetch origin fix-text-infinite-redraw
git checkout fix-text-infinite-redraw
# run test_finiteRedraw

If the fix works, the CI/local log should show:

  • [SWT-DEBUG-PAINT N] step=before NSCell.stringValue() (new step name)
  • followed directly by [SWT-DEBUG-PAINT N] step=after NSCell.stringValue()
  • no [SWT-DEBUG-SND] line between them
  • and the test passes (loop exits)

If the fix does not work, you will still see [SWT-DEBUG-SND] re-entrancy — most likely reported as during step=before NSCell.stringValue(), which would mean NSCell.stringValue also triggers the AppKit side effect and we need a different approach (cache the text on the Java side, or guard Widget.setNeedsDisplay against same-view re-entry).

Either way, if you can attach another test_output.txt.zip from this revision, the traces will tell us definitively whether the fix is correct or whether we need to move up the stack. Thanks!

@basilevs
Copy link
Copy Markdown

Either way, if you can attach another test_output.txt.zip from this revision, the traces will tell us definitively whether the fix is correct or whether we need to move up the stack. Thanks!

Test fails after changes. Traces: test_output.txt.zip

NSCell does not seem any different from NSSearchField.

@vogella
Copy link
Copy Markdown
Owner Author

vogella commented Apr 23, 2026

Thanks @basilevs — the new trace settles it. 876 paints, 876 re-entrant setNeedsDisplay calls, every one now at:

[SWT-DEBUG-PAINT N] step=before NSCell.stringValue()
[SWT-DEBUG-SND N]  re-entrant setNeedsDisplay during step=before NSCell.stringValue()

Stack:

Widget.setNeedsDisplay
Display.windowProc                       ← AppKit callback
OS.objc_msgSend (native)
NSCell.stringValue (NSCell.java:178)     ← now the NSCell wrapper
Text.drawInteriorWithFrame_inView_searchfield

So the side effect is in AppKit's implementation of the stringValue selector on NSSearchFieldCell (presumably re-validating binding / placeholder / focus-ring state), not anywhere in SWT's Java wrappers. Both NSControl.stringValue and NSCell.stringValue hit the exact same Objective-C method, so swapping wrappers is a no-op — you were right.

Text.getCharCount() for SINGLE style also ends up at ((NSControl) view).stringValue().length() (Text.java:937), so that's out too. Any native text query during paint is poisoned.

Proposed next attempt: Java-side text tracking. Maintain a hasText boolean on cocoa Text, updated at each sendEvent(SWT.Modify) / postEvent(SWT.Modify) / setText site (~8 call sites). Use it in drawInteriorWithFrame_inView_searchfield instead of calling stringValue(). No native query in the paint path at all.

Will push that on top of the current branch shortly so you can verify on your workstation that the [SWT-DEBUG-SND] markers disappear. If anyone has a better idea (e.g. a cocoa API that reads the text without triggering AppKit's side effect, or a reason to guard Widget.setNeedsDisplay against same-view re-entry instead), shout.

basilevs's second trace confirmed that both NSControl.stringValue and
NSCell.stringValue dispatch to the same AppKit selector, which posts
setNeedsDisplay: synchronously while the search field is being drawn.
Swapping wrappers cannot break the loop; the paint method must stop
querying native state entirely.

Introduce a searchFieldHasText boolean on Text, refreshed at every
SWT.Modify emission point (append, cut, insert, paste/replace,
setText, setTextChars, verify-listener replacement, and
textDidChange). The helper only queries stringValue when the display
is not currently painting, so the Java-side read cannot re-arm the
loop. The paint method in drawInteriorWithFrame_inView_searchfield
consults the cache instead of calling stringValue on the cell.

Drop the unused NSCell.stringValue wrapper added in the previous
attempt.

Instrumentation is kept so the next trace from basilevs can confirm
the [SWT-DEBUG-SND] re-entrant markers disappear.

Refs eclipse-platform/eclipse.platform.ui#3920

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vogella
Copy link
Copy Markdown
Owner Author

vogella commented Apr 23, 2026

@basilevs new revision pushed (d6ec708): now using a Java-side cache instead of any native text query in the paint path.

What changed

  • Added searchFieldHasText boolean on Text.
  • Added updateSearchFieldHasText() helper that queries NSControl.stringValue() only when display.isPainting is empty — so the read cannot re-arm the loop.
  • Call the helper at every SWT.Modify emission site in Text.java (append, cut, insert, paste/replace, setText, setTextChars, verify-listener replacement, textDidChange).
  • drawInteriorWithFrame_inView_searchfield now checks searchFieldHasText instead of calling stringValue on the cell.
  • Dropped the unused NSCell.stringValue wrapper from the previous attempt (it never helped, since both wrappers hit the same Objective‑C selector).

Instrumentation is kept.

What the trace should look like if the fix works

[SWT-DEBUG-PAINT 1] step=ENTER drawInteriorWithFrame_inView
...
[SWT-DEBUG-PAINT 1] step=before cell.cancelButtonCell() (cached hasText=false)
[SWT-DEBUG-PAINT 1] step=EXIT drawInteriorWithFrame_inView_searchfield
[SWT-DEBUG-PAINT 1] step=before super.drawInteriorWithFrame_inView
[SWT-DEBUG-PAINT 1] step=after super.drawInteriorWithFrame_inView

Expected properties:

  • No [SWT-DEBUG-SND] line at all. Zero. If you see any, the cache isn't the only re-entry source and we need to look further up the stack.
  • Paint counter should stop at a small number (1–3) as the test drains the event queue, instead of climbing to ~800.
  • The test should exit normally (loop exits, no timeout).

What I cannot verify from Linux and what I need you to check

UX regression check — this is the part I can't test: the searchFieldHasText cache has to stay in sync with actual content across all Mac text-input paths I can't exercise. Could you please try these scenarios manually once the loop is fixed?

  1. Type into the search field — cancel "X" icon should appear as soon as there's text.
  2. Clear the field character-by-character with backspace — cancel icon should disappear when the field goes empty.
  3. Click the cancel "X" — icon should disappear.
  4. Paste text via ⌘V — icon should appear.
  5. Cut all text via ⌘X — icon should disappear.
  6. Programmatic text.setText("foo") then text.setText("") — icon should track.
  7. IME / dead keys (compose é via option+e then e) — icon should appear at the right moment.
  8. Drag-drop text onto the field — icon should appear.

If any of these don't update the icon correctly, that's a mutation path I missed and the cache needs an update hook there too.

If you can attach another test_output.txt.zip from this revision, that settles whether the infinite-paint side is fixed. The UX checks are orthogonal but equally important before this can go upstream. Thanks!

@basilevs
Copy link
Copy Markdown

basilevs commented Apr 23, 2026

Test passes on Java 23 (Temurin) and Java 25 (Homebrew): test_output.txt

Turns out my test has never failed on Java 23.

Eclipse Platform

Java 23

When running Eclipse Platform on Java 23 (Temurin):
When I open Preferences Dialog, I still see infinite refresh traces in console. ide_output.txt.zip The logging stops when text control loses focus.

1-2. The refresh of the filtered tree is now reliable despite the infinite logging.
3. Cancel button works after three clicks (old known bug)
5. Paste and cut work
6. Now sure what is expected from me here
7. Seems to work
8. Drag works

Java 25

When running Eclipse Platform on Java 25 (Homebrew):
I see no infinite logging and UI works fine.

@basilevs
Copy link
Copy Markdown

basilevs commented Apr 23, 2026

I think we have to suspend this effort until I can better isolate effects of different JVMs on both my test and Eclipse Platform UI. I suspect that neither Java 23 nor Java 25 are official "default" JVM Eclipse is tested on.

@basilevs
Copy link
Copy Markdown

basilevs commented Apr 23, 2026

@Phillipus has explained in eclipse-platform/eclipse.platform.ui#3920 that I should probably use Java build based on a specific MacOS SDK.

On Java that closely matches (major version) SDK 26.4 used by SWT master:

% otool -l /opt/homebrew/Cellar/openjdk/25.0.2/libexec/openjdk.jdk/Contents/Home/bin/java | grep sdk                                                                               
      sdk 26.2
% /opt/homebrew/Cellar/openjdk/25.0.2/libexec/openjdk.jdk/Contents/Home/bin/java --version
openjdk 25.0.2 2026-01-20
OpenJDK Runtime Environment Homebrew (build 25.0.2)
OpenJDK 64-Bit Server VM Homebrew (build 25.0.2, mixed mode, sharing)

the bug is no longer reproduced in this branch.

However, I suspect that SDK 26.4 is not the intended target for SWT in the near perspective, so I suggest to wait for a confirmation from @HeikoKlare.

@HeikoKlare
Copy link
Copy Markdown

However, I suspect that SDK 26.4 is not the intended target for SWT in the near perspective.

I (and some others) would be in favor of building SWT and Equinox against latest macOS SDK (26.x) as soon as possible. But first issues that would currently occur when just building against macOS SDK 26 (as we did few weeks ago with the Equinox binaries on which of the issues were reported) have to be resolved. Wherever possible, those fixes should be compatible with builds against an older as well as a newer macOS SDK both. But that's quite some effort to test and I am not sure if it's possible in all (or even some) of the cases.

In general, as discussed in eclipse-platform#3264, it does not seem to be so easy to define a common macOS SDK version that we support / build against due to the different executables involved that may be built against different SDKs.

@Phillipus
Copy link
Copy Markdown

Phillipus commented Apr 24, 2026

I (and some others) would be in favor of building SWT and Equinox against latest macOS SDK (26.x) as soon as possible.

And this will not be easy to test from a development setup where running an Eclipse or RCP instance in dev mode will be executed using the java executable binary which is compiled with an earlier macOS SDK (typically 14.x or 15.x). One would have to target a JDK that has been compiled against macOS 26.x SDK. The only one that I'm aware of is the "Homebew" one that @basilevs has.

And if it ever is built against latest macOS SDK I predict bad news for downstream RCP apps... ;-)

@basilevs
Copy link
Copy Markdown

basilevs commented Apr 24, 2026

I've managed to reproduce the problem on Mac OS SDKs:

  • 14.4 (Temurin Java 21)
  • 14.0 (Temurin Java 23)
  • 26.2 (Homebrew Java 25)

I've then confirmed that the fix from d6ec708 fixes the problem on all of these environments.

@vogella thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants