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() + } +}