feat(chat): blur and fade the transcript at the top edge - #1428
Merged
Conversation
The chat bar drew a two-stop fade that was already ~70% transparent where the status bar sits, so a cash bubble scrolling under it showed through behind the clock with a hard cut at the bar's edge. Match iOS, which does this in two parts for a reason it documents: the soft scroll-edge effect blurs what passes under the bar but does not darken it, and a cash card's amount is large white text that still reads over the title once blurred. So the bar now applies a progressive `hazeBlur` (intensity 1 at the status bar, 0 by the tail's end) under a background gradient that holds fully opaque to `statusBars - 12.dp` before fading out — the same hold iOS uses for `TranscriptTopFade`. The blur style carries no tint of its own; the gradient painted over it is the whole of the darkening, and a material's wash on top would double the scrim where the two overlap.
Drop the `hazeBlur` from the top edge and let the gradient carry it. The transcript keeps its `hazeSource` for the bottom bar, so this removes the second blur pass over a scrolling list rather than the dependency. Compose 1.12.0 has no first-party backdrop API — there is no `Backdrop` type in `ui`, `ui-graphics`, `foundation` or `material3` — so the alternative to Haze was hand-rolling the same record-and-replay against `RenderEffect`, which is API 31+ and would fall back to this gradient on minSdk 29 anyway. Without the blur the fade has to do all of the hiding, so its stops are keyed to layout landmarks rather than to fractions of the region: opaque to `statusBars - 12.dp`, still at 0.9 alpha where the bar's own bottom edge is, and clear by the end of a 40dp tail. Keying the middle stop to the measured bar height means the taller selection and editing bars darken over their whole height instead of running out partway down, and spending the whole ramp on the tail keeps its end from reading as a band across whatever bubble it lands on.
The fade-only version had to be strong enough to hide content outright, because a cash card's amount is large white text that stays legible through anything short of an opaque scrim. iOS keeps its fade weak — alpha lands near a third behind the title and is spent before the bar's bottom edge — and gets away with it because `topEdgeEffect.style = .soft` blurs what scrolls underneath. So take iOS's numbers for the fade and supply the blur separately. Compose 1.12 has no backdrop API, so the blur is a `RenderEffect` chain: a blurred copy of the transcript masked to the top of it and composited over the sharp original. The mask is opaque across the bar's measured height and falls off over a 24dp tail, matching how the iOS effect covers its region evenly and softens only at the boundary — a mask that ramps from the very top instead leaves the title's depth barely blurred, which is where the collision was in the first place. `RenderEffect` needs API 31; below that the fade stands alone, which still keeps an amount out of the status bar. The bar reports its measured height up so the transcript can size the blur to it, since selection and editing modes change that height. Scroll cost is nothing measurable: p50 18ms and p90 32ms with the blur against 18ms and 32ms without it, on the same transcript.
iOS's fade is spent just short of the bar's bottom edge, which leaves alpha near a third behind the title. That is enough against ordinary bubbles and not enough against a cash card, whose amount is large white text: it stayed legible over the title. Run the ramp 16dp past the bar instead, which puts roughly half alpha at the title, and take the blur from 20dp to 28dp so the digits stop resolving. The fade is drawn rather than laid out now. As a sibling it would have to be taller than the bar to reach past it, and the scaffold turns the top bar slot's height into the transcript's content padding, so a purely visual tail would have pushed every message down.
bmc08gt
force-pushed
the
feat/chat-top-edge-blur
branch
from
September 8, 2026 17:32
39fe879 to
d67f6ef
Compare
The blurred copy was composited straight over the untouched layer, which does not replace it: the transcript's background is transparent, and so is the spread of a blurred glyph, so the sharp original showed through its own blurred copy. White text on a dark ground came back as a crisp glyph wearing a halo. Mask both halves against complementary gradients instead, so the two cover every pixel once — blurred under the bar, sharp below the tail, a crossfade between. Scale the blurred copy's brightness to 0.6 while there. A blur spreads a pixel's light over its radius, so even without the doubling it hands back more lit area than it was given; taking the light out is a different correction from the fade, which mixes towards the background evenly rather than only where the blur lit up.
Both ramps were straight lines, and a straight line turns a corner at each end — full strength one pixel, already dropping the next. Those corners are what the eye reads as the edges of a band, however long the ramp between them is. Sample a smoothstep instead, so each ramp leaves and arrives flat. Lengthen them too: the blur's falloff goes from 24dp to 64dp and the fade's tail from 16dp to 36dp. This is the distance over which a bubble goes from unreadable to readable and the eye follows it the whole way, so a short one delivers that change of state as an event rather than as a transition. Start the blur easing 16dp above the bar's bottom edge rather than at it. Held to the edge, the falloff begins exactly where a bubble emerges from behind the bar, and the two coincide into a single hard boundary. Combine the two masked copies with PLUS rather than SRC_OVER. They are exact complements, so adding them weights each pixel between sharp and blurred once; compositing one over the other put the lower through its own mask a second time, which dipped the middle of the transition darker.
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.
The chat bar drew a two-stop fade that was already ~70% transparent where the status bar sits, so a cash bubble scrolling under it showed through behind the clock and cut at a hard line at the bar's edge.
The top edge now works the way iOS's does: the transcript passing under the bar is blurred, and a gradient over that takes it to the background colour. Neither half stands alone. iOS's fade is weak — alpha near a third behind the title, clear before the bar's bottom edge — and only reads because
topEdgeEffect.style = .softblurs underneath; without that blur the fade has to be strong enough to hide content outright, and without the fade a cash card's amount stays legible over the title.Compose 1.12 has no backdrop API, so the blur is a
RenderEffectchain: a blurred copy of the transcript and the sharp original, each masked against complementary gradients and added together, so every pixel is weighted between the two exactly once. Masking both halves is the part that matters. Compositing a blurred copy over an untouched original does not replace it — the transcript's background is transparent, and so is the spread of a blurred glyph — so the original showed through its own blurred copy, and white text on a dark ground came back as a crisp glyph wearing a halo.Shape and distance, which took the most iteration:
The fade is drawn, not laid out. Reaching past the bar as a sibling would mean being taller than it, and the scaffold turns the top bar slot's height into the transcript's content padding — a purely visual tail would have pushed every message down.
RenderEffectneeds API 31. Below that the fade stands alone, which still keeps an amount out of the status bar.The bar reports its measured height up so the transcript can size the blur to it, since the selection and editing modes change that height. The transcript keeps its
hazeSource— the bottom bar still blurs through it.Scroll cost is nothing measurable: p50 18ms and p90 32ms with the blur, against 18ms and 32ms without it on the same transcript.