Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -318,17 +318,26 @@ fun DemoDocumentOpen(isActive: Boolean, backdrop: Backdrop, glass: Color, ink: C
* rectangle read as a document once it is down at a third of its size.
*
* [tint] is the sheet's file kind, from [DemoKinds]. Card and bars share it so a sheet reads as one
* coloured object rather than a coloured card with grey lines on it. Both alphas sit a little above
* the neutral ones they replace (0.16 / 0.24): a hue needs more weight than grey to register at the
* 0.32 flight scale, under a 64° fold.
* coloured object rather than a coloured card with grey lines on it.
*
* Fill alphas are high (0.75 card / 1.0 lines), not the washed-out 0.20 / 0.34 this used to carry —
* and deliberately still a real gap between the two (not both pushed equally to ~0.9), since the
* bars are what read as "document lines" against the card and need contrast against it, not just
* high opacity in isolation. Every sheet
* stays at this same opacity for its entire life — through the flight, and after it lands in the
* fanned stack behind the assembled book (only the last-landing sheet ever fades, as the book takes
* over; the rest sit there indefinitely, per `DemoDocumentOpen`). At 0.20 that permanent settled fan
* read as pale grey ghosts on the light onboarding background — exactly what a real user flagged as
* "backside pages are dull and grey." Near-opaque keeps every sheet in the fan a saturated, distinct
* colour instead.
*/
@Composable
private fun OrbitSheet(tint: Color, modifier: Modifier) {
Box(
modifier
.size(width = SheetW, height = SheetH)
.clip(RoundedCornerShape(14.dp))
.background(tint.copy(0.20f))
.background(tint.copy(0.75f))
.padding(horizontal = 14.dp, vertical = 16.dp)
) {
Column(verticalArrangement = Arrangement.spacedBy(9.dp)) {
Expand All @@ -338,7 +347,7 @@ private fun OrbitSheet(tint: Color, modifier: Modifier) {
.fillMaxWidth(w)
.height(5.dp)
.clip(RoundedCornerShape(3.dp))
.background(tint.copy(0.34f))
.background(tint)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Alignment
Expand Down Expand Up @@ -154,6 +155,16 @@ internal fun PdfContinuousPage(
var magnifierFocus by remember(page, activeTool) { mutableStateOf(Offset.Unspecified) }
val selectionHandleDiameterPx = with(LocalDensity.current) { 32.dp.toPx() }
val selectionHandleHitRadiusPx = with(LocalDensity.current) { 30.dp.toPx() }
// Read inside the handle-drag gesture below instead of the raw `selectedOcrRanges` parameter.
// That gesture used to key its `pointerInput` on `selectedOcrRanges` itself — but the gesture
// is also what MUTATES it on every move (`onSelectOcrRange` inside `updateRange()`), so the
// instant a drag moved past touch slop, Compose saw the key change and cancelled + restarted
// the gesture coroutine mid-drag. The restarted `awaitEachGesture` then waited on a fresh
// touch-down that never came (the same finger was already down), which is why a handle drag
// died after exactly one movement in any direction. `selectedOcrRanges` is no longer a key —
// `rememberUpdatedState` is what lets the gesture still read the *current* selection at the
// start of each new touch (to know which handle was grabbed) without needing a key at all.
val latestSelectedOcrRanges by rememberUpdatedState(selectedOcrRanges)

// Page layout (rebuilt on the Pdf_Tools model): the image is drawn at its TRUE
// aspect via ContentScale.FillWidth, so the box height follows the bitmap. There
Expand Down Expand Up @@ -223,9 +234,13 @@ internal fun PdfContinuousPage(
selectedOcrRanges.forEach { range ->
ocrBlocks.firstOrNull { it.id == range.blockId }?.let { b ->
val r = expandedTextHighlightRect(ocrTextRangeToRect(b, range, frame), verticalScale = 1.35f)
// Stock-Android text-selection look: a subtle translucent band in the same
// blue as the selection handles (#4285F4), not an opaque saturated cyan block.
val radius = (r.height * 0.14f).coerceIn(2f, 5f)
// Fully rounded — half the band's own height, a real capsule — not the old 2-5px
// clamp, which read as a barely-softened rectangle. The handles hanging off each
// end are a rounded teardrop; a flat bar between them was what made the whole
// selection look like two balloons stuck onto a straight rod instead of one
// shape. Same blue family as the handles (#4285F4) so the band and the handles
// it connects to read as one selection, not two unrelated pieces.
val radius = r.height * 0.5f
drawRoundRect(Color(0xFF4285F4).copy(0.28f), r.topLeft, r.size, CornerRadius(radius, radius))
}
}
Expand Down Expand Up @@ -373,8 +388,8 @@ internal fun PdfContinuousPage(
// DrawScope is itself a Density, so dp -> px works directly here.
val minPx = 14.dp.toPx()
val maxPx = 30.dp.toPx()
drawTextSelectionHandle(start, (startLineH * 1.1f).coerceIn(minPx, maxPx))
drawTextSelectionHandle(end, (endLineH * 1.1f).coerceIn(minPx, maxPx))
drawTextSelectionHandle(start, (startLineH * 1.1f).coerceIn(minPx, maxPx), isLeftHandle = true)
drawTextSelectionHandle(end, (endLineH * 1.1f).coerceIn(minPx, maxPx), isLeftHandle = false)
}
}

Expand Down Expand Up @@ -641,7 +656,7 @@ internal fun PdfContinuousPage(
// Direct drag (no long-press wait): sweep a contiguous run of words in reading order
// and update the range live. The page stays in text-selection mode, while the parent
// viewer still receives two-finger pinch events for zoom.
.pointerInput(page, ocrBlocks, selectedOcrRanges, selectionHandleHitRadiusPx) {
.pointerInput(page, ocrBlocks, selectionHandleHitRadiusPx) {
fun updateRange() {
val s = selDragStart; val e = selDragEnd
if (s == null || e == null || ocrBlocks.isEmpty()) return
Expand All @@ -660,7 +675,7 @@ internal fun PdfContinuousPage(
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
val frame = Rect(0f, 0f, size.width.toFloat(), size.height.toFloat())
val handles = ocrSelectionHandleAnchors(ocrBlocks, selectedOcrRanges, frame)
val handles = ocrSelectionHandleAnchors(ocrBlocks, latestSelectedOcrRanges, frame)
val handleMode = when {
handles == null -> 0
(down.position - handles.first).getDistance() <= selectionHandleHitRadiusPx -> 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -454,22 +454,51 @@ internal fun ocrSelectionHandleLineHeights(
return (firstBlock.bottom - firstBlock.top) * frame.height to (lastBlock.bottom - lastBlock.top) * frame.height
}

/** Custom organic handle used by the PDF selection layer; deliberately not a platform handle. */
/**
* Stock-Android-style selection handle: a circle with one sharp corner notched out exactly at
* [anchor] — the actual look of the system text-selection handles (and Chrome's, and Google Docs'),
* not a free-floating teardrop. The bulge always leans *toward* the selection it bounds: the left
* handle's sharp corner sits at its top-left with the circle hanging down-right (into the selected
* text), the right handle's sharp corner sits at top-right with the circle hanging down-left.
*
* The construction is the special case where a circle of radius `r` is centered exactly `(r, r)`
* diagonally from the external anchor point: the tangent lines from the anchor to that circle then
* land exactly on the horizontal and vertical through the anchor (no trig needed to find them), and
* the visible arc is the far 270° of the circle — the 90° nearest the anchor is what's replaced by
* the two straight tangent segments that meet at the sharp corner.
*
* The previous shape was a symmetric, free-floating teardrop with a flat rectangular "neck" — it
* didn't sit flush against anything, so next to the highlight band it read as two balloons stuck
* onto a separate bar rather than the band's own corner drawn out into a handle.
*/
internal fun DrawScope.drawTextSelectionHandle(
anchor: Offset,
diameter: Float,
isLeftHandle: Boolean,
color: Color = Color(0xFF4285F4)
) {
val neckHalfWidth = diameter * 0.22f
val bodyRadius = diameter * 0.47f
val top = anchor.y - 1f
val bottom = top + bodyRadius * 2f
val r = diameter * 0.5f
// +1 for the left handle (circle bulges right, into the selection), -1 for the right handle
// (circle bulges left, into the selection).
val sign = if (isLeftHandle) 1f else -1f
val center = Offset(anchor.x + sign * r, anchor.y + r)
val bottomTangent = Offset(anchor.x, anchor.y + r) // straight down from the anchor
// Sweeping the long way (270°, away from the anchor-facing quadrant) from `bottomTangent` back
// to the anchor's other tangent point (straight sideways from it) — direction flips with
// handedness since the circle sits on the opposite side.
val startAngleDeg = if (isLeftHandle) 180f else 0f
val sweepDeg = if (isLeftHandle) -270f else 270f

val path = Path().apply {
moveTo(anchor.x - neckHalfWidth, top)
lineTo(anchor.x + neckHalfWidth, top)
lineTo(anchor.x + neckHalfWidth, top + 6f)
cubicTo(anchor.x + bodyRadius, top + 9f, anchor.x + bodyRadius, bottom - 2f, anchor.x, bottom)
cubicTo(anchor.x - bodyRadius, bottom - 2f, anchor.x - bodyRadius, top + 9f, anchor.x - neckHalfWidth, top + 6f)
moveTo(anchor.x, anchor.y)
lineTo(bottomTangent.x, bottomTangent.y)
arcTo(
rect = Rect(center.x - r, center.y - r, center.x + r, center.y + r),
startAngleDegrees = startAngleDeg,
sweepAngleDegrees = sweepDeg,
forceMoveTo = false
)
lineTo(anchor.x, anchor.y)
close()
}
drawPath(path, color)
Expand Down
Loading