From fa5aa53940f4985ab10aa31ac0cefe9d52a54a87 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 1 Dec 2022 02:41:30 +0100 Subject: refactor(frontend): scrollbar improvements --- subprojects/frontend/src/editor/EditorTheme.ts | 22 +- .../src/editor/indentationMarkerViewPlugin.ts | 2 +- .../frontend/src/editor/scrollbarViewPlugin.ts | 260 ++++++++++++--------- 3 files changed, 168 insertions(+), 116 deletions(-) (limited to 'subprojects/frontend') diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts index 89bc8932..e9907e83 100644 --- a/subprojects/frontend/src/editor/EditorTheme.ts +++ b/subprojects/frontend/src/editor/EditorTheme.ts @@ -84,6 +84,11 @@ export default styled('div', { background: 'transparent', }, }, + '.cm-scroller-track': { + position: 'absolute', + zIndex: 300, + touchAction: 'none', + }, '.cm-scroller-thumb': { position: 'absolute', background: theme.palette.text.secondary, @@ -105,13 +110,18 @@ export default styled('div', { userSelect: 'none', }, }, - '.cm-scroller-thumb-y': { + '.cm-scroller-track-y, .cm-scroller-thumb-y': { top: 0, right: 0, + width: 12, }, - '.cm-scroller-thumb-x': { + '.cm-scroller-track-x, .cm-scroller-thumb-x': { left: 0, bottom: 0, + height: 12, + }, + '.cm-scroller-track-x': { + right: 12, }, '.cm-scroller-gutter-decoration': { position: 'absolute', @@ -141,7 +151,6 @@ export default styled('div', { }, '.cm-content': { ...editorFontStyle, - padding: '0 12px 0 0', }, '.cm-activeLine': { background: showActiveLine @@ -165,6 +174,7 @@ export default styled('div', { }, '.cm-line': { position: 'relative', // For indentation highlights + padding: '0 12px 0 0px', }, }; @@ -440,11 +450,11 @@ export default styled('div', { const foldStyle = { '.cm-foldGutter': { - width: 16, + width: 18, }, '.problem-editor-foldMarker': { display: 'block', - margin: '4px 0', + margin: '4px 2px 4px 0', padding: 0, maskImage: svgURL(expandMoreSVG), maskSize: '16px 16px', @@ -455,7 +465,7 @@ export default styled('div', { cursor: 'pointer', WebkitTapHighlightColor: 'transparent', [theme.breakpoints.down('sm')]: { - margin: '2px 0', + margin: '2px 2px 2px 0', }, }, '.problem-editor-foldMarker-open': { diff --git a/subprojects/frontend/src/editor/indentationMarkerViewPlugin.ts b/subprojects/frontend/src/editor/indentationMarkerViewPlugin.ts index c9480c7d..730fa6e3 100644 --- a/subprojects/frontend/src/editor/indentationMarkerViewPlugin.ts +++ b/subprojects/frontend/src/editor/indentationMarkerViewPlugin.ts @@ -61,7 +61,7 @@ class IndentationWidget extends WidgetType { const wrapper = document.createElement('span'); wrapper.style.top = '0'; - wrapper.style.left = '6px'; + wrapper.style.left = '0'; wrapper.style.position = 'absolute'; wrapper.style.pointerEvents = 'none'; diff --git a/subprojects/frontend/src/editor/scrollbarViewPlugin.ts b/subprojects/frontend/src/editor/scrollbarViewPlugin.ts index c1eb2bbd..8f165e89 100644 --- a/subprojects/frontend/src/editor/scrollbarViewPlugin.ts +++ b/subprojects/frontend/src/editor/scrollbarViewPlugin.ts @@ -1,5 +1,9 @@ import { EditorSelection } from '@codemirror/state'; -import { type PluginValue, ViewPlugin } from '@codemirror/view'; +import { + type EditorView, + type PluginValue, + ViewPlugin, +} from '@codemirror/view'; import { reaction } from 'mobx'; import type EditorStore from './EditorStore'; @@ -8,9 +12,8 @@ import findOccurrences from './findOccurrences'; export const HOLDER_CLASS = 'cm-scroller-holder'; export const SPACER_CLASS = 'cm-scroller-spacer'; +export const TRACK_CLASS = 'cm-scroller-track'; export const THUMB_CLASS = 'cm-scroller-thumb'; -export const THUMB_Y_CLASS = 'cm-scroller-thumb-y'; -export const THUMB_X_CLASS = 'cm-scroller-thumb-x'; export const THUMB_ACTIVE_CLASS = 'active'; export const GUTTER_DECORATION_CLASS = 'cm-scroller-gutter-decoration'; export const TOP_DECORATION_CLASS = 'cm-scroller-top-decoration'; @@ -22,28 +25,42 @@ export const SCROLLBAR_WIDTH = 12; export const ANNOTATION_WIDTH = SCROLLBAR_WIDTH / 2; export const MIN_ANNOTATION_HEIGHT = 1; -function handleDrag( - element: HTMLElement, - callback: (movementX: number, movementY: number) => void, -) { +function createScrollbar( + holder: HTMLElement, + direction: 'x' | 'y', + touchCallback: (offsetX: number, offsetY: number) => void, + moveCallback: (movementX: number, movementY: number) => void, +): { track: HTMLElement; thumb: HTMLElement } { + const track = holder.ownerDocument.createElement('div'); + track.className = `${TRACK_CLASS} ${TRACK_CLASS}-${direction}`; + holder.appendChild(track); + + const thumb = holder.ownerDocument.createElement('div'); + thumb.className = `${THUMB_CLASS} ${THUMB_CLASS}-${direction}`; + track.appendChild(thumb); + let pointerId: number | undefined; - element.addEventListener('pointerdown', (event) => { - if (pointerId === undefined) { - ({ pointerId } = event); - element.setPointerCapture(pointerId); - element.classList.add(THUMB_ACTIVE_CLASS); - } else { + track.addEventListener('pointerdown', (event) => { + if (pointerId !== undefined) { event.preventDefault(); - // Avoid implicit pointer capture, see https://w3c.github.io/pointerevents/#dfn-implicit-pointer-capture - element.releasePointerCapture(event.pointerId); + return; + } + ({ pointerId } = event); + thumb.classList.add(THUMB_ACTIVE_CLASS); + if (event.target === thumb) { + // Prevent implicit pointer capture on mobile. + thumb.releasePointerCapture(pointerId); + } else { + touchCallback(event.offsetX, event.offsetY); } + track.setPointerCapture(pointerId); }); - element.addEventListener('pointermove', (event) => { + track.addEventListener('pointermove', (event) => { if (event.pointerId !== pointerId) { return; } - callback(event.movementX, event.movementY); + moveCallback(event.movementX, event.movementY); event.preventDefault(); }); @@ -52,11 +69,88 @@ function handleDrag( return; } pointerId = undefined; - element.classList.remove(THUMB_ACTIVE_CLASS); + thumb.classList.remove(THUMB_ACTIVE_CLASS); + } + + track.addEventListener('pointerup', scrollEnd, { passive: true }); + track.addEventListener('pointercancel', scrollEnd, { passive: true }); + + return { track, thumb }; +} + +function rebuildAnnotations( + view: EditorView, + scrollHeight: number, + trackYHeight: number, + holder: HTMLElement, + annotations: HTMLDivElement[], +) { + const { state } = view; + const overlayAnnotationsHeight = + (view.contentHeight / scrollHeight) * trackYHeight; + const lineHeight = overlayAnnotationsHeight / state.doc.lines; + + let i = 0; + + function getOrCreateAnnotation(from: number, to?: number): HTMLDivElement { + const startLine = state.doc.lineAt(from).number; + const endLine = to === undefined ? startLine : state.doc.lineAt(to).number; + const top = (startLine - 1) * lineHeight; + const height = Math.max( + MIN_ANNOTATION_HEIGHT, + Math.max(1, endLine - startLine) * lineHeight, + ); + + let annotation: HTMLDivElement | undefined; + if (i < annotations.length) { + annotation = annotations[i]; + } + if (annotation === undefined) { + annotation = holder.ownerDocument.createElement('div'); + annotations.push(annotation); + holder.appendChild(annotation); + } + i += 1; + + annotation.style.top = `${top}px`; + annotation.style.height = `${height}px`; + + return annotation; + } + + state.selection.ranges.forEach(({ head }) => { + const selectionAnnotation = getOrCreateAnnotation(head); + selectionAnnotation.className = ANNOTATION_SELECTION_CLASS; + selectionAnnotation.style.width = `${SCROLLBAR_WIDTH}px`; + }); + + const diagnosticsIter = getDiagnostics(state).iter(); + while (diagnosticsIter.value !== null) { + const diagnosticAnnotation = getOrCreateAnnotation( + diagnosticsIter.from, + diagnosticsIter.to, + ); + diagnosticAnnotation.className = `${ANNOTATION_DIAGNOSTIC_CLASS} ${ANNOTATION_DIAGNOSTIC_CLASS}-${diagnosticsIter.value.severity}`; + diagnosticAnnotation.style.width = `${ANNOTATION_WIDTH}px`; + diagnosticsIter.next(); } - element.addEventListener('pointerup', scrollEnd, { passive: true }); - element.addEventListener('pointercancel', scrollEnd, { passive: true }); + const occurrences = view.state.field(findOccurrences); + const occurrencesIter = occurrences.iter(); + while (occurrencesIter.value !== null) { + const occurrenceAnnotation = getOrCreateAnnotation( + occurrencesIter.from, + occurrencesIter.to, + ); + occurrenceAnnotation.className = ANNOTATION_OCCURRENCE_CLASS; + occurrenceAnnotation.style.width = `${ANNOTATION_WIDTH}px`; + occurrenceAnnotation.style.right = `${ANNOTATION_WIDTH}px`; + occurrencesIter.next(); + } + + annotations + .splice(i) + .forEach((staleAnnotation) => holder.removeChild(staleAnnotation)); } export default function scrollbarViewPlugin( @@ -98,19 +192,33 @@ export default function scrollbarViewPlugin( let factorY = 1; let factorX = 1; - const thumbY = ownerDocument.createElement('div'); - thumbY.className = `${THUMB_CLASS} ${THUMB_Y_CLASS}`; - handleDrag(thumbY, (_movementX, movementY) => - scrollDOM.scrollBy({ top: movementY / factorY }), + const { track: trackY, thumb: thumbY } = createScrollbar( + holder, + 'y', + (_offsetX, offsetY) => { + const scaledOffset = offsetY / factorY; + const { height: scrollerHeight } = scrollDOM.getBoundingClientRect(); + const target = Math.max(0, scaledOffset - scrollerHeight / 2); + scrollDOM.scrollTo({ top: target }); + }, + (_movementX, movementY) => { + scrollDOM.scrollBy({ top: movementY / factorY }); + }, ); - holder.appendChild(thumbY); - const thumbX = ownerDocument.createElement('div'); - thumbX.className = `${THUMB_CLASS} ${THUMB_X_CLASS}`; - handleDrag(thumbX, (movementX) => - scrollDOM.scrollBy({ left: movementX / factorX }), + const { track: trackX, thumb: thumbX } = createScrollbar( + holder, + 'x', + (offsetX) => { + const scaledOffset = offsetX / factorX; + const { width: scrollerWidth } = scrollDOM.getBoundingClientRect(); + const target = Math.max(0, scaledOffset - scrollerWidth / 2); + scrollDOM.scrollTo({ left: target }); + }, + (movementX) => { + scrollDOM.scrollBy({ left: movementX / factorX }); + }, ); - holder.appendChild(thumbX); const gutterDecoration = ownerDocument.createElement('div'); gutterDecoration.className = GUTTER_DECORATION_CLASS; @@ -135,79 +243,6 @@ export default function scrollbarViewPlugin( const annotations: HTMLDivElement[] = []; - function rebuildAnnotations(scrollHeight: number, trackYHeight: number) { - const { state } = view; - const overlayAnnotationsHeight = - (view.contentHeight / scrollHeight) * trackYHeight; - const lineHeight = overlayAnnotationsHeight / state.doc.lines; - - let i = 0; - - function getOrCreateAnnotation( - from: number, - to?: number, - ): HTMLDivElement { - const startLine = state.doc.lineAt(from).number; - const endLine = - to === undefined ? startLine : state.doc.lineAt(to).number; - const top = (startLine - 1) * lineHeight; - const height = Math.max( - MIN_ANNOTATION_HEIGHT, - Math.max(1, endLine - startLine) * lineHeight, - ); - - let annotation: HTMLDivElement | undefined; - if (i < annotations.length) { - annotation = annotations[i]; - } - if (annotation === undefined) { - annotation = ownerDocument.createElement('div'); - annotations.push(annotation); - holder.appendChild(annotation); - } - i += 1; - - annotation.style.top = `${top}px`; - annotation.style.height = `${height}px`; - - return annotation; - } - - state.selection.ranges.forEach(({ head }) => { - const selectionAnnotation = getOrCreateAnnotation(head); - selectionAnnotation.className = ANNOTATION_SELECTION_CLASS; - selectionAnnotation.style.width = `${SCROLLBAR_WIDTH}px`; - }); - - const diagnosticsIter = getDiagnostics(state).iter(); - while (diagnosticsIter.value !== null) { - const diagnosticAnnotation = getOrCreateAnnotation( - diagnosticsIter.from, - diagnosticsIter.to, - ); - diagnosticAnnotation.className = `${ANNOTATION_DIAGNOSTIC_CLASS} ${ANNOTATION_DIAGNOSTIC_CLASS}-${diagnosticsIter.value.severity}`; - diagnosticAnnotation.style.width = `${ANNOTATION_WIDTH}px`; - diagnosticsIter.next(); - } - - const occurrences = view.state.field(findOccurrences); - const occurrencesIter = occurrences.iter(); - while (occurrencesIter.value !== null) { - const occurrenceAnnotation = getOrCreateAnnotation( - occurrencesIter.from, - occurrencesIter.to, - ); - occurrenceAnnotation.className = ANNOTATION_OCCURRENCE_CLASS; - occurrenceAnnotation.style.width = `${ANNOTATION_WIDTH}px`; - occurrenceAnnotation.style.right = `${ANNOTATION_WIDTH}px`; - occurrencesIter.next(); - } - - annotations - .splice(i) - .forEach((staleAnnotation) => holder.removeChild(staleAnnotation)); - } - let observer: ResizeObserver | undefined; function update() { @@ -237,13 +272,15 @@ export default function scrollbarViewPlugin( const trackXWidth = scrollerWidth - gutterWidth - SCROLLBAR_WIDTH; const thumbWidth = trackXWidth * (scrollerWidth / scrollWidth); factorX = (trackXWidth - thumbWidth) / (scrollWidth - scrollerWidth); - thumbX.style.display = 'block'; - thumbX.style.height = `${SCROLLBAR_WIDTH}px`; + trackY.style.bottom = `${SCROLLBAR_WIDTH}px`; + trackX.style.display = 'block'; + trackX.style.left = `${gutterWidth}px`; thumbX.style.width = `${thumbWidth}px`; - thumbX.style.left = `${gutterWidth + scrollLeft * factorX}px`; + thumbX.style.left = `${scrollLeft * factorX}px`; scrollDOM.style.overflowX = 'scroll'; } else { - thumbX.style.display = 'none'; + trackY.style.bottom = '0px'; + trackX.style.display = 'none'; scrollDOM.style.overflowX = 'hidden'; } @@ -251,7 +288,6 @@ export default function scrollbarViewPlugin( factorY = (trackYHeight - thumbHeight) / (scrollHeight - scrollerHeight); thumbY.style.display = 'block'; thumbY.style.height = `${thumbHeight}px`; - thumbY.style.width = `${SCROLLBAR_WIDTH}px`; thumbY.style.top = `${scrollTop * factorY}px`; gutterDecoration.style.left = `${gutterWidth}px`; @@ -266,7 +302,13 @@ export default function scrollbarViewPlugin( )}px`; if (rebuildRequested) { - rebuildAnnotations(scrollHeight, trackYHeight); + rebuildAnnotations( + view, + scrollHeight, + trackYHeight, + holder, + annotations, + ); rebuildRequested = false; } } -- cgit v1.2.3-54-g00ecf