# 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.