feat(client): in-app acrylic material for overlay panels (UI/UX v3 P2b) - #74
Merged
Conversation
Covers the offscreen scene_color capture + Kawase blur chain, the P2b-scoped config surface (in_app_blur_enabled/strength, deferring the window.backdrop enum to P2c), settings panel UI, and the machine- verifiable test strategy given no GPU is available in this environment. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
14 tasks across two PRs: PR1 ships the offscreen capture + Kawase blur engine, panel-fill sampling across all three elevation tiers, and the config-only opt-in toggle (default off, unverified on real GPU); PR2 exposes it on the Window settings tab plus the 8-locale strings. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
…DER (P2b) Adds the 8th BgVertex attribute (acrylic_mix) and the matching WGSL group(0) bindings (acrylic_tex/acrylic_sampler/AcrylicUniform) so BG_SHADER can mix in a blurred/tinted scene sample when acrylic_mix > 0. The acrylic bind group layout is created once in WgpuState::new and stored as a WgpuState field (acrylic_bind_group_layout) rather than being rebuilt independently in shader_reload.rs's hot-reload path, so the pipeline layout and the AcrylicState bind group a later task builds are guaranteed to share the same layout object. Also updates the two BgVertex literal construction sites in vertex_util.rs (plain rect and gradient rect) to set acrylic_mix: 0.0 -- neither is wired to acrylic yet, that lands in a later task.
…P2b) Adds count_open_overlays (pure, unit-tested) and uses it to drive AcrylicCaptureState per frame; inserts the offscreen scene-color capture + 4-pass Kawase blur chain before main_render_pass so blurred_result is ready by the time overlay panels could sample it; binds AcrylicState's composite bind group before every bg_pipeline draw call now that the pipeline layout requires it. Also closes a gap left by Task 6: AcrylicState had no bind groups/uniform buffers for run_blur_chain's 4 render passes to read from, only the final composite bind group. Adds BlurReadResources (3 instances covering all 4 passes, half_res's shared by two of them) and threads a wgpu::Queue through AcrylicState::new/ensure_size to write their texel_size uniforms. Removes the now-provably-unnecessary blur_pipeline_layout field (never read after pipeline construction, matching how bg/text/image pipeline layouts are already local-only) and removes the module-level dead_code allow now that AcrylicState and AcrylicCaptureState are both live.
…s (P2b) Thread a panel_acrylic_mix scalar (derived from window.in_app_blur_enabled / in_app_blur_strength) from render() down to every overlay panel's background fill and the settings-panel tooltip, so panels sample the blurred scene behind them instead of always painting an opaque surface_2 fill. The shadow and border ring stay opaque; only the fill layer opts into acrylic. draw_overlay_panel gains an acrylic_mix parameter, used only by its panel background pass via the new add_px_rounded_rect_sdf_with_acrylic. All 10 draw_overlay_panel call sites (dialog.rs x5, picker.rs x4, settings/mod.rs x1) and the tooltip's inlined chrome thread the value through explicitly, since none of these builder methods have access to Config.
…ath (P2b review fix) Review round 1 findings on e0e76b1, both adjudicated by the coordinator: - add_px_rounded_rect_sdf now delegates to add_px_rounded_rect_sdf_with_acrylic with acrylic_mix=0.0 instead of duplicating its coordinate-transform body. Signature and all ~15 call sites are unchanged. - Extracted the in_app_blur_enabled ? strength : 0.0 decision into a pure acrylic::panel_acrylic_mix(enabled, strength) function, unit-tested for both branches (including a non-zero strength while disabled still yielding exactly 0.0), since render() itself is not testable without a GPU.
…9 schemes (P2b) Also decouples the acrylic uniform's tint opacity from the user's blur strength (ACRYLIC_TINT_OPACITY, fixed at 0.85) so the blend is monotonic in in_app_blur_strength as the design spec requires, instead of folding back to the opaque fallback at maximum strength.
…gs tab (P2b) Adds two new Window-tab rows (toggle + slider) backed by the P2b acrylic engine's existing config keys (window.in_app_blur_enabled / in_app_blur_strength). Wires them through SettingsPanel state, the widget-desc layer (label/kind/apply_window_action), TOML write-back, and the physical arrow-key handler (window_field_increase/decrease), so the new rows are reachable by mouse, keyboard and AccessKit exactly like every sibling row. Also updates settings/row_filter.rs::window_row_labels (not listed in the task brief) since WINDOW_ROW_COUNT now disagrees with it otherwise; see the task report for details. Locale keys referenced here are added by Task 13.
- Stop reallocating the 4 acrylic offscreen textures and 3 BlurReadResources on every WindowEvent::Resized regardless of whether the feature is used. render_frame's own dirty-gated ensure_size call already sizes them to the current surface_config dimensions before anything samples blurred_result, on both the enable-at-runtime and resize-while-enabled paths, and ensure_size early-returns when the size already matches — so the call in WgpuState::resize was pure waste on the default-off path. resize() still invalidates the capture unconditionally via note_resize(). - Invalidate the acrylic capture on WindowEvent::ScaleFactorChanged too (AcrylicCaptureState::note_resize's own doc contract already covered DPI changes; the call site just never existed). - Remove the now-stale #[allow(dead_code)] and "Task 6"/"a later task" comments on KAWASE_BLUR_SHADER and acrylic_bind_group_layout — both were wired in within this same branch. - Correct comments/docs that described the acrylic capture as "the grid layer's cell backgrounds only": it is bg_pipeline's full pre-overlay range (cell backgrounds, gradient, chrome bars, pane/copy-mode overlays), excluding the overlay layer, background image, and text glyphs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Coverage reportGenerated by |
1 similar comment
Coverage reportGenerated by |
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements P2b in-app acrylic from the UI/UX v3 plan: overlay panels — dialogs, pickers, the settings panel and the tooltip — stop being flat opaque fills and instead sample a blurred, tinted copy of the terminal content behind them, Fluent-style.
Design spec:
docs/superpowers/specs/2026-08-22-p2b-in-app-acrylic-design.mdPlan:
docs/superpowers/plans/2026-08-22-p2b-in-app-acrylic.mdShips opt-in and off by default (
window.in_app_blur_enabled = false), because none of it has been seen on real hardware — see "What is not verified" below.What is in here
window.in_app_blur_enabled(bool, defaultfalse) andwindow.in_app_blur_strength(f320.0..=1.0, default0.6), documented indocs/CONFIGURATION.mdAcrylicCaptureState— a pure, unit-tested invalidation state machine keyed on an overlay-open count and a generation counter bumped on resize and DPI changeBgVertexgains an 8th attributeacrylic_mix;BG_SHADERsamples the blurred texture only when it is non-zerorender_framecaptures the pre-overlay background range to an offscreen target, runs the blur chain, updates the composite uniform, then draws overlays — all in one encoder, ordered so nothing samples a texture before it is writtendraw_overlay_paneltakes an explicitacrylic_mix, threaded to all ten call sites plus the tooltip's inlined chrome. Shadows and border rings stay opaque; only the fill is translucentSettingsPanelto thetoml_editwrite-back, with keyboard arrow support matching every sibling rowA spec violation the tests caught
The contrast test in this PR is what surfaced it. The shader's tint uniform was being fed the user's
in_app_blur_strengthat the same time as the vertex'sacrylic_mix, which makes the composite— a curve whose deviation coefficient is zero at both ends. Turning the strength slider to maximum therefore produced a fully opaque panel again, the exact opposite of what the spec requires ("0.0 = fully the existing opaque token fill, 1.0 = fully blur+tint"), and it would have shipped as a user-facing slider whose maximum setting means "no effect".
Fixed by decoupling: the tint amount is now a fixed
ACRYLIC_TINT_OPACITY = 0.85(Fluent's in-app-acrylic tint opacity), leavingfinal = S + m(1 - T)(B - S)— monotonic in the slider, with at most 15% of the backdrop showing through at maximum.Readability
panel_body_text_clears_contrast_floor_across_acrylic_strengthsasserts the project's 4.5:1 floor fortext_primaryandtext_secondaryacross all nine built-in schemes at five strengths, modelling the shader's arithmetic rather than an approximation of it.Two deliberate scope limits, both documented in the test's own doc comment so they cannot be quietly "fixed" later:
surface_0/surface_1— what the grid layer actually paints — not as pure black/white. No non-zero blur can satisfy the pure-extreme bound on this palette set, because Nord'stext_secondaryhas 0.02:1 of baseline headroom; asserting it would measure the themes' lack of headroom rather than acrylic's readability. A program painting a full-screen high-luminance background behind a panel can push body text below the floor at mid-to-high strengths. Measured, accepted for an opt-in feature, logged to the backlog.Unrelated pre-existing finding: Solarized and OneDark already fail that 4.5:1 floor for panel body text over
surface_2with no acrylic involved (text_primary3.33 / 4.48,text_secondary2.61 / 3.38). They are pinned in an explicit allow-list carrying those measured baselines, so the test fires if the tokens are ever fixed without updating it. Worth its own issue.What is not verified
There is no GPU in the environment this was built in. Nothing here has rendered a frame. Every wgpu resource, bind-group/pipeline-layout pairing, uniform byte layout and render-pass ordering decision was checked by reading the code, never by executing it. The on-device verification backlog in
docs/plans/ui-ux-modernization-v3.mdcarries the specifics: blur quality and tap radius, the carried-over P2a focus-ring double-blend risk, frame-time cost of the extra offscreen pass on integrated GPUs, DPI/multi-monitor recapture, whether the noise reads as grain or banding, whetherACRYLIC_TINT_OPACITY = 0.85is the right recipe, and whether the French label overflows its column.The default-off path was specifically checked:
panel_acrylic_mixis exactly0.0when disabled (unit-tested), the capture and blur block never executes, and the resize handler no longer allocates acrylic textures for users who never enable the feature.Test plan
cargo test --workspace— 1737 passed, 0 failed, 6 ignoredcargo clippy -- -D warnings— clean workspace-widecargo fmt --check— cleancargo test -p nexterm-config --test doc_matches_schema— the new config keys are documentedcargo test -p nexterm-i18n— locale key parity across all 8 filesCargo.lockunchanged, sopkg/flatpak/cargo-sources.jsonneeds no regenerationNotes for review
(PR pending)placeholders indocs/plans/ui-ux-modernization-v3.mdneed this PR's number filled in.d86d450(engine, then settings UI). The whole-branch review's fix commit touches engine-side files but lands after the settings commits, so the split is no longer a clean prefix; separating them now would mean reordering history. Landing as one PR instead.jauses 「ブラー」; 「ぼかし」 may read better — a one-line change inja.jsonif preferred.🤖 Generated with Claude Code