From 9775e0a652363154204e00a1bf07dbc833615bc6 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 18 Aug 2026 15:57:17 +0200 Subject: [PATCH] Drop the ellipsis in week and day event chips too (#164) #183 only covered the month grid, but a week column is just as narrow. The all-day chips and the drag copy take the same treatment; the timed blocks only when they are showing a single line, since wrapping needs softWrap on and clipping with it on breaks at the last whole word. The rule now lives in one helper instead of being copied per chip. --- .../calendula/ui/common/EventTitleOverflow.kt | 39 ++++++++++++++++++ .../calendula/ui/common/TimelineDrag.kt | 4 +- .../calendula/ui/day/DayScreen.kt | 10 +++-- .../calendula/ui/month/MonthScreen.kt | 15 ++----- .../calendula/ui/week/WeekScreen.kt | 10 +++-- .../ui/common/EventTitleOverflowTest.kt | 40 +++++++++++++++++++ 6 files changed, 100 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventTitleOverflow.kt create mode 100644 app/src/test/java/de/jeanlucmakiola/calendula/ui/common/EventTitleOverflowTest.kt diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventTitleOverflow.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventTitleOverflow.kt new file mode 100644 index 0000000..89304a3 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventTitleOverflow.kt @@ -0,0 +1,39 @@ +package de.jeanlucmakiola.calendula.ui.common + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.LayoutDirection + +/** How an event chip's title should overflow: what to pass to `Text`. */ +data class EventTitleOverflow(val overflow: TextOverflow, val softWrap: Boolean) + +/** + * Overflow for an event chip's title (#164). Chips are narrow enough that the + * "…" costs a couple of readable characters, so the title runs to the chip's + * edge and clips mid-glyph instead. + * + * Two cases keep the ellipsis: + * + * - **[rtl].** With `softWrap` off Compose lays the line out at its full + * intrinsic width and clips to the node's left edge, which in RTL is the *end* + * of the string — an Arabic title would lose its beginning. The ellipsis + * truncates at the logical end in both directions. + * - **More than one line** ([singleLine] false). Wrapping needs `softWrap` on, + * and clipping with it on breaks the last line at the last whole word — less + * title than the ellipsis showed, not more. + */ +fun eventTitleOverflowFor(rtl: Boolean, singleLine: Boolean): EventTitleOverflow = + if (rtl || !singleLine) { + EventTitleOverflow(TextOverflow.Ellipsis, softWrap = true) + } else { + EventTitleOverflow(TextOverflow.Clip, softWrap = false) + } + +/** [eventTitleOverflowFor] against the current layout direction. */ +@Composable +fun eventTitleOverflow(singleLine: Boolean = true): EventTitleOverflow = + eventTitleOverflowFor( + rtl = LocalLayoutDirection.current == LayoutDirection.Rtl, + singleLine = singleLine, + ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt index 9420cdd..c62c8f6 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt @@ -464,11 +464,13 @@ fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier = .padding(horizontal = 4.dp, vertical = 2.dp), ) { Column { + val titleOverflow = eventTitleOverflow() Text( text = title, style = MaterialTheme.typography.labelMedium, maxLines = 1, - overflow = TextOverflow.Ellipsis, + overflow = titleOverflow.overflow, + softWrap = titleOverflow.softWrap, color = eventInk(fill, alpha = 0.85f), ) Text( diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt index 6b6d4fe..a6ebac8 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt @@ -65,7 +65,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -94,6 +93,7 @@ import de.jeanlucmakiola.calendula.ui.common.TimelineDrop import de.jeanlucmakiola.calendula.ui.common.beginsOn import de.jeanlucmakiola.calendula.ui.common.eventDragAllowed import de.jeanlucmakiola.calendula.ui.common.eventMoveAction +import de.jeanlucmakiola.calendula.ui.common.eventTitleOverflow import de.jeanlucmakiola.calendula.ui.common.rememberEventDragSource import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController import de.jeanlucmakiola.calendula.ui.common.startInstant @@ -519,11 +519,13 @@ private fun AllDayBar( .semantics { contentDescription = title }, contentAlignment = Alignment.CenterStart, ) { + val titleOverflow = eventTitleOverflow() Text( text = title, style = MaterialTheme.typography.labelSmall, maxLines = 1, - overflow = TextOverflow.Ellipsis, + overflow = titleOverflow.overflow, + softWrap = titleOverflow.softWrap, color = eventInk(fill), ) } @@ -766,11 +768,13 @@ private fun EventBlock( ) { Column { if (showTitle) { + val titleOverflow = eventTitleOverflow(singleLine = showTime) Text( text = title, style = MaterialTheme.typography.labelMedium, maxLines = if (showTime) 1 else 2, - overflow = TextOverflow.Ellipsis, + overflow = titleOverflow.overflow, + softWrap = titleOverflow.softWrap, color = eventInk(fill, alpha = 0.85f), ) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt index d207c51..9d0f3b8 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt @@ -89,6 +89,7 @@ import androidx.compose.ui.geometry.Offset import kotlin.math.roundToInt import de.jeanlucmakiola.calendula.ui.common.rememberDragSurface import de.jeanlucmakiola.calendula.ui.common.eventMoveAction +import de.jeanlucmakiola.calendula.ui.common.eventTitleOverflow import de.jeanlucmakiola.calendula.ui.common.MoveTarget import de.jeanlucmakiola.calendula.ui.common.MoveRequest import de.jeanlucmakiola.calendula.ui.common.LocalEventMove @@ -2352,21 +2353,13 @@ private fun MonthBar( }, contentAlignment = Alignment.CenterStart, ) { - // Clipping only keeps the start of the title in an LTR layout: with - // softWrap off Compose lays the line out at its full width and clips to - // the chip's *left* edge, which in RTL is the end of the string. So RTL - // keeps the ellipsis, which truncates at the logical end either way. - val rtl = LocalLayoutDirection.current == LayoutDirection.Rtl + val titleOverflow = eventTitleOverflow() Text( text = title, style = MaterialTheme.typography.labelSmall, maxLines = 1, - // No "…" (#164): the chip is narrow enough that the ellipsis costs a - // couple of readable characters. softWrap is off with it — left on, - // a clipped line breaks at the last whole word instead of running to - // the chip's edge, which reads as *less* title, not more. - overflow = if (rtl) TextOverflow.Ellipsis else TextOverflow.Clip, - softWrap = rtl, + overflow = titleOverflow.overflow, + softWrap = titleOverflow.softWrap, color = eventInk(fill), ) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt index 869c3a2..a515469 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt @@ -72,7 +72,6 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -102,6 +101,7 @@ import de.jeanlucmakiola.calendula.ui.common.TimelineDrop import de.jeanlucmakiola.calendula.ui.common.beginsOn import de.jeanlucmakiola.calendula.ui.common.eventDragAllowed import de.jeanlucmakiola.calendula.ui.common.eventMoveAction +import de.jeanlucmakiola.calendula.ui.common.eventTitleOverflow import de.jeanlucmakiola.calendula.ui.common.rememberEventDragSource import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController import de.jeanlucmakiola.calendula.ui.common.startInstant @@ -656,11 +656,13 @@ private fun AllDayBar( .semantics { contentDescription = title }, contentAlignment = Alignment.CenterStart, ) { + val titleOverflow = eventTitleOverflow() Text( text = title, style = MaterialTheme.typography.labelSmall, maxLines = 1, - overflow = TextOverflow.Ellipsis, + overflow = titleOverflow.overflow, + softWrap = titleOverflow.softWrap, color = eventInk(fill), ) } @@ -934,11 +936,13 @@ private fun EventBlock( ) { Column { if (showTitle) { + val titleOverflow = eventTitleOverflow(singleLine = titleMaxLines == 1) Text( text = title, style = MaterialTheme.typography.labelMedium, maxLines = titleMaxLines, - overflow = TextOverflow.Ellipsis, + overflow = titleOverflow.overflow, + softWrap = titleOverflow.softWrap, color = eventInk(fill, alpha = 0.85f), ) } diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/EventTitleOverflowTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/EventTitleOverflowTest.kt new file mode 100644 index 0000000..7fd56b7 --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/EventTitleOverflowTest.kt @@ -0,0 +1,40 @@ +package de.jeanlucmakiola.calendula.ui.common + +import androidx.compose.ui.text.style.TextOverflow +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +class EventTitleOverflowTest { + + @Test + fun `a single LTR line clips, so the title runs to the chip's edge`() { + val result = eventTitleOverflowFor(rtl = false, singleLine = true) + assertThat(result.overflow).isEqualTo(TextOverflow.Clip) + // Load-bearing: with softWrap on, a clipped line breaks at the last + // whole word and shows less title than the ellipsis did (#164). + assertThat(result.softWrap).isFalse() + } + + @Test + fun `RTL keeps the ellipsis, which truncates at the logical end`() { + // Clipping with softWrap off cuts at the node's left edge, which in RTL + // is the end of the string — the title would lose its beginning. + val result = eventTitleOverflowFor(rtl = true, singleLine = true) + assertThat(result.overflow).isEqualTo(TextOverflow.Ellipsis) + assertThat(result.softWrap).isTrue() + } + + @Test + fun `a multi-line block keeps the ellipsis, since wrapping needs softWrap`() { + val result = eventTitleOverflowFor(rtl = false, singleLine = false) + assertThat(result.overflow).isEqualTo(TextOverflow.Ellipsis) + assertThat(result.softWrap).isTrue() + } + + @Test + fun `multi-line in RTL keeps the ellipsis too`() { + val result = eventTitleOverflowFor(rtl = true, singleLine = false) + assertThat(result.overflow).isEqualTo(TextOverflow.Ellipsis) + assertThat(result.softWrap).isTrue() + } +}