feat(ui): Add Miuix blur and custom card backdrop blur support - #222
Conversation
Co-authored-by: xingguangcuican_bot <xingguangcuican821@gmail.com>
…st.kt Co-authored-by: xingguangcuican_bot <xingguangcuican821@gmail.com>
Co-authored-by: xingguangcuican_bot <xingguangcuican821@gmail.com>
Make frosted glass visible and the software card-blur path usable on all APIs: - Cap the bar blur tint alpha (AbkBlurTintAlpha) and keep card surfaces translucent when a pre-blurred custom background is present, instead of blending an opaque surfaceContainer over the backdrop at the default surface opacity, so the Blur switch has a visible effect by default. - Decouple BlurConfig.wantsBackgroundPainter from the AGSL master switch and stop hiding the "render custom background into blur" toggle behind isBlurCapableDevice(), so API 26-32 devices can enable the StackBlur path. - Turn off the bottom-bar backdrop and its blur pipeline while the bar is off-screen (child page or OOBE) to drop the invisible full-screen recordLayer + AGSL blur pass every frame. - Replace per-card localToScreen/screenToLocal round-trips with LayoutCoordinates.localPositionOf; align blurSource/blurEffect guards to the actual API 33 runtime-shader capability instead of API 31. - Debounce the card-background decode + StackBlur reload across viewport changes so rotation/split-screen do not re-run it per intermediate size. - Share one background painter from AppBackgroundHost across all blur hosts instead of one Coil request per screen scaffold. - Fix the surface-alpha preview state: an interrupted slider drag no longer sticks the in-memory preview for the session, and a late DataStore emission no longer yanks the slider thumb mid-drag. - Drop dead LocalBlurredSurfaceDepth plumbing; route the extension manager through BlurConfig instead of duplicating its condition.
The painter-sharing change dropped the contentScale from the background
Image, so wallpapers with a mismatched aspect ratio were letterboxed with
the default Fit and the blur backdrops / card sampling could read the wrong
region of the wallpaper. Render with ContentScale.Crop to match the previous
AsyncImage behavior and the blur pipeline's crop math.
Coil's AsyncImagePainter sizes its request from the draw viewport
(size { drawSize...first() }), so the shared painter stays bounded to screen
resolution; a large user wallpaper is not decoded at its original dimensions.
Follow-up recheck of the blur subsystem (independent multi-agent audit + manual verification): - Fix the bottom-bar blur gate direction: navProgress is 1f when the bar is shown and animates to 0f when a child page hides it, so the previous `navProgress < 1f` gate disabled blur in the normal tab state and kept it running while the bar was fully off-screen. Gate on `navProgress > 0.02f`. - Restore the master Blur switch as the overall on/off for every blur surface: show it on all API levels and re-tie BlurConfig.wantsBackgroundPainter to blurEnabled, so turning the master off disables the card blur too (it was decoupled in a prior commit, leaving frosted cards with opaque bars). - Stop swallowing CancellationException and OutOfMemoryError in the card-blur reload: catch Exception only and rethrow CancellationException so a viewport restart cannot overwrite the newer effect's result with a stale one, and an OOM from the StackBlur buffers is not masked as a silent fallback. - Reset the un-persisted surface-alpha preview when the app is backgrounded (Home/recents mid-drag never fires onValueChangeFinished), and re-check the dirty flag after the DataStore read to avoid stomping a newer drag. - Clear the blur composition locals under the opaque OOBE overlay so its cards render opaque instead of showing a frosted backdrop. - Only draw the background dim overlay when a wallpaper painter is present. - Make the master blur switch description match its new master-toggle role.
Address the review comment on the shared rememberAsyncImagePainter: instead of relying on the implicit draw-time viewport sizing, build the painter's request from an ImageRequest explicitly sized to this host's onSizeChanged viewport, so a large user wallpaper is decoded at screen resolution rather than its original dimensions. The request is cached on (uri, viewportSize) so it is rebuilt only when the wallpaper or the viewport changes, and the painter stays shared across the visible image and every blur backdrop (no N+1 decode).
The SUSFS child page was migrated to BlurScreenScaffold but its ExpressiveTopBar did not forward enableBlur, so its top bar stayed opaque while every other migrated page frosted its bar. Add enableBlur = state.blurEnabled for consistency across the UI.
Opening the soft keyboard resizes the window (the bottom bar rides above the IME), which recomposed the wallpaper request and restarted the card-blur loader. Both flashed to an empty/opaque state before reloading: - AppBackgroundHost: size the shared background painter's request to the stable full-screen resolution (resources.displayMetrics) instead of the onSizeChanged viewport, so a transient IME inset change no longer re-decodes the wallpaper (which blanked the background while it reloaded). Full-screen is always >= any window viewport, so the crop stays correct. - rememberBlurredCardBackground: keep the previous frosted backdrop while the replacement decodes + StackBlurs, and only swap it in once ready, instead of nulling it first (which flashed every card to an opaque/transparent surface).
Port ReSukiSU's approach for the software card-blur backdrop: instead of a Composable-remembered bitmap that is re-decoded and re-blurred whenever the viewport changes, keep the pre-blurred wallpaper in a module-level cache and persist it to filesDir. Combined with the existing keep-until-replaced swap, transient viewport changes (e.g. the soft keyboard resizing the window on adjustResize) no longer flash cards opaque or re-run the full decode + StackBlur pass; matching viewports are restored from disk on the next launch / re-entry.
Port ReSukiSU's approach to avoiding the visible blur-loading flash: its card alpha is a synchronous config that only depends on the custom background being enabled, not on the async pre-blurred bitmap being ready. Add a synchronous LocalBlurredCardBackgroundEnabled signal (feature on, bitmap may still be decoding) and use it in blurredCardSurfaceColor and ExpressiveListItem's drawsOwnBlurSurface instead of checking LocalBlurredCardBackground != null. Surfaces are now translucent from the very first frame and only gain the frosted backdrop when it lands, so opening the app no longer flashes cards from opaque to translucent while the wallpaper decode + StackBlur runs. OOBE clears the new signal too.
The soft keyboard opening used to resize the window (adjustResize), which shrank the AppBackgroundHost layer and re-ran the frosted-backdrop blur for every show/hide. Switch to adjustNothing so the window (and backdrop layer) keeps its size and the blur is generated once per real size change: - Manifest: adjustNothing on MainActivity and AbkExtensionManagerActivity. - AppBackgroundHost: wrap app content in Modifier.imePadding() so content is lifted over the keyboard while the wallpaper/backdrop layer stays put. - API 26-29 do not deliver full IME insets to imePadding, so those devices fall back to adjustResize at runtime (Android 10 and below). Compose Dialog windows are unaffected (they keep their own UNSPECIFIED soft-input behavior).
Co-authored-by: xingguangcuican_bot <xingguangcuican821@gmail.com>
The bot's cache fix (f8011d6) left the old delete call in place, so the pre-blurred card cache was still deleted before being read on every launch. Key the cache file by uri.hashCode() so a stale wallpaper's blur is never reused across launches, and only delete the old cache on a real in-process wallpaper switch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ain thread - Mirror the background anchor's size into snapshot state. LayoutCoordinates.size is not observable, so split-screen / freeform / IME resize never re-ran the blur and cards drew a stale viewport stretched across the new size. - Run saveBlurCache (JPEG compress + disk write) on Dispatchers.IO instead of the main thread. - Stop resetting the nested 'render background into blur' pref when the master blur switch is toggled off, so the user's setting is preserved.
… switches - Key the on-disk cache by SHA-256(uri) instead of String.hashCode(), so two distinct background URIs cannot collide into the same file and serve the wrong wallpaper's blur. - Stop deleting the previous URI's cache on wallpaper change; switching back now reuses it. Bound disk usage with a count-based prune (keep 4 newest). - Include the blur version/radius/downsample in the filename so a future algorithm change cannot silently reuse an incompatible cache.
…avior - Revert windowSoftInputMode to adjustResize and drop the now-redundant imePadding(): adjustNothing without edge-to-edge never delivered IME insets, so the keyboard covered content on Android 11+. - Key the card-blur re-blur on width (not height) so a height-only IME resize no longer re-runs the decode + StackBlur pass. - Skip drawing the blurred strip while the cached blur's width is stale (rotation / split-screen), instead of stretching the old-viewport bitmap. - Prune orphaned .tmp cache files and delete corrupt cache files on decode failure.
The width-only key avoided IME reloads but also missed real height growth (freeform taller, fold unfold). Key on both dimensions and skip the expensive pass only while the cached bitmap still covers the new viewport: growth past the cached size re-blurs, shrink-only changes (IME) keep the taller bitmap which still samples the correct region.
xingguangcuicanbot
left a comment
There was a problem hiding this comment.
Automated PR Review
Summary
I did not find any actionable correctness, security, regression, or maintainability issues in the visible origin/dev...HEAD diff. The blur gating, fallback paths, scaffold migration, and preference plumbing are internally consistent. Validation was limited to static review because this checkout does not include a Gradle wrapper and no system gradle binary is available here.
Decision
request_changes
Notes
Codex recommendation: approve
This PR has 2024 changed lines, so policy requires request changes.
Keep only the comments that state non-obvious constraints; drop verbose restatements of the code.
xingguangcuicanbot
left a comment
There was a problem hiding this comment.
Automated PR Review
Summary
I reviewed the checked-out PR against origin/dev...HEAD, including the new blur infrastructure, cache/bitmap lifecycle, API-33 gating, manifest/build changes, and the migrated screen scaffolds. I did not find any distinct actionable issues in the visible diff from a read-only static review. Residual risk remains around runtime-only UI/inset behavior on device classes that cannot be exercised here.
Decision
request_changes
Notes
Codex recommendation: approve
This PR has 1968 changed lines, so policy requires request changes.
Child pages (AppPageBackground) re-decoded the wallpaper with an unsized, placeholder-less AsyncImage, so the opaque surface (black in dark theme) flashed for a frame before the image landed. The blurred-card backdrop also issued a second Coil decode, competing with the main wallpaper decode on cold start. Reuse the shared painter/bitmap everywhere so the wallpaper is decoded once, and decode it as a software bitmap so the StackBlur pass can sample it.
Remove the standalone blur card in theme settings. The blur toggles now live inside the background card and animate in above the opacity slider once a custom background is configured, reusing the original expand/fade animation.
Cold starts re-decoded the custom background through a possibly-cold content provider (slow wallpaper / black flash) and the card blur waited on the full-size wallpaper decode before applying. Port the ReSukiSU approach: - copy the picked image into app-private storage (file:// URI) at pick time and migrate legacy content:// URIs on startup, so every launch decodes a local file (type-checked, size-capped, race-safe temp) - when the blur cache misses, decode a downsampled source directly from the URI instead of waiting on the wallpaper painter - run the first blur load immediately (debounce only resize re-runs) - seed the blur viewport from display metrics and wait on a neutral surfaceContainer with a crossfade instead of near-black surface
Summary
Ports Miuix blur (frosted-glass top / bottom bars) into the Expressive UI and adds an optional custom-background card/tile blur (frosted cards over the wallpaper), including a software fallback for pre-API-33 devices.
What's included
1. Miuix blur for the app chrome (
ui/blur/—BlurBackdrop,BlurHost,BlurState,BlurScreenScaffold,BlurBackgroundPainter)ScaffoldtoBlurScreenScaffold.2. Custom card/tile backdrop blur (
BlurredCardBackdrop.kt)RuntimeShaderdependency).Design decisions (and how prior review concerns are addressed)
tools:overrideLibraryin the manifest): every path into the library is gated onisRuntimeShaderSupported()(API 33+), and the AAR registers noContentProvider/ initializer, so nothing library-side executes on API 26-32. This is documented in the manifest comment.ImageRequest.size(displayMetrics)), and the card-blur decode is constrained toviewport/4(ImageRequest.size(...)) withallowHardware(false)— never a full-resolution decode.Dispatchers.Default; the JPEG cache write runs onDispatchers.IO; intermediate bitmaps are recycled;ContentScale.Cropis applied everywhere.SHA-256(uri)(collision-resistant, nothashCode()), with a version + radius + downsample component so a future algorithm change cannot silently reuse stale data. Entries are count-pruned (keep 4 newest) rather than deleted on switch, so toggling between wallpapers reuses the cache. Corrupt files are deleted on decode failure; orphaned.tmpfiles are pruned on save.LocalBlurBackgroundSize, becauseLayoutCoordinates.sizeis not observable). Re-blur is driven by a coverage check — the cached bitmap is re-blurred only when the viewport grows past it (rotation, split-screen, freeform taller, fold unfold), and skipped for shrink-only changes (IME), where the taller bitmap still samples the correct region. This avoids both stale-stretched backdrops and per-keypress re-blurs.windowSoftInputMode="adjustResize"(matchesdev), so the keyboard never covers content on Android 11+; the coverage check ensures an IME height change does not re-run the blur.LocalBlurredCardBackgroundEnabledis a synchronous signal, so cards stay translucent from the first frame instead of flashing opaque→translucent when the frosted backdrop arrives.Testing
BlurConfigTestunit-tests the blur config → enabled-state mapping.