new: mark muted characters
This commit is contained in:
@@ -36,6 +36,7 @@ import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import fr.ajaury.gwenedeg.core.model.Phrase
|
||||
import fr.ajaury.gwenedeg.player.ui.components.TranscriptionText
|
||||
import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences
|
||||
import fr.ajaury.gwenedeg.subtitle.model.SubtitleLine
|
||||
import fr.ajaury.gwenedeg.theme.ChomBevTheme
|
||||
@@ -194,10 +195,10 @@ private fun TranscriptionLine(
|
||||
text: String,
|
||||
contentColor: Color,
|
||||
) {
|
||||
Text(
|
||||
TranscriptionText(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(fontSize = 20.sp),
|
||||
color = contentColor,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(fontSize = 20.sp),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
@@ -234,8 +235,8 @@ private val previewSubtitleLines =
|
||||
SubtitleLine(
|
||||
startTime = 7350.milliseconds,
|
||||
phrase = Phrase(
|
||||
transcription = "Kenavo emberr !",
|
||||
translation = "À ce soir !",
|
||||
transcription = "Tomm eo ma(n) divskouarn",
|
||||
translation = "J'ai chaud aux oreilles",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package fr.ajaury.gwenedeg.player.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.compose.ui.unit.sp
|
||||
import fr.ajaury.gwenedeg.theme.ChomBevTheme
|
||||
|
||||
/**
|
||||
* Matches a liaison/optional letter written between parentheses in a transcription, e.g. the `n`
|
||||
* in `ma(n)`. The captured group is the letter itself, without the surrounding parentheses.
|
||||
*/
|
||||
private val MUTED_LETTER_REGEX = Regex("""\(([A-Za-z])\)""")
|
||||
|
||||
/** Diacritic drawn above an optional letter to mark that it is only sounded on liaison. */
|
||||
private const val MUTED_LETTER_MARK = "ø"
|
||||
|
||||
/**
|
||||
* Renders a transcription, giving special treatment to letters written between parentheses
|
||||
* (liaison consonants such as the `n` in `ma(n)`): the parentheses are dropped, the letter is
|
||||
* shown in a muted grey and topped with a small [MUTED_LETTER_MARK].
|
||||
*
|
||||
* Optional letters are emitted as inline content so the line still wraps like normal text.
|
||||
*/
|
||||
@Composable
|
||||
fun TranscriptionText(
|
||||
text: String,
|
||||
color: Color,
|
||||
style: TextStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
textAlign: TextAlign = TextAlign.Center,
|
||||
) {
|
||||
val matches = remember(text) { MUTED_LETTER_REGEX.findAll(text).toList() }
|
||||
|
||||
if (matches.isEmpty()) {
|
||||
Text(
|
||||
text = text,
|
||||
color = color,
|
||||
style = style,
|
||||
textAlign = textAlign,
|
||||
modifier = modifier,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Muted grey that still follows the surrounding line's dimming (current vs. inactive line).
|
||||
val markColor = color.copy(alpha = color.alpha * 0.6f)
|
||||
|
||||
val annotatedText = buildAnnotatedString {
|
||||
var cursor = 0
|
||||
matches.forEachIndexed { index, match ->
|
||||
append(text.substring(cursor, match.range.first))
|
||||
appendInlineContent(id = inlineId(index), alternateText = match.groupValues[1])
|
||||
cursor = match.range.last + 1
|
||||
}
|
||||
append(text.substring(cursor))
|
||||
}
|
||||
|
||||
val density = LocalDensity.current
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
|
||||
val inlineContent = matches
|
||||
.mapIndexed { index, match ->
|
||||
val letter = match.groupValues[1]
|
||||
// Reserve exactly the letter's advance width (in em, so it is font-scale independent); the
|
||||
// taller placeholder leaves room for the mark above. Aligning to the text bottom keeps the
|
||||
// letter's baseline in step with the rest of the line.
|
||||
val letterWidthEm = remember(letter, style) {
|
||||
val letterWidthPx = textMeasurer.measure(text = letter, style = style).size.width
|
||||
val fontSizePx = with(density) { style.fontSize.toPx() }
|
||||
letterWidthPx / fontSizePx
|
||||
}
|
||||
|
||||
inlineId(index) to InlineTextContent(
|
||||
placeholder = Placeholder(
|
||||
width = letterWidthEm.em,
|
||||
height = 1.7.em,
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.TextBottom,
|
||||
),
|
||||
) {
|
||||
OptionalLetter(letter = letter, color = markColor, style = style)
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
Text(
|
||||
text = annotatedText,
|
||||
color = color,
|
||||
style = style,
|
||||
textAlign = textAlign,
|
||||
inlineContent = inlineContent,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OptionalLetter(
|
||||
letter: String,
|
||||
color: Color,
|
||||
style: TextStyle,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Text(
|
||||
text = MUTED_LETTER_MARK,
|
||||
color = color,
|
||||
style = style.copy(fontSize = style.fontSize * 0.75f, lineHeight = 10.sp),
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
)
|
||||
Text(
|
||||
text = letter,
|
||||
color = color,
|
||||
// Tight line height so the glyph's baseline lands where the surrounding line expects it.
|
||||
style = style.copy(lineHeight = style.fontSize),
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun inlineId(index: Int): String = "optional-letter-$index"
|
||||
|
||||
@Preview(backgroundColor = 0xFFFFFFFF)
|
||||
@Composable
|
||||
private fun TranscriptionTextPreview() {
|
||||
ChomBevTheme {
|
||||
TranscriptionText(
|
||||
text = "Tomm eo ma(n) divskouarn, penn ma fri",
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(fontSize = 20.sp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(backgroundColor = 0xFFFFFFFF)
|
||||
@Composable
|
||||
private fun TranscriptionTextNoOptionalLetterPreview() {
|
||||
ChomBevTheme {
|
||||
TranscriptionText(
|
||||
text = "Demat deoc'h !",
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(fontSize = 20.sp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user