From 9d4b4ead6ee139fd0f2ba1508bc513e1a5fafc57 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 13 Jul 2026 09:05:17 +0000 Subject: docs: add design for widget text-size setting + tomorrow-section fix Covers the root cause for the missing TOMORROW section (grid bounds skewed by tomorrow's events) and the configurable header/content text size+weight preference. --- ...-13-widget-text-size-and-tomorrow-fix-design.md | 78 ++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-13-widget-text-size-and-tomorrow-fix-design.md (limited to 'docs/superpowers') diff --git a/docs/superpowers/specs/2026-07-13-widget-text-size-and-tomorrow-fix-design.md b/docs/superpowers/specs/2026-07-13-widget-text-size-and-tomorrow-fix-design.md new file mode 100644 index 0000000..6b567cd --- /dev/null +++ b/docs/superpowers/specs/2026-07-13-widget-text-size-and-tomorrow-fix-design.md @@ -0,0 +1,78 @@ +# Widget Text Size + Tomorrow-Section Fix — Design + +## Context + +Two small, related requests against `android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt`: + +1. Widget text is too hard to read; wants it larger/heavier, and configurable. +2. The TOMORROW section (added in an earlier cycle) never appears, even when items are scheduled for tomorrow. + +Both are scoped to the same file and are being shipped together. + +## Part 1: Tomorrow-section bug (root cause + fix) + +**Root cause:** `calcGridStart`/`calcGridEnd` (lines 472-488) compute today's hour-grid bounds from `scheduledEvents`, a list that contains both today's and tomorrow's items. Both functions only inspect `.hour` on the parsed `Instant`, discarding the date — so a tomorrow event's hour-of-day skews today's grid range even though it doesn't belong in it. + +Confirmed against the live `/api/widget` response: with `now` at 22:54 and two calendar events tomorrow (10:00–14:30 and 17:35–...), the buggy `gridStart` calculation pulls the "earliest hour" down to 9 (from tomorrow's 10am event) instead of the correct ~21 (based on `nowHour`, since nothing is scheduled *today* this late). That manufactures ~12 empty hour rows between the real content and the widget's bottom edge. Because the widget root is a plain (non-lazy, non-scrolling) `Column`, anything past the widget's visible height is simply clipped — the TOMORROW section is being rendered, just permanently below the fold. + +**Fix:** filter to today-only events before computing grid bounds: + +```kotlin +val todayScheduledEvents = scheduledEvents.filter { Instant.parse(it.start!!) < tomorrowStart } +val gridStart = calcGridStart(todayScheduledEvents, nowZoned.hour) +val gridEnd = calcGridEnd(todayScheduledEvents, nowZoned.hour) +``` + +No other call site changes: the `HourRow` loop keeps receiving the full `scheduledEvents` list (it already filters by real `Instant` per hour, which is date-correct), and `TomorrowSection`'s own filtering is unaffected. + +**Testing:** `calcGridStart`/`calcGridEnd` are currently `private` top-level functions, making them untestable from `app/src/test`. Change visibility to `internal` and add `DootWidgetGridTest.kt` with a case reproducing the live scenario (today empty, one event tomorrow at an early hour, `nowHour` late) asserting `calcGridStart` returns a value derived from `nowHour`, not from the tomorrow event's hour. + +## Part 2: Configurable text size + +**Preference:** new enum, stored as a string preference (`Keys.TEXT_SIZE`) in the same widget DataStore used for server URL/token: + +```kotlin +enum class WidgetTextSize( + val headerScale: Float, val headerWeightBump: Int, + val contentScale: Float, val contentWeightBump: Int +) { + SMALL(1.0f, 0, 1.0f, 0), // reproduces today's exact appearance + NORMAL(1.15f, 1, 1.15f, 1), // new default + LARGE(1.3f, 2, 1.3f, 2) +} +``` + +Header and content are separate fields (currently identical numbers) so either can be retuned later without affecting the other. Default is `NORMAL` — for both new installs and existing installs with no saved preference (i.e. missing/unparseable pref value falls back to `NORMAL`, not `SMALL`). + +**Weight ladder:** both weight-bump fields walk the same three-step ladder, capped at the top: + +```kotlin +private val weightLadder = listOf(FontWeight.Normal, FontWeight.Medium, FontWeight.Bold) + +fun WidgetTextSize.scaledHeaderSize(baseSp: Int): TextUnit = (baseSp * headerScale).sp +fun WidgetTextSize.scaledHeaderWeight(base: FontWeight): FontWeight = bumpWeight(base, headerWeightBump) +fun WidgetTextSize.scaledContentSize(baseSp: Int): TextUnit = (baseSp * contentScale).sp +fun WidgetTextSize.scaledContentWeight(base: FontWeight): FontWeight = bumpWeight(base, contentWeightBump) + +private fun bumpWeight(base: FontWeight, bump: Int): FontWeight { + val idx = weightLadder.indexOf(base).coerceAtLeast(0) + return weightLadder[(idx + bump).coerceAtMost(weightLadder.lastIndex)] +} +``` + +**Call-site mapping** — every `Text(..., style = TextStyle(fontSize = Xsp, fontWeight = Y))` in `DootWidget.kt` switches to `textSize.scaledHeaderSize(X)`/`scaledHeaderWeight(Y)` or the content equivalent, per this split: + +- Header (chrome): TODAY/TOMORROW section titles, `hourLabel` text in `HourRow`, the "TODAY" sub-header inside `TaskFragmentBlock`, the time label in `TomorrowEventRow`. +- Content (readability): `AllDayRow` title, `EventBlock` title, `TaskRow` title, `TomorrowEventRow` title. + +**Propagation:** `WidgetTextSize` is read once in `DootWidget.provideGlance()` alongside the other prefs (parse the stored string, default `NORMAL` on missing/invalid), then threaded as an explicit `textSize: WidgetTextSize` parameter through the composable call chain (`WidgetRoot` → `AllDayRow`/`HourRow`/`EventBlock`/`TaskFragmentBlock`/`TaskRow`/`TomorrowSection`/`TomorrowEventRow`) — matching the file's existing explicit-parameter style; no `CompositionLocal` needed for this few call sites. + +**Settings UI:** `SettingsActivity`/`SettingsScreen` gains a labeled 3-option row (Small / Normal / Large) below the existing Server URL / Token fields. Selection is read from and written to `Keys.TEXT_SIZE` the same way `savedUrl`/`savedToken` are today, and saved via the existing `onSave` path — which already calls `RefreshWorker.runOnce()`, and `RefreshWorker.doWork()` unconditionally calls `DootWidget().updateAll()` regardless of whether the server fetch changed anything, so the new size takes effect immediately without extra plumbing. + +**Testing:** `scaledHeaderSize`/`scaledContentSize`/`bumpWeight` are pure functions — add unit tests in `app/src/test` covering the ladder-capping edge case (`Bold` + any positive bump stays `Bold`) and the scale math for each tier. + +## Out of scope + +- Icon sizes (refresh/quick-add/checkbox glyphs) are unaffected — only text scales. +- No per-element (as opposed to header/content-category) customization. +- No live preview in the settings screen; user judges the result on the actual home-screen widget after saving. -- cgit v1.2.3