Draw widgets at a size you pick, not a measured one (#103, #51) (#104)

Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/104
This commit is contained in:
Jean-Luc Makiola
2026-07-30 19:22:35 +02:00
parent e44d70e18b
commit 02cf32b537
13 changed files with 187 additions and 248 deletions

View File

@@ -23,6 +23,7 @@ import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle
import de.jeanlucmakiola.calendula.ui.theme.FONT_SYSTEM_TOKEN
import de.jeanlucmakiola.calendula.widget.WidgetSize
import java.time.ZoneId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -306,6 +307,23 @@ class SettingsPrefs @Inject constructor(
store.edit { it[AGENDA_WIDGET_RANGE_KEY] = range.storageValue() }
}
/**
* The size step the agenda widget draws its text at (#51). Defaults to
* [WidgetSize.SMALL], which reproduces its original metrics, so an existing
* widget is unchanged until its owner turns the size up.
*
* This replaced deriving a size tier from the widget's measured size, which
* the launcher does not report reliably. The month widget takes no size
* setting — it divides the width it is given by seven (#103).
*/
val widgetSize: Flow<WidgetSize> = store.data.map { prefs ->
prefs[WIDGET_SIZE_KEY].toEnum(WidgetSize.SMALL)
}
suspend fun setWidgetSize(size: WidgetSize) {
store.edit { it[WIDGET_SIZE_KEY] = size.name }
}
/**
* Whether the agenda shows its top range bar — the "showing …" header and
* the session range switcher (v2.11). Default ON.
@@ -781,6 +799,7 @@ class SettingsPrefs @Inject constructor(
internal val AGENDA_SCREEN_RANGE_KEY = stringPreferencesKey("agenda_screen_range")
internal val AGENDA_WIDGET_RANGE_KEY = stringPreferencesKey("agenda_widget_range")
internal val AGENDA_SHOW_RANGE_BAR_KEY = booleanPreferencesKey("agenda_show_range_bar")
internal val WIDGET_SIZE_KEY = stringPreferencesKey("widget_size")
internal val AGENDA_SHOW_TODAY_KEY =
booleanPreferencesKey("agenda_show_today")
internal val TIME_FORMAT_KEY = stringPreferencesKey("time_format")

View File

@@ -117,6 +117,7 @@ import de.jeanlucmakiola.calendula.domain.FontRole
import de.jeanlucmakiola.floret.identity.collapseExit
import de.jeanlucmakiola.floret.identity.expandEnter
import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker
import de.jeanlucmakiola.calendula.ui.common.PickerDescription
import de.jeanlucmakiola.floret.components.FullScreenPicker
import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel
import de.jeanlucmakiola.calendula.ui.common.CalendarView
@@ -150,6 +151,7 @@ import de.jeanlucmakiola.calendula.ui.theme.BundledFont
import de.jeanlucmakiola.calendula.ui.theme.FONT_CUSTOM_TOKEN
import de.jeanlucmakiola.calendula.ui.theme.FONT_SYSTEM_TOKEN
import de.jeanlucmakiola.calendula.ui.theme.resolveFontFamily
import de.jeanlucmakiola.calendula.widget.WidgetSize
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalTime
import java.time.format.TextStyle as JavaTextStyle
@@ -499,6 +501,7 @@ private fun AppearanceScreen(
var showDefaultView by remember { mutableStateOf(false) }
var showAgendaScreenRange by remember { mutableStateOf(false) }
var showAgendaWidgetRange by remember { mutableStateOf(false) }
var showWidgetSize by remember { mutableStateOf(false) }
var showPastEvents by remember { mutableStateOf(false) }
var showBrandFont by remember { mutableStateOf(false) }
var showPlainFont by remember { mutableStateOf(false) }
@@ -684,6 +687,12 @@ private fun AppearanceScreen(
},
onClick = { viewModel.setAgendaShowToday(!state.agendaShowToday) },
)
GroupedRow(
title = stringResource(R.string.settings_widget_size),
summary = widgetSizeLabel(state.widgetSize),
position = Position.Middle,
onClick = { showWidgetSize = true },
)
GroupedRow(
title = stringResource(R.string.settings_agenda_range_bar),
summary = stringResource(R.string.settings_agenda_range_bar_hint),
@@ -810,6 +819,18 @@ private fun AppearanceScreen(
onDismiss = { showAgendaWidgetRange = false },
)
}
if (showWidgetSize) {
OptionPicker(
title = stringResource(R.string.settings_widget_size),
header = { PickerDescription(stringResource(R.string.settings_widget_size_hint)) },
predictiveBack = true,
options = WidgetSize.entries,
selected = state.widgetSize,
label = { widgetSizeLabel(it) },
onSelect = viewModel::setWidgetSize,
onDismiss = { showWidgetSize = false },
)
}
if (showTimeFormat) {
OptionPicker(
title = stringResource(R.string.settings_time_format),
@@ -2072,6 +2093,16 @@ private fun pastEventDisplayLabel(mode: PastEventDisplay): String = stringResour
},
)
@Composable
private fun widgetSizeLabel(size: WidgetSize): String = stringResource(
when (size) {
WidgetSize.SMALL -> R.string.settings_widget_size_small
WidgetSize.MEDIUM -> R.string.settings_widget_size_medium
WidgetSize.LARGE -> R.string.settings_widget_size_large
WidgetSize.EXTRA_LARGE -> R.string.settings_widget_size_extra_large
},
)
@Composable
private fun languageLabel(tag: String?): String =
if (tag == null) stringResource(R.string.settings_language_auto) else AppLanguage.displayName(tag)

View File

@@ -14,6 +14,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle
import de.jeanlucmakiola.calendula.widget.WidgetSize
/**
* Settings screen state (M4). Persisted preferences are instant to read, so
@@ -48,6 +49,8 @@ data class SettingsUiState(
val agendaShowToday: Boolean = true,
/** Whether the agenda shows its top range bar — header + switcher (v2.11). */
val agendaShowRangeBar: Boolean = true,
/** The size step the agenda widget draws its text at (#51). */
val widgetSize: WidgetSize = WidgetSize.SMALL,
/** The calendar view the app opens on, and the home of the view back stack (M1). */
val defaultView: CalendarView = CalendarView.Week,
/** Which views the top-bar quick-switch button cycles through, and their order (#24). */

View File

@@ -39,9 +39,11 @@ import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle
import de.jeanlucmakiola.calendula.ui.theme.AppFontSettings
import de.jeanlucmakiola.calendula.ui.theme.FONT_CUSTOM_TOKEN
import de.jeanlucmakiola.calendula.widget.WidgetSize
import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_PAST_DISPLAY_KEY
import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_RANGE_KEY
import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_SHOW_TODAY_STATE_KEY
import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_SIZE_KEY
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget
import de.jeanlucmakiola.calendula.widget.month.MonthWidget
import kotlinx.coroutines.CoroutineDispatcher
@@ -162,8 +164,9 @@ class SettingsViewModel @Inject constructor(
prefs.quickSwitchConfig,
prefs.drawerViewOrder,
prefs.monthViewStyle,
) { quickSwitch, drawer, monthStyle ->
ViewCustomization(quickSwitch, drawer, monthStyle)
prefs.widgetSize,
) { quickSwitch, drawer, monthStyle, widgetSize ->
ViewCustomization(quickSwitch, drawer, monthStyle, widgetSize)
},
) { showRangeBar, autofocus, pastEvents, dimCompleted, viewCustomization ->
MiscSettings(showRangeBar, autofocus, pastEvents, dimCompleted, viewCustomization)
@@ -186,6 +189,7 @@ class SettingsViewModel @Inject constructor(
quickSwitchConfig = misc.viewCustomization.quickSwitch,
drawerViewOrder = misc.viewCustomization.drawerOrder,
monthViewStyle = misc.viewCustomization.monthViewStyle,
widgetSize = misc.viewCustomization.widgetSize,
allowColorOnUnsupportedCalendars = defaults.allowColor,
defaultReminderMinutes = defaults.defaultReminder,
defaultAllDayReminderMinutes = defaults.allDayReminder,
@@ -300,6 +304,7 @@ class SettingsViewModel @Inject constructor(
val quickSwitch: QuickSwitchConfig,
val drawerOrder: List<CalendarView>,
val monthViewStyle: MonthViewStyle,
val widgetSize: WidgetSize,
)
/** Contact special-dates sub-page (issue #15); its own flow — the main state is full. */
@@ -478,6 +483,28 @@ class SettingsViewModel @Inject constructor(
viewModelScope.launch { prefs.setAgendaShowRangeBar(enabled) }
}
/**
* Set the size the agenda widget draws its text at (#51). Pushed into every
* instance's Glance state and recomposed — the same reliable-update path as
* [setAgendaWidgetRange], since `updateAll` alone won't re-run the data
* preamble a live session already ran.
*
* Agenda-only on purpose: the month widget sizes itself from the space it is
* given and always has, so it takes no size setting (#103).
*/
fun setWidgetSize(size: WidgetSize) {
viewModelScope.launch {
prefs.setWidgetSize(size)
widgetRefreshMutex.withLock {
val manager = GlanceAppWidgetManager(appContext)
manager.getGlanceIds(AgendaWidget::class.java).forEach { id ->
updateAppWidgetState(appContext, id) { it[AGENDA_SIZE_KEY] = size.name }
}
AgendaWidget().updateAll(appContext)
}
}
}
fun setAgendaShowToday(enabled: Boolean) {
viewModelScope.launch {
prefs.setAgendaShowToday(enabled)

View File

@@ -74,6 +74,8 @@ sealed interface AgendaWidgetData {
* from Glance state before an instance has its own state set.
*/
val savedShowToday: Boolean,
/** Saved widget size step (#103) — the fallback before Glance state is set. */
val savedWidgetSize: WidgetSize,
/** Snapshot instant the data was read at, for "has this event ended?" tests. */
val now: Instant,
) : AgendaWidgetData
@@ -145,6 +147,7 @@ internal suspend fun Context.loadAgendaWidgetData(): AgendaWidgetData {
savedRange = savedRange,
savedPastDisplay = savedPastDisplay,
savedShowToday = showToday,
savedWidgetSize = prefs.widgetSize.first(),
now = Clock.System.now(),
)
}

View File

@@ -1,70 +0,0 @@
package de.jeanlucmakiola.calendula.widget
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
/**
* Size tiers a widget scales its typography and metrics across (#51).
*
* Shared by every Glance widget so the bucketing rule can't drift between them:
* each widget keeps its own metrics table, but they all agree on *when* a widget
* counts as compact, regular, large or extra-large. Both widgets already declare
* [androidx.glance.appwidget.SizeMode.Exact], so the composition sees the live
* size via `LocalSize.current` and passes it to [scaleFor].
*
* Kept in a pure, Glance-free file (only `compose.ui.unit`) so the bucketing is
* covered by plain JVM tests.
*/
internal enum class WidgetScale { COMPACT, REGULAR, LARGE, XLARGE }
/**
* Chrome a widget spends before its first content row: outer vertical padding
* plus a header row and its spacer. Subtracted from the raw height so the height
* thresholds below talk about *usable* space rather than gross widget height.
*/
private val CHROME_HEIGHT = 60.dp
/**
* Buckets a live widget size into a [WidgetScale] by **width**.
*
* Width is the right axis: it 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 — a
* tall, narrow widget wants *more events*, not bigger text — so it never raises
* the tier. It does act as a **cap**, though: a genuinely squashed widget is
* stepped back down so it can't keep oversized type in a sliver of space.
*
* The width thresholds are spread across the range a phone can actually produce
* (~180dp up to roughly the screen width) rather than over a theoretical range,
* so the tiers are reachable in practice. Calibrated on-device (Pixel / Nova): a
* compact 222dp-wide widget stays COMPACT (the app's baseline, unchanged) and a
* full-width 378dp one reaches LARGE. XLARGE is reserved for genuinely wide
* surfaces — tablets, foldables, landscape — where the extra size reads well.
*
* The height cap is deliberately generous: it exists to catch a widget squashed
* to one or two rows, **not** to gate ordinary placements. A full-width widget at
* the usual three cells tall (~270dp) must still reach the tier its width earned
* — that is exactly the resize #51 reports, and an aggressive cap would make the
* whole feature a no-op for it.
*/
internal fun scaleFor(size: DpSize): WidgetScale {
val byWidth = when {
size.width < 260.dp -> WidgetScale.COMPACT
size.width < 330.dp -> WidgetScale.REGULAR
size.width < 420.dp -> WidgetScale.LARGE
else -> WidgetScale.XLARGE
}
// Height can only ever pull the tier *down*, never push it up: a squashed
// widget would otherwise keep the big type its width earned and look absurd
// in the little space left. Keeping this a cap (rather than a second scaling
// axis) is what preserves "tall and narrow shows more events, not bigger
// text". Thresholds are usable height — roughly one, two and three rows of
// breathing room once the header is paid for.
val usable = size.height - CHROME_HEIGHT
val heightCap = when {
usable < 70.dp -> WidgetScale.COMPACT
usable < 130.dp -> WidgetScale.REGULAR
usable < 200.dp -> WidgetScale.LARGE
else -> WidgetScale.XLARGE
}
return minOf(byWidth, heightCap)
}

View File

@@ -0,0 +1,20 @@
package de.jeanlucmakiola.calendula.widget
/**
* The size step the **agenda** widget draws its text and rows at — a user
* setting, not something derived from the widget's measured size (#51).
*
* The agenda widget used to bucket its live size (`SizeMode.Exact` +
* `LocalSize.current`) into a tier and scale from that. The size a launcher
* reports is not the size the widget is drawn into, so the tier it picked could
* disagree with what was on screen; a size the user sets is predictable and is
* what the #51 thread actually asked for.
*
* The month widget deliberately has no size setting: its grid divides whatever
* width it is given by seven and always has, which is the behaviour to keep
* (#103).
*
* [SMALL] is the default and reproduces the widget's original constants, so an
* existing widget looks as it did until its owner turns the size up.
*/
enum class WidgetSize { SMALL, MEDIUM, LARGE, EXTRA_LARGE }

View File

@@ -4,12 +4,12 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import de.jeanlucmakiola.calendula.widget.WidgetScale
import de.jeanlucmakiola.calendula.widget.WidgetSize
/**
* Horizontal layout constants for an agenda event row. These don't scale with the
* tier — a wider stripe or gap would eat title width, which is the thing the row
* is short of — but [TEXT_INDENT] is *derived* from them so the day header and
* size step — a wider stripe or gap would eat title width, which is the thing the
* row is short of — but [TEXT_INDENT] is *derived* from them so the day header and
* the "nothing left today" line can never drift out of alignment with the event
* title column the way a hardcoded 19dp could.
*/
@@ -21,23 +21,9 @@ internal val STRIPE_GAP = 10.dp
internal val TEXT_INDENT = ROW_H_PAD + STRIPE_WIDTH + STRIPE_GAP
/**
* The width band a *default* agenda placement can land in, per
* `app/src/main/res/xml/appwidget_info_agenda.xml` (`android:minWidth="180dp"`,
* `android:targetCellWidth="3"`). Measured at 222dp on a Pixel running Nova, but
* launcher cell grids vary, so the whole band — not one measured point — has to
* stay [WidgetScale.COMPACT] for the "default size is unchanged" promise of #51
* to hold. A test pins that.
*
* If the provider's `targetCellWidth` ever changes, this band and the first
* width threshold in [de.jeanlucmakiola.calendula.widget.scaleFor] must be
* revisited together.
*/
internal val AGENDA_DEFAULT_WIDTH_BAND = 180.dp..255.dp
/**
* Every size the agenda widget varies by tier. Values that genuinely shouldn't
* grow (the horizontal row constants above, corner radii) stay constants rather
* than routing through here.
* Every size the agenda widget varies by [WidgetSize]. Values that genuinely
* shouldn't grow (the horizontal row constants above, corner radii) stay
* constants rather than routing through here.
*/
internal data class AgendaMetrics(
val title: TextUnit, // header "Upcoming"
@@ -59,7 +45,7 @@ internal data class AgendaMetrics(
* they scale with the accessibility font setting and the stripe does not —
* at "Largest" the text outgrows the stripe and it visibly under-runs the row
* it is supposed to mark. Multiplying by the same factor keeps them locked,
* and at the default scale of 1.0 reproduces the tier's value exactly.
* and at the default scale of 1.0 reproduces the step's value exactly.
*/
fun scaledForFont(fontScale: Float): AgendaMetrics =
if (fontScale == 1f) this else copy(stripeH = stripeH * fontScale)
@@ -72,18 +58,18 @@ internal data class AgendaMetrics(
*
* Two documented deviations:
*
* ‡ COMPACT's 13sp day header is off-scale. It is held there deliberately —
* COMPACT reproduces the widget's original constants verbatim so a
* default-sized widget looks exactly as it did (#51), and snapping it to
* Title Small (14sp) would break that promise for a 1sp gain.
* ‡ SMALL's 13sp day header is off-scale. It is held there deliberately —
* SMALL reproduces the widget's original constants verbatim so a widget whose
* owner never touches the size setting looks exactly as it did (#51), and
* snapping it to Title Small (14sp) would break that promise for a 1sp gain.
*
* † Above Title Medium the M3 scale jumps 16 → 22 → 24 with nothing in between,
* which is far too coarse for four widget tiers. Where a role would force a
* ≥1.4x step between adjacent tiers we hold an interpolated value instead and
* which is far too coarse for four size steps. Where a role would force a
* ≥1.4x step between adjacent steps we hold an interpolated value instead and
* mark it. The endpoints stay on real roles.
*/
private val COMPACT_METRICS = AgendaMetrics(
private val SMALL_METRICS = AgendaMetrics(
title = 16.sp, // M3 Title Medium
dayHeader = 13.sp, // ‡ off-scale, holds the #51 baseline
eventTitle = 14.sp, // M3 Body Medium
@@ -97,7 +83,7 @@ private val COMPACT_METRICS = AgendaMetrics(
dayHeaderTopPad = 10.dp,
)
private val REGULAR_METRICS = AgendaMetrics(
private val MEDIUM_METRICS = AgendaMetrics(
title = 18.sp, // †
dayHeader = 14.sp, // M3 Title Small
eventTitle = 16.sp, // M3 Body Large
@@ -125,7 +111,7 @@ private val LARGE_METRICS = AgendaMetrics(
dayHeaderTopPad = 12.dp,
)
private val XLARGE_METRICS = AgendaMetrics(
private val EXTRA_LARGE_METRICS = AgendaMetrics(
title = 22.sp, // M3 Title Large
dayHeader = 18.sp, // †
eventTitle = 20.sp, // †
@@ -139,12 +125,12 @@ private val XLARGE_METRICS = AgendaMetrics(
dayHeaderTopPad = 14.dp,
)
/** Indexed by [WidgetScale.ordinal] so lookup allocates nothing per recomposition. */
/** Indexed by [WidgetSize.ordinal] so lookup allocates nothing per recomposition. */
private val AGENDA_METRICS = listOf(
COMPACT_METRICS,
REGULAR_METRICS,
SMALL_METRICS,
MEDIUM_METRICS,
LARGE_METRICS,
XLARGE_METRICS,
EXTRA_LARGE_METRICS,
)
internal fun metricsFor(scale: WidgetScale): AgendaMetrics = AGENDA_METRICS[scale.ordinal]
internal fun metricsFor(size: WidgetSize): AgendaMetrics = AGENDA_METRICS[size.ordinal]

View File

@@ -13,7 +13,6 @@ import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.LocalSize
import androidx.glance.action.ActionParameters
import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget
@@ -62,7 +61,7 @@ import de.jeanlucmakiola.floret.locale.localizedDateFormatter
import de.jeanlucmakiola.calendula.ui.common.eventFill
import de.jeanlucmakiola.calendula.widget.AgendaWidgetData
import de.jeanlucmakiola.calendula.widget.CalendulaGlanceTheme
import de.jeanlucmakiola.calendula.widget.scaleFor
import de.jeanlucmakiola.calendula.widget.WidgetSize
import de.jeanlucmakiola.calendula.widget.loadAgendaWidgetData
import de.jeanlucmakiola.calendula.widget.systemZone
import de.jeanlucmakiola.calendula.widget.today
@@ -105,22 +104,27 @@ internal val AGENDA_PAST_DISPLAY_KEY = stringPreferencesKey("agenda_past_display
*/
internal val AGENDA_SHOW_TODAY_STATE_KEY = booleanPreferencesKey("agenda_show_today")
/**
* Per-instance Glance state key holding the chosen [WidgetSize]. Read reactively
* in the composition for the same reason as [AGENDA_RANGE_KEY] — so changing the
* setting reflects on the live widget without depending on the `provideGlance`
* preamble re-running.
*/
internal val AGENDA_SIZE_KEY = stringPreferencesKey("widget_size")
class AgendaWidget : GlanceAppWidget() {
override val stateDefinition = PreferencesGlanceStateDefinition
// Exact so the composition sees the widget's live size and can scale type/rows
// from it ([scaleFor]/[metricsFor]); at the default size that resolves to
// COMPACT, i.e. the layout is unchanged (#51). MonthWidget already does the
// same.
//
// Note Exact still asks Glance for one RemoteViews per host size (typically
// portrait + landscape) where the old SizeMode.Single produced exactly one —
// so the serialized payload roughly doubles. That is why the row list below is
// capped: an uncapped agenda (the range goes up to AgendaRange.MAX_CUSTOM_DAYS
// = 365) could otherwise push the RemoteViews past the binder transaction
// limit and the host would just show "Problem loading widget".
override val sizeMode = SizeMode.Exact
// Single: type and row metrics come from the user's chosen WidgetSize, not
// from the widget's measured size (#103, #51), so there is nothing to gain
// from Glance building one RemoteViews per host size bucket — and plenty to
// lose, since that roughly doubles the serialized payload. The row list is
// still capped below: an uncapped agenda (the range goes up to
// AgendaRange.MAX_CUSTOM_DAYS = 365) could push the RemoteViews past the
// binder transaction limit and the host would just show "Problem loading
// widget".
override val sizeMode = SizeMode.Single
override suspend fun provideGlance(context: Context, id: GlanceId) {
val data = context.loadAgendaWidgetData()
@@ -160,12 +164,18 @@ private sealed interface AgendaRow {
@Composable
private fun AgendaWidgetBody(data: AgendaWidgetData, dark: Boolean) {
// Type and row metrics scale with the widget's live size (SizeMode.Exact); a
// short/compact widget resolves to COMPACT, leaving the layout unchanged (#51).
// Type and row metrics come from the user's chosen size step, read reactively
// from per-instance Glance state and falling back to the saved pref for a
// freshly placed widget (#103, #51). The permission screen has no loaded prefs
// to fall back to, so it takes the default.
// The stripe is then re-resolved against the system font scale so it tracks the
// sp-sized text beside it instead of drifting at large accessibility settings.
val savedSize = (data as? AgendaWidgetData.Ready)?.savedWidgetSize ?: WidgetSize.SMALL
val size = currentState(AGENDA_SIZE_KEY)
?.let { stored -> WidgetSize.entries.firstOrNull { it.name == stored } }
?: savedSize
val fontScale = androidx.glance.LocalContext.current.resources.configuration.fontScale
val metrics = metricsFor(scaleFor(LocalSize.current)).scaledForFont(fontScale)
val metrics = metricsFor(size).scaledForFont(fontScale)
Column(
modifier = GlanceModifier
.fillMaxSize()

View File

@@ -354,6 +354,13 @@
<string name="settings_agenda_range_hint">How far ahead the Agenda screen lists events.</string>
<string name="settings_agenda_widget_range">Agenda widget range</string>
<string name="settings_agenda_widget_range_hint">How far ahead the agenda home-screen widget lists events.</string>
<string name="settings_widget_size">Agenda widget size</string>
<string name="settings_widget_size_hint">How large the agenda home-screen widget draws its text. The month widget has no setting — its grid always fits itself to the space you give it.</string>
<string name="settings_widget_size_small">Small</string>
<string name="settings_widget_size_medium">Medium</string>
<string name="settings_widget_size_large">Large</string>
<string name="settings_widget_size_extra_large">Extra large</string>
<string name="settings_agenda_show_today">Always show today</string>
<string name="settings_agenda_show_today_hint">Keep today at the top of the agenda and its widget, even once nothing is left today.</string>
<string name="settings_agenda_range_bar">Range bar</string>