Summary
Under GNOME on Wayland (Mutter + Xwayland), a tool tip appears the first time and never again: hovering the same control, or another control with a tool tip of the same size, shows nothing. With tool tips of different sizes it comes and goes, so it looks intermittent.
The tool tip panel is still ordered front each time, with the right frame, and X reports it mapped. Mutter is holding back its content: to hide the tool tip, -[GSToolTips _endDisplay:] shrinks the still-visible panel to NSZeroRect and then orders it out. Mutter freezes a window's commits when it is resized, and thaws them only after it next paints the window. That paint never happens, because the window is unmapped straight away. The Mutter side is reported as GNOME/mutter#5080, with a plain Xlib reproducer.
Related
#404 reported the same tool tip symptom and attached the tool tip panel to its owner window (0b29e9c). That is on master, and tool tips still show only once there; the cause turned out to be this one.
Steps to reproduce
The complete program, tooltip_rehover.m, is below: a window with one button that has a tool tip. Half a second after each time the tool tip panel is ordered front, it prints the X window's _XWAYLAND_ALLOW_COMMITS.
- Build:
clang `gnustep-config --objc-flags` tooltip_rehover.m `gnustep-config --gui-libs` -lX11 -o tooltip_rehover
- Run it in a GNOME Wayland session (the X11 backend runs under Xwayland).
- Hover over the button until the tool tip appears, move away, and repeat twice.
tooltip_rehover.m
/* Tool tips show only once under GNOME Wayland (Mutter + Xwayland).
*
* Hover over the button until its tool tip appears, move away, and hover
* again. Each time the tool tip panel is ordered front, this prints the X
* window's _NET_WM_WINDOW_TYPE and _XWAYLAND_ALLOW_COMMITS. From the second
* hover on, the panel is ordered front but nothing appears on screen, and
* _XWAYLAND_ALLOW_COMMITS is 0.
*
* Build: clang `gnustep-config --objc-flags` tooltip_rehover.m \
* `gnustep-config --gui-libs` -lX11 -o tooltip_rehover
*/
#import <AppKit/AppKit.h>
#import <GNUstepGUI/GSDisplayServer.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
static Display *dpy;
static NSString *
atomProperty(Window w, const char *name)
{
Atom type;
int format;
unsigned long n, after;
unsigned char *data = NULL;
NSString *result = @"(none)";
if (XGetWindowProperty(dpy, w, XInternAtom(dpy, name, False), 0, 1, False,
AnyPropertyType, &type, &format, &n, &after,
&data) == Success && data != NULL && n == 1)
{
long v = *(long *)data;
if (type == XA_ATOM)
{
char *s = XGetAtomName(dpy, (Atom)v);
result = [NSString stringWithUTF8String: s];
XFree(s);
}
else
{
result = [NSString stringWithFormat: @"%ld", v];
}
}
if (data != NULL)
XFree(data);
return result;
}
@interface Controller : NSObject
{
BOOL wasVisible;
int shown;
}
@end
@implementation Controller
- (void) report: (NSWindow *)w
{
Window xw = (Window)[GSServerForWindow(w) windowDevice: [w windowNumber]];
NSLog(@"tool tip shown (%d): visible %@, frame %@, _NET_WM_WINDOW_TYPE %@,"
@" _XWAYLAND_ALLOW_COMMITS %@", ++shown,
[w isVisible] ? @"YES" : @"NO", NSStringFromRect([w frame]),
atomProperty(xw, "_NET_WM_WINDOW_TYPE"),
atomProperty(xw, "_XWAYLAND_ALLOW_COMMITS"));
}
- (void) check: (NSTimer *)timer
{
NSEnumerator *e = [[NSApp windows] objectEnumerator];
NSWindow *w;
while ((w = [e nextObject]) != nil)
{
if (![NSStringFromClass([w class]) isEqual: @"GSTTPanel"])
continue;
if ([w isVisible] && !wasVisible)
{
/* Read the properties once Mutter has handled the new frame. */
[self performSelector: @selector(report:)
withObject: w
afterDelay: 0.5];
}
wasVisible = [w isVisible];
}
}
- (void) applicationDidFinishLaunching: (NSNotification *)n
{
NSWindow *window;
NSButton *button;
window = [[NSWindow alloc]
initWithContentRect: NSMakeRect(200, 200, 300, 160)
styleMask: NSTitledWindowMask
backing: NSBackingStoreBuffered
defer: NO];
[window setTitle: @"Tool tip"];
button = [[NSButton alloc] initWithFrame: NSMakeRect(50, 40, 200, 80)];
[button setTitle: @"Hover here"];
[button setToolTip: @"This is the tool tip"];
[[window contentView] addSubview: button];
[window makeKeyAndOrderFront: nil];
/* Watch for the tool tip panel; a second connection (dpy) reads its
* properties. */
[NSTimer scheduledTimerWithTimeInterval: 0.2
target: self
selector: @selector(check:)
userInfo: nil
repeats: YES];
}
@end
int
main(int argc, const char **argv)
{
NSAutoreleasePool *pool = [NSAutoreleasePool new];
dpy = XOpenDisplay(NULL);
[NSApplication sharedApplication];
[NSApp setDelegate: [Controller new]];
[NSApp run];
[pool release];
return 0;
}
Output on master:
tool tip shown (1): visible YES, ..., _XWAYLAND_ALLOW_COMMITS (none)
tool tip shown (2): visible YES, ..., _XWAYLAND_ALLOW_COMMITS 0
tool tip shown (3): visible YES, ..., _XWAYLAND_ALLOW_COMMITS 0
The tool tip is on screen only the first time. 0 means Mutter has told Xwayland not to show the window's updates.
Expected
The tool tip appears every time.
Cause
-[GSToolTips _endDisplay:] (Source/GSToolTips.m, line 596 on master):
if (window != nil)
{
[window setFrame: NSZeroRect display: NO];
[window orderOut:self];
GSDetachToolTipParentWindow(window);
}
The shrink was added in f3d8072 (2012); its ChangeLog entry says "This prevents ugly rectangles in some desktops." -[GSDragView _clearupWindow] (Source/GSDragView.m, line 419) got the same change, and drag images are affected the same way.
Drag images
A view that starts a drag with a solid 60x60 image, dragged three times. Screenshots in the middle of each drag, counting the image's pixels (5625 at 1.25x scale):
|
drag 1 |
drag 2 |
drag 3 |
| 0.32.0 (two runs) |
5625 |
0 |
0 |
| master (two runs) |
5625 |
0 or 5625 |
0 |
| shrink removed, 0.32.0 (two runs) and master |
5625 |
5625 |
5625 |
During the drags that show nothing, the drag window (the X11 backend's XGRawWindow) is mapped at the right place with _XWAYLAND_ALLOW_COMMITS = 0.
Suggested fix
Order the tool tip panel and the drag window out without shrinking them. Screenshots in the middle of each tool tip showing, with the shrink removed at runtime (the panel's setFrame:display: ignores empty rects):
|
tool tip visible |
| master, same tool tip, 3 hovers |
1 of 3 (also on 0.32.0, several runs) |
| shrink removed, same tool tip, 3 hovers |
3 of 3 (master and 0.32.0) |
| master, alternating short and long tool tips, 6 hovers |
5 of 6 |
| shrink removed, alternating short and long tool tips, 6 hovers |
6 of 6 |
Ordering out first and shrinking afterwards is not enough: it showed the tool tip 2 times out of 3.
If the shrink is still needed for the desktops it was added for, skipping it where _XWAYLAND_ALLOW_COMMITS is used (Xwayland under Mutter) would keep it there.
Workaround
An application or theme can replace -setFrame:display: on the private GSTTPanel and XGRawWindow classes at runtime (class_replaceMethod) with one that ignores empty rects and calls the original otherwise.
Environment
- libs-gui master ff49ac8 (2026-09-22), libs-back master 5db2ae7 (2026-09-11), libs-base 1.31.1. Also seen with the released gui 0.32.0 and back 0.32.0. (Building master needed
-Wno-error=format-security for NSApplication.m:986, unrelated.)
- Debian 13, GNOME Shell / Mutter 48.7, Xwayland 24.1.6, clang 19, libobjc2, cairo/xlib backend, default theme.
Summary
Under GNOME on Wayland (Mutter + Xwayland), a tool tip appears the first time and never again: hovering the same control, or another control with a tool tip of the same size, shows nothing. With tool tips of different sizes it comes and goes, so it looks intermittent.
The tool tip panel is still ordered front each time, with the right frame, and X reports it mapped. Mutter is holding back its content: to hide the tool tip,
-[GSToolTips _endDisplay:]shrinks the still-visible panel toNSZeroRectand then orders it out. Mutter freezes a window's commits when it is resized, and thaws them only after it next paints the window. That paint never happens, because the window is unmapped straight away. The Mutter side is reported as GNOME/mutter#5080, with a plain Xlib reproducer.Related
#404 reported the same tool tip symptom and attached the tool tip panel to its owner window (0b29e9c). That is on master, and tool tips still show only once there; the cause turned out to be this one.
Steps to reproduce
The complete program,
tooltip_rehover.m, is below: a window with one button that has a tool tip. Half a second after each time the tool tip panel is ordered front, it prints the X window's_XWAYLAND_ALLOW_COMMITS.clang `gnustep-config --objc-flags` tooltip_rehover.m `gnustep-config --gui-libs` -lX11 -o tooltip_rehovertooltip_rehover.m
Output on master:
The tool tip is on screen only the first time.
0means Mutter has told Xwayland not to show the window's updates.Expected
The tool tip appears every time.
Cause
-[GSToolTips _endDisplay:](Source/GSToolTips.m, line 596 on master):The shrink was added in f3d8072 (2012); its ChangeLog entry says "This prevents ugly rectangles in some desktops."
-[GSDragView _clearupWindow](Source/GSDragView.m, line 419) got the same change, and drag images are affected the same way.Drag images
A view that starts a drag with a solid 60x60 image, dragged three times. Screenshots in the middle of each drag, counting the image's pixels (5625 at 1.25x scale):
During the drags that show nothing, the drag window (the X11 backend's
XGRawWindow) is mapped at the right place with_XWAYLAND_ALLOW_COMMITS = 0.Suggested fix
Order the tool tip panel and the drag window out without shrinking them. Screenshots in the middle of each tool tip showing, with the shrink removed at runtime (the panel's
setFrame:display:ignores empty rects):Ordering out first and shrinking afterwards is not enough: it showed the tool tip 2 times out of 3.
If the shrink is still needed for the desktops it was added for, skipping it where
_XWAYLAND_ALLOW_COMMITSis used (Xwayland under Mutter) would keep it there.Workaround
An application or theme can replace
-setFrame:display:on the privateGSTTPanelandXGRawWindowclasses at runtime (class_replaceMethod) with one that ignores empty rects and calls the original otherwise.Environment
-Wno-error=format-securityforNSApplication.m:986, unrelated.)