From 4c5f07f9eceafddbf4310bcd620acf38c1809503 Mon Sep 17 00:00:00 2001 From: Window Maker Date: Sat, 23 May 2026 14:45:15 +0200 Subject: [PATCH] Phase C complete: balloon tooltips + window opacity C1 (slide animations): already works - misc.c calls vtable window_move C2 (menu scrolling): pure policy code, already works C3 (balloon tooltips): pixman text render to wlr_scene_buffer overlay C4 (cursor themes): wlr_xcursor_manager already in place C5 (window opacity): read _NET_WM_WINDOW_OPACITY, apply to frame + child buffers + client surface via wlr_scene_buffer_set_opacity --- WAYLAND_ROADMAP.md | 28 ++++-- src/backend/wayland/wl_backend.c | 156 ++++++++++++++++++++++++++++++- 2 files changed, 170 insertions(+), 14 deletions(-) diff --git a/WAYLAND_ROADMAP.md b/WAYLAND_ROADMAP.md index 48ffbcf..880d384 100644 --- a/WAYLAND_ROADMAP.md +++ b/WAYLAND_ROADMAP.md @@ -98,6 +98,12 @@ Three major libraries: - Button scene_bufs raised above titlebar (commit a34d828) - Titlebar button rendering — proper colors, bevel, pressed state (commit 5a4d989) - Button bevel colors derived from titlebar background (commit f3cb593) +- **Phase C complete** (2026-05-23): + - C1: Slide animations — already works (misc.c calls vtable window_move per frame) + - C2: Menu scrolling — pure policy code, already works + - C3: Balloon tooltips — pixman text render → wlr_scene_buffer overlay in WL_LAYER_OVERLAY + - C4: Cursor themes — wlr_xcursor_manager + wl_pointer_change_cursor already in place + - C5: Window opacity — reads _NET_WM_WINDOW_OPACITY from XWayland, applies wlr_scene_buffer_set_opacity --- @@ -136,11 +142,13 @@ Priority: HIGH — needed for daily-driver use. | # | Task | Key Files | Complexity | |---|------|-----------|------------| -| C1 | Slide animations (iconify/deiconify zoom) | vtable slide_window | Medium | -| C2 | Menu scrolling (long menus) | menu.c scroll logic | Low | -| C3 | Balloon tooltips (dock icon hover) | vtable balloon_show | Low | -| C4 | Cursor themes (xcursor loading) | wlr_xcursor_manager | Low | -| C5 | Window opacity (NETWM alpha) | vtable netwm_set_opacity | Low | +| C1 | ~~Slide animations (iconify/deiconify zoom)~~ | misc.c → vtable window_move | ✅ Done (already works) | +| C2 | ~~Menu scrolling (long menus)~~ | menu.c scroll logic | ✅ Done (pure policy) | +| C3 | ~~Balloon tooltips (dock icon hover)~~ | vtable balloon_show | ✅ Done | +| C4 | ~~Cursor themes (xcursor loading)~~ | wlr_xcursor_manager | ✅ Done (already works) | +| C5 | ~~Window opacity (NETWM alpha)~~ | vtable netwm_update_opacity | ✅ Done | + +**Phase C: COMPLETE** --- @@ -254,11 +262,11 @@ NOW (daily-driver basics): NEXT (visual completeness): ~~B1. App icon rendering (wrlib → pixman)~~ ✅ ~~F1. wrlib RContext for Wayland~~ ✅ - C1. Slide animations - C2. Menu scrolling - C3. Balloon tooltips - C4. Cursor themes - C5. Window opacity + ~~C1. Slide animations~~ ✅ + ~~C2. Menu scrolling~~ ✅ + ~~C3. Balloon tooltips~~ ✅ + ~~C4. Cursor themes~~ ✅ + ~~C5. Window opacity~~ ✅ THEN (full feature parity): D*. Test all WINGs widgets diff --git a/src/backend/wayland/wl_backend.c b/src/backend/wayland/wl_backend.c index 92da821..341eb12 100644 --- a/src/backend/wayland/wl_backend.c +++ b/src/backend/wayland/wl_backend.c @@ -7680,7 +7680,56 @@ static void wl_noop_netwm_init(WScreen *scr) { (void)scr; } static void wl_noop_netwm_cleanup(WScreen *scr) { (void)scr; } static void wl_noop_netwm_update_desktop(WScreen *scr) { (void)scr; } static int wl_noop_netwm_get_current_desktop(WScreen *scr) { (void)scr; return -1; } -static void wl_noop_netwm_update_opacity(WWindow *wwin) { (void)wwin; } +static void wl_netwm_update_opacity(WWindow *wwin) +{ + if (!wwin->frame) + return; + + struct wl_frame_buf *fb = frame_buf_find(wwin->frame->core->window); + if (!fb || !fb->scene_buf) + return; + + float alpha = 1.0f; + if (wl_state.x_display && wwin->client_win) { + Atom type; + int format; + unsigned long items, rest; + unsigned char *prop = NULL; + Atom opacity_atom = XInternAtom(wl_state.x_display, + "_NET_WM_WINDOW_OPACITY", False); + if (XGetWindowProperty(wl_state.x_display, (Window)wwin->client_win, + opacity_atom, 0L, 1L, False, AnyPropertyType, + &type, &format, &items, &rest, &prop) == Success + && format == 32 && items == 1 && prop) { + unsigned long val = *(unsigned long *)prop; + alpha = (float)val / (float)0xFFFFFFFF; + } + if (prop) + XFree(prop); + } + + wlr_scene_buffer_set_opacity(fb->scene_buf, alpha); + + /* Apply to all child frame_bufs (titlebar buttons, resizebar) */ + struct wl_frame_buf *child; + wl_list_for_each(child, &wl_state.frame_list, link) { + if (child->parent_id == fb->id && child->scene_buf) + wlr_scene_buffer_set_opacity(child->scene_buf, alpha); + } + + /* Apply to the client surface */ + struct wl_toplevel_view *v; + wl_list_for_each(v, &wl_state.toplevel_list, link) { + if (v->frame_id == fb->id && v->scene_node) { + struct wlr_scene_node *node; + wl_list_for_each(node, &v->scene_node->children, link) { + if (node->type == WLR_SCENE_NODE_BUFFER) + wlr_scene_buffer_set_opacity( + wlr_scene_buffer_from_node(node), alpha); + } + } + } +} static void wl_noop_netwm_update_actions(WWindow *wwin, int del) { (void)wwin; (void)del; } static void wl_noop_netwm_check_client_hints(WWindow *wwin, int *layer, int *workspace) { (void)wwin; (void)layer; (void)workspace; } @@ -8187,6 +8236,35 @@ static void wl_drawer_icon_paint(WAppIcon *aicon) static void wl_balloon_init(WScreen *scr) { scr->balloon = wmalloc(sizeof(WBalloon)); + memset(scr->balloon, 0, sizeof(WBalloon)); + + /* Create a frame_buf in the overlay layer for balloon tooltips */ + if (wl_state.scene) { + struct wl_frame_buf *fb = wmalloc(sizeof(*fb)); + memset(fb, 0, sizeof(*fb)); + + int w = 1, h = 1; + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, w, h, NULL, 0); + wlr_buffer_init(&fb->base, &frame_buf_impl, w, h); + fb->id = wl_state.next_synthetic_id++; + fb->width = w; + fb->height = h; + + struct wlr_scene_tree *parent = wl_state.level_trees[WL_LAYER_OVERLAY] + ? wl_state.level_trees[WL_LAYER_OVERLAY] : &wl_state.scene->tree; + fb->tree = wlr_scene_tree_create(parent); + if (fb->tree) { + wlr_scene_node_set_position(&fb->tree->node, 0, 0); + fb->scene_buf = wlr_scene_buffer_create(fb->tree, &fb->base); + if (fb->scene_buf) + wlr_scene_node_set_position(&fb->scene_buf->node, 0, 0); + wlr_scene_node_set_enabled(&fb->tree->node, false); + } + + wl_list_insert(&wl_state.frame_list, &fb->link); + W_RegisterBacking(fb->id, fb->image); + scr->balloon->window = (Window)fb->id; + } } static void wl_noop_do_kaboom(WScreen *scr, WNativeWindow win, int x, int y) { (void)scr; (void)win; (void)x; (void)y; } @@ -8462,7 +8540,77 @@ static void wl_noop_icon_select_paint(WIcon *icon, int step) { (void)icon; (void)step; } static unsigned long wl_noop_window_get_event_mask(WNativeWindow window) { (void)window; return 0; } -static void wl_noop_balloon_show(WScreen *scr, void *bal) { (void)scr; (void)bal; } +static void wl_balloon_show(WScreen *scr, void *bal_ptr) +{ + WBalloon *bal = (WBalloon *)bal_ptr; + if (!bal->text || !bal->text[0]) + return; + + struct wl_frame_buf *fb = frame_buf_find((WNativeWindow)bal->window); + if (!fb) + return; + + WMFont *font = scr->info_text_font; + int textlen = strlen(bal->text); + int tw = WMWidthOfString(font, bal->text, textlen); + int th = WMFontHeight(font); + int pad = 4; + int bw = tw + pad * 2; + int bh = th + pad * 2; + + /* Resize pixman buffer if needed */ + if (bw != fb->width || bh != fb->height) { + W_UnregisterBacking(fb->id); + pixman_image_unref(fb->image); + fb->image = pixman_image_create_bits(PIXMAN_a8r8g8b8, bw, bh, NULL, 0); + fb->width = bw; + fb->height = bh; + fb->base.width = bw; + fb->base.height = bh; + W_RegisterBacking(fb->id, fb->image); + } + + /* Fill pale yellow background */ + uint32_t *data = pixman_image_get_data(fb->image); + for (int i = 0; i < bw * bh; i++) + data[i] = 0xFFFFFFE0; + + /* 1px black border */ + for (int x = 0; x < bw; x++) { + data[x] = 0xFF000000; + data[(bh - 1) * bw + x] = 0xFF000000; + } + for (int y = 0; y < bh; y++) { + data[y * bw] = 0xFF000000; + data[y * bw + bw - 1] = 0xFF000000; + } + + /* Draw text */ + WMDrawString(scr->wmscreen, (WMDrawable)fb->id, scr->black, font, + pad, pad, bal->text, textlen); + + /* Position near objectWindow */ + int bx = 0, by = 0; + struct wl_frame_buf *obj = frame_buf_find((WNativeWindow)bal->objectWindow); + if (obj) { + bx = obj->x; + by = obj->y + bal->h + 2; + } + + /* Clamp to screen */ + if (bx + bw > scr->scr_width) + bx = scr->scr_width - bw; + if (by + bh > scr->scr_height) + by = by - bal->h - bh - 2; + + if (fb->scene_buf) { + wlr_scene_node_set_position(&fb->scene_buf->node, bx, by); + wlr_scene_buffer_set_buffer(fb->scene_buf, &fb->base); + wlr_scene_node_set_enabled(&fb->tree->node, true); + } + + bal->mapped = 1; +} static void wl_noop_window_fill_stipple(WNativeWindow win, int x, int y, int w, int h) { (void)win; (void)x; (void)y; (void)w; (void)h; } static int wl_noop_event_check_enter_leave(void *dock) { (void)dock; return 1; } @@ -8670,7 +8818,7 @@ const WMBackend wl_backend = { .netwm_cleanup = wl_noop_netwm_cleanup, .netwm_update_desktop = wl_noop_netwm_update_desktop, .netwm_get_current_desktop = wl_noop_netwm_get_current_desktop, - .netwm_update_opacity = wl_noop_netwm_update_opacity, + .netwm_update_opacity = wl_netwm_update_opacity, .netwm_update_actions = wl_noop_netwm_update_actions, .netwm_check_client_hints = wl_noop_netwm_check_client_hints, .netwm_check_initial_state = wl_noop_netwm_check_initial_state, @@ -8743,7 +8891,7 @@ const WMBackend wl_backend = { .icon_paint_title_bg = wl_noop_icon_paint_title_bg, .icon_select_paint = wl_noop_icon_select_paint, .window_get_event_mask = wl_noop_window_get_event_mask, - .balloon_show = wl_noop_balloon_show, + .balloon_show = wl_balloon_show, .window_fill_stipple = wl_noop_window_fill_stipple, .event_check_enter_leave = wl_noop_event_check_enter_leave, .event_allow_async = wl_noop_event_allow_async,