Compare commits
3 Commits
release/v2
...
fix/267-dr
| Author | SHA1 | Date | |
|---|---|---|---|
| c2fbfb0bef | |||
| 8db99868af | |||
| 3a29e6f676 |
@@ -33,6 +33,9 @@ val BLOCK_OUTER_INSET = 1.dp
|
||||
/** Padding between a timed block's edge and its text. */
|
||||
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. */
|
||||
const val MAX_TIME_LINES = 2
|
||||
|
||||
@@ -61,6 +64,33 @@ fun blockTextLines(text: String, style: TextStyle, textWidth: Dp, max: Int): Int
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
@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].
|
||||
*
|
||||
|
||||
@@ -81,6 +81,13 @@ data class TimelineDrag(
|
||||
val eventSpanMin: Int,
|
||||
/** The event as the grid would draw it, one piece per day column it covers. */
|
||||
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
|
||||
* every column this timeline shows. A hint under the finger, not where the
|
||||
@@ -311,6 +318,9 @@ class TimelineDragController {
|
||||
*/
|
||||
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
|
||||
* as its start, since the target snaps to the grid (09:07 lifts to 09:00).
|
||||
@@ -320,11 +330,13 @@ class TimelineDragController {
|
||||
fun begin(
|
||||
block: TimedBlock,
|
||||
clipOffsetMin: Int,
|
||||
titleLines: Int,
|
||||
pointerInRoot: Offset,
|
||||
blockInRoot: Offset,
|
||||
) {
|
||||
source = block
|
||||
this.clipOffsetMin = clipOffsetMin
|
||||
this.titleLines = titleLines
|
||||
settling = null
|
||||
isDragging = true
|
||||
liftedInstanceId = block.event.instanceId
|
||||
@@ -343,6 +355,7 @@ class TimelineDragController {
|
||||
fun cancel() {
|
||||
source = null
|
||||
clipOffsetMin = 0
|
||||
titleLines = 0
|
||||
isDragging = false
|
||||
liftedInstanceId = null
|
||||
originSlot = null
|
||||
@@ -465,6 +478,7 @@ class TimelineDragController {
|
||||
startMin = startMin,
|
||||
eventStartMin = eventStartMin,
|
||||
eventSpanMin = eventSpan,
|
||||
titleLines = titleLines,
|
||||
// Bounded to the columns this timeline actually shows: a day it
|
||||
// doesn't has no piece to draw. The write is unaffected.
|
||||
pieces = slices
|
||||
@@ -677,6 +691,7 @@ fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier =
|
||||
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 +711,28 @@ private fun DragCopy(
|
||||
lift: Float,
|
||||
alpha: Float,
|
||||
title: String,
|
||||
titleLines: Int,
|
||||
label: String?,
|
||||
) {
|
||||
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, so nothing reflows under the
|
||||
// finger (#267) — but the range is paid for first, since it may be showing
|
||||
// where a block too short for it will land, and nothing clips the overrun.
|
||||
val available = height - BLOCK_TEXT_INSET * 2
|
||||
val reserved = if (label == null) 0.dp else timeLineHeight
|
||||
val titleBudget = ((available - reserved) / titleLineHeight).toInt().coerceAtLeast(0)
|
||||
val lines = titleLines.coerceAtMost(titleBudget)
|
||||
val spare = available - titleLineHeight * lines - reserved
|
||||
val timeMaxLines = if (label == null) 1 else blockTimeLines(label, textWidth, spare)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
// Absolute: these are root coordinates, and the direction-aware
|
||||
@@ -709,10 +743,7 @@ private fun DragCopy(
|
||||
(topLeftInRoot.y - overlayOrigin.y).roundToInt(),
|
||||
)
|
||||
}
|
||||
.size(
|
||||
width = with(density) { sizePx.width.toDp() },
|
||||
height = with(density) { sizePx.height.toDp() },
|
||||
)
|
||||
.size(width = width, height = height)
|
||||
.padding(horizontal = BLOCK_OUTER_INSET)
|
||||
.graphicsLayer {
|
||||
scaleX = 1f + 0.02f * lift
|
||||
@@ -723,28 +754,28 @@ private fun DragCopy(
|
||||
clip = false
|
||||
}
|
||||
.eventSurface(paint, shape, cuts)
|
||||
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = 2.dp),
|
||||
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = BLOCK_TEXT_INSET),
|
||||
) {
|
||||
Column {
|
||||
val titleOverflow = eventTitleOverflow()
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 1,
|
||||
overflow = titleOverflow.overflow,
|
||||
softWrap = titleOverflow.softWrap,
|
||||
color = paint.titleInk,
|
||||
fontWeight = paint.titleWeight,
|
||||
textDecoration = paint.decoration,
|
||||
)
|
||||
if (lines > 0) {
|
||||
BlockTitle(
|
||||
title = title,
|
||||
maxLines = lines,
|
||||
textWidth = textWidth,
|
||||
color = paint.titleInk,
|
||||
textDecoration = paint.decoration,
|
||||
fontWeight = paint.titleWeight,
|
||||
)
|
||||
}
|
||||
if (label != null) {
|
||||
val overflow = eventTitleOverflow(singleLine = timeMaxLines == 1)
|
||||
Text(
|
||||
text = label,
|
||||
// As the block it lifted off sets its own time (#219).
|
||||
style = MaterialTheme.typography.labelSmall.asEventTime(),
|
||||
maxLines = 1,
|
||||
overflow = titleOverflow.overflow,
|
||||
softWrap = titleOverflow.softWrap,
|
||||
maxLines = timeMaxLines,
|
||||
overflow = overflow.overflow,
|
||||
softWrap = overflow.softWrap,
|
||||
color = paint.secondaryInk,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.BLOCK_OUTER_INSET
|
||||
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.BlockTitle
|
||||
import de.jeanlucmakiola.calendula.ui.common.MAX_TIME_LINES
|
||||
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.ghostAlpha
|
||||
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.LocalTimelineZoom
|
||||
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.rememberTimelinePinchZoom
|
||||
import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
||||
@@ -738,33 +737,20 @@ private fun EventBlock(
|
||||
val timeLineHeight = with(density) {
|
||||
MaterialTheme.typography.labelSmall.lineHeight.toDp()
|
||||
}
|
||||
// What's left for text once the 2.dp top/bottom padding is paid for. A block
|
||||
// that cannot afford both lines spends its space on the title, and one too
|
||||
// short even for that drops the title rather than serving a sliced one.
|
||||
// A block that cannot afford both lines spends its space on the title, and
|
||||
// one too 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
|
||||
// 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 showTitle = available >= titleLineHeight
|
||||
val textWidth = width - (BLOCK_OUTER_INSET + BLOCK_TEXT_PADDING) * 2
|
||||
val titleMaxLines = if (showTime) 1 else 2
|
||||
// The range only wraps out of a line the title has not claimed, which on a
|
||||
// day column — wide enough for "09:30–11:00" several times over — means it
|
||||
// never does, until lanes cut the column down.
|
||||
// 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 -
|
||||
if (showTime) timeLineHeight else 0.dp
|
||||
val timeMaxLines = if (showTime && spare >= timeLineHeight) {
|
||||
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 timeMaxLines = if (showTime) blockTimeLines(timeLabel, textWidth, spare) else 1
|
||||
val paint = eventPaint(block.event, dark)
|
||||
val zone = remember { TimeZone.currentSystemDefault() }
|
||||
val moveAction = eventMoveAction(block.event)
|
||||
@@ -780,7 +766,13 @@ private fun EventBlock(
|
||||
enabled = draggable,
|
||||
key = block.event.instanceId,
|
||||
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,
|
||||
onDrop = { dragController.finish()?.let(onDrop) },
|
||||
@@ -797,7 +789,7 @@ private fun EventBlock(
|
||||
// After clickable, so it is the inner node and wins the main pass;
|
||||
// the tap still works, since a drag consumes the up.
|
||||
.then(dragModifier)
|
||||
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = 2.dp)
|
||||
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = BLOCK_TEXT_INSET)
|
||||
.semantics {
|
||||
contentDescription = "$title, $timeLabel"
|
||||
if (moveAction != null) customActions = listOf(moveAction)
|
||||
|
||||
@@ -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.BLOCK_OUTER_INSET
|
||||
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.BlockTitle
|
||||
import de.jeanlucmakiola.calendula.ui.common.MAX_TIME_LINES
|
||||
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.ghostAlpha
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
||||
@@ -130,7 +131,6 @@ import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalTimelineZoom
|
||||
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.rememberTimelinePinchZoom
|
||||
import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
||||
@@ -883,8 +883,7 @@ private fun EventBlock(
|
||||
val timeLineHeight = with(density) {
|
||||
MaterialTheme.typography.labelSmall.lineHeight.toDp()
|
||||
}
|
||||
// What's left for text once the 2.dp top/bottom padding is paid for.
|
||||
val available = height - 4.dp
|
||||
val available = height - BLOCK_TEXT_INSET * 2
|
||||
// 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
|
||||
// fill the whole block, mirroring Google Calendar — and a block that cannot
|
||||
@@ -918,23 +917,9 @@ private fun EventBlock(
|
||||
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 -
|
||||
if (showTime) timeLineHeight else 0.dp
|
||||
val timeMaxLines = if (showTime && spare >= timeLineHeight) {
|
||||
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 timeMaxLines = if (showTime) blockTimeLines(timeLabel, textWidth, spare) else 1
|
||||
val dimCutoff = LocalDimCutoff.current
|
||||
val dimmed = dimCutoff != null && block.event.hasEnded(dimCutoff)
|
||||
val zone = remember { TimeZone.currentSystemDefault() }
|
||||
@@ -951,7 +936,13 @@ private fun EventBlock(
|
||||
enabled = draggable,
|
||||
key = block.event.instanceId,
|
||||
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,
|
||||
onDrop = { dragController.finish()?.let(onDrop) },
|
||||
@@ -968,7 +959,7 @@ private fun EventBlock(
|
||||
// After clickable, so it is the inner node and wins the main pass;
|
||||
// the tap still works, since a drag consumes the up.
|
||||
.then(dragModifier)
|
||||
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = 2.dp)
|
||||
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = BLOCK_TEXT_INSET)
|
||||
.semantics {
|
||||
contentDescription = "$title, $timeLabel"
|
||||
if (moveAction != null) customActions = listOf(moveAction)
|
||||
|
||||
Reference in New Issue
Block a user