From b714cb783d54dc7af88cd15e4bf72a453dffbccc Mon Sep 17 00:00:00 2001 From: Window Maker Date: Mon, 25 May 2026 17:04:30 +0200 Subject: [PATCH] x11: fix menu rendering, balloon tooltips, clip arrows, and event handling Menu entry painting: - Add drawFrame() calls for 3D beveled entries (WTEX_SOLID) - Add menu_text_clearance to text Y positioning - Add left-side indicator drawing (radio/check/snap pixmaps) - Add indicator X offset (MENU_INDICATOR_SPACE) - Fix disabled+selected text color - Fix texture-aware background clearing - Add cascade arrow for submenu entries Balloon tooltips: - Implement x11_balloon_show() with text and mini-preview rendering - Previously was a no-op stub Clip arrows: - Draw workspace-switching triangles on clip icon - Previously stubbed out Event handling: - Add MapRequest, ConfigureRequest, ReparentNotify translation to xev_to_wme() in WINGs/wevent_x11.c - These WM-critical events were silently dropped, preventing new X clients (e.g. xterm) from being managed --- WINGs/wevent_x11.c | 30 ++++ src/backend/x11/x11_backend.c | 261 ++++++++++++++++++++++++++++++++-- 2 files changed, 276 insertions(+), 15 deletions(-) diff --git a/WINGs/wevent_x11.c b/WINGs/wevent_x11.c index 07f6f31..f653365 100644 --- a/WINGs/wevent_x11.c +++ b/WINGs/wevent_x11.c @@ -194,6 +194,36 @@ xev_to_wme(const XEvent *xev, WMEvent *out) out->u.mapping.count = xev->xmapping.count; break; + case MapRequest: + out->type = WME_MAP_REQUEST; + out->u.mapRequest.window = (WNativeWindow)xev->xmaprequest.window; + out->u.mapRequest.parent = (WNativeWindow)xev->xmaprequest.parent; + break; + + case ConfigureRequest: + out->type = WME_CONFIGURE_REQUEST; + out->u.configureRequest.window = (WNativeWindow)xev->xconfigurerequest.window; + out->u.configureRequest.parent = (WNativeWindow)xev->xconfigurerequest.parent; + out->u.configureRequest.x = xev->xconfigurerequest.x; + out->u.configureRequest.y = xev->xconfigurerequest.y; + out->u.configureRequest.width = xev->xconfigurerequest.width; + out->u.configureRequest.height = xev->xconfigurerequest.height; + out->u.configureRequest.border_width = xev->xconfigurerequest.border_width; + out->u.configureRequest.above = (WNativeWindow)xev->xconfigurerequest.above; + out->u.configureRequest.detail = xev->xconfigurerequest.detail; + out->u.configureRequest.value_mask = xev->xconfigurerequest.value_mask; + break; + + case ReparentNotify: + out->type = WME_REPARENT; + out->u.reparent.window = (WNativeWindow)xev->xreparent.window; + out->u.reparent.event = (WNativeWindow)xev->xreparent.event; + out->u.reparent.parent = (WNativeWindow)xev->xreparent.parent; + out->u.reparent.x = xev->xreparent.x; + out->u.reparent.y = xev->xreparent.y; + out->u.reparent.override_redirect = xev->xreparent.override_redirect; + break; + default: out->type = WME_NONE; break; diff --git a/src/backend/x11/x11_backend.c b/src/backend/x11/x11_backend.c index 23f3e18..aef2aed 100644 --- a/src/backend/x11/x11_backend.c +++ b/src/backend/x11/x11_backend.c @@ -3527,13 +3527,140 @@ static unsigned long x11_window_get_event_mask(WNativeWindow window) return 0; } +static int balloon_countLines(const char *text) +{ + int count = 1; + while (*text) { if (*text == '\n') count++; text++; } + return count; +} + +static int balloon_getMaxStringWidth(WMFont *font, const char *text) +{ + int w = 0, wt, pos = 0; + const char *p = text, *pb = p; + while (*p) { + if (*p == '\n') { + wt = WMWidthOfString(font, pb, pos); + if (wt > w) w = wt; + pos = 0; pb = p + 1; + } else { pos++; } + p++; + } + if (pos > 0) { wt = WMWidthOfString(font, pb, pos); if (wt > w) w = wt; } + return w; +} + +static void balloon_drawMultiLineString(WMScreen *wmscr, Pixmap pixmap, WMColor *color, + WMFont *font, int x, int y, const char *text, int len) +{ + const char *p = text, *pb = p; + int l = 0, pos = 0, height = WMFontHeight(font); + while (*p && p - text < len) { + if (*p == '\n') { + WMDrawString(wmscr, pixmap, color, font, x, y + l * height, pb, pos); + l++; pos = 0; pb = p + 1; + } else { pos++; } + p++; + } + if (pos > 0) + WMDrawString(wmscr, pixmap, color, font, x, y + l * height, pb, pos); +} + static void x11_balloon_show(WScreen *scr, void *bal_ptr) { - /* Balloon rendering is complex X11 code (shape extension, pixmap masks). - * For the architecture transition, we just position and map the window - * with the text. Full rendering will be restored when balloon.c's - * rendering functions are moved here. */ - (void)scr; (void)bal_ptr; + WBalloon *bal = (WBalloon *)bal_ptr; + Window foow; + unsigned foo, obj_w; + int x, y, width, height, bx, by; + Pixmap pixmap; + WMFont *font = scr->info_text_font; + + if (!bal->text || !bal->objectWindow) + return; + + if (!XGetGeometry(dpy, (Window)bal->objectWindow, &foow, &x, &y, &obj_w, &foo, &foo, &foo)) { + bal->prevType = 0; + return; + } + + if (wPreferences.miniwin_preview_balloon && bal->mini_preview != WM_NATIVE_NONE) { + /* Mini-preview balloon */ + int titleHeight = 0; + char *shortenTitle = NULL; + width = wPreferences.minipreview_size; + height = wPreferences.minipreview_size; + + if (wPreferences.miniwin_title_balloon) { + shortenTitle = ShrinkString(font, bal->text, width - MINIPREVIEW_BORDER * 2); + titleHeight = balloon_countLines(shortenTitle) * WMFontHeight(font) + 4; + height += titleHeight; + } + + bx = x; + if (bx < 0) bx = 0; + else if (bx + width > scr->scr_width - 1) bx = scr->scr_width - width - 1; + + if (y - height - 2 < 0) { by = y + wPreferences.icon_size; if (by < 0) by = 0; } + else { by = y - height - 2; } + + if (scr->window_title_texture[0]) + XSetForeground(dpy, scr->draw_gc, scr->window_title_texture[0]->any.color.pixel); + else + XSetForeground(dpy, scr->draw_gc, scr->light_pixel); + + pixmap = XCreatePixmap(dpy, scr->root_win, width, height, scr->w_depth); + XFillRectangle(dpy, pixmap, scr->draw_gc, 0, 0, width, height); + + if (shortenTitle) { + balloon_drawMultiLineString(scr->wmscreen, pixmap, scr->window_title_color[0], font, + MINIPREVIEW_BORDER, MINIPREVIEW_BORDER, shortenTitle, strlen(shortenTitle)); + wfree(shortenTitle); + } + + XCopyArea(dpy, (Pixmap)bal->mini_preview, pixmap, scr->draw_gc, + 0, 0, wPreferences.minipreview_size - 1 - MINIPREVIEW_BORDER * 2, + wPreferences.minipreview_size - 1 - MINIPREVIEW_BORDER * 2, + MINIPREVIEW_BORDER, MINIPREVIEW_BORDER + titleHeight); + + XResizeWindow(dpy, (Window)bal->window, width, height); + XMoveWindow(dpy, (Window)bal->window, bx, by); + XSetWindowBackgroundPixmap(dpy, (Window)bal->window, pixmap); + XClearWindow(dpy, (Window)bal->window); + XMapRaised(dpy, (Window)bal->window); + } else { + /* Text-only balloon */ + width = balloon_getMaxStringWidth(font, bal->text) + 8; + height = balloon_countLines(bal->text) * WMFontHeight(font) + 4; + + bx = x; + if (bx < 0) bx = 0; + else if (bx + width > scr->scr_width - 1) bx = scr->scr_width - width; + + if (y - height - 2 < 0) { by = y + bal->h; if (by < 0) by = 0; } + else { by = y - height - 2; } + + if (scr->window_title_texture[0]) + XSetForeground(dpy, scr->draw_gc, scr->window_title_texture[0]->any.color.pixel); + else + XSetForeground(dpy, scr->draw_gc, scr->light_pixel); + + pixmap = XCreatePixmap(dpy, scr->root_win, width, height, scr->w_depth); + XFillRectangle(dpy, pixmap, scr->draw_gc, 0, 0, width, height); + + balloon_drawMultiLineString(scr->wmscreen, pixmap, scr->window_title_color[0], font, + 4, 2, bal->text, strlen(bal->text)); + + XResizeWindow(dpy, (Window)bal->window, width, height); + XMoveWindow(dpy, (Window)bal->window, bx, by); + XSetWindowBackgroundPixmap(dpy, (Window)bal->window, pixmap); + XClearWindow(dpy, (Window)bal->window); + XMapRaised(dpy, (Window)bal->window); + } + + if (bal->contents) + XFreePixmap(dpy, (Pixmap)bal->contents); + bal->contents = (WMNativePixmap)pixmap; + bal->mapped = 1; } @@ -4251,9 +4378,28 @@ static void x11_clip_icon_paint(WAppIcon *aicon) GC gc = scr->draw_gc; XPoint p[4]; int pt = CLIP_BUTTON_SIZE * ICON_SIZE / 64; + int as = pt - 15; XSetForeground(dpy, gc, WMColorPixel(color)); - /* arrows omitted for brevity — clip buttons are cosmetic */ - (void)p; (void)pt; (void)gc; + + /* top-right arrow (next workspace) */ + p[0].x = p[3].x = ICON_SIZE - 5 - as; + p[0].y = p[3].y = 5; + p[1].x = ICON_SIZE - 6; + p[1].y = 5; + p[2].x = ICON_SIZE - 6; + p[2].y = 4 + as; + XFillPolygon(dpy, win, gc, p, 3, Convex, CoordModeOrigin); + XDrawLines(dpy, win, gc, p, 4, CoordModeOrigin); + + /* bottom-left arrow (prev workspace) */ + p[0].x = p[3].x = 5; + p[0].y = p[3].y = ICON_SIZE - 5 - as; + p[1].x = 5; + p[1].y = ICON_SIZE - 6; + p[2].x = 4 + as; + p[2].y = ICON_SIZE - 6; + XFillPolygon(dpy, win, gc, p, 3, Convex, CoordModeOrigin); + XDrawLines(dpy, win, gc, p, 4, CoordModeOrigin); } } static void x11_drawer_icon_paint(WAppIcon *dicon) @@ -4310,33 +4456,118 @@ static void x11_do_kaboom(WScreen *scr, WNativeWindow win, int x, int y) * For now, just free the pixmap without animation in Wayland transition. */ #endif } +static void x11_menu_draw_frame(WScreen *scr, WNativeWindow win, int y, int w, int h, int type); static void x11_menu_paint_entry(WMenu *menu, int index, int selected) { - /* TODO: inline the full menu entry rendering from the old wMenuPaintEntry. - * For now, just draw the text without decorations. */ WScreen *scr = menu->frame->screen_ptr; WMenuEntry *entry = menu->entries[index]; Window win = menu->menu->window; int y = menu->entry_height * index; int h = menu->entry_height; int w = menu->menu->width; + int x, tw, type; WMColor *color; + if (!menu->flags.realized) + return; + + /* Determine frame type for flat menu style */ + if (wPreferences.menu_style == MS_FLAT && menu->entry_no > 1) { + if (index == 0) + type = 1; /* F_TOP */ + else if (index == menu->entry_no - 1) + type = 2; /* F_BOTTOM */ + else + type = 3; /* F_NONE */ + } else { + type = 0; /* F_NORMAL */ + } + + /* Paint background */ if (selected) { XFillRectangle(dpy, win, WMColorGC(scr->select_color), 1, y + 1, w - 2, h - 3); - color = scr->select_text_color; + if (scr->menu_item_texture->any.type == WTEX_SOLID) + x11_menu_draw_frame(scr, (WNativeWindow)win, y, w, h, type); + } else { + if (scr->menu_item_texture->any.type == WTEX_SOLID) { + XClearArea(dpy, win, 0, y + 1, w - 1, h - 3, False); + x11_menu_draw_frame(scr, (WNativeWindow)win, y, w, h, type); + } else { + XClearArea(dpy, win, 0, y, w, h, False); + } + } + + /* Determine text color */ + if (selected) { + color = entry->flags.enabled ? scr->select_text_color : scr->dtext_color; } else { - XClearArea(dpy, win, 0, y, w, h, False); color = entry->flags.enabled ? scr->mtext_color : scr->dtext_color; } - WMDrawString(scr->wmscreen, win, color, scr->menu_entry_font, - 8, y + 3, entry->text, strlen(entry->text)); + /* Draw text */ + x = 5; + if (entry->flags.indicator) + x += MENU_INDICATOR_SPACE + 2; + WMDrawString(scr->wmscreen, win, color, scr->menu_entry_font, + x, y + 3 + wPreferences.menu_text_clearance, + entry->text, strlen(entry->text)); + + /* Draw cascade arrow */ + if (entry->cascade >= 0) { + GC dim = scr->menu_item_auxtexture->dim_gc; + GC light = scr->menu_item_auxtexture->light_gc; + GC dark = scr->menu_item_auxtexture->dark_gc; + XDrawLine(dpy, win, dim, w - 11, y + 6, w - 6, y + h / 2 - 1); + XDrawLine(dpy, win, light, w - 11, y + h - 8, w - 6, y + h / 2 - 1); + XDrawLine(dpy, win, dark, w - 12, y + 6, w - 12, y + h - 8); + } + + /* Draw left indicator */ + if (entry->flags.indicator && entry->flags.indicator_on) { + WPixmap *indicator = NULL; + switch (entry->flags.indicator_type) { + case MI_DIAMOND: indicator = scr->menu_radio_indicator; break; + case MI_CHECK: indicator = scr->menu_check_indicator; break; + case MI_MINIWINDOW: indicator = scr->menu_mini_indicator; break; + case MI_HIDDEN: indicator = scr->menu_hide_indicator; break; + case MI_SHADED: indicator = scr->menu_shade_indicator; break; + case MI_SNAP_V: indicator = scr->menu_snap_vertical_indicator; break; + case MI_SNAP_H: indicator = scr->menu_snap_horizontal_indicator; break; + case MI_CENTRAL: indicator = scr->menu_central_indicator; break; + case MI_SNAP_RH: indicator = scr->menu_snap_rh_indicator; break; + case MI_SNAP_LH: indicator = scr->menu_snap_lh_indicator; break; + case MI_SNAP_TH: indicator = scr->menu_snap_th_indicator; break; + case MI_SNAP_BH: indicator = scr->menu_snap_bh_indicator; break; + case MI_SNAP_TL: indicator = scr->menu_snap_tl_indicator; break; + case MI_SNAP_TR: indicator = scr->menu_snap_tr_indicator; break; + case MI_SNAP_BL: indicator = scr->menu_snap_bl_indicator; break; + case MI_SNAP_BR: indicator = scr->menu_snap_br_indicator; break; + case MI_SNAP_TILED: indicator = scr->menu_snap_tiled_indicator; break; + default: break; + } + if (indicator) { + int iw = indicator->width; + int ih = indicator->height; + XSetClipMask(dpy, scr->copy_gc, indicator->mask); + XSetClipOrigin(dpy, scr->copy_gc, 5, y + (h - ih) / 2); + if (selected) + XSetForeground(dpy, scr->copy_gc, + WMColorPixel(entry->flags.enabled ? scr->select_text_color : scr->dtext_color)); + else + XSetForeground(dpy, scr->copy_gc, + WMColorPixel(entry->flags.enabled ? scr->mtext_color : scr->dtext_color)); + XFillRectangle(dpy, win, scr->copy_gc, 5, y + (h - ih) / 2, iw, ih); + XSetClipMask(dpy, scr->copy_gc, None); + } + } + + /* Draw right text */ if (entry->rtext && entry->cascade < 0) { - int tw = WMWidthOfString(scr->menu_entry_font, entry->rtext, strlen(entry->rtext)); + tw = WMWidthOfString(scr->menu_entry_font, entry->rtext, strlen(entry->rtext)); WMDrawString(scr->wmscreen, win, color, scr->menu_entry_font, - w - 8 - tw, y + 3, entry->rtext, strlen(entry->rtext)); + w - 6 - tw, y + 3 + wPreferences.menu_text_clearance, + entry->rtext, strlen(entry->rtext)); } } static void x11_menu_draw_frame(WScreen *scr, WNativeWindow win, int y, int w, int h, int type)