Compare commits

..
Author SHA1 Message Date
renovate-bot 7c67e94d93 fix(deps): update lifecyclecompose to v2.11.0 2026-09-21 05:02:50 +00:00
9 changed files with 23 additions and 138 deletions
@@ -1,41 +0,0 @@
package de.jeanlucmakiola.calendula.ui.common
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
/**
* The calendar surfaces' line-height treatment. Material wraps an 11sp glyph in
* a 16sp line box — room a label wants when it stands on its own, and close to
* a fifth of an event row when it doesn't. The outer edges only, so a wrapped
* title keeps its interior line spacing (#190).
*
* Trimmed rather than set to a smaller line height: the font picker can load a
* serif, a monospace or a file of the user's own, and a line height under a
* face's own ascent and descent overlaps its lines. There is nothing to trim
* below that, so this is safe whatever font is chosen.
*/
private val TrimmedLines = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.Both,
)
/** Ascenders and descenders both, so a line is measured at its full extent. */
private const val LINE_SAMPLE = "Ag"
/** [this] with Material's outer leading trimmed — see [TrimmedLines]. */
fun TextStyle.trimmedLines(): TextStyle = copy(lineHeightStyle = TrimmedLines)
/** What one trimmed line of [style] actually draws in. */
@Composable
fun rememberTrimmedLineHeight(style: TextStyle): Dp {
val measurer = rememberTextMeasurer()
val density = LocalDensity.current
return remember(style, density, measurer) {
with(density) { measurer.measure(LINE_SAMPLE, style.trimmedLines()).size.height.toDp() }
}
}
@@ -19,6 +19,7 @@ import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.Dp
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.roundToInt
@@ -196,26 +197,18 @@ fun rememberTimelinePinchZoom(
* names, jumping the whole column as a pinch drifts across each half pixel.
* Pinning the hour to whole pixels keeps every part of the timeline on one grid.
*
* The ceiling is pulled onto that grid too, downwards, so it stays a height the
* pinch can actually land on.
*
* [fillPx] is deliberately *not* rounded (#290). It is the one height the whole
* day exactly fills the viewport at, and it is the same value
* [TimelineScale.FitDay] resolves to — rounding it up by the fraction of a pixel
* that 24 hours don't divide the viewport into leaves the timeline a pixel per
* hour taller than its own viewport, so a pinched-all-the-way-out day still
* scrolls a hair and bounces off Android's overscroll stretch, while the
* identical FitDay preset sits still. Being the clamp result rather than a bound
* the gesture is merely held against, it is a height the pinch does land on: the
* next frame reads it back unchanged and the focal anchor is handed nothing to
* correct.
* The bounds themselves are pulled onto that grid too, each in the direction
* that keeps its own promise — up for the fill floor, so no dead space opens
* under midnight, down for the ceiling. A fractional bound would be a height the
* pinch can be held against but never actually land on, and the difference feeds
* the focal anchor a scroll correction on every frame the fingers sit still.
*/
internal fun pinchedHourHeightPx(target: Float, fillPx: Float, maxPx: Float): Float =
// Filling the viewport wins over the ceiling: on a screen tall enough for
// the two to disagree, dead space is the worse of the two failures.
target.roundToInt().toFloat()
.coerceAtMost(floor(maxPx))
.coerceAtLeast(fillPx)
.coerceAtLeast(ceil(fillPx))
/**
* The scroll offset that keeps the moment under [centroidY] under it after the
@@ -115,7 +115,6 @@ import de.jeanlucmakiola.calendula.ui.common.rememberCalendarPageSwipe
import de.jeanlucmakiola.floret.identity.rememberReduceMotion
import de.jeanlucmakiola.calendula.ui.common.next
import de.jeanlucmakiola.calendula.ui.common.EventChipShape
import de.jeanlucmakiola.calendula.ui.common.trimmedLines
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
import de.jeanlucmakiola.floret.locale.currentLocale
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
@@ -141,8 +140,7 @@ import kotlin.time.Clock
import java.util.Locale
import kotlin.math.roundToInt
/** One lane of the all-day strip, sized to a trimmed bar line (#190). */
private val ALL_DAY_ROW_HEIGHT = 20.dp
private val ALL_DAY_ROW_HEIGHT = 24.dp
private val ALL_DAY_VERTICAL_PADDING = 6.dp
/** Total all-day strip height for the day (0 when there are no all-day events). */
@@ -531,7 +529,7 @@ private fun AllDayBar(
val titleOverflow = eventTitleOverflow()
Text(
text = title,
style = MaterialTheme.typography.labelSmall.trimmedLines(),
style = MaterialTheme.typography.labelSmall,
maxLines = 1,
overflow = titleOverflow.overflow,
softWrap = titleOverflow.softWrap,
@@ -153,7 +153,6 @@ import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
import de.jeanlucmakiola.calendula.ui.common.inlineTimeLabel
import de.jeanlucmakiola.calendula.ui.common.eventAccent
import de.jeanlucmakiola.calendula.ui.common.EventChipShape
import de.jeanlucmakiola.calendula.ui.common.trimmedLines
import de.jeanlucmakiola.calendula.ui.common.monthBarShape
import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
@@ -761,12 +760,7 @@ internal fun WeekdayHeader(weekStart: DayOfWeek, showWeekNumbers: Boolean) {
}
}
/**
* One lane of a day cell. Sized to a trimmed chip line rather than to Material's
* line box, so the three lanes a cell can draw cost it nine dp less and a
* six-row month keeps its third chip instead of demoting it to a dot (#190).
*/
private val EVENT_ROW_HEIGHT = 17.dp
private val EVENT_ROW_HEIGHT = 20.dp
private val DAY_NUMBER_HEIGHT = 22.dp
/** Width of the optional left calendar-week gutter (#25); narrow, since it only
* seats a one- or two-digit week number in a full-height tonal pill. */
@@ -2550,7 +2544,7 @@ private fun MonthBar(
val titleOverflow = eventTitleOverflow()
Text(
text = label,
style = MaterialTheme.typography.labelSmall.trimmedLines(),
style = MaterialTheme.typography.labelSmall,
maxLines = 1,
overflow = titleOverflow.overflow,
softWrap = titleOverflow.softWrap,
@@ -1,24 +1,10 @@
package de.jeanlucmakiola.calendula.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.unit.sp
/**
* Tracking the two label roles the calendar grids are set in. Material gives
* both 0.5sp, tuned for isolated UI labels with room around them; a month chip
* is a text box some 37dp wide, where 0.5sp on an 11sp glyph spends most of a
* character on spacing alone. 0.1sp is what Material itself sets labelLarge to,
* so the label family stays coherent (#190).
* Default Material 3 Expressive typography. Custom font + tuned scale will
* land in a later UI-design iteration; the defaults are intentional for V1
* scaffolding to keep the foundation lean.
*/
private val LabelTracking = 0.1.sp
/**
* Material 3 Expressive typography with the label roles' tracking tightened.
* Everything else is the default scale.
*/
val CalendulaTypography: Typography = Typography().let { base ->
base.copy(
labelMedium = base.labelMedium.copy(letterSpacing = LabelTracking),
labelSmall = base.labelSmall.copy(letterSpacing = LabelTracking),
)
}
val CalendulaTypography = Typography()
@@ -118,7 +118,6 @@ import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController
import de.jeanlucmakiola.calendula.ui.common.startInstant
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
import de.jeanlucmakiola.calendula.ui.common.EventChipShape
import de.jeanlucmakiola.calendula.ui.common.trimmedLines
import de.jeanlucmakiola.calendula.ui.common.NowLine
import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
@@ -156,8 +155,7 @@ import kotlin.time.Clock
import java.time.format.TextStyle as JavaTextStyle
import java.util.Locale
/** One lane of the all-day strip, sized to a trimmed bar line (#190). */
private val ALL_DAY_ROW_HEIGHT = 20.dp
private val ALL_DAY_ROW_HEIGHT = 24.dp
private val ALL_DAY_VERTICAL_PADDING = 6.dp
/** Gap between day columns; part of the column pitch a drag maps positions through. */
private val COLUMN_GAP = 2.dp
@@ -666,7 +664,7 @@ private fun AllDayBar(
val titleOverflow = eventTitleOverflow()
Text(
text = title,
style = MaterialTheme.typography.labelSmall.trimmedLines(),
style = MaterialTheme.typography.labelSmall,
maxLines = 1,
overflow = titleOverflow.overflow,
softWrap = titleOverflow.softWrap,
@@ -1,6 +1,5 @@
package de.jeanlucmakiola.calendula.ui.common
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
@@ -76,16 +75,17 @@ class TimelineZoomTest {
}
@Test
fun `a pinch held against either bound stays put`() {
fun `a pinch held against a fractional bound stays put`() {
// A bound that is not a whole pixel is a height the pinch can be pushed
// against but never land on, so every frame of a held gesture would look
// like a scale change and hand the focal anchor a scroll correction.
val fillPx = 62.083f
val maxPx = 616.5f
val floor = pinchedHourHeightPx(target = 1f, fillPx, maxPx)
val ceiling = pinchedHourHeightPx(target = 9_000f, fillPx, maxPx)
// The ceiling is pulled onto the pixel grid so it stays landable; the
// fill floor is landable as it is, being the clamp result itself.
assertThat(floor).isEqualTo(fillPx)
assertThat(floor).isEqualTo(63f)
assertThat(ceiling).isEqualTo(616f)
// Landing there and being pushed further must not move them again.
assertThat(pinchedHourHeightPx(floor * 0.9f, fillPx, maxPx)).isEqualTo(floor)
@@ -100,35 +100,6 @@ class TimelineZoomTest {
assertThat(floor * 24).isAtLeast(viewport)
}
@Test
fun `pinching all the way out leaves nothing to scroll`() {
// #290: rounding the fill floor up to a whole pixel made the day one
// pixel per hour taller than the viewport it was supposed to fill, so a
// fully zoomed-out timeline still scrolled a hair and bounced off
// Android's overscroll stretch -- while FitDay, at the same zoom, sat
// still. 1490 is deliberately not divisible by 24.
val viewport = 1490f
val floor = pinchedHourHeightPx(target = 1f, fillPx = viewport / 24f, maxPx = 616f)
assertThat(floor * 24).isWithin(0.01f).of(viewport)
}
@Test
fun `the pinch floor is the height FitDay resolves to`() {
// The inconsistency the issue is about: the two ways to reach "the whole
// day on one screen" have to arrive at the same height.
val density = Density(2.5f)
val viewport = 596.dp
with(density) {
val fitDay = TimelineScale.FitDay.hourHeight(viewport).toPx()
val pinched = pinchedHourHeightPx(
target = 1f,
fillPx = fillHourHeight(viewport).toPx(),
maxPx = MAX_PINCH_HOUR_HEIGHT.toPx(),
)
assertThat(pinched).isWithin(0.01f).of(fitDay)
}
}
@Test
fun `a settled pinch is what gets persisted`() {
var persisted: TimelineScale? = null
@@ -1,6 +1,5 @@
package de.jeanlucmakiola.calendula.ui.theme
import androidx.compose.ui.unit.sp
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
@@ -49,19 +48,6 @@ class FontsTest {
assertThat(typography.labelSmall.fontFamily).isEqualTo(plain)
}
@Test
fun `picking a font keeps the tightened label tracking`() {
// The font picker only swaps the family; the tracking the grids are laid
// out against has to survive it (#190).
val typography = calendulaTypography(
brand = BundledFont.Lora.family,
plain = BundledFont.AtkinsonHyperlegible.family,
)
assertThat(typography.labelSmall.letterSpacing).isEqualTo(0.1.sp)
assertThat(typography.labelMedium.letterSpacing).isEqualTo(0.1.sp)
}
@Test
fun `a null role keeps that role's default family while the other is applied`() {
val plain = BundledFont.Lora.family
+1 -1
View File
@@ -22,7 +22,7 @@ kotlinxDatetime = "0.7.0"
kotlinxCoroutines = "1.11.0"
turbine = "1.2.1"
hiltNavigationCompose = "1.4.0"
lifecycleCompose = "2.10.0"
lifecycleCompose = "2.11.0"
androidxTestRules = "1.7.0"
# Glance: 1.1.1 is the latest stable (1.2.0 is still rc, 1.3.0 alpha).
glance = "1.1.1"