diff --git a/app/gui2/e2e/componentBrowser.spec.ts b/app/gui2/e2e/componentBrowser.spec.ts index 6ed8d5b559..6d4bddcdde 100644 --- a/app/gui2/e2e/componentBrowser.spec.ts +++ b/app/gui2/e2e/componentBrowser.spec.ts @@ -52,10 +52,11 @@ test('Different ways of opening Component Browser', async ({ page }) => { await page.mouse.click(40, 300) await expectAndCancelBrowser('final.') // Double-clicking port + // TODO[ao] Without timeout, even the first click would be treated as double due to previous + // event. Probably we need a better way to simulate double clicks. + await page.waitForTimeout(600) + await page.mouse.click(outputPortX, outputPortY) await page.mouse.click(outputPortX, outputPortY) - // TODO[ao] the above click is already treated as double (due to previous event) - // But perhaps we should have more reliable method of simulating double clicks. - // await outputPortArea.dispatchEvent('pointerdown') await expectAndCancelBrowser('final.') }) diff --git a/app/gui2/src/components/ComponentBrowser.vue b/app/gui2/src/components/ComponentBrowser.vue index 00c24e7dbf..3a90297778 100644 --- a/app/gui2/src/components/ComponentBrowser.vue +++ b/app/gui2/src/components/ComponentBrowser.vue @@ -21,6 +21,7 @@ import { tryGetIndex } from '@/util/data/array' import type { Opt } from '@/util/data/opt' import { allRanges } from '@/util/data/range' import { Vec2 } from '@/util/data/vec2' +import { debouncedGetter } from '@/util/reactivity' import type { SuggestionId } from 'shared/languageServerTypes/suggestions' import { computed, onMounted, ref, watch, type ComputedRef, type Ref } from 'vue' @@ -158,29 +159,9 @@ useEvent( { capture: true }, ) -// === Preview === - const inputElement = ref() const inputSize = useResizeObserver(inputElement, false) -const previewedExpression = computed(() => { - if (selectedSuggestion.value == null) return input.code.value - else return input.inputAfterApplyingSuggestion(selectedSuggestion.value).newCode -}) - -const previewDataSource: ComputedRef = computed(() => { - if (!previewedExpression.value.trim()) return - if (!graphStore.methodAst) return - const body = graphStore.methodAst.body - if (!body) return - - return { - type: 'expression', - expression: previewedExpression.value, - contextId: body.externalId, - } -}) - // === Components List and Positions === const components = computed(() => @@ -257,6 +238,26 @@ function selectWithoutScrolling(index: number) { selected.value = index } +// === Preview === + +const previewedExpression = debouncedGetter(() => { + if (selectedSuggestion.value == null) return input.code.value + else return input.inputAfterApplyingSuggestion(selectedSuggestion.value).newCode +}, 200) + +const previewDataSource: ComputedRef = computed(() => { + if (!previewedExpression.value.trim()) return + if (!graphStore.methodAst) return + const body = graphStore.methodAst.body + if (!body) return + + return { + type: 'expression', + expression: previewedExpression.value, + contextId: body.externalId, + } +}) + // === Scrolling === const scroller = ref() diff --git a/app/gui2/src/util/reactivity.ts b/app/gui2/src/util/reactivity.ts index 36758aa9cc..85ee1e0524 100644 --- a/app/gui2/src/util/reactivity.ts +++ b/app/gui2/src/util/reactivity.ts @@ -119,16 +119,37 @@ export function cachedGetter( getter: () => T, equalFn: (a: T, b: T) => boolean = defaultEquality, ): Ref { - const valueRef = shallowRef() + const valueRef = shallowRef(getter()) watch( getter, (newValue) => { const oldValue = valueRef.value - if (oldValue === undefined || !equalFn(oldValue, newValue)) valueRef.value = newValue + if (!equalFn(oldValue, newValue)) valueRef.value = newValue }, - { immediate: true, flush: 'sync' }, + { flush: 'sync' }, ) - // Since the watch is immediate, the value is guaranteed to be assigned at least once this point. - return valueRef as Ref + return valueRef +} + +/** + * Same as `cachedGetter`, except that any changes will be not applied immediately, but only after + * the timer set for `delayMs` milliseconds will expire. If any further update arrives in that + * time, the timer is restarted + */ +export function debouncedGetter( + getter: () => T, + delayMs: number, + equalFn: (a: T, b: T) => boolean = defaultEquality, +): Ref { + const valueRef = shallowRef(getter()) + let currentTimer: ReturnType | undefined + watch(getter, (newValue) => { + clearTimeout(currentTimer) + currentTimer = setTimeout(() => { + const oldValue = valueRef.value + if (!equalFn(oldValue, newValue)) valueRef.value = newValue + }, delayMs) + }) + return valueRef }