fix(nests): avatar circular on narrow grid cells

The stage/audience LazyVerticalGrid uses Adaptive(80dp) cells while
the avatar wants 75dp. Wrapping the avatar in an inner Box with
`Modifier.padding(ringPadding=12dp)` reserved room for the speaking
glow + outer ring, but it also reduced the avatar's max **width** to
56dp on a typical 82dp cell while leaving its height at the requested
75dp — Compose grid items are tight on width and free on height.
Result: a 56×75 ellipse instead of a circle.

Drop the inner padding and size the outer drawBehind Box explicitly
to `avatarSize + ringPadding * 2`. AvatarAndBadges still sizes tight
to the picture (so role/hand/mic/reaction badge corner alignment is
unchanged), but no longer inherits a width-only constraint from the
cell — the 75dp picture stays a circle.
This commit is contained in:
Claude
2026-05-11 20:47:23 +00:00
parent e82cd0f6f8
commit c75349feb5
@@ -422,14 +422,18 @@ private fun MemberCell(
label = "speaker-outer-ring-width",
)
// Reserve enough space around the avatar Box to fit the outer ring
// and glow halo. Without this padding the rings clip against the
// and glow halo. Without this margin the rings clip against the
// surrounding Surface / LazyVerticalGrid bounds (most visibly at
// the top edge for the first row, where the stage card's rounded
// corner cuts into the glow). The glow extends up to MAX_GLOW_RADIUS
// past the avatar; the outer ring extends OUTER_RING_GAP +
// OUTER_RING_MAX_WIDTH past it.
// OUTER_RING_MAX_WIDTH past it. Applied as an explicit outer-Box
// size below rather than as inner padding so a width-constrained
// grid cell doesn't squish the avatar horizontally into an oval
// (cells are tight on width but free on height).
val ringPadding =
maxOf(MAX_GLOW_RADIUS.value, (OUTER_RING_GAP + OUTER_RING_MAX_WIDTH).value).dp
val outerBoxSize = avatarSize + ringPadding * 2
val avatarModifier =
Modifier
.border(animatedRingWidth, animatedRingColor, CircleShape)
@@ -472,54 +476,54 @@ private fun MemberCell(
modifier = modifier.fillMaxWidth().padding(vertical = 4.dp),
) {
// Outer Box paints the glow halo + detached outer ring on a
// canvas that's bigger than the avatar by [ringPadding]. The
// inner Box keeps its tight-to-avatar bounds so badge corner
// alignment (TopStart, TopEnd, BottomCenter, BottomEnd) still
// tracks the avatar circle, not the padded outer area.
// canvas that's bigger than the avatar by [ringPadding]. Size
// is set explicitly so the AvatarAndBadges child (which sizes
// tight to the 75dp picture, keeping badge corner alignment
// against the avatar circle rather than the padded area) is
// centered without inheriting a width-only constraint from the
// grid cell — that asymmetry was rendering the avatar as an
// oval on cells narrower than [outerBoxSize].
Box(
modifier =
Modifier.drawBehind {
val avatarRadiusPx = avatarSize.toPx() / 2f
val cx = size.width / 2f
val cy = size.height / 2f
if (animatedGlowAlpha > 0.001f) {
val extra = MAX_GLOW_RADIUS.toPx() * clampedLevel
drawCircle(
color = NEST_SPEAKING_COLOR.copy(alpha = animatedGlowAlpha),
radius = avatarRadiusPx + extra,
center = Offset(cx, cy),
)
}
if (animatedOuterRingAlpha > 0.001f && animatedOuterRingWidth > 0.dp) {
val strokePx = animatedOuterRingWidth.toPx()
val ringRadius = avatarRadiusPx + OUTER_RING_GAP.toPx() + strokePx / 2f
drawCircle(
color = NEST_SPEAKING_COLOR.copy(alpha = animatedOuterRingAlpha),
radius = ringRadius,
center = Offset(cx, cy),
style = Stroke(width = strokePx),
)
}
},
Modifier
.size(outerBoxSize)
.drawBehind {
val avatarRadiusPx = avatarSize.toPx() / 2f
val cx = size.width / 2f
val cy = size.height / 2f
if (animatedGlowAlpha > 0.001f) {
val extra = MAX_GLOW_RADIUS.toPx() * clampedLevel
drawCircle(
color = NEST_SPEAKING_COLOR.copy(alpha = animatedGlowAlpha),
radius = avatarRadiusPx + extra,
center = Offset(cx, cy),
)
}
if (animatedOuterRingAlpha > 0.001f && animatedOuterRingWidth > 0.dp) {
val strokePx = animatedOuterRingWidth.toPx()
val ringRadius = avatarRadiusPx + OUTER_RING_GAP.toPx() + strokePx / 2f
drawCircle(
color = NEST_SPEAKING_COLOR.copy(alpha = animatedOuterRingAlpha),
radius = ringRadius,
center = Offset(cx, cy),
style = Stroke(width = strokePx),
)
}
},
contentAlignment = Alignment.Center,
) {
Box(
modifier = Modifier.padding(ringPadding),
contentAlignment = Alignment.Center,
) {
AvatarAndBadges(
member = member,
avatarSize = avatarSize,
accountViewModel = accountViewModel,
avatarModifier = avatarModifier,
onClick = onClick,
onLongClick = onLongClick,
isConnecting = isConnecting,
showMicBadge = showMicBadge,
isSpeaking = isSpeaking,
reactions = reactions,
)
}
AvatarAndBadges(
member = member,
avatarSize = avatarSize,
accountViewModel = accountViewModel,
avatarModifier = avatarModifier,
onClick = onClick,
onLongClick = onLongClick,
isConnecting = isConnecting,
showMicBadge = showMicBadge,
isSpeaking = isSpeaking,
reactions = reactions,
)
}
UsernameDisplay(
baseUser = user,