The floating drag copy hardcoded `maxLines = 1` on both its time label and its title, so picking up a block whose text wrapped clipped it to one line for as long as it was held, and it snapped back on drop. - `DragCopy` now measures the range at its own width and height, exactly as the block does — same `MAX_TIME_LINES` cap, same spare-height rule. - The block's measured title budget rides along on `TimelineDrag`, so the copy spends its height the same way the block did; the copy draws through `BlockTitle` now rather than a bare `Text`, so a wrapped title clips the way the block's does. - That measurement moves into a shared `blockTimeLines`, which Week and Day each had their own copy of. Deviation from the issue: it names only the time label, but the title had the identical hardcode and the two share one height budget — fixing the range alone would just have moved the mismatch to the title. The copy still shows the range on a block too short to carry one; that is the feedback the drag exists for and is left as is. Closes #267 Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de> Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/269
This commit is contained in:
@@ -4,7 +4,6 @@ import androidx.compose.animation.Crossfade
|
|||||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.core.snap
|
import androidx.compose.animation.core.snap
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -15,14 +14,11 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.rememberTextMeasurer
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.Constraints
|
import androidx.compose.ui.unit.Constraints
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.jeanlucmakiola.floret.identity.rememberReduceMotion
|
import de.jeanlucmakiola.floret.identity.rememberReduceMotion
|
||||||
@@ -33,6 +29,9 @@ val BLOCK_OUTER_INSET = 1.dp
|
|||||||
/** Padding between a timed block's edge and its text. */
|
/** Padding between a timed block's edge and its text. */
|
||||||
val BLOCK_TEXT_PADDING = 4.dp
|
val BLOCK_TEXT_PADDING = 4.dp
|
||||||
|
|
||||||
|
/** The same, above and below — what a block's height has to pay before any text. */
|
||||||
|
val BLOCK_TEXT_INSET = 2.dp
|
||||||
|
|
||||||
/** Most lines a time label may wrap over before it is worth more than a title line. */
|
/** Most lines a time label may wrap over before it is worth more than a title line. */
|
||||||
const val MAX_TIME_LINES = 2
|
const val MAX_TIME_LINES = 2
|
||||||
|
|
||||||
@@ -62,88 +61,61 @@ fun blockTextLines(text: String, style: TextStyle, textWidth: Dp, max: Int): Int
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A timed block's title, over at most [maxLines].
|
* Lines the time label may take at [textWidth], out of the [spare] height left
|
||||||
|
* once the title and the label's own first line are paid for.
|
||||||
*
|
*
|
||||||
* Wrapping and clipping pull against each other, which is why #164 left the
|
* A week column is narrower than a "09:30–11:00" range, so the label takes a
|
||||||
* ellipsis on multi-line chips: with `softWrap` on, the last visible line ends
|
* second line rather than lose its end — but only out of a line the title
|
||||||
* at a word boundary, so "Farmers Market" in a six-character column would clip
|
* measured itself as not needing, never one it would have filled.
|
||||||
* to "Farmer" / "s" where the ellipsis at least reached "s Mar…".
|
*/
|
||||||
|
@Composable
|
||||||
|
fun blockTimeLines(label: String, textWidth: Dp, spare: Dp): Int {
|
||||||
|
val timeLineHeight = with(LocalDensity.current) {
|
||||||
|
MaterialTheme.typography.labelSmall.lineHeight.toDp()
|
||||||
|
}
|
||||||
|
return if (spare >= timeLineHeight) {
|
||||||
|
blockTextLines(
|
||||||
|
text = label,
|
||||||
|
// The style it is drawn in, or the budget measures a line the label
|
||||||
|
// never uses (#219).
|
||||||
|
style = MaterialTheme.typography.labelSmall.asEventTime(),
|
||||||
|
textWidth = textWidth,
|
||||||
|
max = MAX_TIME_LINES,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A timed block's title, over at most [maxLines], breaking at word boundaries.
|
||||||
*
|
*
|
||||||
* So the block wraps every line but the last through one `Text` and hands the
|
* #164 clipped the last line mid-glyph so none of a narrow chip's few
|
||||||
* remainder to a second that clips mid-glyph the way a single-line chip does.
|
* characters went on an ellipsis. On a block that can wrap, whole words read
|
||||||
* Every line is then full and none of them spends two of its few characters on
|
* better than full lines do: "Farmers Market" over two lines beats "Farmer" /
|
||||||
* a "…". RTL keeps the ellipsis for the reason [eventTitleOverflowFor] gives.
|
* "s Marke". A single line still clips, having nowhere to wrap to.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BlockTitle(
|
fun BlockTitle(
|
||||||
title: String,
|
title: String,
|
||||||
maxLines: Int,
|
maxLines: Int,
|
||||||
textWidth: Dp,
|
|
||||||
color: Color,
|
color: Color,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
textDecoration: TextDecoration? = null,
|
textDecoration: TextDecoration? = null,
|
||||||
fontWeight: FontWeight? = null,
|
fontWeight: FontWeight? = null,
|
||||||
) {
|
) {
|
||||||
// Folded into the style rather than passed to the Text, so the wrap measured
|
val overflow = eventTitleOverflow(singleLine = maxLines == 1)
|
||||||
// below is the wrap that gets drawn.
|
Text(
|
||||||
val style = MaterialTheme.typography.labelMedium
|
text = title,
|
||||||
.let { if (fontWeight == null) it else it.copy(fontWeight = fontWeight) }
|
modifier = modifier,
|
||||||
val rtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
style = MaterialTheme.typography.labelMedium
|
||||||
val measurer = rememberTextMeasurer()
|
.let { if (fontWeight == null) it else it.copy(fontWeight = fontWeight) },
|
||||||
val widthPx = with(LocalDensity.current) { textWidth.roundToPx() }
|
maxLines = maxLines,
|
||||||
// Where the wrapped lines stop and the clipped tail starts — null when the
|
overflow = overflow.overflow,
|
||||||
// title fits, and nothing needs splitting.
|
softWrap = overflow.softWrap,
|
||||||
val headEnd = remember(title, style, widthPx, maxLines, rtl, measurer) {
|
color = color,
|
||||||
if (rtl || maxLines < 2 || widthPx <= 0) {
|
textDecoration = textDecoration,
|
||||||
null
|
)
|
||||||
} else {
|
|
||||||
val layout = measurer.measure(
|
|
||||||
text = title,
|
|
||||||
style = style,
|
|
||||||
constraints = Constraints(maxWidth = widthPx),
|
|
||||||
)
|
|
||||||
if (layout.lineCount <= maxLines) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
layout.getLineEnd(maxLines - 2, visibleEnd = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (headEnd == null) {
|
|
||||||
val overflow = eventTitleOverflow(singleLine = maxLines == 1)
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
modifier = modifier,
|
|
||||||
style = style,
|
|
||||||
maxLines = maxLines,
|
|
||||||
overflow = overflow.overflow,
|
|
||||||
softWrap = overflow.softWrap,
|
|
||||||
color = color,
|
|
||||||
textDecoration = textDecoration,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val tail = eventTitleOverflow(singleLine = true)
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
Text(
|
|
||||||
text = title.substring(0, headEnd),
|
|
||||||
style = style,
|
|
||||||
maxLines = maxLines - 1,
|
|
||||||
overflow = TextOverflow.Clip,
|
|
||||||
softWrap = true,
|
|
||||||
color = color,
|
|
||||||
textDecoration = textDecoration,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = title.substring(headEnd).trimStart(),
|
|
||||||
style = style,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = tail.overflow,
|
|
||||||
softWrap = tail.softWrap,
|
|
||||||
color = color,
|
|
||||||
textDecoration = textDecoration,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,23 +11,21 @@ data class EventTitleOverflow(val overflow: TextOverflow, val softWrap: Boolean)
|
|||||||
/**
|
/**
|
||||||
* Overflow for an event chip's title (#164). Chips are narrow enough that the
|
* 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
|
* "…" costs a couple of readable characters, so the title runs to the chip's
|
||||||
* edge and clips mid-glyph instead.
|
* edge and clips instead — mid-glyph on one line, at the last whole word it
|
||||||
|
* could fit on several.
|
||||||
*
|
*
|
||||||
* Two cases keep the ellipsis:
|
* One case keeps the ellipsis: a single **[rtl]** line. With `softWrap` off
|
||||||
*
|
* Compose lays the line out at its full intrinsic width and clips to the node's
|
||||||
* - **[rtl].** With `softWrap` off Compose lays the line out at its full
|
* left edge, which in RTL is the *end* of the string — an Arabic title would
|
||||||
* intrinsic width and clips to the node's left edge, which in RTL is the *end*
|
* lose its beginning. The ellipsis truncates at the logical end in both
|
||||||
* of the string — an Arabic title would lose its beginning. The ellipsis
|
* directions. Wrapping needs `softWrap` on, which clips the right end either
|
||||||
* truncates at the logical end in both directions.
|
* way, so more than one line needs no such exception.
|
||||||
* - **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 =
|
fun eventTitleOverflowFor(rtl: Boolean, singleLine: Boolean): EventTitleOverflow =
|
||||||
if (rtl || !singleLine) {
|
when {
|
||||||
EventTitleOverflow(TextOverflow.Ellipsis, softWrap = true)
|
!singleLine -> EventTitleOverflow(TextOverflow.Clip, softWrap = true)
|
||||||
} else {
|
rtl -> EventTitleOverflow(TextOverflow.Ellipsis, softWrap = true)
|
||||||
EventTitleOverflow(TextOverflow.Clip, softWrap = false)
|
else -> EventTitleOverflow(TextOverflow.Clip, softWrap = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** [eventTitleOverflowFor] against the current layout direction. */
|
/** [eventTitleOverflowFor] against the current layout direction. */
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.absoluteOffset
|
import androidx.compose.foundation.layout.absoluteOffset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -24,7 +25,9 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.withFrameNanos
|
import androidx.compose.runtime.withFrameNanos
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.LayoutCoordinates
|
import androidx.compose.ui.layout.LayoutCoordinates
|
||||||
import androidx.compose.ui.layout.boundsInRoot
|
import androidx.compose.ui.layout.boundsInRoot
|
||||||
@@ -81,6 +84,13 @@ data class TimelineDrag(
|
|||||||
val eventSpanMin: Int,
|
val eventSpanMin: Int,
|
||||||
/** The event as the grid would draw it, one piece per day column it covers. */
|
/** The event as the grid would draw it, one piece per day column it covers. */
|
||||||
val pieces: List<TimelineDragPiece>,
|
val pieces: List<TimelineDragPiece>,
|
||||||
|
/**
|
||||||
|
* Lines the block the finger picked up drew its title over, zero for one too
|
||||||
|
* short to have drawn it at all. The floating copy is that block's size, so
|
||||||
|
* it has to spend its height the same way or the text re-wraps under the
|
||||||
|
* finger and snaps back on drop (#267).
|
||||||
|
*/
|
||||||
|
val titleLines: Int,
|
||||||
/**
|
/**
|
||||||
* True when [pieces] is only [edgeDragSlice]'s stand-in — the event has left
|
* True when [pieces] is only [edgeDragSlice]'s stand-in — the event has left
|
||||||
* every column this timeline shows. A hint under the finger, not where the
|
* every column this timeline shows. A hint under the finger, not where the
|
||||||
@@ -311,6 +321,9 @@ class TimelineDragController {
|
|||||||
*/
|
*/
|
||||||
private var clipOffsetMin = 0
|
private var clipOffsetMin = 0
|
||||||
|
|
||||||
|
/** What the picked-up block drew its title over — see [TimelineDrag.titleLines]. */
|
||||||
|
private var titleLines = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The slot the block already occupied when it was picked up — not the same
|
* The slot the block already occupied when it was picked up — not the same
|
||||||
* as its start, since the target snaps to the grid (09:07 lifts to 09:00).
|
* as its start, since the target snaps to the grid (09:07 lifts to 09:00).
|
||||||
@@ -320,11 +333,13 @@ class TimelineDragController {
|
|||||||
fun begin(
|
fun begin(
|
||||||
block: TimedBlock,
|
block: TimedBlock,
|
||||||
clipOffsetMin: Int,
|
clipOffsetMin: Int,
|
||||||
|
titleLines: Int,
|
||||||
pointerInRoot: Offset,
|
pointerInRoot: Offset,
|
||||||
blockInRoot: Offset,
|
blockInRoot: Offset,
|
||||||
) {
|
) {
|
||||||
source = block
|
source = block
|
||||||
this.clipOffsetMin = clipOffsetMin
|
this.clipOffsetMin = clipOffsetMin
|
||||||
|
this.titleLines = titleLines
|
||||||
settling = null
|
settling = null
|
||||||
isDragging = true
|
isDragging = true
|
||||||
liftedInstanceId = block.event.instanceId
|
liftedInstanceId = block.event.instanceId
|
||||||
@@ -343,6 +358,7 @@ class TimelineDragController {
|
|||||||
fun cancel() {
|
fun cancel() {
|
||||||
source = null
|
source = null
|
||||||
clipOffsetMin = 0
|
clipOffsetMin = 0
|
||||||
|
titleLines = 0
|
||||||
isDragging = false
|
isDragging = false
|
||||||
liftedInstanceId = null
|
liftedInstanceId = null
|
||||||
originSlot = null
|
originSlot = null
|
||||||
@@ -465,6 +481,7 @@ class TimelineDragController {
|
|||||||
startMin = startMin,
|
startMin = startMin,
|
||||||
eventStartMin = eventStartMin,
|
eventStartMin = eventStartMin,
|
||||||
eventSpanMin = eventSpan,
|
eventSpanMin = eventSpan,
|
||||||
|
titleLines = titleLines,
|
||||||
// Bounded to the columns this timeline actually shows: a day it
|
// Bounded to the columns this timeline actually shows: a day it
|
||||||
// doesn't has no piece to draw. The write is unaffected.
|
// doesn't has no piece to draw. The write is unaffected.
|
||||||
pieces = slices
|
pieces = slices
|
||||||
@@ -602,6 +619,7 @@ const val SETTLE_FADE_MILLIS: Int = 250
|
|||||||
@Composable
|
@Composable
|
||||||
fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier = Modifier) {
|
fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier = Modifier) {
|
||||||
var origin by remember { mutableStateOf(Offset.Zero) }
|
var origin by remember { mutableStateOf(Offset.Zero) }
|
||||||
|
val density = LocalDensity.current
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
val use24Hour = LocalUse24HourFormat.current
|
val use24Hour = LocalUse24HourFormat.current
|
||||||
val locale = currentLocale()
|
val locale = currentLocale()
|
||||||
@@ -663,23 +681,61 @@ fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier =
|
|||||||
val endMin = if (rawEnd > MINUTES_PER_DAY) rawEnd % MINUTES_PER_DAY else rawEnd
|
val endMin = if (rawEnd > MINUTES_PER_DAY) rawEnd % MINUTES_PER_DAY else rawEnd
|
||||||
val label = "${formatMinuteOfDay(startMin, use24Hour, locale)}–" +
|
val label = "${formatMinuteOfDay(startMin, use24Hour, locale)}–" +
|
||||||
formatMinuteOfDay(endMin, use24Hour, locale)
|
formatMinuteOfDay(endMin, use24Hour, locale)
|
||||||
// The tallest piece carries the range: on the smallest it would be
|
// The timeline's own bounds. The copy is drawn over the whole calendar
|
||||||
// clipped away, which is exactly the case when a short tail is held.
|
// so no column clip or rounded corner cuts it, but it still belongs to
|
||||||
val labelled = drag.pieces.indices.maxByOrNull { drag.pieces[it].sizePx.height }
|
// the timeline: a piece a whole day tall reaches far past both ends of
|
||||||
drag.pieces.forEachIndexed { index, piece ->
|
// the viewport, and unclipped it paints over the headers above (#267).
|
||||||
DragCopy(
|
val port = controller.geometry.viewport?.takeIf { it.isAttached }?.boundsInRoot()
|
||||||
topLeftInRoot = piece.topLeftInRoot,
|
// How much of each piece the viewport actually shows. A multi-day event
|
||||||
overlayOrigin = origin,
|
// has a piece per day, and the widest one is a full 24 hours — taller
|
||||||
sizePx = piece.sizePx,
|
// than the screen, its top at a midnight scrolled out of sight. Picking
|
||||||
paint = paint,
|
// the range's piece by raw height put it there, off the top of the
|
||||||
shape = timedBlockShape(piece.continuesBefore, piece.continuesAfter),
|
// timeline; picking by *visible* height puts it where it can be read.
|
||||||
cuts = timedBlockCuts(piece.continuesBefore, piece.continuesAfter),
|
val shown = drag.pieces.map { piece ->
|
||||||
lift = lift,
|
if (port == null) {
|
||||||
alpha = copyAlpha,
|
piece.sizePx.height.toFloat()
|
||||||
title = title,
|
} else {
|
||||||
// Once for the whole event: repeated, it would name it per day.
|
val top = maxOf(piece.topLeftInRoot.y, port.top)
|
||||||
label = label.takeIf { index == labelled },
|
val bottom = minOf(piece.topLeftInRoot.y + piece.sizePx.height, port.bottom)
|
||||||
)
|
bottom - top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val labelled = shown.indices.maxByOrNull { shown[it] }
|
||||||
|
Box(
|
||||||
|
modifier = if (port == null) {
|
||||||
|
Modifier
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
.absoluteOffset {
|
||||||
|
IntOffset(
|
||||||
|
(port.left - origin.x).roundToInt(),
|
||||||
|
(port.top - origin.y).roundToInt(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.size(
|
||||||
|
width = with(density) { port.width.toDp() },
|
||||||
|
height = with(density) { port.height.toDp() },
|
||||||
|
)
|
||||||
|
.clipToBounds()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val pieceOrigin = port?.topLeft ?: origin
|
||||||
|
drag.pieces.forEachIndexed { index, piece ->
|
||||||
|
DragCopy(
|
||||||
|
topLeftInRoot = piece.topLeftInRoot,
|
||||||
|
overlayOrigin = pieceOrigin,
|
||||||
|
sizePx = piece.sizePx,
|
||||||
|
paint = paint,
|
||||||
|
shape = timedBlockShape(piece.continuesBefore, piece.continuesAfter),
|
||||||
|
cuts = timedBlockCuts(piece.continuesBefore, piece.continuesAfter),
|
||||||
|
lift = lift,
|
||||||
|
alpha = copyAlpha,
|
||||||
|
title = title,
|
||||||
|
titleLines = drag.titleLines,
|
||||||
|
// Once for the whole event: repeated, it would name it per day.
|
||||||
|
label = label.takeIf { index == labelled },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -696,9 +752,48 @@ private fun DragCopy(
|
|||||||
lift: Float,
|
lift: Float,
|
||||||
alpha: Float,
|
alpha: Float,
|
||||||
title: String,
|
title: String,
|
||||||
|
titleLines: Int,
|
||||||
label: String?,
|
label: String?,
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
val width = with(density) { sizePx.width.toDp() }
|
||||||
|
val height = with(density) { sizePx.height.toDp() }
|
||||||
|
val textWidth = width - (BLOCK_OUTER_INSET + BLOCK_TEXT_PADDING) * 2
|
||||||
|
val titleLineHeight = with(density) {
|
||||||
|
MaterialTheme.typography.labelMedium.lineHeight.toDp()
|
||||||
|
}
|
||||||
|
val timeLineHeight = with(density) {
|
||||||
|
MaterialTheme.typography.labelSmall.lineHeight.toDp()
|
||||||
|
}
|
||||||
|
// The block's size, spent the block's way — text sits at the top as it does
|
||||||
|
// on the block, so the copy hands back to the grid without shifting (#267).
|
||||||
|
// The title is served in full first and the range lives off what is left:
|
||||||
|
// the hour gutter down the side still says where the copy sits, so the
|
||||||
|
// range is the half that can afford to go.
|
||||||
|
val available = height - BLOCK_TEXT_INSET * 2
|
||||||
|
val titleBudget = (available / titleLineHeight).toInt().coerceAtLeast(0)
|
||||||
|
val allowed = titleLines.coerceAtMost(titleBudget)
|
||||||
|
// Re-measured at the copy's own width rather than spent on the source's
|
||||||
|
// count: a block sharing its column with another is a lane wide where the
|
||||||
|
// copy is a whole column, so the source's second line is one the copy never
|
||||||
|
// draws — and reserving it took the range away with it (#267).
|
||||||
|
val lines = if (allowed <= 0) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
blockTextLines(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.labelMedium.withTitleWeight(paint),
|
||||||
|
textWidth = textWidth,
|
||||||
|
max = allowed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val left = available - titleLineHeight * lines
|
||||||
|
val showTime = label != null && left >= timeLineHeight
|
||||||
|
val timeMaxLines = if (showTime) {
|
||||||
|
blockTimeLines(label!!, textWidth, left - timeLineHeight)
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
// Absolute: these are root coordinates, and the direction-aware
|
// Absolute: these are root coordinates, and the direction-aware
|
||||||
@@ -709,12 +804,20 @@ private fun DragCopy(
|
|||||||
(topLeftInRoot.y - overlayOrigin.y).roundToInt(),
|
(topLeftInRoot.y - overlayOrigin.y).roundToInt(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.size(
|
// Required: the parent is the viewport's size, and a plain `size`
|
||||||
width = with(density) { sizePx.width.toDp() },
|
// would let it cap a piece a whole day tall at one screen — drawn
|
||||||
height = with(density) { sizePx.height.toDp() },
|
// from a midnight scrolled off the top, that ended the column a
|
||||||
)
|
// scroll offset short of its own bottom (#267). The parent clips it.
|
||||||
|
.requiredSize(width = width, height = height)
|
||||||
.padding(horizontal = BLOCK_OUTER_INSET)
|
.padding(horizontal = BLOCK_OUTER_INSET)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
|
// Anchored at the top edge, not the middle: scaled about the
|
||||||
|
// centre, the 2% lift raises the top by 1% of the height — a few
|
||||||
|
// pixels on an hour, but a steady upward creep of the text as a
|
||||||
|
// dragged multi-day piece grows, and a shear against the
|
||||||
|
// timeline's clip. It snapped back when the lift animated out
|
||||||
|
// on drop (#267).
|
||||||
|
transformOrigin = TransformOrigin(0.5f, 0f)
|
||||||
scaleX = 1f + 0.02f * lift
|
scaleX = 1f + 0.02f * lift
|
||||||
scaleY = 1f + 0.02f * lift
|
scaleY = 1f + 0.02f * lift
|
||||||
shadowElevation = 8.dp.toPx() * lift
|
shadowElevation = 8.dp.toPx() * lift
|
||||||
@@ -723,28 +826,27 @@ private fun DragCopy(
|
|||||||
clip = false
|
clip = false
|
||||||
}
|
}
|
||||||
.eventSurface(paint, shape, cuts)
|
.eventSurface(paint, shape, cuts)
|
||||||
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = 2.dp),
|
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = BLOCK_TEXT_INSET),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
val titleOverflow = eventTitleOverflow()
|
if (lines > 0) {
|
||||||
Text(
|
BlockTitle(
|
||||||
text = title,
|
title = title,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
maxLines = lines,
|
||||||
maxLines = 1,
|
color = paint.titleInk,
|
||||||
overflow = titleOverflow.overflow,
|
textDecoration = paint.decoration,
|
||||||
softWrap = titleOverflow.softWrap,
|
fontWeight = paint.titleWeight,
|
||||||
color = paint.titleInk,
|
)
|
||||||
fontWeight = paint.titleWeight,
|
}
|
||||||
textDecoration = paint.decoration,
|
if (showTime) {
|
||||||
)
|
val overflow = eventTitleOverflow(singleLine = timeMaxLines == 1)
|
||||||
if (label != null) {
|
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
// As the block it lifted off sets its own time (#219).
|
// As the block it lifted off sets its own time (#219).
|
||||||
style = MaterialTheme.typography.labelSmall.asEventTime(),
|
style = MaterialTheme.typography.labelSmall.asEventTime(),
|
||||||
maxLines = 1,
|
maxLines = timeMaxLines,
|
||||||
overflow = titleOverflow.overflow,
|
overflow = overflow.overflow,
|
||||||
softWrap = titleOverflow.softWrap,
|
softWrap = overflow.softWrap,
|
||||||
color = paint.secondaryInk,
|
color = paint.secondaryInk,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,17 +96,6 @@ fun tappedMinuteOfDay(offsetY: Float, hourPx: Float): Int =
|
|||||||
*/
|
*/
|
||||||
const val MIN_EVENT_FRACTION = 26f / 60f
|
const val MIN_EVENT_FRACTION = 26f / 60f
|
||||||
|
|
||||||
/**
|
|
||||||
* Narrowest an event block may be and still wrap its title over several lines.
|
|
||||||
*
|
|
||||||
* Wrapping is driven by the block's height, so a tall block on a lane-split
|
|
||||||
* column would otherwise stack two or three characters per line — "Da/ily",
|
|
||||||
* "Fa/rmer/s…" — which reads worse than one ellipsised line. A full week column
|
|
||||||
* clears this on any phone; a split one never does, while the day view's much
|
|
||||||
* wider columns keep wrapping even several lanes deep.
|
|
||||||
*/
|
|
||||||
val MIN_TITLE_WRAP_WIDTH = 36.dp
|
|
||||||
|
|
||||||
/** Smallest hour height [TimelineScale.FitDay] will resolve to. */
|
/** Smallest hour height [TimelineScale.FitDay] will resolve to. */
|
||||||
val FIT_DAY_MIN = 24.dp
|
val FIT_DAY_MIN = 24.dp
|
||||||
|
|
||||||
|
|||||||
@@ -83,10 +83,10 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||||
import de.jeanlucmakiola.calendula.ui.common.BLOCK_OUTER_INSET
|
import de.jeanlucmakiola.calendula.ui.common.BLOCK_OUTER_INSET
|
||||||
import de.jeanlucmakiola.calendula.ui.common.BLOCK_TEXT_PADDING
|
import de.jeanlucmakiola.calendula.ui.common.BLOCK_TEXT_PADDING
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.BLOCK_TEXT_INSET
|
||||||
import de.jeanlucmakiola.calendula.ui.common.BlockTimeLabel
|
import de.jeanlucmakiola.calendula.ui.common.BlockTimeLabel
|
||||||
import de.jeanlucmakiola.calendula.ui.common.BlockTitle
|
import de.jeanlucmakiola.calendula.ui.common.BlockTitle
|
||||||
import de.jeanlucmakiola.calendula.ui.common.MAX_TIME_LINES
|
import de.jeanlucmakiola.calendula.ui.common.blockTimeLines
|
||||||
import de.jeanlucmakiola.calendula.ui.common.blockTextLines
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.animatedBlockPlacement
|
import de.jeanlucmakiola.calendula.ui.common.animatedBlockPlacement
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ghostAlpha
|
import de.jeanlucmakiola.calendula.ui.common.ghostAlpha
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
||||||
@@ -120,7 +120,6 @@ import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalTimelineZoom
|
import de.jeanlucmakiola.calendula.ui.common.LocalTimelineZoom
|
||||||
import de.jeanlucmakiola.calendula.ui.common.MIN_EVENT_FRACTION
|
import de.jeanlucmakiola.calendula.ui.common.MIN_EVENT_FRACTION
|
||||||
import de.jeanlucmakiola.calendula.ui.common.asEventTime
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.hourHeight
|
import de.jeanlucmakiola.calendula.ui.common.hourHeight
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberTimelinePinchZoom
|
import de.jeanlucmakiola.calendula.ui.common.rememberTimelinePinchZoom
|
||||||
import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
||||||
@@ -738,33 +737,25 @@ private fun EventBlock(
|
|||||||
val timeLineHeight = with(density) {
|
val timeLineHeight = with(density) {
|
||||||
MaterialTheme.typography.labelSmall.lineHeight.toDp()
|
MaterialTheme.typography.labelSmall.lineHeight.toDp()
|
||||||
}
|
}
|
||||||
// What's left for text once the 2.dp top/bottom padding is paid for. A block
|
// A block that cannot afford both lines spends its space on the title, and
|
||||||
// that cannot afford both lines spends its space on the title, and one too
|
// one too short even for that drops the title rather than serving a sliced one.
|
||||||
// short even for that drops the title rather than serving a sliced one.
|
|
||||||
// Height alone decides: a duration threshold would keep hiding the time on a
|
// Height alone decides: a duration threshold would keep hiding the time on a
|
||||||
// half-hour block the user has pinched open to three times the room it needs.
|
// half-hour block the user has pinched open to three times the room it needs.
|
||||||
val available = height - 4.dp
|
val available = height - BLOCK_TEXT_INSET * 2
|
||||||
val showTime = available >= titleLineHeight + timeLineHeight
|
val showTime = available >= titleLineHeight + timeLineHeight
|
||||||
val showTitle = available >= titleLineHeight
|
val showTitle = available >= titleLineHeight
|
||||||
val textWidth = width - (BLOCK_OUTER_INSET + BLOCK_TEXT_PADDING) * 2
|
val textWidth = width - (BLOCK_OUTER_INSET + BLOCK_TEXT_PADDING) * 2
|
||||||
val titleMaxLines = if (showTime) 1 else 2
|
// Only lines the block can actually draw: a block too short for the time is
|
||||||
// The range only wraps out of a line the title has not claimed, which on a
|
// too short for a second title line too, and asking for one served a sliced
|
||||||
// day column — wide enough for "09:30–11:00" several times over — means it
|
// one — as well as handing the drag copy a count it couldn't honour, so the
|
||||||
// never does, until lanes cut the column down.
|
// title re-wrapped the moment the block was lifted (#267).
|
||||||
|
val titleBudget = (available / titleLineHeight).toInt().coerceAtLeast(1)
|
||||||
|
val titleMaxLines = if (showTime) 1 else titleBudget.coerceAtMost(2)
|
||||||
|
// On a day column — wide enough for "09:30–11:00" several times over — the
|
||||||
|
// range never needs the second line, until lanes cut the column down.
|
||||||
val spare = available - titleLineHeight * titleMaxLines -
|
val spare = available - titleLineHeight * titleMaxLines -
|
||||||
if (showTime) timeLineHeight else 0.dp
|
if (showTime) timeLineHeight else 0.dp
|
||||||
val timeMaxLines = if (showTime && spare >= timeLineHeight) {
|
val timeMaxLines = if (showTime) blockTimeLines(timeLabel, textWidth, spare) else 1
|
||||||
blockTextLines(
|
|
||||||
text = timeLabel,
|
|
||||||
// The style it is drawn in, or the budget measures a line the label
|
|
||||||
// never uses (#219).
|
|
||||||
style = MaterialTheme.typography.labelSmall.asEventTime(),
|
|
||||||
textWidth = textWidth,
|
|
||||||
max = MAX_TIME_LINES,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
}
|
|
||||||
val paint = eventPaint(block.event, dark)
|
val paint = eventPaint(block.event, dark)
|
||||||
val zone = remember { TimeZone.currentSystemDefault() }
|
val zone = remember { TimeZone.currentSystemDefault() }
|
||||||
val moveAction = eventMoveAction(block.event)
|
val moveAction = eventMoveAction(block.event)
|
||||||
@@ -780,7 +771,13 @@ private fun EventBlock(
|
|||||||
enabled = draggable,
|
enabled = draggable,
|
||||||
key = block.event.instanceId,
|
key = block.event.instanceId,
|
||||||
onPickUp = { pointer, blockRoot, _ ->
|
onPickUp = { pointer, blockRoot, _ ->
|
||||||
dragController.begin(block, clipOffset, pointer, blockRoot)
|
dragController.begin(
|
||||||
|
block = block,
|
||||||
|
clipOffsetMin = clipOffset,
|
||||||
|
titleLines = if (showTitle) titleMaxLines else 0,
|
||||||
|
pointerInRoot = pointer,
|
||||||
|
blockInRoot = blockRoot,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onMove = dragController::move,
|
onMove = dragController::move,
|
||||||
onDrop = { dragController.finish()?.let(onDrop) },
|
onDrop = { dragController.finish()?.let(onDrop) },
|
||||||
@@ -797,7 +794,7 @@ private fun EventBlock(
|
|||||||
// After clickable, so it is the inner node and wins the main pass;
|
// After clickable, so it is the inner node and wins the main pass;
|
||||||
// the tap still works, since a drag consumes the up.
|
// the tap still works, since a drag consumes the up.
|
||||||
.then(dragModifier)
|
.then(dragModifier)
|
||||||
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = 2.dp)
|
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = BLOCK_TEXT_INSET)
|
||||||
.semantics {
|
.semantics {
|
||||||
contentDescription = "$title, $timeLabel"
|
contentDescription = "$title, $timeLabel"
|
||||||
if (moveAction != null) customActions = listOf(moveAction)
|
if (moveAction != null) customActions = listOf(moveAction)
|
||||||
@@ -808,7 +805,6 @@ private fun EventBlock(
|
|||||||
BlockTitle(
|
BlockTitle(
|
||||||
title = title,
|
title = title,
|
||||||
maxLines = titleMaxLines,
|
maxLines = titleMaxLines,
|
||||||
textWidth = textWidth,
|
|
||||||
color = paint.titleInk,
|
color = paint.titleInk,
|
||||||
textDecoration = paint.decoration,
|
textDecoration = paint.decoration,
|
||||||
fontWeight = paint.titleWeight,
|
fontWeight = paint.titleWeight,
|
||||||
|
|||||||
@@ -91,10 +91,11 @@ import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||||
import de.jeanlucmakiola.calendula.ui.common.BLOCK_OUTER_INSET
|
import de.jeanlucmakiola.calendula.ui.common.BLOCK_OUTER_INSET
|
||||||
import de.jeanlucmakiola.calendula.ui.common.BLOCK_TEXT_PADDING
|
import de.jeanlucmakiola.calendula.ui.common.BLOCK_TEXT_PADDING
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.BLOCK_TEXT_INSET
|
||||||
import de.jeanlucmakiola.calendula.ui.common.BlockTimeLabel
|
import de.jeanlucmakiola.calendula.ui.common.BlockTimeLabel
|
||||||
import de.jeanlucmakiola.calendula.ui.common.BlockTitle
|
import de.jeanlucmakiola.calendula.ui.common.BlockTitle
|
||||||
import de.jeanlucmakiola.calendula.ui.common.MAX_TIME_LINES
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.blockTextLines
|
import de.jeanlucmakiola.calendula.ui.common.blockTextLines
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.blockTimeLines
|
||||||
import de.jeanlucmakiola.calendula.ui.common.animatedBlockPlacement
|
import de.jeanlucmakiola.calendula.ui.common.animatedBlockPlacement
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ghostAlpha
|
import de.jeanlucmakiola.calendula.ui.common.ghostAlpha
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
||||||
@@ -129,8 +130,6 @@ import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalTimelineZoom
|
import de.jeanlucmakiola.calendula.ui.common.LocalTimelineZoom
|
||||||
import de.jeanlucmakiola.calendula.ui.common.MIN_EVENT_FRACTION
|
import de.jeanlucmakiola.calendula.ui.common.MIN_EVENT_FRACTION
|
||||||
import de.jeanlucmakiola.calendula.ui.common.MIN_TITLE_WRAP_WIDTH
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.asEventTime
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.hourHeight
|
import de.jeanlucmakiola.calendula.ui.common.hourHeight
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberTimelinePinchZoom
|
import de.jeanlucmakiola.calendula.ui.common.rememberTimelinePinchZoom
|
||||||
import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
||||||
@@ -883,8 +882,7 @@ private fun EventBlock(
|
|||||||
val timeLineHeight = with(density) {
|
val timeLineHeight = with(density) {
|
||||||
MaterialTheme.typography.labelSmall.lineHeight.toDp()
|
MaterialTheme.typography.labelSmall.lineHeight.toDp()
|
||||||
}
|
}
|
||||||
// What's left for text once the 2.dp top/bottom padding is paid for.
|
val available = height - BLOCK_TEXT_INSET * 2
|
||||||
val available = height - 4.dp
|
|
||||||
// Only full-width (non-overlapping) blocks that are tall enough show the
|
// Only full-width (non-overlapping) blocks that are tall enough show the
|
||||||
// time. On narrow overlapping columns we drop it so the title can wrap to
|
// time. On narrow overlapping columns we drop it so the title can wrap to
|
||||||
// fill the whole block, mirroring Google Calendar — and a block that cannot
|
// fill the whole block, mirroring Google Calendar — and a block that cannot
|
||||||
@@ -906,35 +904,20 @@ private fun EventBlock(
|
|||||||
val contentHeight = available - if (showTime) timeLineHeight else 0.dp
|
val contentHeight = available - if (showTime) timeLineHeight else 0.dp
|
||||||
val titleBudget = (contentHeight / titleLineHeight).toInt().coerceAtLeast(1)
|
val titleBudget = (contentHeight / titleLineHeight).toInt().coerceAtLeast(1)
|
||||||
val paint = eventPaint(block.event, dark)
|
val paint = eventPaint(block.event, dark)
|
||||||
val titleMaxLines = if (width < MIN_TITLE_WRAP_WIDTH) {
|
// Every line the height affords, however narrow the lane: two events side by
|
||||||
1
|
// side leave columns well under a word wide, and cutting the title to one
|
||||||
} else {
|
// line there lost it outright where the block had the room to wrap it.
|
||||||
blockTextLines(
|
val titleMaxLines = blockTextLines(
|
||||||
text = title,
|
text = title,
|
||||||
// At the weight BlockTitle will set it in, or an invited block —
|
// At the weight BlockTitle will set it in, or an invited block — drawn a
|
||||||
// drawn a weight lighter — is budgeted a line it never fills (#230).
|
// weight lighter — is budgeted a line it never fills (#230).
|
||||||
style = MaterialTheme.typography.labelMedium.withTitleWeight(paint),
|
style = MaterialTheme.typography.labelMedium.withTitleWeight(paint),
|
||||||
textWidth = textWidth,
|
textWidth = textWidth,
|
||||||
max = titleBudget,
|
max = titleBudget,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
// A week column is narrower than a "09:30–11:00" range, so the label takes a
|
|
||||||
// second line rather than lose its end — but only out of a line the title
|
|
||||||
// measured itself as not needing, never one it would have filled.
|
|
||||||
val spare = available - titleLineHeight * titleMaxLines -
|
val spare = available - titleLineHeight * titleMaxLines -
|
||||||
if (showTime) timeLineHeight else 0.dp
|
if (showTime) timeLineHeight else 0.dp
|
||||||
val timeMaxLines = if (showTime && spare >= timeLineHeight) {
|
val timeMaxLines = if (showTime) blockTimeLines(timeLabel, textWidth, spare) else 1
|
||||||
blockTextLines(
|
|
||||||
text = timeLabel,
|
|
||||||
// The style it is drawn in, or the budget measures a line the label
|
|
||||||
// never uses (#219).
|
|
||||||
style = MaterialTheme.typography.labelSmall.asEventTime(),
|
|
||||||
textWidth = textWidth,
|
|
||||||
max = MAX_TIME_LINES,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
}
|
|
||||||
val dimCutoff = LocalDimCutoff.current
|
val dimCutoff = LocalDimCutoff.current
|
||||||
val dimmed = dimCutoff != null && block.event.hasEnded(dimCutoff)
|
val dimmed = dimCutoff != null && block.event.hasEnded(dimCutoff)
|
||||||
val zone = remember { TimeZone.currentSystemDefault() }
|
val zone = remember { TimeZone.currentSystemDefault() }
|
||||||
@@ -951,7 +934,13 @@ private fun EventBlock(
|
|||||||
enabled = draggable,
|
enabled = draggable,
|
||||||
key = block.event.instanceId,
|
key = block.event.instanceId,
|
||||||
onPickUp = { pointer, blockRoot, _ ->
|
onPickUp = { pointer, blockRoot, _ ->
|
||||||
dragController.begin(block, clipOffset, pointer, blockRoot)
|
dragController.begin(
|
||||||
|
block = block,
|
||||||
|
clipOffsetMin = clipOffset,
|
||||||
|
titleLines = if (showTitle) titleMaxLines else 0,
|
||||||
|
pointerInRoot = pointer,
|
||||||
|
blockInRoot = blockRoot,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onMove = dragController::move,
|
onMove = dragController::move,
|
||||||
onDrop = { dragController.finish()?.let(onDrop) },
|
onDrop = { dragController.finish()?.let(onDrop) },
|
||||||
@@ -968,7 +957,7 @@ private fun EventBlock(
|
|||||||
// After clickable, so it is the inner node and wins the main pass;
|
// After clickable, so it is the inner node and wins the main pass;
|
||||||
// the tap still works, since a drag consumes the up.
|
// the tap still works, since a drag consumes the up.
|
||||||
.then(dragModifier)
|
.then(dragModifier)
|
||||||
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = 2.dp)
|
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = BLOCK_TEXT_INSET)
|
||||||
.semantics {
|
.semantics {
|
||||||
contentDescription = "$title, $timeLabel"
|
contentDescription = "$title, $timeLabel"
|
||||||
if (moveAction != null) customActions = listOf(moveAction)
|
if (moveAction != null) customActions = listOf(moveAction)
|
||||||
@@ -979,7 +968,6 @@ private fun EventBlock(
|
|||||||
BlockTitle(
|
BlockTitle(
|
||||||
title = title,
|
title = title,
|
||||||
maxLines = titleMaxLines,
|
maxLines = titleMaxLines,
|
||||||
textWidth = textWidth,
|
|
||||||
color = paint.titleInk,
|
color = paint.titleInk,
|
||||||
textDecoration = paint.decoration,
|
textDecoration = paint.decoration,
|
||||||
fontWeight = paint.titleWeight,
|
fontWeight = paint.titleWeight,
|
||||||
|
|||||||
@@ -25,16 +25,16 @@ class EventTitleOverflowTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `a multi-line block keeps the ellipsis, since wrapping needs softWrap`() {
|
fun `a multi-line block wraps whole words and clips, spending none on a dot`() {
|
||||||
val result = eventTitleOverflowFor(rtl = false, singleLine = false)
|
val result = eventTitleOverflowFor(rtl = false, singleLine = false)
|
||||||
assertThat(result.overflow).isEqualTo(TextOverflow.Ellipsis)
|
assertThat(result.overflow).isEqualTo(TextOverflow.Clip)
|
||||||
assertThat(result.softWrap).isTrue()
|
assertThat(result.softWrap).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `multi-line in RTL keeps the ellipsis too`() {
|
fun `multi-line in RTL clips too, since softWrap cuts the logical end`() {
|
||||||
val result = eventTitleOverflowFor(rtl = true, singleLine = false)
|
val result = eventTitleOverflowFor(rtl = true, singleLine = false)
|
||||||
assertThat(result.overflow).isEqualTo(TextOverflow.Ellipsis)
|
assertThat(result.overflow).isEqualTo(TextOverflow.Clip)
|
||||||
assertThat(result.softWrap).isTrue()
|
assertThat(result.softWrap).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user