diff options
| author | srdusr <[email protected]> | 2026-01-30 09:39:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-01-30 09:39:00 +0200 |
| commit | 2a51fb86e2a123ff9e810a0b4105c9fe5f85e3ae (patch) | |
| tree | 7a1fc6b2b87cd0bf82bc8953ba0a2a9e65c9e7cf | |
| parent | d16f7dab25bd9508271017bc6dc15024c30cbf71 (diff) | |
| download | srdwm-2a51fb86e2a123ff9e810a0b4105c9fe5f85e3ae.tar.gz srdwm-2a51fb86e2a123ff9e810a0b4105c9fe5f85e3ae.zip | |
Fix a real regression: dynamic-mode windows lost their shadow via toggle_floating
The tiled-shadow-tint fix earlier today gated the shadow on Window::floating
alone. arrange_workspace only reads floating under the "tiling" layout, so
every window on this project's own default "dynamic" layout starts, and
stays, floating: false - the gate misread that as "tiled, no shadow"
regardless of which layout was actually running, so shadows silently
vanished under dynamic mode entirely, recoverable only by pressing Super+S
(toggle_floating), which then looked like that key toggles a tint rather
than floating. Fixed by checking the workspace's own layout name first:
a window is only "currently tiled" when its workspace runs "tiling" AND
it hasn't opted out via floating. DecorationSignature's floating field is
now currently_tiled, since a layout switch changes this for every window
on a workspace without touching any of their own floating fields.
Also disabled general.shadows in the user's own config per direct
request - never asked for, on by default, and a real problem for
color-accuracy work regardless of how correctly it renders otherwise.
Also fixed both context menus (titlebar and desktop) silently truncating
labels past a fixed 170px width with no indication - widened dynamically
to each menu's own real widest label via a new measure_text_width helper.
| -rw-r--r-- | crates/wayland/src/decoration.rs | 2 | ||||
| -rw-r--r-- | crates/wayland/src/decoration/font.rs | 12 | ||||
| -rw-r--r-- | crates/wayland/src/state/desktop_icons.rs | 11 | ||||
| -rw-r--r-- | crates/wayland/src/state/lifecycle.rs | 49 | ||||
| -rw-r--r-- | crates/wayland/src/state/menu.rs | 18 | ||||
| -rw-r--r-- | crates/wayland/src/state/mod.rs | 21 | ||||
| -rw-r--r-- | docs/TODO.md | 18 |
7 files changed, 108 insertions, 23 deletions
diff --git a/crates/wayland/src/decoration.rs b/crates/wayland/src/decoration.rs index d24c035..49cf416 100644 --- a/crates/wayland/src/decoration.rs +++ b/crates/wayland/src/decoration.rs @@ -42,7 +42,7 @@ pub(crate) use border::{border_bottom_visible_rows, border_top_visible_rows}; pub(crate) use buttons::HOVER_GLYPH_DURATION; pub(crate) use color::{mix_rgb, rgb_to_bgra}; pub(crate) use corners::{round_bottom_corners, round_top_corners}; -pub(crate) use font::{blit_glyph, find_system_font, FONT_PIXELS, TEXT_LEFT_PADDING}; +pub(crate) use font::{blit_glyph, find_system_font, measure_text_width, FONT_PIXELS, TEXT_LEFT_PADDING}; pub use shadow::{shadow_bitmap, shadow_rect}; pub(crate) use shadow::{SHADOW_MAX_ALPHA, SHADOW_SIZE}; pub use titlebar::render_titlebar; diff --git a/crates/wayland/src/decoration/font.rs b/crates/wayland/src/decoration/font.rs index 27df347..6a2a312 100644 --- a/crates/wayland/src/decoration/font.rs +++ b/crates/wayland/src/decoration/font.rs @@ -19,6 +19,18 @@ pub(crate) fn find_system_font() -> Option<Font> { FONT.get_or_init(load_any_monospace_font).clone() } +/// The real rendered pixel width of `text` at `size`, in this font - the +/// same two-pass "sum every glyph's own advance width" measurement `render_ +/// header_box`'s `draw_centered` already does inline, pulled out here so a +/// caller that needs to *size a box* around text (not just draw it) has one +/// place to ask, rather than repeating the sum. Returns `0.0` with no +/// system font found, matching every other text-rendering path's own +/// "solid colour only, no text at all" fallback in that case. +pub(crate) fn measure_text_width(font: &Option<Font>, text: &str, size: f32) -> f32 { + let Some(font) = font else { return 0.0 }; + text.chars().filter(|c| !c.is_control()).map(|ch| font.rasterize(ch, size).0.advance_width).sum() +} + fn load_any_monospace_font() -> Option<Font> { let roots = ["/usr/share/fonts", "/usr/local/share/fonts"]; let mut home_roots = Vec::new(); diff --git a/crates/wayland/src/state/desktop_icons.rs b/crates/wayland/src/state/desktop_icons.rs index 9d2dd91..05bd85a 100644 --- a/crates/wayland/src/state/desktop_icons.rs +++ b/crates/wayland/src/state/desktop_icons.rs @@ -591,8 +591,17 @@ impl CompState { self.build_desktop_menu_buffer(menu); } - fn build_desktop_menu_buffer(&mut self, menu: DesktopMenu) { + fn build_desktop_menu_buffer(&mut self, mut menu: DesktopMenu) { let theme = self.wm.borrow().theme; + // Same fixed-width-guess bug the titlebar menu was reported for + // ("text goes out of view") - "Open in File Manager"/"New Text + // Document" both run past a fixed 170px guess. Widened to this + // menu's own widest real label, same measurement `open_context_ + // menu` (`state/menu.rs`) uses. + let font = decoration::find_system_font(); + let widest_label = menu.items.iter().map(|(label, _)| decoration::measure_text_width(&font, label, decoration::FONT_PIXELS)).fold(0.0_f32, f32::max); + let content_width = (widest_label + decoration::TEXT_LEFT_PADDING * 2.0).ceil() as u32; + menu.width = menu.width.max(content_width); // Not redesigned the way the titlebar menu was (`srdwm_core:: // context_menu`'s own module doc comment) - this menu's rows are // still all one uniform height, `Separator` included, matching diff --git a/crates/wayland/src/state/lifecycle.rs b/crates/wayland/src/state/lifecycle.rs index 112c64e..51297bb 100644 --- a/crates/wayland/src/state/lifecycle.rs +++ b/crates/wayland/src/state/lifecycle.rs @@ -145,6 +145,17 @@ impl CompState { // nothing this function reads has actually changed since the last // call turns those redundant calls into a cheap signature // comparison instead of a full re-rasterization. + // Whether this window is *actually* occupying a tiled slot right + // now - see the shadow gate further down for the full reasoning. + // Computed once, here, and reused there rather than recomputed: + // it depends on the workspace's own layout name, not just this + // window's own `floating` flag, so it has to be part of the + // signature too - switching a workspace's layout (`Super+Shift+ + // t`/`s`) changes this for every window on it without touching + // any of their own `floating` fields, and a signature that didn't + // track it would keep serving whichever shadow state was cached + // before the switch. + let currently_tiled = self.wm.borrow().workspace(w.workspace).is_some_and(|ws| ws.layout == "tiling") && !w.floating; let signature = DecorationSignature { width: frame.width, height: frame.height, @@ -157,7 +168,7 @@ impl CompState { maximized: w.maximized, fullscreen: w.fullscreen, shadows_enabled: self.wm.borrow().shadows_enabled, - floating: w.floating, + currently_tiled, hovered_button, title_centered: theme.title_centered, buttons_left: theme.buttons_left, @@ -252,20 +263,36 @@ impl CompState { // Hyprland/GNOME convention `MISSING.md` measures this compositor // against. // - // No shadow for a TILED window either - a real, reported bug, not - // a style choice made up front: a drop shadow exists to separate a - // window from whatever is visually *behind* it, but tiled windows - // are coplanar and adjacent by construction, with nothing behind - // them to separate from. `SHADOW_SIZE` pixels of shadow with only + // No shadow for a window that is *actually being tiled right now* + // - a real, reported bug, not a style choice made up front: a + // drop shadow exists to separate a window from whatever is + // visually *behind* it, but a tiled window is coplanar and + // adjacent to its neighbours by construction, with nothing behind + // it to separate from. `SHADOW_SIZE` pixels of shadow with only // `gap_inner` pixels of real gap to fall into (as little as 1px) // has nowhere to land except on the neighbouring tile, darkening // it by up to `SHADOW_MAX_ALPHA` - reported live as "some windows - // are dark tinted." Floating windows keep their shadow: they - // genuinely do sit above other windows, which is exactly where a - // shadow does its job, and it's also the one case `visible_border_ - // fragments`' own occluder clipping already handles correctly. + // are dark tinted." + // + // Gating this on `w.floating` alone (the first version of this + // fix) was a real regression, caught live: `arrange_workspace` + // only reads `floating` under the `"tiling"` layout, so every + // window on this project's own default `"dynamic"` layout starts + // - and stays - `floating: false` unless something explicitly + // flips it. That first version read `floating: false` as "this + // window is tiled, no shadow" regardless of which layout was + // actually running, so *every* window under dynamic/floating mode + // silently lost its shadow, recoverable only by toggling `Super+S` + // (`srd.window.toggle_floating()`) - which then looked like that + // key toggles a "tint", not floating, since floating itself does + // nothing visible under a layout that never tiles anyone. `Window:: + // floating` only ever means "opted out of tiling" *within* a + // workspace that tiles at all - checking the workspace's own + // layout name first is what this needed instead: a window is only + // "currently tiled" when both are true. `currently_tiled` itself + // was already computed above, alongside the signature. let shadows_enabled = self.wm.borrow().shadows_enabled; - if shadows_enabled && w.floating && !w.maximized && !w.fullscreen { + if shadows_enabled && !currently_tiled && !w.maximized && !w.fullscreen { // A decorated window's corners are *always* rounded (the // titlebar/border strips round to `corner_radius` regardless of // this setting - see their own call sites); an undecorated diff --git a/crates/wayland/src/state/menu.rs b/crates/wayland/src/state/menu.rs index b0b0a6e..82e4434 100644 --- a/crates/wayland/src/state/menu.rs +++ b/crates/wayland/src/state/menu.rs @@ -7,12 +7,28 @@ impl CompState { /// and caches the rasterised buffer once here rather than per frame -- /// same reasoning as `redraw_decoration_buffer`. pub(crate) fn open_context_menu(&mut self, window: WindowId, pos: (i32, i32)) { - let Some(menu) = ({ + let Some(mut menu) = ({ let wm = self.wm.borrow(); crate::context_menu::ContextMenu::open(&wm, window, pos) }) else { return; }; + // `ContextMenu::width` is a backend-agnostic placeholder - `core` + // has no font of its own to measure real text against, so it can + // only ever pick a fixed guess. Widened here to whatever this + // menu's own widest real label actually needs, or reported live + // as "text goes out of view": the old fixed width comfortably fit + // every label back when this menu only listed short ones + // ("Minimize", "Close"), but a longer one added since ("Button + // Style: Traffic Lights", or a user-configurable workspace name) + // just ran past the panel's own right edge, silently cut off + // mid-character by `render_context_menu`'s own overflow guard. + // Only ever grows the width, never shrinks it below the built-in + // minimum `ContextMenu::open` already picked. + let font = decoration::find_system_font(); + let widest_label = menu.items.iter().map(|&(label, _)| decoration::measure_text_width(&font, label, decoration::FONT_PIXELS)).fold(0.0_f32, f32::max); + let content_width = (widest_label + decoration::TEXT_LEFT_PADDING * 2.0).ceil() as u32; + menu.width = menu.width.max(content_width); let theme = self.wm.borrow().theme; let rows: Vec<(&str, bool, u32, decoration::MenuRowKind)> = menu .items diff --git a/crates/wayland/src/state/mod.rs b/crates/wayland/src/state/mod.rs index e07dc1d..6396692 100644 --- a/crates/wayland/src/state/mod.rs +++ b/crates/wayland/src/state/mod.rs @@ -114,15 +114,18 @@ pub(crate) struct DecorationSignature { pub(crate) maximized: bool, pub(crate) fullscreen: bool, pub(crate) shadows_enabled: bool, - /// A tiled window gets no shadow at all (see `redraw_decoration_ - /// buffer`'s own gate) - included here for the same "one signature, - /// every real input" reason as `maximized`/`fullscreen` just above: - /// toggling floating on its own (`srd.window.toggle_floating()`, - /// switching layouts) changes nothing else this signature already - /// tracks, so without this the cache would keep serving whichever - /// shadow state happened to be cached until some unrelated field - /// (a resize, a focus change) forced a rebuild anyway. - pub(crate) floating: bool, + /// A window actually occupying a tiled slot right now (its workspace + /// is running the `"tiling"` layout, and it hasn't opted out via + /// `Window::floating`) gets no shadow at all - see `redraw_ + /// decoration_buffer`'s own gate. Deliberately *not* just `Window:: + /// floating` on its own: a workspace's layout name changing (`Super+ + /// Shift+t`/`s`) affects every window on it without touching any of + /// their own `floating` fields, and toggling `floating` alone + /// (`srd.window.toggle_floating()`) changes nothing else this + /// signature already tracks - without this field, either change + /// would leave the cache serving whichever shadow state it last + /// computed until some unrelated field forced a rebuild anyway. + pub(crate) currently_tiled: bool, /// Which of *this* window's own titlebar buttons (if any) is currently /// hovered, and the glyph-reveal animation's current progress (0..=255) /// - see `CompState::hovered_titlebar_button`'s own doc comment. diff --git a/docs/TODO.md b/docs/TODO.md index 691e63b..68b8486 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,5 +1,23 @@ # TODO / planned features - master checklist +## A real regression in the shadow-tint fix: dynamic-mode windows lost their shadow entirely (2026-08-28) + +Reported live: "why is super+s toggling tint/shadow of window. it should toggle tiling/floating." Caught immediately, not defended: the tiled-shadow-tint fix earlier today (`b7f3eff`) gated the shadow on `Window::floating` alone - `if shadows_enabled && w.floating && !w.maximized && !w.fullscreen`. `arrange_workspace` only ever reads `floating` under the `"tiling"` layout; every window on this project's own default `"dynamic"` layout starts, and stays, `floating: false` unless something explicitly flips it. That gate therefore read `floating: false` as "this window is tiled, no shadow" regardless of which layout was actually running - so every window under dynamic/floating mode (this session's own stated daily-driver preference) silently lost its shadow outright, recoverable only by toggling `Super+S` (`srd.window.toggle_floating()`), which then looked like that key toggles a "tint" rather than floating - floating itself does nothing visible under a layout that never tiles anyone, so the shadow reappearing was the *only* thing Super+S visibly did. + +Fixed by checking the window's own workspace layout first: `currently_tiled = workspace.layout == "tiling" && !w.floating`, shadow shown whenever `!currently_tiled`. `Window::floating` only ever means "opted out of tiling" *within* a workspace that tiles at all; a dynamic-workspace window is never "tiled" in the first place; regardless of its own `floating` flag, so it now always keeps its shadow, matching every other floating desktop window on any real OS. `DecorationSignature`'s own `floating: bool` field (added alongside the original fix, to invalidate the decoration cache when floating changes) is now `currently_tiled: bool` instead, computed from the workspace's layout too - a `Super+Shift+t`/`s` layout switch changes this for every window on that workspace without touching any of their own `floating` fields, and the old field would have kept serving a stale cached shadow state across exactly that switch. + +Separately and directly: "i never ever asked you to tint windows ever and that has caused me a lot of problems in trying to get color accuracy... shadowed borders like how other systems do yes. tinting no." `general.shadows` was never actually asked for - it defaults `true` in the engine itself and had been on the whole time with no line in the user's own `~/.config/srd/init.lua` to show it, confirmed by grep. Set to `false` there now, with the reasoning written into the config itself: a drop shadow is mechanically a translucent dark gradient blended over whatever's behind the window, unavoidably colour-affecting near its own edge regardless of how correctly it renders otherwise - exactly wrong for colour-accuracy work. The window's own solid `border_color`/`border_width` is untouched by this setting and unaffected by any of today's shadow work, still giving every window a fully opaque, crisp edge. + +Full workspace build/test/clippy clean. + +## Context menu text was silently cut off past a fixed 170px width (2026-08-28) + +Reported live: "some of the text goes out of view in the context menu" - clarifying an earlier misdiagnosis (the previous entry below wrongly read this complaint as being about the "Floating" toggle's own inapplicability, which was real but not what was meant). Root cause: both `ContextMenu` (titlebar) and `DesktopMenu` (desktop icons) used a fixed `MENU_WIDTH = 170` regardless of their own actual longest label - comfortably fit short ones ("Minimize", "Close") but not longer ones added since ("Button Style: Traffic Lights", "Open in File Manager", a user-configurable workspace name), which `render_context_menu`'s own overflow guard (`if pen_x as usize >= width { break; }`) just silently truncated mid-character with no ellipsis or indication anything was cut. + +`srdwm_core::context_menu` has no font of its own to measure real glyph widths against (it's backend-agnostic by design), so the fix lives on the Wayland side, where one already exists: a new `decoration::measure_text_width` (real per-glyph advance-width summation, the same measurement `render_header_box`'s `draw_centered` already did inline for the lock screen) lets `open_context_menu`/`build_desktop_menu_buffer` widen `menu.width` to whichever real label is actually widest, before rendering - only ever grows the width past the built-in minimum, never shrinks it. Applied to both menus, since both had the identical fixed-width bug, not just the titlebar one that was reported. + +Full workspace build/test/clippy clean. + ## Titlebar right-click menu redesigned: real separators/headers, an inapplicable item hidden, live customization added (2026-08-28) Reported live: "looks very ugly currently and some of it doesn't make sense." Both were real, found by reading `srdwm_core::context_menu` and `decoration::render_context_menu` directly rather than guessing at what "ugly" meant. |