Skip to content

Commit

Permalink
feat #60: double click to select word in input
Browse files Browse the repository at this point in the history
  • Loading branch information
bbohlender committed Nov 3, 2024
1 parent 4de5898 commit e11259f
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 8 deletions.
43 changes: 37 additions & 6 deletions packages/uikit/src/components/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { createResponsivePropertyTransformers } from '../responsive.js'
import { ElementType, ZIndexProperties, computedOrderInfo } from '../order.js'
import { createActivePropertyTransfomers } from '../active.js'
import { Signal, computed, effect, signal } from '@preact/signals-core'
import { ReadonlySignal, Signal, computed, effect, signal } from '@preact/signals-core'
import {
UpdateMatrixWorldProperties,
VisibilityProperties,
Expand Down Expand Up @@ -304,7 +304,7 @@ export function createInput(
properties.peek()?.onFocusChange?.(hasFocus)
style.peek()?.onFocusChange?.(hasFocus)
})
const selectionHandlers = computedSelectionHandlers(flexState, instancedTextRef, focus, disabled)
const selectionHandlers = computedSelectionHandlers(type, valueSignal, flexState, instancedTextRef, focus, disabled)

return Object.assign(flexState, {
pointerEventsProperties,
Expand Down Expand Up @@ -332,7 +332,11 @@ export function createInput(
})
}

const segmenter = typeof Intl === 'undefined' ? undefined : new Intl.Segmenter(undefined, { granularity: 'word' })

export function computedSelectionHandlers(
type: Signal<InputType>,
text: ReadonlySignal<string>,
flexState: FlexNodeState,
instancedTextRef: { current?: InstancedText },
focus: (start?: number, end?: number, direction?: 'forward' | 'backward' | 'none') => void,
Expand Down Expand Up @@ -360,22 +364,48 @@ export function computedSelectionHandlers(
if ('setPointerCapture' in e.object && typeof e.object.setPointerCapture === 'function') {
e.object.setPointerCapture(e.pointerId)
}
const startCharIndex = uvToCharIndex(flexState, e.uv, instancedTextRef.current)
const startCharIndex = uvToCharIndex(flexState, e.uv, instancedTextRef.current, 'between')
dragState = {
pointerId: e.pointerId,
startCharIndex,
}
setTimeout(() => focus(startCharIndex, startCharIndex))
},
onDoubleClick: (e) => {
if (segmenter == null || e.defaultPrevented || e.uv == null || instancedTextRef.current == null) {
return
}
e.stopImmediatePropagation?.()
if (type.peek() === 'password') {
setTimeout(() => focus(0, text.peek().length, 'none'))
return
}
const charIndex = uvToCharIndex(flexState, e.uv, instancedTextRef.current, 'on')
const segments = segmenter.segment(text.peek())
let segmentLengthSum = 0
for (const { segment } of segments) {
const segmentLength = segment.length
if (charIndex < segmentLengthSum + segmentLength) {
setTimeout(() => focus(segmentLengthSum, segmentLengthSum + segmentLength, 'none'))
break
}
segmentLengthSum += segmentLength
}
},
onPointerUp: onPointerFinish,
onPointerLeave: onPointerFinish,
onPointerCancel: onPointerFinish,
onPointerMove: (e) => {
if (dragState?.pointerId != e.pointerId || e.uv == null || instancedTextRef.current == null) {
if (
dragState?.pointerId != e.pointerId ||
e.defaultPrevented ||
e.uv == null ||
instancedTextRef.current == null
) {
return
}
e.stopImmediatePropagation?.()
const charIndex = uvToCharIndex(flexState, e.uv, instancedTextRef.current)
const charIndex = uvToCharIndex(flexState, e.uv, instancedTextRef.current, 'between')

const start = Math.min(dragState.startCharIndex, charIndex)
const end = Math.max(dragState.startCharIndex, charIndex)
Expand Down Expand Up @@ -478,6 +508,7 @@ function uvToCharIndex(
{ size: s, borderInset: b, paddingInset: p }: FlexNodeState,
uv: Vector2,
instancedText: InstancedText,
position: 'between' | 'on',
): number {
const size = s.peek()
const borderInset = b.peek()
Expand All @@ -490,5 +521,5 @@ function uvToCharIndex(
const [pTop, , , pLeft] = paddingInset
const x = uv.x * width - bLeft - pLeft
const y = (uv.y - 1) * height + bTop + pTop
return instancedText.getCharIndex(x, y)
return instancedText.getCharIndex(x, y, position)
}
4 changes: 2 additions & 2 deletions packages/uikit/src/text/render/instanced-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export class InstancedText {
]
}

public getCharIndex(x: number, y: number): number {
public getCharIndex(x: number, y: number, position: 'between' | 'on'): number {
const layout = this.lastLayout
if (layout == null) {
return 0
Expand All @@ -175,7 +175,7 @@ export class InstancedText {
let glyphsLength = glyphs.length
for (let i = 0; i < glyphsLength; i++) {
const entry = glyphs[i]
if (x < this.getGlyphX(entry, 0.5, whitespaceWidth) + layout.availableWidth / 2) {
if (x < this.getGlyphX(entry, position === 'between' ? 0.5 : 1, whitespaceWidth) + layout.availableWidth / 2) {
return i + line.charIndexOffset
}
}
Expand Down

0 comments on commit e11259f

Please sign in to comment.