From 36b068c5dca94a1875ba8dd2b631ca283340fdf9 Mon Sep 17 00:00:00 2001 From: Baku Hashimoto Date: Sat, 30 Sep 2023 01:13:45 +0900 Subject: [PATCH] Create InputNumber --- src/components/App.vue | 3 + src/tweeq/InputNumber/InputNumber.vue | 442 ++++++++++++++++++++++++++ src/tweeq/InputNumber/index.ts | 2 + src/tweeq/common.styl | 17 +- src/tweeq/useDrag.ts | 152 +++++++++ src/tweeq/useTheme.ts | 2 +- src/tweeq/util.ts | 7 + 7 files changed, 620 insertions(+), 5 deletions(-) create mode 100644 src/tweeq/InputNumber/InputNumber.vue create mode 100644 src/tweeq/InputNumber/index.ts create mode 100644 src/tweeq/useDrag.ts create mode 100644 src/tweeq/util.ts diff --git a/src/components/App.vue b/src/components/App.vue index a356e87..799923a 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -23,6 +23,7 @@ PaperOffset(paper) import {useTweeq} from '@/tweeq' import CommandPalette from '@/tweeq/CommandPalette' import FloatingPane from '@/tweeq/FloatingPane' +import InputNumber from '@/tweeq/InputNumber' import InputString from '@/tweeq/InputString' import MonacoEditor, {ErrorInfo} from '@/tweeq/MonacoEditor' import RoundButton from '@/tweeq/RoundButton' @@ -41,6 +42,7 @@ const {appStorage, registerActions, performAction} = useTweeq( ) const testString = ref('Hello World') +const testNumber = ref(100) // interface PaperDesc { // id?: string @@ -411,6 +413,7 @@ window.addEventListener('drop', async e => { :position="{anchor: 'bottom', height: 200}" > + diff --git a/src/tweeq/InputNumber/InputNumber.vue b/src/tweeq/InputNumber/InputNumber.vue new file mode 100644 index 0000000..118fccb --- /dev/null +++ b/src/tweeq/InputNumber/InputNumber.vue @@ -0,0 +1,442 @@ + + + + + + + diff --git a/src/tweeq/InputNumber/index.ts b/src/tweeq/InputNumber/index.ts new file mode 100644 index 0000000..6f86598 --- /dev/null +++ b/src/tweeq/InputNumber/index.ts @@ -0,0 +1,2 @@ +import InputNumber from './InputNumber.vue' +export default InputNumber diff --git a/src/tweeq/common.styl b/src/tweeq/common.styl index 6acf58e..92e80c7 100644 --- a/src/tweeq/common.styl +++ b/src/tweeq/common.styl @@ -19,6 +19,7 @@ hover-transition(prop = all) transition unquote(join(',', values)) input-base() + position relative padding 4px 12px width 188px height var(--tq-input-height) @@ -27,11 +28,13 @@ input-base() color var(--tq-color-on-input) font-size 12px hover-transition() + overflow hidden &:hover background-color var(--tq-color-input-hover) - &:focus-within + &:focus-within, &.tweaking + z-index 1 background var(--tq-color-primary-container) box-shadow 0 0 0 1px var(--tq-color-primary) @@ -40,6 +43,12 @@ input-base() --tq-color-primary var(--tq-color-error) --tq-color-primary-container var(--tq-color-error-container) - input::selection - background var(--tq-color-primary) - color var(--tq-color-on-primary) + input + width 100% + + &::selection + background transparent + + &:focus::selection + background var(--tq-color-primary) + color var(--tq-color-on-primary) diff --git a/src/tweeq/useDrag.ts b/src/tweeq/useDrag.ts new file mode 100644 index 0000000..8ac39f2 --- /dev/null +++ b/src/tweeq/useDrag.ts @@ -0,0 +1,152 @@ +import {unrefElement} from '@vueuse/core' +import {Vec2, vec2} from 'linearly' +import {reactive, Ref, toRefs, watch} from 'vue' + +interface DragState { + xy: Vec2 + previous: Vec2 + initial: Vec2 + delta: Vec2 + dragging: boolean + pointerLocked: boolean +} + +type PointerType = 'mouse' | 'pen' | 'touch' + +interface UseDragOptions { + disabled?: Ref + lockPointer?: boolean + pointerType?: PointerType[] + dragDelaySeconds?: number + + onClick?: () => void + onDrag?: (state: DragState, event: PointerEvent) => void + onDragStart?: (state: DragState, event: PointerEvent) => void + onDragEnd?: (state: DragState, event: PointerEvent) => void +} + +export function useDrag( + target: Ref, + { + disabled, + lockPointer = false, + pointerType = ['mouse', 'pen', 'touch'], + dragDelaySeconds = 0.5, + onClick, + onDrag, + onDragStart, + onDragEnd, + }: UseDragOptions = {} +) { + const state = reactive>({ + // All coordinates are relative to the viewport + xy: vec2.zero, + previous: vec2.zero, + initial: vec2.zero, + delta: vec2.zero, + + dragging: false, + pointerLocked: false, + }) + + let dragDelayTimer = -1 + + function setup(el: HTMLElement) { + el.addEventListener('pointerdown', onPointerDown) + + function fireDragStart(event: PointerEvent) { + if (lockPointer && 'requestPointerLock' in el) { + el.requestPointerLock() + state.pointerLocked = true + } else { + state.pointerLocked = false + } + + state.dragging = true + state.initial = state.previous + onDragStart?.(state, event) + } + + function onPointerDown(event: PointerEvent) { + if (disabled?.value) return + if (event.button === 2) return // Ignore right click + if (!event.isPrimary) return + if (!pointerType.includes(event.pointerType as PointerType)) return + + // Initialzize pointer position + state.xy = state.previous = state.initial = [event.clientX, event.clientY] + + dragDelayTimer = setTimeout(fireDragStart, dragDelaySeconds * 1000) + + el.setPointerCapture(event.pointerId) + + window.addEventListener('pointermove', onPointerMove) + window.addEventListener('pointerup', onPointerUp) + } + + function onPointerMove(event: PointerEvent) { + if (disabled?.value) return + if (!event.isPrimary) return + + if (event.movementX !== undefined && event.movementY !== undefined) { + const movement: Vec2 = [event.movementX, event.movementY] + state.xy = vec2.add(state.xy, movement) + } else { + state.xy = [event.clientX, event.clientY] + } + + state.delta = vec2.sub(state.xy, state.previous) + + if (vec2.squaredLength(state.delta) === 0) return + + if (state.dragging) { + onDrag?.(state, event) + } else { + // Determine whether dragging has started + const d = vec2.dist(state.initial, state.xy) + const minDragDistance = event.pointerType === 'mouse' ? 3 : 7 + if (d >= minDragDistance) { + fireDragStart(event) + clearTimeout(dragDelayTimer) + } + } + + state.previous = vec2.clone(state.xy) + } + + function onPointerUp(event: PointerEvent) { + if (disabled?.value) return + if (!event.isPrimary) return + + if (lockPointer && 'exitPointerLock' in document) { + document.exitPointerLock() + } + state.pointerLocked = false + + if (state.dragging) { + onDragEnd?.(state, event) + } else { + onClick?.() + } + + // Reset + clearTimeout(dragDelayTimer) + state.dragging = false + state.xy = state.initial = state.delta = vec2.zero + window.removeEventListener('pointermove', onPointerMove) + window.removeEventListener('pointerup', onPointerUp) + } + } + + // Hooks + watch( + target, + () => { + const el = unrefElement(target) + if (el) setup(el) + }, + {immediate: true, flush: 'post'} + ) + + return toRefs(state) +} diff --git a/src/tweeq/useTheme.ts b/src/tweeq/useTheme.ts index 2bb0d24..6ec2a43 100644 --- a/src/tweeq/useTheme.ts +++ b/src/tweeq/useTheme.ts @@ -101,7 +101,7 @@ export function provideTheme( colorShadow: toColor(colors.shadow), colorInput: toColor(materialTheme.palettes.neutral.tone(97)), colorInputHover: toColor( - materialTheme.palettes.neutralVariant.tone(90) + materialTheme.palettes.neutralVariant.tone(95) ), colorOnInput: toColor(colors.onBackground), colorError: toColor(colors.error), diff --git a/src/tweeq/util.ts b/src/tweeq/util.ts new file mode 100644 index 0000000..ee48560 --- /dev/null +++ b/src/tweeq/util.ts @@ -0,0 +1,7 @@ +export function toFixedWithNoTrailingZeros(value: number, precision: number) { + return value + .toFixed(precision) + .replace(/\.(.*?)[0]+$/, '.$1') + .replace(/\.$/, '') +} +export const unsignedMod = (x: number, y: number) => ((x % y) + y) % y