Classic agenda: invert text colour instead of backings at low opacity (#489)

This commit is contained in:
makiolaj committed 2026-11-09 20:07:18 +01:00
1 parent 88d5614158
commit 06f658917f
6 files changed
+103 -28

No files matched your search

@@ -41,6 +41,8 @@ data class AgendaWidgetOptions(
val showToday: Boolean,
val pastDisplay: PastEventDisplay,
val style: AgendaWidgetStyle = AgendaWidgetStyle.CARDS,
/** Classic style only: draw the text in the opposite theme's colours, for a faded fill over the wallpaper. */
val invertText: Boolean = false,
)
internal fun WidgetTheme.resolve(app: ThemeMode): WidgetThemeMode = when (this) {
@@ -57,6 +59,7 @@ private val BackgroundKey = stringPreferencesKey("cfg_background")
private val ThemeKey = stringPreferencesKey("cfg_theme")
private val OpacityKey = floatPreferencesKey("cfg_opacity")
private val AgendaStyleKey = stringPreferencesKey("cfg_agenda_style")
private val InvertTextKey = booleanPreferencesKey("cfg_invert_text")
private val AgendaRangeKey = stringPreferencesKey("cfg_agenda_range")
private val TextSizeKey = stringPreferencesKey("cfg_text_size")
private val ShowTodayKey = booleanPreferencesKey("cfg_show_today")
@@ -81,6 +84,7 @@ internal fun Preferences.agendaWidgetOptions(defaults: AgendaWidgetOptions) = Ag
showToday = this[ShowTodayKey] ?: defaults.showToday,
pastDisplay = this[PastDisplayKey].toEnum(defaults.pastDisplay),
style = this[AgendaStyleKey].toEnum(defaults.style),
invertText = this[InvertTextKey] ?: defaults.invertText,
)
/** Whether this widget has agenda options of its own, rather than following the app's. */
@@ -98,6 +102,7 @@ internal fun MutablePreferences.putAgendaOptions(options: AgendaWidgetOptions) {
this[ShowTodayKey] = options.showToday
this[PastDisplayKey] = options.pastDisplay.name
this[AgendaStyleKey] = options.style.name
this[InvertTextKey] = options.invertText
}
/**
@@ -127,6 +127,7 @@ sealed interface AgendaWidgetData {
val showToday: Boolean,
val textSize: WidgetSize,
val style: AgendaWidgetStyle = AgendaWidgetStyle.CARDS,
val invertText: Boolean = false,
/** Snapshot instant the data was read at, for "has this event ended?" tests. */
val now: Instant,
) : AgendaWidgetData
@@ -186,6 +187,7 @@ internal suspend fun Context.loadAgendaWidgetData(
showToday = options.showToday,
textSize = options.textSize,
style = options.style,
invertText = options.invertText,
now = Clock.System.now(),
)
}
@@ -60,7 +60,10 @@ import de.jeanlucmakiola.calendula.ui.common.eventFill
import de.jeanlucmakiola.calendula.ui.common.eventAccent
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
import de.jeanlucmakiola.calendula.widget.AgendaWidgetStyle
import de.jeanlucmakiola.floret.glance.LocalWidgetBackground
import de.jeanlucmakiola.floret.glance.LocalWidgetSeeThrough
import androidx.compose.runtime.CompositionLocalProvider
import de.jeanlucmakiola.floret.glance.WidgetBackground
import de.jeanlucmakiola.floret.glance.withAlpha
import de.jeanlucmakiola.calendula.widget.WidgetDimFillAlpha
import de.jeanlucmakiola.calendula.widget.widgetEventInk
@@ -177,16 +180,21 @@ private fun AgendaWidgetBody(data: AgendaWidgetData, preview: Boolean) {
val size = (data as? AgendaWidgetData.Ready)?.textSize ?: WidgetSize.SMALL
val fontScale = androidx.glance.LocalContext.current.resources.configuration.fontScale
val metrics = metricsFor(size).scaledForFont(fontScale)
Column(
modifier = GlanceModifier
.fillMaxSize()
.padding(horizontal = 8.dp, vertical = 6.dp),
) {
AgendaHeader()
Spacer(GlanceModifier.height(6.dp))
when (data) {
AgendaWidgetData.NeedsPermission -> WidgetMessage(R.string.widget_needs_permission, metrics)
is AgendaWidgetData.Ready -> AgendaRows(data = data, metrics = metrics, preview = preview)
// The classic style adds no backings for a faded fill, only on a transparent one (#489).
val classic = (data as? AgendaWidgetData.Ready)?.style == AgendaWidgetStyle.CLASSIC
val seeThrough = if (classic) LocalWidgetBackground.current == WidgetBackground.TRANSPARENT else LocalWidgetSeeThrough.current
CompositionLocalProvider(LocalWidgetSeeThrough provides seeThrough) {
Column(
modifier = GlanceModifier
.fillMaxSize()
.padding(horizontal = 8.dp, vertical = 6.dp),
) {
AgendaHeader()
Spacer(GlanceModifier.height(6.dp))
when (data) {
AgendaWidgetData.NeedsPermission -> WidgetMessage(R.string.widget_needs_permission, metrics)
is AgendaWidgetData.Ready -> AgendaRows(data = data, metrics = metrics, preview = preview)
}
}
}
}
@@ -314,13 +322,15 @@ internal fun classicItems(rows: List<AgendaRow>, limit: Int = MAX_AGENDA_ROWS):
/**
* v2.23's look (#489): a label line per day over plain rows with a colour
* stripe. On a see-through root each day sits on its own backing card, so the
* text stays readable over the wallpaper.
* stripe. On a transparent root each day sits on its own backing card, so the
* text stays readable over the wallpaper; a faded fill is left to the opacity
* and "Invert text colour" options.
*/
@Composable
private fun ClassicRows(rows: List<AgendaRow>, data: AgendaWidgetData.Ready, metrics: AgendaMetrics, preview: Boolean) {
val items = classicItems(rows, if (preview) PREVIEW_ROWS else MAX_AGENDA_ROWS)
val backed = LocalWidgetSeeThrough.current
val backed = LocalWidgetBackground.current == WidgetBackground.TRANSPARENT
val ink = classicInk(inverted = data.invertText && !backed)
val positions = if (backed) dayPositions(items.map { it.date }) else emptyList()
val cardColor = widgetCardColor()
val item: @Composable (Int) -> Unit = { index ->
@@ -336,11 +346,12 @@ private fun ClassicRows(rows: List<AgendaRow>, data: AgendaWidgetData.Ready, met
index == 0 -> 0.dp
else -> metrics.dayHeaderTopPad
},
ink = ink,
metrics = metrics,
)
is ClassicItem.Entry -> when (val row = entry.row) {
is AgendaRow.Placeholder -> ClassicPlaceholderRow(row.date, backed, metrics)
is AgendaRow.Event -> ClassicEventRow(row, data, backed, metrics)
is AgendaRow.Placeholder -> ClassicPlaceholderRow(row.date, backed, ink, metrics)
is AgendaRow.Event -> ClassicEventRow(row, data, backed, ink, metrics)
}
}
}
@@ -360,15 +371,27 @@ private fun ClassicRows(rows: List<AgendaRow>, data: AgendaWidgetData.Ready, met
RowList(items.size, preview, item)
}
/** The classic style's text colours: the theme's, or its inverse for the "Invert text colour" option. */
private data class ClassicInk(val title: ColorProvider, val secondary: ColorProvider, val today: ColorProvider)
@Composable
private fun classicInk(inverted: Boolean): ClassicInk = with(GlanceTheme.colors) {
if (inverted) {
ClassicInk(inverseOnSurface, inverseOnSurface.withAlpha(INVERTED_SECONDARY_INK), inversePrimary)
} else {
ClassicInk(onSurface, onSurfaceVariant, primary)
}
}
/** A classic day label, "Today · Wed, 17 Jun"; opens that day in the app. */
@Composable
private fun DayHeaderRow(date: LocalDate, today: LocalDate, startPad: Dp, topPad: Dp, metrics: AgendaMetrics) {
private fun DayHeaderRow(date: LocalDate, today: LocalDate, startPad: Dp, topPad: Dp, ink: ClassicInk, metrics: AgendaMetrics) {
val context = androidx.glance.LocalContext.current
Text(
text = agendaDayLabel(context, date, today),
maxLines = 1,
style = TextStyle(
color = if (date == today) GlanceTheme.colors.primary else GlanceTheme.colors.onSurfaceVariant,
color = if (date == today) ink.today else ink.secondary,
fontSize = metrics.dayHeader,
fontWeight = FontWeight.Medium,
),
@@ -381,12 +404,12 @@ private fun DayHeaderRow(date: LocalDate, today: LocalDate, startPad: Dp, topPad
/** "No more events today" as a muted line under an anchored, event-less today (#35), indented to the titles. */
@Composable
private fun ClassicPlaceholderRow(date: LocalDate, backed: Boolean, metrics: AgendaMetrics) {
private fun ClassicPlaceholderRow(date: LocalDate, backed: Boolean, ink: ClassicInk, metrics: AgendaMetrics) {
val context = androidx.glance.LocalContext.current
Text(
text = context.getString(R.string.agenda_no_more_today),
maxLines = 1,
style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = metrics.placeholder),
style = TextStyle(color = ink.secondary, fontSize = metrics.placeholder),
modifier = GlanceModifier
.fillMaxWidth()
.padding(start = if (backed) CLASSIC_CARD_H_PAD + STRIPE_INSET else TEXT_INDENT, end = 8.dp, top = 2.dp, bottom = metrics.rowVPad + 2.dp)
@@ -396,7 +419,7 @@ private fun ClassicPlaceholderRow(date: LocalDate, backed: Boolean, metrics: Age
/** A classic event row: the calendar's colour stripe beside the title and time; a dimmed one fades both. */
@Composable
private fun ClassicEventRow(row: AgendaRow.Event, data: AgendaWidgetData.Ready, backed: Boolean, metrics: AgendaMetrics) {
private fun ClassicEventRow(row: AgendaRow.Event, data: AgendaWidgetData.Ready, backed: Boolean, ink: ClassicInk, metrics: AgendaMetrics) {
val context = androidx.glance.LocalContext.current
val event = row.event
val dimmed = row.dimmed(data)
@@ -423,7 +446,7 @@ private fun ClassicEventRow(row: AgendaRow.Event, data: AgendaWidgetData.Ready,
text = event.title.ifBlank { context.getString(R.string.event_untitled) },
maxLines = 1,
style = TextStyle(
color = if (dimmed) GlanceTheme.colors.onSurface.withAlpha(CLASSIC_DIM_INK) else GlanceTheme.colors.onSurface,
color = if (dimmed) ink.title.withAlpha(CLASSIC_DIM_INK) else ink.title,
fontSize = metrics.eventTitle,
textDecoration = glanceDeclinedDecoration(event.isCrossedOut),
),
@@ -432,11 +455,7 @@ private fun ClassicEventRow(row: AgendaRow.Event, data: AgendaWidgetData.Ready,
text = eventTimeSummary(context, event, row.date, data.is24Hour),
maxLines = 1,
style = TextStyle(
color = if (dimmed) {
GlanceTheme.colors.onSurfaceVariant.withAlpha(CLASSIC_DIM_INK)
} else {
GlanceTheme.colors.onSurfaceVariant
},
color = if (dimmed) ink.secondary.withAlpha(CLASSIC_DIM_INK) else ink.secondary,
fontSize = metrics.eventTime,
),
)
@@ -451,6 +470,9 @@ private val CLASSIC_CARD_H_PAD = 10.dp
/** How strongly a finished event's text shows in the classic style, on any fill. */
private const val CLASSIC_DIM_INK = 0.5f
/** The inverted secondary text's strength, as `onSurfaceVariant` sits below `onSurface`. */
private const val INVERTED_SECONDARY_INK = 0.75f
/** Where each row sits among the rows of its day, from each row's date, for the grouped-card shapes. */
private fun dayPositions(dates: List<LocalDate>): List<Position> = buildList {
var start = 0
@@ -74,6 +74,10 @@ import de.jeanlucmakiola.calendula.widget.AgendaWidgetStyle
import java.text.NumberFormat
import de.jeanlucmakiola.floret.locale.currentLocale
import androidx.compose.runtime.remember
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.Switch
import androidx.compose.foundation.layout.Row
private val PreviewMaxHeight = 280.dp
@@ -241,7 +245,11 @@ private fun ConfigOptions(
onSelect = { onAppearance(state.appearance.copy(background = it)) },
position = Position.Top,
)
if (state.appearance.background != WidgetBackground.TRANSPARENT) {
var showReadability by rememberSaveable { mutableStateOf(false) }
val faded = state.appearance.background != WidgetBackground.TRANSPARENT
val content = state.draft.content
val classic = state.kind == WidgetKind.AGENDA && content.style == AgendaWidgetStyle.CLASSIC
if (faded) {
val opacity = state.appearance.opacity
GroupedSliderRow(
title = stringResource(R.string.widget_config_opacity),
@@ -253,6 +261,21 @@ private fun ConfigOptions(
},
valueRange = MIN_OPACITY..1f,
steps = OPACITY_STEPS,
trailing = { ReadabilityInfoButton { showReadability = !showReadability } },
)
}
if (faded && classic) {
GroupedRow(
title = stringResource(R.string.widget_config_invert_text),
summary = stringResource(R.string.widget_config_invert_text_summary),
position = Position.Middle,
trailing = {
Row(verticalAlignment = Alignment.CenterVertically) {
ReadabilityInfoButton { showReadability = !showReadability }
Switch(checked = content.invertText, onCheckedChange = { onContent(content.copy(invertText = it)) })
}
},
onClick = { onContent(content.copy(invertText = !content.invertText)) },
)
}
GroupedChoiceRow(
@@ -263,6 +286,12 @@ private fun ConfigOptions(
onSelect = { onAppearance(state.appearance.copy(theme = it)) },
position = Position.Bottom,
)
AnimatedVisibility(visible = faded && showReadability) {
GroupedHint(
stringResource(if (classic) R.string.widget_config_readability_hint_classic else R.string.widget_config_readability_hint),
verticalPadding = 8.dp,
)
}
if (state.kind == WidgetKind.COUNTDOWN) {
CountdownOptionsSection(
options = state.draft.countdown,
@@ -393,6 +422,18 @@ private fun ContentOptions(
}
}
/** The "i" beside the opacity and text-colour options; toggles the readability hint below them. */
@Composable
private fun ReadabilityInfoButton(onClick: () -> Unit) {
IconButton(onClick = onClick) {
Icon(
Icons.Outlined.Info,
contentDescription = stringResource(R.string.widget_config_readability_info),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@Composable
private fun agendaStyleLabel(style: AgendaWidgetStyle): String = stringResource(
when (style) {
+5
View File
@@ -518,6 +518,11 @@
<string name="widget_config_background_transparent">Transparent</string>
<string name="widget_config_theme_follow_app">Follow app</string>
<string name="widget_config_opacity">Opacity</string>
<string name="widget_config_invert_text">Invert text colour</string>
<string name="widget_config_invert_text_summary">Light text on a dark theme turns dark, and the other way round</string>
<string name="widget_config_readability_info">About readability</string>
<string name="widget_config_readability_hint">At low opacity the wallpaper shows through, so some text may be harder to read. Raise the opacity if it is.</string>
<string name="widget_config_readability_hint_classic">At low opacity the wallpaper shows through, so some text may be hard to read. If it is, raise the opacity or invert the text colour.</string>
<string name="widget_config_style">Style</string>
<string name="widget_config_style_cards">Cards</string>
<string name="widget_config_style_classic">Classic</string>