From 77325b1e00f54f695b0efcbd417734a670c9bf88 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sat, 23 May 2026 12:54:13 +0700 Subject: [PATCH] fix(bubble): restore alpha after GDI text so glyphs aren't transparent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User report on v0.1.14: text appears semi-transparent, desktop wallpaper bleeds through glyph pixels. Root cause: GDI's DrawTextW writes only RGB into 32bpp BI_RGB DIBs — the "reserved" alpha byte (byte 3) is not preserved per the BITMAPINFOHEADER contract. When UpdateLayeredWindow later composites with AC_SRC_ALPHA, it reads alpha=0 at every glyph pixel and shows them as fully transparent. The pre-v0.1.13 pipeline worked around this with an apply_alpha_mask post-pass that OR'd 0xFF000000 into every pixel inside the rounded rect. The stadium-shape rewrite (526786b) removed it on the false assumption that tiny-skia's per-pixel alpha would "stick" through subsequent GDI writes — but GDI runs *after* tiny-skia in the pipeline, so any pixel GDI text writes to loses the alpha that tiny-skia set. Fix: re-stamp the alpha channel from the original Pixmap after the GDI text overlay. This restores tiny-skia's exact alpha values (255 in the stadium interior, partial on the AA curved perimeter, 0 outside), including the AA fade at the stadium's rounded ends. Implementation: - new helper `restore_alpha_from_pixmap(pixmap, dst)` next to the existing `copy_pixmap_to_dib` - hoist `pixmap` out of the if-let arm in render() so it survives until after `paint_bubble_text` - call `restore_alpha_from_pixmap` post-text Two parallel reviewers (debugger + code-reviewer) converged on the same diagnosis; the debugger preferred this approach for its simplicity and because it's robust to any GDI behavior (whether alpha is zeroed, untouched, or scribbled on, we overwrite with the known-good value). Build clean. --- src/bubble.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/bubble.rs b/src/bubble.rs index b22a762..e0add90 100644 --- a/src/bubble.rs +++ b/src/bubble.rs @@ -1226,6 +1226,19 @@ fn copy_pixmap_to_dib(pixmap: &Pixmap, dst: &mut [u32]) { } } +/// Re-stamp the alpha byte of every DIB pixel from the source `Pixmap`. Used +/// after GDI text rendering, which writes RGB but leaves the BI_RGB DIB's +/// "reserved" alpha byte at zero — making glyph pixels appear transparent +/// when `UpdateLayeredWindow` composites with `AC_SRC_ALPHA`. +fn restore_alpha_from_pixmap(pixmap: &Pixmap, dst: &mut [u32]) { + let src = pixmap.data(); + let pixel_count = (pixmap.width() * pixmap.height()) as usize; + for i in 0..pixel_count { + let a = src[i * 4 + 3] as u32; + dst[i] = (dst[i] & 0x00FF_FFFF) | (a << 24); + } +} + fn measure_text_w(hdc: HDC, text: &str, font_height_px: i32) -> i32 { use windows::Win32::Foundation::SIZE; let font_name = wide_str("Segoe UI"); @@ -1326,12 +1339,25 @@ fn render(hwnd: HWND) { // Paint shape via tiny-skia (AA), then copy into the DIB. GDI text // overlays on top of the resulting bitmap. - if let Some(pixmap) = paint_bubble_pixmap(&layout, &inputs) { - copy_pixmap_to_dib(&pixmap, pixels); + let pixmap_opt = paint_bubble_pixmap(&layout, &inputs); + if let Some(ref pixmap) = pixmap_opt { + copy_pixmap_to_dib(pixmap, pixels); } else { pixels.fill(0); } paint_bubble_text(mem_dc, &layout, &inputs); + // GDI text writes RGB into the 32bpp BI_RGB DIB but does not preserve + // the alpha byte (per the BITMAPINFOHEADER contract: byte 3 is + // "reserved/0" for BI_RGB). UpdateLayeredWindow with AC_SRC_ALPHA then + // reads those zeroed alpha bytes and paints the glyph pixels as fully + // transparent — desktop bleeds through. Fix: re-stamp the alpha + // channel from the tiny-skia Pixmap we still have in scope. This + // preserves the AA alpha on the stadium's curved perimeter and forces + // glyph pixels back to the opacity tiny-skia computed for that + // location (255 in the interior, 0 outside). + if let Some(ref pixmap) = pixmap_opt { + restore_alpha_from_pixmap(pixmap, pixels); + } let mut wr = RECT::default(); let _ = GetWindowRect(hwnd, &mut wr);