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:
parent
6ea414c9b2
commit
8c7c8cb48e
8 changed files with 203 additions and 61 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue