Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bb91c480b | ||
|
|
49d21903c6 | ||
|
|
9b8da92157 |
@@ -0,0 +1,57 @@
|
||||
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)
|
||||
|
||||
/**
|
||||
* [style] with its wrapped lines packed onto the face's own extent instead of
|
||||
* Material's line box.
|
||||
*
|
||||
* [trimmedLines] takes the leading off the outer edges of a run of text; this
|
||||
* takes it from between the lines as well, which is the half a wrapped event
|
||||
* title pays for twice over. The line height is *measured* from the font rather
|
||||
* than picked, so it lands exactly on the face's ascent-plus-descent and can
|
||||
* never be short enough to overlap — whatever the font picker has loaded (#190).
|
||||
*/
|
||||
@Composable
|
||||
fun rememberPackedLines(style: TextStyle): TextStyle {
|
||||
val line = rememberTrimmedLineHeight(style)
|
||||
return with(LocalDensity.current) { style.trimmedLines().copy(lineHeight = line.toSp()) }
|
||||
}
|
||||
|
||||
/** 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() }
|
||||
}
|
||||
}
|
||||
@@ -83,10 +83,8 @@ fun calendarSlideTransition(
|
||||
initialContentExit =
|
||||
slideOutHorizontally(spec) { w -> -dir * w / SLIDE_TRAVEL_DIVISOR } + fadeOut(fadeSpec),
|
||||
// AnimatedContent clips to the animating container by default, which
|
||||
// shears the pages against the viewport edge as they pass. Left off even
|
||||
// where the two pages differ in height — the split grid stands as many
|
||||
// rows as its month spans (#162) — since a page sliding out over the row
|
||||
// below it reads as travel, and the shear reads as a fault.
|
||||
// shears the pages against the viewport edge as they pass. There is no
|
||||
// size change here to contain — both pages are the same grid.
|
||||
sizeTransform = SizeTransform(clip = false),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ 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
|
||||
@@ -140,7 +141,8 @@ import kotlin.time.Clock
|
||||
import java.util.Locale
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val ALL_DAY_ROW_HEIGHT = 24.dp
|
||||
/** 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_VERTICAL_PADDING = 6.dp
|
||||
|
||||
/** Total all-day strip height for the day (0 when there are no all-day events). */
|
||||
@@ -529,7 +531,7 @@ private fun AllDayBar(
|
||||
val titleOverflow = eventTitleOverflow()
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
style = MaterialTheme.typography.labelSmall.trimmedLines(),
|
||||
maxLines = 1,
|
||||
overflow = titleOverflow.overflow,
|
||||
softWrap = titleOverflow.softWrap,
|
||||
|
||||
@@ -153,6 +153,7 @@ 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
|
||||
@@ -1141,10 +1142,10 @@ private val MONTH_EXPAND_THRESHOLD = 48.dp
|
||||
* lists whatever day is selected — and a downward drag trades the pane away for
|
||||
* the full paged grid, an upward one brings it back (#53).
|
||||
*
|
||||
* The grid slides between months like the paged style, and stands only as many
|
||||
* rows tall as its own month spans (#162). A swipe between a five-row month and
|
||||
* a six-row one therefore moves the pane by a row as well as swapping the grid;
|
||||
* the row it hands back is worth more to the pane than a still edge is.
|
||||
* The grid slides between months like the paged style, which it can only do
|
||||
* because it always reserves [SPLIT_GRID_ROWS] rows. Sized to its own month it
|
||||
* stood 4–6 rows tall, so every swipe shunted the pane up or down by a row on
|
||||
* top of swapping the grid — the pane now holds still and only the grid moves.
|
||||
*
|
||||
* Expansion is deliberately **not** a stored preference. It is a way to look at
|
||||
* the month you are on, not a fourth style; persisted, someone would expand it
|
||||
@@ -1441,6 +1442,13 @@ private val SPLIT_DOT_SIZE = 5.dp
|
||||
// the paged grid's lanes, so the two caps have to be the same number or a dot
|
||||
// would have no bar to become (#53).
|
||||
|
||||
/**
|
||||
* Rows the split grid always reserves — the most any month needs. A month that
|
||||
* fits in fewer pads the remainder with blank rows rather than shrinking, which
|
||||
* is what lets the pane below hold still from month to month.
|
||||
*/
|
||||
private const val SPLIT_GRID_ROWS = 6
|
||||
|
||||
/**
|
||||
* The expand handle: M3's drag-handle pill (32×4dp), in a row tall enough to be a
|
||||
* comfortable tap target on its own.
|
||||
@@ -1453,11 +1461,6 @@ private val SPLIT_HANDLE_ROW_HEIGHT = 24.dp
|
||||
* The split style's grid (#53): the month compressed to day numbers and event
|
||||
* dots, with the selected day listed underneath by [SplitDayPane].
|
||||
*
|
||||
* Only the rows the month actually spans. It used to pad every month out to six
|
||||
* so the pane below held still from page to page, but a row is a sixth of the
|
||||
* grid and a third of what the pane gets to show — too much to leave blank on
|
||||
* the months that don't need it (#162).
|
||||
*
|
||||
* Tapping selects rather than drilling into the Day view — the pane is the
|
||||
* answer to "what's on this day", so opening a whole screen for it would defeat
|
||||
* the layout. The full Day view stays one tap away on the pane's date header.
|
||||
@@ -1516,6 +1519,12 @@ internal fun SplitMonthGrid(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Hold the grid at a constant height whatever shape the month is, so the
|
||||
// pane beneath it doesn't move as you page and one month can slide over
|
||||
// another without a height change under it.
|
||||
repeat(SPLIT_GRID_ROWS - state.weeks.size) {
|
||||
Spacer(Modifier.fillMaxWidth().height(SPLIT_ROW_HEIGHT))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2536,7 +2545,7 @@ private fun MonthBar(
|
||||
val titleOverflow = eventTitleOverflow()
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
style = MaterialTheme.typography.labelSmall.trimmedLines(),
|
||||
maxLines = 1,
|
||||
overflow = titleOverflow.overflow,
|
||||
softWrap = titleOverflow.softWrap,
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
package de.jeanlucmakiola.calendula.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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).
|
||||
*/
|
||||
val CalendulaTypography = Typography()
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ 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
|
||||
@@ -155,7 +156,8 @@ import kotlin.time.Clock
|
||||
import java.time.format.TextStyle as JavaTextStyle
|
||||
import java.util.Locale
|
||||
|
||||
private val ALL_DAY_ROW_HEIGHT = 24.dp
|
||||
/** 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_VERTICAL_PADDING = 6.dp
|
||||
/** Gap between day columns; part of the column pitch a drag maps positions through. */
|
||||
private val COLUMN_GAP = 2.dp
|
||||
@@ -664,7 +666,7 @@ private fun AllDayBar(
|
||||
val titleOverflow = eventTitleOverflow()
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
style = MaterialTheme.typography.labelSmall.trimmedLines(),
|
||||
maxLines = 1,
|
||||
overflow = titleOverflow.overflow,
|
||||
softWrap = titleOverflow.softWrap,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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
|
||||
|
||||
@@ -48,6 +49,19 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user