diff --git a/CHANGELOG.md b/CHANGELOG.md index 45721da..e87c126 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,11 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The calendar picker in the event form and in the .ics import screen now ends with a **"Missing a calendar?"** row that opens Settings → Calendars, where those marks then explain why a calendar isn't offered ([#76]). -- Both home-screen widgets have a size you set yourself. **Settings → Widgets → - Widget size** offers Small, Medium, Large and Extra large: it sets how big the - agenda widget's text is, and how big the month widget's day columns and text - are. Small is what the widgets look like today, so nothing changes until you - turn it up ([#51], [#103]). +- The agenda widget's text size is yours to set. **Settings → Agenda → Agenda + widget size** offers Small, Medium, Large and Extra large, replacing the guess + the widget used to make from its own measurements. Small is what it looks like + today, so nothing changes until you turn it up ([#51]). ### Changed - Calendula's source code now lives on **Codeberg**, where its issues already @@ -42,19 +41,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 unaffected. ### Fixed -- The month widget shows all seven days again. Unless it happened to be resized - to just the right shape, it drew only about four columns and cut the last one - off part-way through — the rest of the week was simply missing. The widget was - sizing its columns from the width the launcher reported, which is not the width - it is actually drawn into, so the columns came out too wide to fit. Nothing in - either widget is measured any more: they draw at the size you pick under - Settings → Widgets. The month widget can no longer be resized narrower than its - grid needs, either ([#103], [#51]). -- A month-grid widget stays a month grid. Since 2.16.0 a placed month widget - could redraw itself as the agenda widget a little after any change to your - events, because the release build merged the two widgets into a single class - and Android could no longer tell which of them a widget on your home screen - was ([#89]). +- A month-grid widget stays a month grid, and draws all seven days again. Since + 2.16.0 a placed month widget could redraw itself as the agenda widget a little + after any change to your events, and could draw only about four day columns + with the last one cut off part-way through. Both came from the release build + merging the two widgets into a single class, so Android could no longer tell + which of them a widget on your home screen was — and the month grid was handed + the wrong widget's measurements to lay its columns out against ([#89], [#103]). - The back gesture on **Settings → Views** returns to Settings instead of leaving Settings altogether and dropping you on the calendar. Special dates did the same ([#81]). diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt index 257e613..52a2518 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt @@ -308,13 +308,13 @@ class SettingsPrefs @Inject constructor( } /** - * The size step both home-screen widgets draw themselves at (#103, #51). - * Defaults to [WidgetSize.SMALL], which reproduces their original metrics, so - * an existing widget is unchanged until its owner turns the size up. + * 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: the - * width a launcher reports is not the width the widget is drawn into, and the - * month grid sized its seven columns from it and lost the last three. + * 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 = store.data.map { prefs -> prefs[WIDGET_SIZE_KEY].toEnum(WidgetSize.SMALL) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index 2eb36da..2ef5434 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -687,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), @@ -702,18 +708,6 @@ private fun AppearanceScreen( Spacer(Modifier.height(16.dp)) - // Widgets — the size step applies to both home-screen widgets, so it sits - // in its own group rather than under Agenda. - SectionHeader(stringResource(R.string.settings_widgets_header)) - GroupedRow( - title = stringResource(R.string.settings_widget_size), - summary = widgetSizeLabel(state.widgetSize), - position = Position.Alone, - onClick = { showWidgetSize = true }, - ) - - Spacer(Modifier.height(16.dp)) - // App name — chooses the launcher label between "Calendula" and "Calendar" // (issue #44). Own group: it's a launcher/system concern, not calendar // formatting. A sub-page chooser (not a switch), matching the app's other diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt index 557457b..5c5c19e 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt @@ -49,7 +49,7 @@ 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 both home-screen widgets draw themselves at (#103, #51). */ + /** 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, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index 825a7e0..fddee1d 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -45,7 +45,6 @@ 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.MONTH_SIZE_KEY import de.jeanlucmakiola.calendula.widget.month.MonthWidget import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -485,10 +484,13 @@ class SettingsViewModel @Inject constructor( } /** - * Set the size step both widgets draw at (#103, #51). Pushed into every + * 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 { @@ -498,11 +500,7 @@ class SettingsViewModel @Inject constructor( manager.getGlanceIds(AgendaWidget::class.java).forEach { id -> updateAppWidgetState(appContext, id) { it[AGENDA_SIZE_KEY] = size.name } } - manager.getGlanceIds(MonthWidget::class.java).forEach { id -> - updateAppWidgetState(appContext, id) { it[MONTH_SIZE_KEY] = size.name } - } AgendaWidget().updateAll(appContext) - MonthWidget().updateAll(appContext) } } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetSize.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetSize.kt index 730de5b..f03b0ab 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetSize.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetSize.kt @@ -1,18 +1,20 @@ package de.jeanlucmakiola.calendula.widget /** - * The size step a widget draws itself at — a **user setting**, not something - * derived from the widget's measured size (#103, #51). + * 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). * - * Both Glance widgets used to bucket their live size (`SizeMode.Exact` + + * 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 width the widget is actually drawn into, which broke the - * month grid outright: its seven columns were sized from that number, came out - * too wide, and only the first four fitted. Nothing here reads a measured size - * any more — the user picks a step, every metric follows from it, and both - * widgets declare `SizeMode.Single`. + * 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. * - * [SMALL] is the default and reproduces the widgets' original constants, so an + * 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 } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthScale.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthScale.kt deleted file mode 100644 index a22346d..0000000 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthScale.kt +++ /dev/null @@ -1,114 +0,0 @@ -package de.jeanlucmakiola.calendula.widget.month - -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.WidgetSize - -/** Event rows (lanes) shown per week before the rest collapse into "+N". */ -internal const val MAX_LANES = 3 - -/** - * Every size the month widget draws from, resolved from the user's chosen - * [WidgetSize]. - * - * [columnWidth] is the load-bearing one. The grid lays its seven day columns out - * at this fixed width and centres the result in whatever space the host gives it, - * rather than dividing a launcher-reported width by seven (#103) — a reported - * width that overshoots the real one used to push the last columns off the edge. - * A fixed column also keeps a multi-day event a single connected bar - * `columnWidth * n` wide, which is why the columns can't simply be weighted. - */ -internal data class MonthMetrics( - val columnWidth: Dp, // one day column - val gridPadding: Dp, // horizontal padding either side of the grid - val laneHeight: Dp, // one event-bar row - val dayNumberHeight: Dp, // day-number row, and the today circle's diameter - val headerTitle: TextUnit, - val weekday: TextUnit, // narrow weekday initials - val dayNumber: TextUnit, - val eventTitle: TextUnit, // title inside an event bar - val overflow: TextUnit, // the "+N" line - val iconImage: Dp, // header arrow/today glyph - val iconBox: Dp, // header action touch target -) { - /** Width the seven columns plus their padding occupy — what a placement must fit. */ - val gridWidth: Dp get() = columnWidth * 7 + gridPadding * 2 -} - -/* - * SMALL reproduces the widget's original constants so an existing widget is - * unchanged until its owner turns the size up — except [columnWidth], which had - * no fixed value before (it was derived from the reported width, at roughly 33dp - * on a default 4-cell placement). 30dp sits just under that so the smallest step - * still fits the narrowest placement the provider allows; see - * `appwidget_info_month.xml`'s minResizeWidth, which is pinned against - * SMALL.gridWidth by a test. - */ - -private val SMALL_METRICS = MonthMetrics( - columnWidth = 30.dp, - gridPadding = 4.dp, - laneHeight = 14.dp, - dayNumberHeight = 18.dp, - headerTitle = 15.sp, - weekday = 11.sp, - dayNumber = 11.sp, - eventTitle = 9.sp, - overflow = 9.sp, - iconImage = 20.dp, - iconBox = 40.dp, -) - -private val MEDIUM_METRICS = MonthMetrics( - columnWidth = 38.dp, - gridPadding = 6.dp, - laneHeight = 16.dp, - dayNumberHeight = 22.dp, - headerTitle = 17.sp, - weekday = 12.sp, - dayNumber = 13.sp, - eventTitle = 10.sp, - overflow = 10.sp, - iconImage = 22.dp, - iconBox = 44.dp, -) - -private val LARGE_METRICS = MonthMetrics( - columnWidth = 46.dp, - gridPadding = 8.dp, - laneHeight = 19.dp, - dayNumberHeight = 26.dp, - headerTitle = 19.sp, - weekday = 13.sp, - dayNumber = 15.sp, - eventTitle = 11.sp, - overflow = 11.sp, - iconImage = 24.dp, - iconBox = 48.dp, -) - -private val EXTRA_LARGE_METRICS = MonthMetrics( - columnWidth = 54.dp, - gridPadding = 8.dp, - laneHeight = 22.dp, - dayNumberHeight = 30.dp, - headerTitle = 22.sp, - weekday = 14.sp, - dayNumber = 17.sp, - eventTitle = 12.sp, - overflow = 12.sp, - iconImage = 26.dp, - iconBox = 52.dp, -) - -/** Indexed by [WidgetSize.ordinal] so lookup allocates nothing per recomposition. */ -private val MONTH_METRICS = listOf( - SMALL_METRICS, - MEDIUM_METRICS, - LARGE_METRICS, - EXTRA_LARGE_METRICS, -) - -internal fun monthMetricsFor(size: WidgetSize): MonthMetrics = MONTH_METRICS[size.ordinal] diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt index 6550389..24f19a3 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey import androidx.glance.ColorFilter import androidx.glance.GlanceId import androidx.glance.GlanceModifier @@ -14,6 +13,7 @@ import androidx.glance.GlanceTheme import androidx.glance.Image import androidx.glance.ImageProvider import androidx.glance.LocalContext +import androidx.glance.LocalSize import androidx.glance.action.ActionParameters import androidx.glance.action.actionParametersOf import androidx.glance.action.clickable @@ -56,7 +56,6 @@ import de.jeanlucmakiola.calendula.ui.common.eventFill import de.jeanlucmakiola.calendula.ui.common.eventInk import de.jeanlucmakiola.calendula.widget.CalendulaGlanceTheme import de.jeanlucmakiola.calendula.widget.MonthWidgetSource -import de.jeanlucmakiola.calendula.widget.WidgetSize import de.jeanlucmakiola.calendula.widget.loadMonthWidgetSource import de.jeanlucmakiola.calendula.widget.systemZone import de.jeanlucmakiola.calendula.widget.today @@ -74,13 +73,11 @@ import java.util.Locale /** Per-widget state: the displayed month as `year * 12 + monthOrdinal`. */ private val MONTH_INDEX_KEY = intPreferencesKey("month_index") -/** - * Per-instance Glance state key holding the chosen [WidgetSize]. Read reactively - * in the composition so changing the setting reflects on a live widget by plain - * recomposition — `updateAll` does not reliably re-run the `provideGlance` - * preamble for a live session (same reason as [MONTH_INDEX_KEY]). - */ -internal val MONTH_SIZE_KEY = stringPreferencesKey("widget_size") +/** Event rows (lanes) shown per week before the rest collapse into "+N". */ +private const val MAX_LANES = 3 +private val LANE_HEIGHT = 14.dp +private val DAY_NUMBER_HEIGHT = 18.dp +private val GRID_HPADDING = 8.dp private fun currentMonthIndex(zone: TimeZone): Int { val t = today(zone) @@ -95,45 +92,27 @@ private fun yearMonthOf(index: Int): YearMonth = * event bars and titled single-day pills (the in-app lane layout via * [layoutMonthWeeks]), and prev/next/today navigation. * - * Columns are a fixed [MonthMetrics.columnWidth] wide — the user's chosen - * [WidgetSize], never a measured size — and the grid is centred in whatever space - * the host gives it. Sizing the columns off the launcher-reported width is what - * broke the grid (#103): the reported width overshot the width actually drawn - * into, so `width / 7` came out too wide and only the first four columns fitted. - * A fixed column also keeps a multi-day event one connected Box spanning its - * columns — no inter-cell seam, rounded end caps — which weighted columns, the - * other way to be measurement-free, could not express. - * - * The displayed month lives in Glance state and is read reactively in the - * composition ([currentState]) so the arrows move it via plain recomposition, not - * a (here-unreliable) widget session reload. + * Columns are sized explicitly from [LocalSize] (hence [SizeMode.Exact]) so a + * multi-day span renders as a single Box spanning its columns — connected, no + * inter-cell seam, with rounded end caps. The displayed month lives in Glance + * state and is read reactively in the composition ([currentState]) so the arrows + * move it via plain recomposition, not a (here-unreliable) widget session reload. */ class MonthWidget : GlanceAppWidget() { override val stateDefinition = PreferencesGlanceStateDefinition - - // Single, not Exact: nothing in the layout depends on the widget's measured - // size any more (#103), so there is no reason to pay for one RemoteViews per - // host size bucket. - override val sizeMode = SizeMode.Single + override val sizeMode = SizeMode.Exact override suspend fun provideGlance(context: Context, id: GlanceId) { val source = context.loadMonthWidgetSource() val dark = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES - // Read fresh (not through the cached source) so toggling the softener or - // the size redraws with the new choice; two cheap DataStore reads. - val prefs = context.widgetEntryPoint().settingsPrefs() - val soften = prefs.softenCalendarColors.first() - val savedSize = prefs.widgetSize.first() + // Read fresh (not through the cached source) so toggling the softener + // redraws with the new choice; it's one cheap DataStore read. + val soften = context.widgetEntryPoint().settingsPrefs().softenCalendarColors.first() provideContent { CalendulaGlanceTheme { - MonthWidgetBody( - source = source, - dark = dark, - soften = soften, - savedSize = savedSize, - ) + MonthWidgetBody(source = source, dark = dark, soften = soften) } } } @@ -164,35 +143,29 @@ class ResetMonthAction : ActionCallback { } @Composable -private fun MonthWidgetBody( - source: MonthWidgetSource, - dark: Boolean, - soften: Boolean, - savedSize: WidgetSize, -) { - // Size read reactively from per-instance Glance state (falling back to the - // saved pref for a freshly placed widget), never from the measured size. - val metrics = monthMetricsFor(currentState(MONTH_SIZE_KEY).toWidgetSize(savedSize)) +private fun MonthWidgetBody(source: MonthWidgetSource, dark: Boolean, soften: Boolean) { Column( modifier = GlanceModifier .fillMaxSize() .background(GlanceTheme.colors.surface) - .padding(horizontal = metrics.gridPadding, vertical = 6.dp), + .padding(horizontal = GRID_HPADDING, vertical = 6.dp), ) { when (source) { MonthWidgetSource.NeedsPermission -> { - MonthHeader(label = "Calendula", metrics = metrics) + MonthHeader(label = "Calendula") PermissionMessage() } is MonthWidgetSource.Ready -> { val zone = systemZone() val index = currentState(MONTH_INDEX_KEY) ?: currentMonthIndex(zone) val ym = yearMonthOf(index) + // Column width from the live widget size, minus our H padding. + val colW = (LocalSize.current.width - GRID_HPADDING * 2) / 7 val weeks = layoutMonthWeeks(ym, source.weekStart, source.instances, zone) - MonthHeader(label = monthLabel(ym, source.today.year), metrics = metrics) + MonthHeader(label = monthLabel(ym, source.today.year)) Spacer(GlanceModifier.height(2.dp)) - WeekdayHeader(weekStart = source.weekStart, metrics = metrics) + WeekdayHeader(weekStart = source.weekStart, colW = colW) weeks.forEach { week -> WeekRow( week = week, @@ -200,7 +173,7 @@ private fun MonthWidgetBody( today = source.today, dark = dark, soften = soften, - metrics = metrics, + colW = colW, modifier = GlanceModifier.defaultWeight(), ) } @@ -209,26 +182,8 @@ private fun MonthWidgetBody( } } -/** - * A grid row: the seven fixed-width columns, centred in the space the host gave - * us. Every row of the grid goes through this so they can never drift out of - * alignment, and centring means leftover width shows as an even margin either - * side rather than a ragged edge (turn the size up to fill it). - */ @Composable -private fun GridRow(content: @Composable androidx.glance.layout.RowScope.() -> Unit) { - Row( - modifier = GlanceModifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - content = content, - ) -} - -private fun String?.toWidgetSize(default: WidgetSize): WidgetSize = - this?.let { stored -> WidgetSize.entries.firstOrNull { it.name == stored } } ?: default - -@Composable -private fun MonthHeader(label: String, metrics: MonthMetrics) { +private fun MonthHeader(label: String) { val context = LocalContext.current Row( modifier = GlanceModifier.fillMaxWidth(), @@ -237,7 +192,6 @@ private fun MonthHeader(label: String, metrics: MonthMetrics) { HeaderIcon( resId = R.drawable.ic_widget_chevron_left, contentDescription = context.getString(R.string.widget_prev_month), - metrics = metrics, onClick = GlanceModifier.clickable( actionRunCallback( actionParametersOf(ShiftMonthAction.deltaKey to -1), @@ -250,7 +204,7 @@ private fun MonthHeader(label: String, metrics: MonthMetrics) { text = label, style = TextStyle( color = GlanceTheme.colors.primary, - fontSize = metrics.headerTitle, + fontSize = 15.sp, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center, ), @@ -266,13 +220,11 @@ private fun MonthHeader(label: String, metrics: MonthMetrics) { HeaderIcon( resId = R.drawable.ic_widget_today, contentDescription = context.getString(R.string.widget_today), - metrics = metrics, onClick = GlanceModifier.clickable(actionRunCallback()), ) HeaderIcon( resId = R.drawable.ic_widget_chevron_right, contentDescription = context.getString(R.string.widget_next_month), - metrics = metrics, onClick = GlanceModifier.clickable( actionRunCallback( actionParametersOf(ShiftMonthAction.deltaKey to 1), @@ -283,37 +235,32 @@ private fun MonthHeader(label: String, metrics: MonthMetrics) { } @Composable -private fun HeaderIcon( - resId: Int, - contentDescription: String, - metrics: MonthMetrics, - onClick: GlanceModifier, -) { +private fun HeaderIcon(resId: Int, contentDescription: String, onClick: GlanceModifier) { Box( - modifier = GlanceModifier.size(metrics.iconBox).then(onClick), + modifier = GlanceModifier.size(40.dp).then(onClick), contentAlignment = Alignment.Center, ) { Image( provider = ImageProvider(resId), contentDescription = contentDescription, colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurfaceVariant), - modifier = GlanceModifier.size(metrics.iconImage), + modifier = GlanceModifier.size(20.dp), ) } } @Composable -private fun WeekdayHeader(weekStart: DayOfWeek, metrics: MonthMetrics) { - GridRow { +private fun WeekdayHeader(weekStart: DayOfWeek, colW: Dp) { + Row(modifier = GlanceModifier.fillMaxWidth()) { weekdayNarrowNames(weekStart).forEach { name -> Text( text = name, style = TextStyle( color = GlanceTheme.colors.onSurfaceVariant, - fontSize = metrics.weekday, + fontSize = 11.sp, textAlign = TextAlign.Center, ), - modifier = GlanceModifier.width(metrics.columnWidth), + modifier = GlanceModifier.width(colW), ) } } @@ -336,30 +283,29 @@ private fun WeekRow( today: LocalDate, dark: Boolean, soften: Boolean, - metrics: MonthMetrics, + colW: Dp, modifier: GlanceModifier, ) { Column(modifier = modifier.fillMaxWidth()) { // Day numbers. - GridRow { + Row(modifier = GlanceModifier.fillMaxWidth()) { week.days.forEach { date -> DayNumber( date = date, isToday = date == today, inMonth = date.month == currentMonth, - metrics = metrics, + colW = colW, ) } } Spacer(GlanceModifier.height(2.dp)) // One lane row per event row. A multi-day span is a single Box spanning - // its columns (columnWidth * n) so it's connected with no seam and - // rounded ends. + // its columns (colW * n) so it's connected with no seam and rounded ends. repeat(MAX_LANES) { lane -> - LaneRow(week = week, lane = lane, dark = dark, soften = soften, metrics = metrics) + LaneRow(week = week, lane = lane, dark = dark, soften = soften, colW = colW) Spacer(GlanceModifier.height(1.dp)) } - OverflowRow(week = week, metrics = metrics) + OverflowRow(week = week, colW = colW) } } @@ -373,20 +319,19 @@ private fun openDayAction(context: Context, date: LocalDate) = actionStartActivity(MainActivity.openDateIntent(context, date, CalendarView.Month)) @Composable -private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, metrics: MonthMetrics) { +private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, colW: Dp) { val context = LocalContext.current - val diameter = metrics.dayNumberHeight Box( modifier = GlanceModifier - .width(metrics.columnWidth) - .height(diameter) + .width(colW) + .height(DAY_NUMBER_HEIGHT) .clickable(openDayAction(context, date)), contentAlignment = Alignment.Center, ) { Box( modifier = GlanceModifier - .size(diameter) - .then(if (isToday) GlanceModifier.cornerRadius(diameter / 2).background(GlanceTheme.colors.primary) else GlanceModifier), + .size(DAY_NUMBER_HEIGHT) + .then(if (isToday) GlanceModifier.cornerRadius(DAY_NUMBER_HEIGHT / 2).background(GlanceTheme.colors.primary) else GlanceModifier), contentAlignment = Alignment.Center, ) { Text( @@ -397,7 +342,7 @@ private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, metri inMonth -> GlanceTheme.colors.onSurface else -> GlanceTheme.colors.onSurfaceVariant }, - fontSize = metrics.dayNumber, + fontSize = 11.sp, fontWeight = if (isToday) FontWeight.Bold else FontWeight.Normal, ), ) @@ -406,39 +351,27 @@ private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, metri } @Composable -private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, soften: Boolean, metrics: MonthMetrics) { +private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, soften: Boolean, colW: Dp) { val context = LocalContext.current - GridRow { + Row(modifier = GlanceModifier.fillMaxWidth()) { var col = 0 while (col < 7) { val span = week.spans.firstOrNull { it.lane == lane && col in it.startCol..it.endCol } if (span != null) { val cols = span.endCol - col + 1 - SpanBar( - event = span.event, - dark = dark, - soften = soften, - width = metrics.columnWidth * cols, - metrics = metrics, - ) + SpanBar(event = span.event, dark = dark, soften = soften, width = colW * cols) col = span.endCol + 1 } else { val timed = timedEventAt(week, lane, col, week.days[col]) if (timed != null) { - SpanBar( - event = timed, - dark = dark, - soften = soften, - width = metrics.columnWidth, - metrics = metrics, - ) + SpanBar(event = timed, dark = dark, soften = soften, width = colW) } else { // Empty lane cell: a tap opens that day, so blank space in a // day column is a day-open target just like the number is. Box( GlanceModifier - .width(metrics.columnWidth) - .height(metrics.laneHeight) + .width(colW) + .height(LANE_HEIGHT) .clickable(openDayAction(context, week.days[col])), ) {} } @@ -450,19 +383,13 @@ private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, soften: Boolean, /** A single connected, rounded event bar [width] wide with its clipped title. */ @Composable -private fun SpanBar( - event: EventInstance, - dark: Boolean, - soften: Boolean, - width: Dp, - metrics: MonthMetrics, -) { +private fun SpanBar(event: EventInstance, dark: Boolean, soften: Boolean, width: Dp) { val context = LocalContext.current val fill = eventFill(event.color, dark, soften) Box( modifier = GlanceModifier .width(width) - .height(metrics.laneHeight) + .height(LANE_HEIGHT) .padding(horizontal = 1.dp) // Tap an event bar to open its detail, rooted in the month view. .clickable( @@ -487,7 +414,7 @@ private fun SpanBar( Text( text = event.title.ifBlank { context.getString(R.string.event_untitled) }, maxLines = 1, - style = TextStyle(color = ColorProvider(eventInk(fill)), fontSize = metrics.eventTitle), + style = TextStyle(color = ColorProvider(eventInk(fill)), fontSize = 9.sp), modifier = GlanceModifier.padding(horizontal = 3.dp), ) } @@ -495,9 +422,9 @@ private fun SpanBar( } @Composable -private fun OverflowRow(week: MonthWeek, metrics: MonthMetrics) { +private fun OverflowRow(week: MonthWeek, colW: Dp) { val context = LocalContext.current - GridRow { + Row(modifier = GlanceModifier.fillMaxWidth()) { week.days.forEachIndexed { col, date -> val shownSpans = week.spans.count { col in it.startCol..it.endCol && it.lane < MAX_LANES } val freeSlots = (MAX_LANES - shownSpans).coerceAtLeast(0) @@ -507,8 +434,8 @@ private fun OverflowRow(week: MonthWeek, metrics: MonthMetrics) { // it shows "+N" or is blank) opens that day, same as the app. Box( modifier = GlanceModifier - .width(metrics.columnWidth) - .height(metrics.laneHeight) + .width(colW) + .height(LANE_HEIGHT) .clickable(openDayAction(context, date)), contentAlignment = Alignment.CenterStart, ) { @@ -516,7 +443,7 @@ private fun OverflowRow(week: MonthWeek, metrics: MonthMetrics) { Text( text = "+$hidden", maxLines = 1, - style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = metrics.overflow), + style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = 9.sp), modifier = GlanceModifier.padding(start = 3.dp), ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b7efbf..10d5521 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -355,9 +355,8 @@ Agenda widget range How far ahead the agenda home-screen widget lists events. - Widgets - Widget size - How large both home-screen widgets draw their text and, for the month widget, its day columns. Pick a bigger size to fill a bigger widget. + Agenda widget size + 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. Small Medium Large diff --git a/app/src/main/res/xml/appwidget_info_month.xml b/app/src/main/res/xml/appwidget_info_month.xml index 05094c8..cfa7acb 100644 --- a/app/src/main/res/xml/appwidget_info_month.xml +++ b/app/src/main/res/xml/appwidget_info_month.xml @@ -1,12 +1,10 @@ - - assertThat(m.gridWidth).isEqualTo(m.columnWidth * 7 + m.gridPadding * 2) - } - } - - @Test - fun `the largest step still fits a full-width phone widget`() { - // EXTRA_LARGE is opt-in, but it should be reachable on a normal handset - // rather than being a tablet-only trap. - assertThat(monthMetricsFor(WidgetSize.EXTRA_LARGE).gridWidth.value).isAtMost(400f) - } - - // --- the ramp ------------------------------------------------------------ - - @Test - fun `sizes are non-decreasing across the steps`() { - monthMetricsFor(WidgetSize.SMALL) - WidgetSize.entries.map(::monthMetricsFor).zipWithNext { small, big -> - assertThat(big.columnWidth.value).isAtLeast(small.columnWidth.value) - assertThat(big.laneHeight.value).isAtLeast(small.laneHeight.value) - assertThat(big.dayNumberHeight.value).isAtLeast(small.dayNumberHeight.value) - assertThat(big.headerTitle.value).isAtLeast(small.headerTitle.value) - assertThat(big.weekday.value).isAtLeast(small.weekday.value) - assertThat(big.dayNumber.value).isAtLeast(small.dayNumber.value) - assertThat(big.eventTitle.value).isAtLeast(small.eventTitle.value) - assertThat(big.overflow.value).isAtLeast(small.overflow.value) - assertThat(big.iconImage.value).isAtLeast(small.iconImage.value) - assertThat(big.iconBox.value).isAtLeast(small.iconBox.value) - } - } - - @Test - fun `the today circle always fits inside its column`() { - // The day number sits in a circle dayNumberHeight across, centred in a - // column. If it ever outgrew the column it would collide with its - // neighbours. - WidgetSize.entries.map(::monthMetricsFor).forEach { m -> - assertThat(m.dayNumberHeight.value).isLessThan(m.columnWidth.value) - } - } - - @Test - fun `SMALL keeps the widget's original row metrics`() { - // SMALL is the default, so a month widget whose owner never touched the - // setting must keep the metrics it shipped with. - val m = monthMetricsFor(WidgetSize.SMALL) - assertThat(m.laneHeight).isEqualTo(14.dp) - assertThat(m.dayNumberHeight).isEqualTo(18.dp) - assertThat(m.headerTitle.value).isEqualTo(15f) - assertThat(m.weekday.value).isEqualTo(11f) - assertThat(m.dayNumber.value).isEqualTo(11f) - assertThat(m.eventTitle.value).isEqualTo(9f) - assertThat(m.overflow.value).isEqualTo(9f) - } - - @Test - fun `three lanes plus the day number stay inside a compact week row`() { - // A week row gets roughly a sixth of the grid's height. If the metrics - // outgrew that, lanes would be clipped rather than merely tight. - val m = monthMetricsFor(WidgetSize.SMALL) - val weekRow = m.dayNumberHeight + 2.dp + (m.laneHeight + 1.dp) * MAX_LANES + m.laneHeight - assertThat(weekRow.value).isLessThan(90f) - } -}