Feature/highlighter snap (#48)

* perf: fix Home and Library screen stutter by removing main thread file checks

* feat(pdf): implement straight-line highlighter snap

- Added "Straight Line" toggle to highlighter settings popup with persistent state.
- Implemented "rubber-band" drawing effect for highlighters with cardinal direction snapping (0°, 90°, 180°, 270°).
- Optimized gesture loops in Vertical and Paginated readers to respond instantly to setting toggles.
- Rewrote eraser hit detection using point-to-line segment distance math for pixel-perfect erasure of straight lines.
This commit is contained in:
Aryan 2026-03-08 20:19:59 +05:30 committed by GitHub
parent 6ea414c9b2
commit 8c7c8cb48e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 203 additions and 61 deletions

View file

@ -474,7 +474,7 @@ private fun RecentFilesContent(
androidx.compose.material3.Button(onClick = onSelectFileClick) { androidx.compose.material3.Button(onClick = onSelectFileClick) {
Text("Select File") Text("Select File")
} }
androidx.compose.material3.OutlinedButton(onClick = onNavigateToFolderSync) { androidx.compose.material3.Button(onClick = onNavigateToFolderSync) {
Text("Sync Folder") Text("Sync Folder")
} }
} }
@ -552,7 +552,7 @@ fun RecentFileCard(
FileType.EPUB, FileType.MOBI, FileType.MD, FileType.TXT, FileType.HTML -> R.drawable.epub_placeholder FileType.EPUB, FileType.MOBI, FileType.MD, FileType.TXT, FileType.HTML -> R.drawable.epub_placeholder
} }
val imageModel = remember(item.coverImagePath) { val imageModel = remember(item.coverImagePath) {
item.coverImagePath?.let { File(it) }?.takeIf { it.exists() } ?: placeholder item.coverImagePath?.let { File(it) } ?: placeholder
} }
Surface( Surface(
@ -584,7 +584,7 @@ fun RecentFileCard(
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(8.dp) .padding(8.dp)
.background( .background(
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.9f), color = MaterialTheme.colorScheme.secondaryContainer,
shape = CircleShape shape = CircleShape
) )
.padding(4.dp) .padding(4.dp)

View file

@ -1044,7 +1044,7 @@ private fun ShelfCover(shelf: Shelf) {
) { ) {
if (booksForCovers.size <= 1) { if (booksForCovers.size <= 1) {
val imageModel = remember(shelf.topBook?.coverImagePath) { val imageModel = remember(shelf.topBook?.coverImagePath) {
shelf.topBook?.coverImagePath?.let { File(it) }?.takeIf { it.exists() } ?: placeholder shelf.topBook?.coverImagePath?.let { File(it) } ?: placeholder
} }
AsyncImage( AsyncImage(
model = ImageRequest.Builder(context) model = ImageRequest.Builder(context)
@ -1067,7 +1067,7 @@ private fun ShelfCover(shelf: Shelf) {
) { ) {
booksForCovers.forEachIndexed { index, book -> booksForCovers.forEachIndexed { index, book ->
val imageModel = remember(book.coverImagePath) { val imageModel = remember(book.coverImagePath) {
book.coverImagePath?.let { File(it) }?.takeIf { it.exists() } ?: placeholder book.coverImagePath?.let { File(it) } ?: placeholder
} }
Surface( Surface(
shape = MaterialTheme.shapes.small, shape = MaterialTheme.shapes.small,
@ -1160,7 +1160,7 @@ private fun LibraryListItem(
FileType.EPUB, FileType.MOBI, FileType.MD, FileType.TXT, FileType.HTML -> R.drawable.epub_placeholder FileType.EPUB, FileType.MOBI, FileType.MD, FileType.TXT, FileType.HTML -> R.drawable.epub_placeholder
} }
val imageModel = remember(item.coverImagePath) { val imageModel = remember(item.coverImagePath) {
item.coverImagePath?.let { File(it) }?.takeIf { it.exists() } ?: placeholder item.coverImagePath?.let { File(it) } ?: placeholder
} }
Surface( Surface(

View file

@ -417,7 +417,8 @@ internal fun PdfPageComposable(
draggingBoxId: String? = null, draggingBoxId: String? = null,
isScrollLocked: Boolean = false, isScrollLocked: Boolean = false,
isVisible: Boolean = true, isVisible: Boolean = true,
isStylusOnlyMode: Boolean = false isStylusOnlyMode: Boolean = false,
isHighlighterSnapEnabled: Boolean = false
) { ) {
SideEffect { Timber.tag("PdfDrawPerf").v("PdfPageComposable Recompose: Page $pageIndex") } SideEffect { Timber.tag("PdfDrawPerf").v("PdfPageComposable Recompose: Page $pageIndex") }
val pdfDocumentItem = pdfDocument.item val pdfDocumentItem = pdfDocument.item
@ -2608,7 +2609,8 @@ internal fun PdfPageComposable(
isScrolling, isScrolling,
isVerticalScroll, isVerticalScroll,
selectedTool, selectedTool,
isStylusOnlyMode isStylusOnlyMode,
isHighlighterSnapEnabled
) { ) {
val canDraw = isEditMode && selectedTool != InkType.TEXT && !isScrolling && !isVerticalScroll && actualBitmapWidthPx > 0 && actualBitmapHeightPx > 0 val canDraw = isEditMode && selectedTool != InkType.TEXT && !isScrolling && !isVerticalScroll && actualBitmapWidthPx > 0 && actualBitmapHeightPx > 0
@ -4611,6 +4613,16 @@ class PdfDrawingState {
currentPoints.clear() currentPoints.clear()
return finalAnnot return finalAnnot
} }
fun updateDrag(point: PdfPoint) {
if (currentPoints.isNotEmpty()) {
val start = currentPoints.first()
currentPoints.clear()
currentPoints.add(start)
currentPoints.add(point)
currentAnnotation = currentAnnotation?.copy(points = currentPoints.toList())
}
}
} }
@Composable @Composable

View file

@ -222,7 +222,8 @@ internal fun PdfVerticalReader(
isScrollLocked: Boolean = false, isScrollLocked: Boolean = false,
autoScrollSpeed: Float = 1.0f, autoScrollSpeed: Float = 1.0f,
onInteractionListener: () -> Unit = {}, onInteractionListener: () -> Unit = {},
isStylusOnlyMode: Boolean = false isStylusOnlyMode: Boolean = false,
isHighlighterSnapEnabled: Boolean = false
) { ) {
SideEffect { Timber.tag("PdfDrawPerf").v("LIST: PdfVerticalReader Recomposing.") } SideEffect { Timber.tag("PdfDrawPerf").v("LIST: PdfVerticalReader Recomposing.") }
var globalEraserPosition by remember { mutableStateOf<Offset?>(null) } var globalEraserPosition by remember { mutableStateOf<Offset?>(null) }
@ -748,7 +749,13 @@ internal fun PdfVerticalReader(
} }
} }
val globalDrawingModifier = Modifier.pointerInput(isEditMode, layoutInfo, selectedTool, isStylusOnlyMode) { val globalDrawingModifier = Modifier.pointerInput(
isEditMode,
layoutInfo,
selectedTool,
isStylusOnlyMode,
isHighlighterSnapEnabled
) {
if (!isEditMode) return@pointerInput if (!isEditMode) return@pointerInput
if (selectedTool == InkType.TEXT) return@pointerInput if (selectedTool == InkType.TEXT) return@pointerInput
@ -864,7 +871,14 @@ internal fun PdfVerticalReader(
onDoubleTapToZoom(offset) onDoubleTapToZoom(offset)
}) })
} }
.pointerInput(totalDocHeight, isEditMode, selectedTool, isScrollLocked, isStylusOnlyMode) { .pointerInput(
totalDocHeight,
isEditMode,
selectedTool,
isScrollLocked,
isStylusOnlyMode,
isHighlighterSnapEnabled
) {
val tracker = VelocityTracker() val tracker = VelocityTracker()
val decay = exponentialDecay<Float>() val decay = exponentialDecay<Float>()
val touchSlop = viewConfiguration.touchSlop val touchSlop = viewConfiguration.touchSlop

View file

@ -89,7 +89,6 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material.icons.filled.ArrowDownward import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowUpward import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.Brush import androidx.compose.material.icons.filled.Brush
@ -981,6 +980,8 @@ fun PdfViewerScreen(
var showToolSettings by remember { mutableStateOf(false) } var showToolSettings by remember { mutableStateOf(false) }
val isHighlighterSnapEnabled = toolSettings.isHighlighterSnapEnabled
val selectedTool = toolSettings.getActiveTool() val selectedTool = toolSettings.getActiveTool()
val lastPenTool = toolSettings.getLastPenTool() val lastPenTool = toolSettings.getLastPenTool()
@ -999,6 +1000,9 @@ fun PdfViewerScreen(
val isCurrentToolHighlighter = val isCurrentToolHighlighter =
selectedTool == InkType.HIGHLIGHTER || selectedTool == InkType.HIGHLIGHTER_ROUND selectedTool == InkType.HIGHLIGHTER || selectedTool == InkType.HIGHLIGHTER_ROUND
val currentSnapEnabled by rememberUpdatedState(isHighlighterSnapEnabled)
val currentIsHighlighter by rememberUpdatedState(isCurrentToolHighlighter)
val penPalette = remember(toolSettings.penPaletteArgb) { toolSettings.getPenPalette() } val penPalette = remember(toolSettings.penPaletteArgb) { toolSettings.getPenPalette() }
val highlighterPalette = val highlighterPalette =
remember(toolSettings.highlighterPaletteArgb) { toolSettings.getHighlighterPalette() } remember(toolSettings.highlighterPaletteArgb) { toolSettings.getHighlighterPalette() }
@ -1272,6 +1276,36 @@ fun PdfViewerScreen(
} }
} }
val calculateSnappedPoint = remember(pageAspectRatios) {
{ pageIndex: Int, currentPoint: PdfPoint, startPoint: PdfPoint? ->
if (startPoint == null) {
currentPoint
} else {
val aspectRatio = pageAspectRatios.getOrElse(pageIndex) { 1f }
val dx = (currentPoint.x - startPoint.x) * aspectRatio
val dy = (currentPoint.y - startPoint.y)
val angleRad = kotlin.math.atan2(dy, dx)
val angleDeg = (angleRad * 180 / kotlin.math.PI)
val absAngle = kotlin.math.abs(angleDeg)
val threshold = 10.0
val isHorizontal = absAngle < threshold || kotlin.math.abs(absAngle - 180.0) < threshold
val isVertical = kotlin.math.abs(absAngle - 90.0) < threshold
if (isHorizontal) {
currentPoint.copy(y = startPoint.y)
} else if (isVertical) {
currentPoint.copy(x = startPoint.x)
} else {
currentPoint
}
}
}
}
val onDeletePage: () -> Unit = { val onDeletePage: () -> Unit = {
coroutineScope.launch { coroutineScope.launch {
if (currentBookId != null && currentPage in virtualPages.indices) { if (currentBookId != null && currentPage in virtualPages.indices) {
@ -2095,13 +2129,42 @@ fun PdfViewerScreen(
} }
fun isAnnotationHit( fun isAnnotationHit(
annotation: PdfAnnotation, hitPoint: PdfPoint, threshold: Float = 0.05f annotation: PdfAnnotation,
hitPoint: PdfPoint,
pageAspectRatio: Float,
threshold: Float = 0.025f
): Boolean { ): Boolean {
return annotation.points.any { p -> if (annotation.points.isEmpty()) return false
val dx = p.x - hitPoint.x
val dy = p.y - hitPoint.y if (annotation.points.size == 1) {
(dx * dx + dy * dy) < (threshold * threshold) val p = annotation.points[0]
val dx = (p.x - hitPoint.x) * pageAspectRatio
val dy = (p.y - hitPoint.y)
return (dx * dx + dy * dy) < (threshold * threshold)
} }
for (i in 0 until annotation.points.size - 1) {
val a = annotation.points[i]
val b = annotation.points[i + 1]
val pax = (hitPoint.x - a.x) * pageAspectRatio
val pay = (hitPoint.y - a.y)
val bax = (b.x - a.x) * pageAspectRatio
val bay = (b.y - a.y)
val segmentLenSq = (bax * bax + bay * bay).coerceAtLeast(1e-6f)
val t = (pax * bax + pay * bay) / segmentLenSq
val tClamped = t.coerceIn(0f, 1f)
val closestX = bax * tClamped
val closestY = bay * tClamped
val distSq = (pax - closestX) * (pax - closestX) + (pay - closestY) * (pay - closestY)
if (distSq < (threshold * threshold)) return true
}
return false
} }
fun startTts(pageToReadOverride: Int? = null) { fun startTts(pageToReadOverride: Int? = null) {
@ -3407,10 +3470,10 @@ fun PdfViewerScreen(
{ point: PdfPoint -> { point: PdfPoint ->
if (currentSelectedTool == InkType.TEXT) { if (currentSelectedTool == InkType.TEXT) {
} else if (currentSelectedTool == InkType.ERASER) { } else if (currentSelectedTool == InkType.ERASER) {
val existing = val aspectRatio = pageAspectRatios.getOrElse(pageIndex) { 1f }
allAnnotations[pageIndex] ?: emptyList() val existing = allAnnotations[pageIndex] ?: emptyList()
val toRemove = existing.filter { val toRemove = existing.filter {
isAnnotationHit(it, point) isAnnotationHit(it, point, aspectRatio)
} }
if (toRemove.isNotEmpty()) { if (toRemove.isNotEmpty()) {
val batch = val batch =
@ -3427,10 +3490,13 @@ fun PdfViewerScreen(
allAnnotations + (pageIndex to newList) allAnnotations + (pageIndex to newList)
} }
} else { } else {
val pointWithTime = point.copy( if (currentIsHighlighter && currentSnapEnabled) {
timestamp = System.currentTimeMillis() val startPoint = drawingState.currentAnnotation?.points?.firstOrNull()
) val effectivePoint = calculateSnappedPoint(pageIndex, point, startPoint)
drawingState.onDraw(pointWithTime) drawingState.updateDrag(effectivePoint.copy(timestamp = System.currentTimeMillis()))
} else {
drawingState.onDraw(point.copy(timestamp = System.currentTimeMillis()))
}
} }
} }
} }
@ -3451,13 +3517,10 @@ fun PdfViewerScreen(
if (currentSelectedTool == InkType.TEXT) { if (currentSelectedTool == InkType.TEXT) {
} else if (currentSelectedTool == InkType.ERASER) { } else if (currentSelectedTool == InkType.ERASER) {
erasedAnnotationsFromStroke.clear() erasedAnnotationsFromStroke.clear()
val aspectRatio = pageAspectRatios.getOrElse(pageIndex) { 1f }
val existing = allAnnotations[pageIndex] val existing = allAnnotations[pageIndex] ?: emptyList()
?: emptyList()
val toRemove = existing.filter { val toRemove = existing.filter {
isAnnotationHit( isAnnotationHit(it, point, aspectRatio)
it, point
)
} }
if (toRemove.isNotEmpty()) { if (toRemove.isNotEmpty()) {
val batch = val batch =
@ -3581,6 +3644,7 @@ fun PdfViewerScreen(
}, },
richTextController = richTextController, richTextController = richTextController,
isStylusOnlyMode = isStylusOnlyMode, isStylusOnlyMode = isStylusOnlyMode,
isHighlighterSnapEnabled = isHighlighterSnapEnabled,
isEditMode = isDrawingActive, isEditMode = isDrawingActive,
textBoxes = textBoxes.filter { it.pageIndex == pageIndex }, textBoxes = textBoxes.filter { it.pageIndex == pageIndex },
selectedTextBoxId = selectedTextBoxId, selectedTextBoxId = selectedTextBoxId,
@ -3791,10 +3855,10 @@ fun PdfViewerScreen(
} else if (currentSelectedTool == InkType.ERASER) { } else if (currentSelectedTool == InkType.ERASER) {
erasedAnnotationsFromStroke.clear() erasedAnnotationsFromStroke.clear()
val existing = val aspectRatio = pageAspectRatios.getOrElse(pageIndex) { 1f }
allAnnotations[pageIndex] ?: emptyList() val existing = allAnnotations[pageIndex] ?: emptyList()
val toRemove = existing.filter { val toRemove = existing.filter {
isAnnotationHit(it, point) isAnnotationHit(it, point, aspectRatio)
} }
if (toRemove.isNotEmpty()) { if (toRemove.isNotEmpty()) {
val batch = val batch =
@ -3826,13 +3890,13 @@ fun PdfViewerScreen(
} }
} }
val onDrawStable = remember { val onDrawStable = remember(isHighlighterSnapEnabled, isCurrentToolHighlighter, calculateSnappedPoint) {
{ pageIndex: Int, point: PdfPoint -> { pageIndex: Int, point: PdfPoint ->
if (currentSelectedTool == InkType.ERASER) { if (currentSelectedTool == InkType.ERASER) {
val existing = val aspectRatio = pageAspectRatios.getOrElse(pageIndex) { 1f }
allAnnotations[pageIndex] ?: emptyList() val existing = allAnnotations[pageIndex] ?: emptyList()
val toRemove = existing.filter { val toRemove = existing.filter {
isAnnotationHit(it, point) isAnnotationHit(it, point, aspectRatio)
} }
if (toRemove.isNotEmpty()) { if (toRemove.isNotEmpty()) {
val batch = val batch =
@ -3846,10 +3910,13 @@ fun PdfViewerScreen(
allAnnotations + (pageIndex to newList) allAnnotations + (pageIndex to newList)
} }
} else { } else {
val pointWithTime = point.copy( if (currentIsHighlighter && currentSnapEnabled) {
timestamp = System.currentTimeMillis() val startPoint = drawingState.currentAnnotation?.points?.firstOrNull()
) val effectivePoint = calculateSnappedPoint(pageIndex, point, startPoint)
drawingState.onDraw(pointWithTime) drawingState.updateDrag(effectivePoint.copy(timestamp = System.currentTimeMillis()))
} else {
drawingState.onDraw(point.copy(timestamp = System.currentTimeMillis()))
}
} }
} }
} }
@ -3898,6 +3965,7 @@ fun PdfViewerScreen(
allAnnotations = allAnnotationsProvider, allAnnotations = allAnnotationsProvider,
drawingState = drawingState, drawingState = drawingState,
onDrawStart = onDrawStartStable, onDrawStart = onDrawStartStable,
isHighlighterSnapEnabled = isHighlighterSnapEnabled,
onDraw = onDrawStable, onDraw = onDrawStable,
onDrawEnd = { onDrawEnd = {
val finalAnnotation = drawingState.onDrawEnd() val finalAnnotation = drawingState.onDrawEnd()
@ -5198,7 +5266,10 @@ fun PdfViewerScreen(
} else { } else {
annotationSettingsRepo.updatePenPalette(newPalette) annotationSettingsRepo.updatePenPalette(newPalette)
} }
}) },
isHighlighterSnapEnabled = isHighlighterSnapEnabled,
onSnapToggle = { annotationSettingsRepo.updateHighlighterSnap(it) }
)
} }
snapPreviewLocation?.let { location -> snapPreviewLocation?.let { location ->
@ -5357,7 +5428,7 @@ fun PdfViewerScreen(
saveStylusOnlyMode(context, isStylusOnlyMode) saveStylusOnlyMode(context, isStylusOnlyMode)
}, },
onToolClick = { clickedTool -> onToolClick = { clickedTool ->
if (clickedTool == InkType.ERASER) { if (clickedTool == InkType.ERASER || clickedTool == InkType.TEXT) {
annotationSettingsRepo.updateSelectedTool( annotationSettingsRepo.updateSelectedTool(
clickedTool clickedTool
) )

View file

@ -60,7 +60,8 @@ fun PenIcon(
isSelected: Boolean = false, isSelected: Boolean = false,
strokeWidth: Float = 0.005f, strokeWidth: Float = 0.005f,
forcedInkType: InkType? = null, forcedInkType: InkType? = null,
inkColor: Color? = null inkColor: Color? = null,
isSnappingEnabled: Boolean = false
) { ) {
val animatedColor by animateColorAsState(targetValue = color, label = "color") val animatedColor by animateColorAsState(targetValue = color, label = "color")
@ -129,10 +130,11 @@ fun PenIcon(
drawInkSquiggle( drawInkSquiggle(
type = type, type = type,
forcedInkType = forcedInkType, forcedInkType = forcedInkType,
color = animatedInkColor, // Use the specific ink color color = animatedInkColor,
progress = inkProgress, progress = inkProgress,
startPoint = Offset(tipX, tipY), startPoint = Offset(tipX, tipY),
baseStrokeWidth = strokeWidth baseStrokeWidth = strokeWidth,
isStraight = isSnappingEnabled
) )
} }
} }
@ -413,7 +415,8 @@ private fun DrawScope.drawInkSquiggle(
color: Color, color: Color,
progress: Float, progress: Float,
startPoint: Offset, startPoint: Offset,
baseStrokeWidth: Float baseStrokeWidth: Float,
isStraight: Boolean = false
) { ) {
val x = startPoint.x val x = startPoint.x
val y = startPoint.y - 2f val y = startPoint.y - 2f
@ -422,13 +425,17 @@ private fun DrawScope.drawInkSquiggle(
if (type == PenType.HIGHLIGHTER || type == PenType.HIGHLIGHTER_ROUND) { if (type == PenType.HIGHLIGHTER || type == PenType.HIGHLIGHTER_ROUND) {
val waveWidth = 70f val waveWidth = 70f
val amplitude = 20f
if (isStraight) {
lineTo(x + waveWidth, y)
} else {
val amplitude = 20f
cubicTo( cubicTo(
x + waveWidth * 0.35f, y - amplitude, x + waveWidth * 0.35f, y - amplitude,
x + waveWidth * 0.65f, y + amplitude, x + waveWidth * 0.65f, y + amplitude,
x + waveWidth, y x + waveWidth, y
) )
}
} else { } else {
cubicTo( cubicTo(
x + 35f, y - 40f, x + 35f, y - 40f,

View file

@ -49,6 +49,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -103,7 +105,9 @@ fun ToolSettingsPopup(
onColorChanged: (Color) -> Unit, onColorChanged: (Color) -> Unit,
onThicknessChanged: (Float) -> Unit, onThicknessChanged: (Float) -> Unit,
onPaletteChange: (List<Color>) -> Unit, onPaletteChange: (List<Color>) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
isHighlighterSnapEnabled: Boolean = false,
onSnapToggle: (Boolean) -> Unit = {}
) { ) {
val isHighlighter = selectedTool == InkType.HIGHLIGHTER || selectedTool == InkType.HIGHLIGHTER_ROUND val isHighlighter = selectedTool == InkType.HIGHLIGHTER || selectedTool == InkType.HIGHLIGHTER_ROUND
@ -157,10 +161,9 @@ fun ToolSettingsPopup(
tonalElevation = 0.dp tonalElevation = 0.dp
) { ) {
Column( Column(
modifier = Modifier.padding(20.dp), // Reduced padding modifier = Modifier.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// Pen Type Selector
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -179,7 +182,8 @@ fun ToolSettingsPopup(
inkColor = highlighterColor, inkColor = highlighterColor,
isSelected = selectedTool == InkType.HIGHLIGHTER, isSelected = selectedTool == InkType.HIGHLIGHTER,
strokeWidth = activeToolThickness, strokeWidth = activeToolThickness,
onClick = { onToolTypeChanged(InkType.HIGHLIGHTER) } onClick = { onToolTypeChanged(InkType.HIGHLIGHTER) },
isSnappingEnabled = isHighlighterSnapEnabled
) )
PenItem( PenItem(
@ -189,7 +193,8 @@ fun ToolSettingsPopup(
inkColor = highlighterRoundColor, inkColor = highlighterRoundColor,
isSelected = selectedTool == InkType.HIGHLIGHTER_ROUND, isSelected = selectedTool == InkType.HIGHLIGHTER_ROUND,
strokeWidth = activeToolThickness, strokeWidth = activeToolThickness,
onClick = { onToolTypeChanged(InkType.HIGHLIGHTER_ROUND) } onClick = { onToolTypeChanged(InkType.HIGHLIGHTER_ROUND) },
isSnappingEnabled = isHighlighterSnapEnabled
) )
} else { } else {
PenItem( PenItem(
@ -224,6 +229,32 @@ fun ToolSettingsPopup(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
if (isHighlighter) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Straight Line",
color = Color.White,
style = MaterialTheme.typography.bodyMedium
)
Switch(
checked = isHighlighterSnapEnabled,
onCheckedChange = onSnapToggle,
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = activeColor.copy(alpha = 1f),
uncheckedThumbColor = Color.Gray,
uncheckedTrackColor = Color(0xFF424242)
),
modifier = Modifier.scale(0.8f)
)
}
Spacer(Modifier.height(16.dp))
}
// THICKNESS SLIDER // THICKNESS SLIDER
StyledPropertySlider( StyledPropertySlider(
value = activeToolThickness, value = activeToolThickness,
@ -811,7 +842,8 @@ private fun PenItem(
strokeWidth: Float, strokeWidth: Float,
onClick: () -> Unit, onClick: () -> Unit,
forcedInkType: InkType? = null, forcedInkType: InkType? = null,
inkColor: Color? = null inkColor: Color? = null,
isSnappingEnabled: Boolean = false
) { ) {
val scale by animateFloatAsState( val scale by animateFloatAsState(
targetValue = if (isSelected) 1.15f else 0.9f, label = "scale" targetValue = if (isSelected) 1.15f else 0.9f, label = "scale"
@ -834,7 +866,8 @@ private fun PenItem(
isSelected = isSelected, isSelected = isSelected,
strokeWidth = strokeWidth, strokeWidth = strokeWidth,
forcedInkType = forcedInkType, forcedInkType = forcedInkType,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize(),
isSnappingEnabled = isSnappingEnabled
) )
} }
} }

View file

@ -74,7 +74,8 @@ data class AnnotationToolSettings(
"#8C64B5F6".toColorInt(), // Blue "#8C64B5F6".toColorInt(), // Blue
"#8CE1BEE7".toColorInt(), // Purple "#8CE1BEE7".toColorInt(), // Purple
), ),
val textStyle: TextStyleConfig = TextStyleConfig() val textStyle: TextStyleConfig = TextStyleConfig(),
val isHighlighterSnapEnabled: Boolean = false
) { ) {
fun getActiveTool(): InkType = try { fun getActiveTool(): InkType = try {
InkType.valueOf(selectedToolName) InkType.valueOf(selectedToolName)
@ -180,6 +181,10 @@ class AnnotationSettingsRepository(context: Context) {
saveSettings(_settings.value.copy(highlighterPaletteArgb = colors.map { it.toArgb() })) saveSettings(_settings.value.copy(highlighterPaletteArgb = colors.map { it.toArgb() }))
} }
fun updateHighlighterSnap(enabled: Boolean) {
saveSettings(_settings.value.copy(isHighlighterSnapEnabled = enabled))
}
fun updateTextStyle(style: TextStyleConfig) { fun updateTextStyle(style: TextStyleConfig) {
saveSettings(_settings.value.copy(textStyle = style)) saveSettings(_settings.value.copy(textStyle = style))
} }