Compare commits
2 Commits
renovate/c
...
feat/164-w
| Author | SHA1 | Date | |
|---|---|---|---|
| 9775e0a652 | |||
|
|
8c76cbdf5e |
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Month-view event titles no longer end in an ellipsis. The "…" took the width
|
||||||
|
of a couple of characters and told you nothing you couldn't already see, so
|
||||||
|
the title now simply runs to the edge of its chip — a few more letters per
|
||||||
|
cell, which is often the difference between two events you can tell apart and
|
||||||
|
two you can't ([#164]).
|
||||||
|
|
||||||
## [2.19.1] — 2026-08-11
|
## [2.19.1] — 2026-08-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -1414,3 +1421,4 @@ automatically, with zero telemetry and no internet permission.
|
|||||||
[#123]: https://codeberg.org/jlmakiola/calendula/issues/123
|
[#123]: https://codeberg.org/jlmakiola/calendula/issues/123
|
||||||
[#163]: https://codeberg.org/jlmakiola/calendula/issues/163
|
[#163]: https://codeberg.org/jlmakiola/calendula/issues/163
|
||||||
[#173]: https://codeberg.org/jlmakiola/calendula/issues/173
|
[#173]: https://codeberg.org/jlmakiola/calendula/issues/173
|
||||||
|
[#164]: https://codeberg.org/jlmakiola/calendula/issues/164
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -464,11 +464,13 @@ fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier =
|
|||||||
.padding(horizontal = 4.dp, vertical = 2.dp),
|
.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
|
val titleOverflow = eventTitleOverflow()
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = titleOverflow.overflow,
|
||||||
|
softWrap = titleOverflow.softWrap,
|
||||||
color = eventInk(fill, alpha = 0.85f),
|
color = eventInk(fill, alpha = 0.85f),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.customActions
|
import androidx.compose.ui.semantics.customActions
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
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.beginsOn
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventDragAllowed
|
import de.jeanlucmakiola.calendula.ui.common.eventDragAllowed
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventMoveAction
|
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.rememberEventDragSource
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController
|
import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController
|
||||||
import de.jeanlucmakiola.calendula.ui.common.startInstant
|
import de.jeanlucmakiola.calendula.ui.common.startInstant
|
||||||
@@ -519,11 +519,13 @@ private fun AllDayBar(
|
|||||||
.semantics { contentDescription = title },
|
.semantics { contentDescription = title },
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
) {
|
) {
|
||||||
|
val titleOverflow = eventTitleOverflow()
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = titleOverflow.overflow,
|
||||||
|
softWrap = titleOverflow.softWrap,
|
||||||
color = eventInk(fill),
|
color = eventInk(fill),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -766,11 +768,13 @@ private fun EventBlock(
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
if (showTitle) {
|
if (showTitle) {
|
||||||
|
val titleOverflow = eventTitleOverflow(singleLine = showTime)
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
maxLines = if (showTime) 1 else 2,
|
maxLines = if (showTime) 1 else 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = titleOverflow.overflow,
|
||||||
|
softWrap = titleOverflow.softWrap,
|
||||||
color = eventInk(fill, alpha = 0.85f),
|
color = eventInk(fill, alpha = 0.85f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ import androidx.compose.ui.geometry.Offset
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberDragSurface
|
import de.jeanlucmakiola.calendula.ui.common.rememberDragSurface
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventMoveAction
|
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.MoveTarget
|
||||||
import de.jeanlucmakiola.calendula.ui.common.MoveRequest
|
import de.jeanlucmakiola.calendula.ui.common.MoveRequest
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
||||||
@@ -2352,11 +2353,13 @@ private fun MonthBar(
|
|||||||
},
|
},
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
) {
|
) {
|
||||||
|
val titleOverflow = eventTitleOverflow()
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = titleOverflow.overflow,
|
||||||
|
softWrap = titleOverflow.softWrap,
|
||||||
color = eventInk(fill),
|
color = eventInk(fill),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ import androidx.compose.ui.semantics.contentDescription
|
|||||||
import androidx.compose.ui.semantics.customActions
|
import androidx.compose.ui.semantics.customActions
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.Dp
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
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.beginsOn
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventDragAllowed
|
import de.jeanlucmakiola.calendula.ui.common.eventDragAllowed
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventMoveAction
|
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.rememberEventDragSource
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController
|
import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController
|
||||||
import de.jeanlucmakiola.calendula.ui.common.startInstant
|
import de.jeanlucmakiola.calendula.ui.common.startInstant
|
||||||
@@ -656,11 +656,13 @@ private fun AllDayBar(
|
|||||||
.semantics { contentDescription = title },
|
.semantics { contentDescription = title },
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
) {
|
) {
|
||||||
|
val titleOverflow = eventTitleOverflow()
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = titleOverflow.overflow,
|
||||||
|
softWrap = titleOverflow.softWrap,
|
||||||
color = eventInk(fill),
|
color = eventInk(fill),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -934,11 +936,13 @@ private fun EventBlock(
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
if (showTitle) {
|
if (showTitle) {
|
||||||
|
val titleOverflow = eventTitleOverflow(singleLine = titleMaxLines == 1)
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
maxLines = titleMaxLines,
|
maxLines = titleMaxLines,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = titleOverflow.overflow,
|
||||||
|
softWrap = titleOverflow.softWrap,
|
||||||
color = eventInk(fill, alpha = 0.85f),
|
color = eventInk(fill, alpha = 0.85f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user