fix(widget): scale the agenda widget with its size (#51)
The "Upcoming" agenda widget used Glance's default SizeMode.Single: it was composed once at the minimum size and the launcher stretched that single RemoteViews when enlarged, so the text stayed small-widget-sized no matter how big the widget grew. Reported as a "font size" request (#51), but it's really a missing size-response. Switch to SizeMode.Exact (like MonthWidget) and read LocalSize.current to pick one of four tiers (COMPACT/REGULAR/LARGE/XLARGE), scaling type and row metrics. Exact over Responsive so the ~30-day LazyColumn isn't replicated per tier. Width picks the tier, height can only lower it. Width governs how much of a title fits on a row, so it's what should drive type size; height only decides how many rows are visible, so a tall narrow widget shows more events rather than bigger text. Height does act as a cap, though, or a squashed widget would keep the large type its width earned in a sliver of space. Thresholds are spread over the width range a phone actually produces (measured on a Pixel/Nova: a compact widget is 222dp wide, a large one 378dp) rather than a theoretical range, so the tiers are reachable in practice; XLARGE is reserved for tablets/foldables. COMPACT reproduces the original constants verbatim, so an existing widget is visually unchanged. The tier logic lives in a pure, Glance-free AgendaScale.kt (compose.ui.unit only) and is JVM-tested: the COMPACT baseline, the width buckets, the height cap stepping a squashed widget down, and that height never raises the tier. No new setting: the widget follows the size the launcher/user already chose. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package de.jeanlucmakiola.calendula.widget.agenda
|
||||
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AgendaScaleTest {
|
||||
|
||||
// --- scaleFor: bucketing -------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun `the on-device calibration points map to their tiers`() {
|
||||
// The two sizes measured on-device: the compact widget stays COMPACT (the
|
||||
// baseline, unchanged), the large one steps up to LARGE — not XLARGE,
|
||||
// which read as too big on a phone (#51).
|
||||
assertThat(scaleFor(DpSize(222.dp, 270.dp))).isEqualTo(AgendaScale.COMPACT)
|
||||
assertThat(scaleFor(DpSize(378.dp, 672.dp))).isEqualTo(AgendaScale.LARGE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `width buckets into the four tiers`() {
|
||||
// Tall enough that the height cap never binds, isolating the width rule.
|
||||
val h = 500.dp
|
||||
assertThat(scaleFor(DpSize(180.dp, h))).isEqualTo(AgendaScale.COMPACT)
|
||||
assertThat(scaleFor(DpSize(259.dp, h))).isEqualTo(AgendaScale.COMPACT)
|
||||
assertThat(scaleFor(DpSize(260.dp, h))).isEqualTo(AgendaScale.REGULAR)
|
||||
assertThat(scaleFor(DpSize(329.dp, h))).isEqualTo(AgendaScale.REGULAR)
|
||||
assertThat(scaleFor(DpSize(330.dp, h))).isEqualTo(AgendaScale.LARGE)
|
||||
assertThat(scaleFor(DpSize(419.dp, h))).isEqualTo(AgendaScale.LARGE)
|
||||
assertThat(scaleFor(DpSize(420.dp, h))).isEqualTo(AgendaScale.XLARGE)
|
||||
assertThat(scaleFor(DpSize(900.dp, h))).isEqualTo(AgendaScale.XLARGE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extra height never raises the tier`() {
|
||||
// Height decides how many rows are visible, not how big they are: a tall,
|
||||
// narrow widget wants more events, not bigger text.
|
||||
val short = scaleFor(DpSize(222.dp, 200.dp))
|
||||
val tall = scaleFor(DpSize(222.dp, 900.dp))
|
||||
assertThat(short).isEqualTo(AgendaScale.COMPACT)
|
||||
assertThat(tall).isEqualTo(AgendaScale.COMPACT)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a squashed widget is stepped back down`() {
|
||||
// Same (wide) width, shrinking height: the tier must walk down rather than
|
||||
// keep big type in a sliver of space.
|
||||
val wide = 378.dp
|
||||
assertThat(scaleFor(DpSize(wide, 672.dp))).isEqualTo(AgendaScale.LARGE)
|
||||
assertThat(scaleFor(DpSize(wide, 400.dp))).isEqualTo(AgendaScale.LARGE)
|
||||
assertThat(scaleFor(DpSize(wide, 300.dp))).isEqualTo(AgendaScale.REGULAR)
|
||||
assertThat(scaleFor(DpSize(wide, 200.dp))).isEqualTo(AgendaScale.COMPACT)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the height cap never exceeds what the width earned`() {
|
||||
// A tall but narrow widget stays at its width's tier — the cap only ever
|
||||
// lowers, so a huge height can't promote a 222dp-wide widget.
|
||||
assertThat(scaleFor(DpSize(222.dp, 900.dp))).isEqualTo(AgendaScale.COMPACT)
|
||||
assertThat(scaleFor(DpSize(300.dp, 900.dp))).isEqualTo(AgendaScale.REGULAR)
|
||||
}
|
||||
|
||||
// --- metricsFor: the "default unchanged" regression guard -----------------
|
||||
|
||||
@Test
|
||||
fun `COMPACT metrics equal the widget's original constants`() {
|
||||
// If this fails, a default-sized agenda widget no longer looks as it did.
|
||||
val m = metricsFor(AgendaScale.COMPACT)
|
||||
assertThat(m.title).isEqualTo(16.sp)
|
||||
assertThat(m.dayHeader).isEqualTo(13.sp)
|
||||
assertThat(m.eventTitle).isEqualTo(14.sp)
|
||||
assertThat(m.eventTime).isEqualTo(12.sp)
|
||||
assertThat(m.placeholder).isEqualTo(14.sp)
|
||||
assertThat(m.message).isEqualTo(14.sp)
|
||||
assertThat(m.stripeH).isEqualTo(36.dp)
|
||||
assertThat(m.iconImage).isEqualTo(22.dp)
|
||||
assertThat(m.iconBox).isEqualTo(40.dp)
|
||||
assertThat(m.rowVPad).isEqualTo(4.dp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `type sizes are non-decreasing across the tiers`() {
|
||||
val tiers = listOf(
|
||||
AgendaScale.COMPACT,
|
||||
AgendaScale.REGULAR,
|
||||
AgendaScale.LARGE,
|
||||
AgendaScale.XLARGE,
|
||||
).map(::metricsFor)
|
||||
|
||||
tiers.zipWithNext { small, big ->
|
||||
assertThat(big.title.value).isAtLeast(small.title.value)
|
||||
assertThat(big.dayHeader.value).isAtLeast(small.dayHeader.value)
|
||||
assertThat(big.eventTitle.value).isAtLeast(small.eventTitle.value)
|
||||
assertThat(big.eventTime.value).isAtLeast(small.eventTime.value)
|
||||
assertThat(big.placeholder.value).isAtLeast(small.placeholder.value)
|
||||
assertThat(big.message.value).isAtLeast(small.message.value)
|
||||
assertThat(big.stripeH.value).isAtLeast(small.stripeH.value)
|
||||
assertThat(big.iconImage.value).isAtLeast(small.iconImage.value)
|
||||
assertThat(big.iconBox.value).isAtLeast(small.iconBox.value)
|
||||
assertThat(big.rowVPad.value).isAtLeast(small.rowVPad.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user