Files
ai-app/app/androidApp/src/main/kotlin/com/example/aiapp/Bubble.kt
T
iris-ai 45f249ae91 Even out the composer's row, and put its two pickers in settings
The composer's row gave the model and the permission mode whatever width
their words asked for, after three word-shaped action buttons had taken
theirs. A llama session's model names run long, so the permission mode
was squeezed to a chip too small to tap.

The three actions are circles now -- one diameter, the platform's minimum
touch target -- so they take what their glyph needs and nothing more, and
the two pickers share what is left evenly rather than by the length of
what they say. Every gap on the row is the same.

Both pickers are also rows in the session settings dialog, for every
provider that offers them: the dialog has a line each, so the whole model
name is readable there. Choosing goes through the same two functions as
the composer's pickers, so the model switch warning cannot be skipped by
picking from one of the two places -- and it closes the dialog rather than
stacking a question behind it.

Looked at on the emulator against the sandbox: a llama session with the
27B loaded, and a claude-cli one, both composer and dialog, with the
keyboard up and down.
2026-09-19 14:28:38 -04:00

103 lines
3.6 KiB
Kotlin

package com.example.aiapp
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
// The composer's row of settings and pickers, and the menus they open. One file because the outline
// and the corner are one appearance: a control shaped like this opens a surface shaped like this.
/**
* A bordered pill: a control that can be seen without being pressed.
*
* The composer's row -- attach, model, permission mode -- was text buttons, which draw nothing at
* all until they are touched. Three bare words under the message field read as a caption about the
* field rather than as three things to press. The outline says "control" without the weight of a
* filled button, which is reserved for the two that act on the session.
*/
@Composable
fun BubbleButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
content: @Composable () -> Unit,
) {
OutlinedButton(
onClick = onClick,
enabled = enabled,
shape = BubbleShape,
// A text button's padding rather than a filled button's 24dp: these sit three across under
// the message field, and the wider padding is what decides whether the row fits.
contentPadding = ButtonDefaults.TextButtonContentPadding,
modifier = modifier,
) {
content()
}
}
/** Fully round ends, so the control reads as a bubble rather than as a box. */
val BubbleShape: Shape = RoundedCornerShape(percent = 50)
/**
* The corner on a menu one of these opens.
*
* A radius rather than [BubbleShape]'s half-height: a menu is as tall as its options, and rounding
* ends that tall would bow its sides.
*/
val BubbleMenuShape: Shape = RoundedCornerShape(20.dp)
/**
* A round button sized to the mark it draws.
*
* The composer's three actions -- attach, stop, send -- are single glyphs, and a pill's word-shaped
* padding around one glyph was width taken from the pickers beside it: with a long model name on
* the row, the permission mode ended up too small to hit. One diameter for all three, and it is the
* platform's minimum touch target rather than a button's shorter default height.
*
* [fill] null draws the outlined form, for the one of the three that does not act on the session.
*/
@Composable
fun CircleButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
fill: Color? = null,
enabled: Boolean = true,
content: @Composable () -> Unit,
) {
val sized = modifier.size(CircleButtonSize)
if (fill == null) {
OutlinedButton(
onClick = onClick,
enabled = enabled,
shape = CircleShape,
contentPadding = PaddingValues(0.dp),
modifier = sized,
) {
content()
}
} else {
Button(
onClick = onClick,
enabled = enabled,
shape = CircleShape,
colors = actionButtonColors(fill),
contentPadding = PaddingValues(0.dp),
modifier = sized,
) {
content()
}
}
}
/** How wide and tall one of those is; see [CircleButton]. */
val CircleButtonSize = 48.dp