Compare commits

...

2 commits

Author SHA1 Message Date
Bruno Deanoz
70aa5c56da fix ChatInput — restore mic/call toggle, softer send button
Mic (voice recording) stays left next to +. Right side toggles
between AudioLines icon (call, when empty) and send button
(when content). Send button: removed border, translucent gradient,
38dp instead of 44dp.
2026-06-22 19:16:50 +02:00
Bruno Deanoz
313c21d47d perf: reduce sensor rate and pause blob animation during chat
Sensor: SENSOR_DELAY_GAME → SENSOR_DELAY_UI (~15Hz instead of ~60Hz),
parallax effect unchanged. Blob drift animation only runs on empty
state, frozen at midpoint during chat (blobs still visible, no GPU
animation cost).
2026-06-22 19:01:25 +02:00
5 changed files with 47 additions and 23 deletions

View file

@ -41,20 +41,25 @@ import dev.kaizen.app.ui.theme.rememberDitherBrush
/** Mesh/blob background: large, heavily blurred color circles that drift slowly. */ /** Mesh/blob background: large, heavily blurred color circles that drift slowly. */
@Composable @Composable
fun MeshBackground(content: @Composable BoxScope.() -> Unit) { fun MeshBackground(animateBlobs: Boolean = true, content: @Composable BoxScope.() -> Unit) {
val dark = isSystemInDarkTheme() val dark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current val accent = LocalKaizenAccent.current
val base = MaterialTheme.colorScheme.background val base = MaterialTheme.colorScheme.background
// Blobs are subtler in light mode.
val factor = if (dark) 1f else 0.65f val factor = if (dark) 1f else 0.65f
val transition = rememberInfiniteTransition(label = "blobs")
val drift by transition.animateFloat( val drift = if (animateBlobs) {
initialValue = 0f, val transition = rememberInfiniteTransition(label = "blobs")
targetValue = 1f, val d by transition.animateFloat(
animationSpec = infiniteRepeatable(tween(22000, easing = LinearEasing), RepeatMode.Reverse), initialValue = 0f,
label = "drift", targetValue = 1f,
) animationSpec = infiniteRepeatable(tween(22000, easing = LinearEasing), RepeatMode.Reverse),
// Dither overlay (drawn last, over everything) — kills 8-bit gradient banding. label = "drift",
)
d
} else {
0.5f
}
val ditherBrush = rememberDitherBrush() val ditherBrush = rememberDitherBrush()
Box( Box(
Modifier Modifier

View file

@ -575,23 +575,30 @@ fun ChatInput(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Box( Box(
Modifier.size(40.dp).clip(KaizenShapes.circle).background(iconBg).clickable(onClick = onAddClick), Modifier.size(38.dp).clip(KaizenShapes.circle).background(iconBg).clickable(onClick = onAddClick),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Icon(KaizenIcons.Plus, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(20.dp)) Icon(KaizenIcons.Plus, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(19.dp))
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Box( Box(
Modifier.size(40.dp).clip(KaizenShapes.circle).background(iconBg), Modifier.size(38.dp).clip(KaizenShapes.circle).background(iconBg),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Icon(KaizenIcons.Mic, stringResource(R.string.chat_call), tint = cs.onSurfaceVariant, modifier = Modifier.size(20.dp)) Icon(KaizenIcons.Mic, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(19.dp))
} }
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
if (hasContent) { if (hasContent) {
SendButton(enabled = canSend, onClick = onSend) SendButton(enabled = canSend, onClick = onSend)
} else {
Box(
Modifier.size(38.dp).clip(KaizenShapes.circle).background(iconBg),
contentAlignment = Alignment.Center,
) {
Icon(KaizenIcons.AudioLines, stringResource(R.string.chat_call), tint = cs.onSurfaceVariant, modifier = Modifier.size(19.dp))
}
} }
} }
} }
@ -643,20 +650,31 @@ private fun PendingFileChip(pf: PendingFile, onRemove: () -> Unit) {
@Composable @Composable
private fun SendButton(enabled: Boolean, onClick: () -> Unit) { private fun SendButton(enabled: Boolean, onClick: () -> Unit) {
val cs = MaterialTheme.colorScheme val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val interaction = remember { MutableInteractionSource() } val interaction = remember { MutableInteractionSource() }
val pressed by interaction.collectIsPressedAsState() val pressed by interaction.collectIsPressedAsState()
val pressScale by animateFloatAsState(if (pressed) 0.86f else 1f, label = "send") val pressScale by animateFloatAsState(if (pressed) 0.88f else 1f, label = "send")
val accent = LocalKaizenAccent.current val accent = LocalKaizenAccent.current
val fill = if (enabled) Modifier.background(Brush.linearGradient(listOf(accent.primary, accent.primaryDeep)))
else Modifier.background(cs.surface) val fill = if (enabled) {
Modifier.background(
Brush.verticalGradient(
listOf(
accent.primary.copy(alpha = if (isDark) 0.85f else 0.80f),
accent.primaryDeep.copy(alpha = if (isDark) 0.90f else 0.85f),
)
)
)
} else {
Modifier.background(cs.surfaceVariant.copy(alpha = 0.5f))
}
Box( Box(
Modifier.size(44.dp).scale(pressScale).clip(KaizenShapes.circle).then(fill) Modifier.size(38.dp).scale(pressScale).clip(KaizenShapes.circle).then(fill)
.border(1.dp, cs.outline, KaizenShapes.circle)
.clickable(interactionSource = interaction, indication = null, enabled = enabled, onClick = onClick), .clickable(interactionSource = interaction, indication = null, enabled = enabled, onClick = onClick),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
SendArrow(color = if (enabled) accent.onPrimary else cs.onSurfaceVariant) SendArrow(color = if (enabled) accent.onPrimary else cs.onSurfaceVariant.copy(alpha = 0.5f))
} }
} }

View file

@ -603,7 +603,7 @@ fun ChatScreen(
}, },
gesturesEnabled = true // Allows swiping from the screen's left edge to open the drawer gesturesEnabled = true // Allows swiping from the screen's left edge to open the drawer
) { ) {
MeshBackground { MeshBackground(animateBlobs = messages.isEmpty()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
// 1. Scrollable Chat Content // 1. Scrollable Chat Content
// Stretches to the full screen, allowing bubbles to scroll cleanly behind the floating bottom dock. // Stretches to the full screen, allowing bubbles to scroll cleanly behind the floating bottom dock.

View file

@ -31,6 +31,7 @@ object KaizenIcons {
// ── Chat ──────────────────────────────────────────────────────────────── // ── Chat ────────────────────────────────────────────────────────────────
val Mic: ImageVector get() = Lucide.Mic val Mic: ImageVector get() = Lucide.Mic
val Paperclip: ImageVector get() = Lucide.Paperclip val Paperclip: ImageVector get() = Lucide.Paperclip
val AudioLines: ImageVector get() = Lucide.AudioLines
val Plus: ImageVector get() = Lucide.Plus val Plus: ImageVector get() = Lucide.Plus
val Send: ImageVector get() = Lucide.SendHorizontal val Send: ImageVector get() = Lucide.SendHorizontal
val Sparkles: ImageVector get() = Lucide.Sparkles val Sparkles: ImageVector get() = Lucide.Sparkles

View file

@ -115,7 +115,7 @@ fun rememberTiltState(): State<Offset> {
Lifecycle.Event.ON_START -> { Lifecycle.Event.ON_START -> {
if (!registered) { if (!registered) {
sensorManager.registerListener( sensorManager.registerListener(
listener, sensor, SensorManager.SENSOR_DELAY_GAME listener, sensor, SensorManager.SENSOR_DELAY_UI
) )
registered = true registered = true
} }
@ -131,7 +131,7 @@ fun rememberTiltState(): State<Offset> {
lifecycle.addObserver(lifecycleObserver) lifecycle.addObserver(lifecycleObserver)
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_GAME) sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_UI)
registered = true registered = true
} }