aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-12-01 02:41:30 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-12-09 00:07:39 +0100
commitfa5aa53940f4985ab10aa31ac0cefe9d52a54a87 (patch)
treecb206fe9e980e3fa6752c5882a15a701b3aec7af
parentfeat(frontend): dev mode badge (diff)
downloadrefinery-fa5aa53940f4985ab10aa31ac0cefe9d52a54a87.tar.gz
refinery-fa5aa53940f4985ab10aa31ac0cefe9d52a54a87.tar.zst
refinery-fa5aa53940f4985ab10aa31ac0cefe9d52a54a87.zip
refactor(frontend): scrollbar improvements
-rw-r--r--subprojects/frontend/src/editor/EditorTheme.ts22
-rw-r--r--subprojects/frontend/src/editor/indentationMarkerViewPlugin.ts2
-rw-r--r--subprojects/frontend/src/editor/scrollbarViewPlugin.ts260
3 files changed, 168 insertions, 116 deletions
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', {
84 background: 'transparent', 84 background: 'transparent',
85 }, 85 },
86 }, 86 },
87 '.cm-scroller-track': {
88 position: 'absolute',
89 zIndex: 300,
90 touchAction: 'none',
91 },
87 '.cm-scroller-thumb': { 92 '.cm-scroller-thumb': {
88 position: 'absolute', 93 position: 'absolute',
89 background: theme.palette.text.secondary, 94 background: theme.palette.text.secondary,
@@ -105,13 +110,18 @@ export default styled('div', {
105 userSelect: 'none', 110 userSelect: 'none',
106 }, 111 },
107 }, 112 },
108 '.cm-scroller-thumb-y': { 113 '.cm-scroller-track-y, .cm-scroller-thumb-y': {
109 top: 0, 114 top: 0,
110 right: 0, 115 right: 0,
116 width: 12,
111 }, 117 },
112 '.cm-scroller-thumb-x': { 118 '.cm-scroller-track-x, .cm-scroller-thumb-x': {
113 left: 0, 119 left: 0,
114 bottom: 0, 120 bottom: 0,
121 height: 12,
122 },
123 '.cm-scroller-track-x': {
124 right: 12,
115 }, 125 },
116 '.cm-scroller-gutter-decoration': { 126 '.cm-scroller-gutter-decoration': {
117 position: 'absolute', 127 position: 'absolute',
@@ -141,7 +151,6 @@ export default styled('div', {
141 }, 151 },
142 '.cm-content': { 152 '.cm-content': {
143 ...editorFontStyle, 153 ...editorFontStyle,
144 padding: '0 12px 0 0',
145 }, 154 },
146 '.cm-activeLine': { 155 '.cm-activeLine': {
147 background: showActiveLine 156 background: showActiveLine
@@ -165,6 +174,7 @@ export default styled('div', {
165 }, 174 },
166 '.cm-line': { 175 '.cm-line': {
167 position: 'relative', // For indentation highlights 176 position: 'relative', // For indentation highlights
177 padding: '0 12px 0 0px',
168 }, 178 },
169 }; 179 };
170 180
@@ -440,11 +450,11 @@ export default styled('div', {
440 450
441 const foldStyle = { 451 const foldStyle = {
442 '.cm-foldGutter': { 452 '.cm-foldGutter': {
443 width: 16, 453 width: 18,
444 }, 454 },
445 '.problem-editor-foldMarker': { 455 '.problem-editor-foldMarker': {
446 display: 'block', 456 display: 'block',
447 margin: '4px 0', 457 margin: '4px 2px 4px 0',
448 padding: 0, 458 padding: 0,
449 maskImage: svgURL(expandMoreSVG), 459 maskImage: svgURL(expandMoreSVG),
450 maskSize: '16px 16px', 460 maskSize: '16px 16px',
@@ -455,7 +465,7 @@ export default styled('div', {
455 cursor: 'pointer', 465 cursor: 'pointer',
456 WebkitTapHighlightColor: 'transparent', 466 WebkitTapHighlightColor: 'transparent',
457 [theme.breakpoints.down('sm')]: { 467 [theme.breakpoints.down('sm')]: {
458 margin: '2px 0', 468 margin: '2px 2px 2px 0',
459 }, 469 },
460 }, 470 },
461 '.problem-editor-foldMarker-open': { 471 '.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 {
61 61
62 const wrapper = document.createElement('span'); 62 const wrapper = document.createElement('span');
63 wrapper.style.top = '0'; 63 wrapper.style.top = '0';
64 wrapper.style.left = '6px'; 64 wrapper.style.left = '0';
65 wrapper.style.position = 'absolute'; 65 wrapper.style.position = 'absolute';
66 wrapper.style.pointerEvents = 'none'; 66 wrapper.style.pointerEvents = 'none';
67 67
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 @@
1import { EditorSelection } from '@codemirror/state'; 1import { EditorSelection } from '@codemirror/state';
2import { type PluginValue, ViewPlugin } from '@codemirror/view'; 2import {
3 type EditorView,
4 type PluginValue,
5 ViewPlugin,
6} from '@codemirror/view';
3import { reaction } from 'mobx'; 7import { reaction } from 'mobx';
4 8
5import type EditorStore from './EditorStore'; 9import type EditorStore from './EditorStore';
@@ -8,9 +12,8 @@ import findOccurrences from './findOccurrences';
8 12
9export const HOLDER_CLASS = 'cm-scroller-holder'; 13export const HOLDER_CLASS = 'cm-scroller-holder';
10export const SPACER_CLASS = 'cm-scroller-spacer'; 14export const SPACER_CLASS = 'cm-scroller-spacer';
15export const TRACK_CLASS = 'cm-scroller-track';
11export const THUMB_CLASS = 'cm-scroller-thumb'; 16export const THUMB_CLASS = 'cm-scroller-thumb';
12export const THUMB_Y_CLASS = 'cm-scroller-thumb-y';
13export const THUMB_X_CLASS = 'cm-scroller-thumb-x';
14export const THUMB_ACTIVE_CLASS = 'active'; 17export const THUMB_ACTIVE_CLASS = 'active';
15export const GUTTER_DECORATION_CLASS = 'cm-scroller-gutter-decoration'; 18export const GUTTER_DECORATION_CLASS = 'cm-scroller-gutter-decoration';
16export const TOP_DECORATION_CLASS = 'cm-scroller-top-decoration'; 19export const TOP_DECORATION_CLASS = 'cm-scroller-top-decoration';
@@ -22,28 +25,42 @@ export const SCROLLBAR_WIDTH = 12;
22export const ANNOTATION_WIDTH = SCROLLBAR_WIDTH / 2; 25export const ANNOTATION_WIDTH = SCROLLBAR_WIDTH / 2;
23export const MIN_ANNOTATION_HEIGHT = 1; 26export const MIN_ANNOTATION_HEIGHT = 1;
24 27
25function handleDrag( 28function createScrollbar(
26 element: HTMLElement, 29 holder: HTMLElement,
27 callback: (movementX: number, movementY: number) => void, 30 direction: 'x' | 'y',
28) { 31 touchCallback: (offsetX: number, offsetY: number) => void,
32 moveCallback: (movementX: number, movementY: number) => void,
33): { track: HTMLElement; thumb: HTMLElement } {
34 const track = holder.ownerDocument.createElement('div');
35 track.className = `${TRACK_CLASS} ${TRACK_CLASS}-${direction}`;
36 holder.appendChild(track);
37
38 const thumb = holder.ownerDocument.createElement('div');
39 thumb.className = `${THUMB_CLASS} ${THUMB_CLASS}-${direction}`;
40 track.appendChild(thumb);
41
29 let pointerId: number | undefined; 42 let pointerId: number | undefined;
30 element.addEventListener('pointerdown', (event) => { 43 track.addEventListener('pointerdown', (event) => {
31 if (pointerId === undefined) { 44 if (pointerId !== undefined) {
32 ({ pointerId } = event);
33 element.setPointerCapture(pointerId);
34 element.classList.add(THUMB_ACTIVE_CLASS);
35 } else {
36 event.preventDefault(); 45 event.preventDefault();
37 // Avoid implicit pointer capture, see https://w3c.github.io/pointerevents/#dfn-implicit-pointer-capture 46 return;
38 element.releasePointerCapture(event.pointerId); 47 }
48 ({ pointerId } = event);
49 thumb.classList.add(THUMB_ACTIVE_CLASS);
50 if (event.target === thumb) {
51 // Prevent implicit pointer capture on mobile.
52 thumb.releasePointerCapture(pointerId);
53 } else {
54 touchCallback(event.offsetX, event.offsetY);
39 } 55 }
56 track.setPointerCapture(pointerId);
40 }); 57 });
41 58
42 element.addEventListener('pointermove', (event) => { 59 track.addEventListener('pointermove', (event) => {
43 if (event.pointerId !== pointerId) { 60 if (event.pointerId !== pointerId) {
44 return; 61 return;
45 } 62 }
46 callback(event.movementX, event.movementY); 63 moveCallback(event.movementX, event.movementY);
47 event.preventDefault(); 64 event.preventDefault();
48 }); 65 });
49 66
@@ -52,11 +69,88 @@ function handleDrag(
52 return; 69 return;
53 } 70 }
54 pointerId = undefined; 71 pointerId = undefined;
55 element.classList.remove(THUMB_ACTIVE_CLASS); 72 thumb.classList.remove(THUMB_ACTIVE_CLASS);
73 }
74
75 track.addEventListener('pointerup', scrollEnd, { passive: true });
76 track.addEventListener('pointercancel', scrollEnd, { passive: true });
77
78 return { track, thumb };
79}
80
81function rebuildAnnotations(
82 view: EditorView,
83 scrollHeight: number,
84 trackYHeight: number,
85 holder: HTMLElement,
86 annotations: HTMLDivElement[],
87) {
88 const { state } = view;
89 const overlayAnnotationsHeight =
90 (view.contentHeight / scrollHeight) * trackYHeight;
91 const lineHeight = overlayAnnotationsHeight / state.doc.lines;
92
93 let i = 0;
94
95 function getOrCreateAnnotation(from: number, to?: number): HTMLDivElement {
96 const startLine = state.doc.lineAt(from).number;
97 const endLine = to === undefined ? startLine : state.doc.lineAt(to).number;
98 const top = (startLine - 1) * lineHeight;
99 const height = Math.max(
100 MIN_ANNOTATION_HEIGHT,
101 Math.max(1, endLine - startLine) * lineHeight,
102 );
103
104 let annotation: HTMLDivElement | undefined;
105 if (i < annotations.length) {
106 annotation = annotations[i];
107 }
108 if (annotation === undefined) {
109 annotation = holder.ownerDocument.createElement('div');
110 annotations.push(annotation);
111 holder.appendChild(annotation);
112 }
113 i += 1;
114
115 annotation.style.top = `${top}px`;
116 annotation.style.height = `${height}px`;
117
118 return annotation;
119 }
120
121 state.selection.ranges.forEach(({ head }) => {
122 const selectionAnnotation = getOrCreateAnnotation(head);
123 selectionAnnotation.className = ANNOTATION_SELECTION_CLASS;
124 selectionAnnotation.style.width = `${SCROLLBAR_WIDTH}px`;
125 });
126
127 const diagnosticsIter = getDiagnostics(state).iter();
128 while (diagnosticsIter.value !== null) {
129 const diagnosticAnnotation = getOrCreateAnnotation(
130 diagnosticsIter.from,
131 diagnosticsIter.to,
132 );
133 diagnosticAnnotation.className = `${ANNOTATION_DIAGNOSTIC_CLASS} ${ANNOTATION_DIAGNOSTIC_CLASS}-${diagnosticsIter.value.severity}`;
134 diagnosticAnnotation.style.width = `${ANNOTATION_WIDTH}px`;
135 diagnosticsIter.next();
56 } 136 }
57 137
58 element.addEventListener('pointerup', scrollEnd, { passive: true }); 138 const occurrences = view.state.field(findOccurrences);
59 element.addEventListener('pointercancel', scrollEnd, { passive: true }); 139 const occurrencesIter = occurrences.iter();
140 while (occurrencesIter.value !== null) {
141 const occurrenceAnnotation = getOrCreateAnnotation(
142 occurrencesIter.from,
143 occurrencesIter.to,
144 );
145 occurrenceAnnotation.className = ANNOTATION_OCCURRENCE_CLASS;
146 occurrenceAnnotation.style.width = `${ANNOTATION_WIDTH}px`;
147 occurrenceAnnotation.style.right = `${ANNOTATION_WIDTH}px`;
148 occurrencesIter.next();
149 }
150
151 annotations
152 .splice(i)
153 .forEach((staleAnnotation) => holder.removeChild(staleAnnotation));
60} 154}
61 155
62export default function scrollbarViewPlugin( 156export default function scrollbarViewPlugin(
@@ -98,19 +192,33 @@ export default function scrollbarViewPlugin(
98 let factorY = 1; 192 let factorY = 1;
99 let factorX = 1; 193 let factorX = 1;
100 194
101 const thumbY = ownerDocument.createElement('div'); 195 const { track: trackY, thumb: thumbY } = createScrollbar(
102 thumbY.className = `${THUMB_CLASS} ${THUMB_Y_CLASS}`; 196 holder,
103 handleDrag(thumbY, (_movementX, movementY) => 197 'y',
104 scrollDOM.scrollBy({ top: movementY / factorY }), 198 (_offsetX, offsetY) => {
199 const scaledOffset = offsetY / factorY;
200 const { height: scrollerHeight } = scrollDOM.getBoundingClientRect();
201 const target = Math.max(0, scaledOffset - scrollerHeight / 2);
202 scrollDOM.scrollTo({ top: target });
203 },
204 (_movementX, movementY) => {
205 scrollDOM.scrollBy({ top: movementY / factorY });
206 },
105 ); 207 );
106 holder.appendChild(thumbY);
107 208
108 const thumbX = ownerDocument.createElement('div'); 209 const { track: trackX, thumb: thumbX } = createScrollbar(
109 thumbX.className = `${THUMB_CLASS} ${THUMB_X_CLASS}`; 210 holder,
110 handleDrag(thumbX, (movementX) => 211 'x',
111 scrollDOM.scrollBy({ left: movementX / factorX }), 212 (offsetX) => {
213 const scaledOffset = offsetX / factorX;
214 const { width: scrollerWidth } = scrollDOM.getBoundingClientRect();
215 const target = Math.max(0, scaledOffset - scrollerWidth / 2);
216 scrollDOM.scrollTo({ left: target });
217 },
218 (movementX) => {
219 scrollDOM.scrollBy({ left: movementX / factorX });
220 },
112 ); 221 );
113 holder.appendChild(thumbX);
114 222
115 const gutterDecoration = ownerDocument.createElement('div'); 223 const gutterDecoration = ownerDocument.createElement('div');
116 gutterDecoration.className = GUTTER_DECORATION_CLASS; 224 gutterDecoration.className = GUTTER_DECORATION_CLASS;
@@ -135,79 +243,6 @@ export default function scrollbarViewPlugin(
135 243
136 const annotations: HTMLDivElement[] = []; 244 const annotations: HTMLDivElement[] = [];
137 245
138 function rebuildAnnotations(scrollHeight: number, trackYHeight: number) {
139 const { state } = view;
140 const overlayAnnotationsHeight =
141 (view.contentHeight / scrollHeight) * trackYHeight;
142 const lineHeight = overlayAnnotationsHeight / state.doc.lines;
143
144 let i = 0;
145
146 function getOrCreateAnnotation(
147 from: number,
148 to?: number,
149 ): HTMLDivElement {
150 const startLine = state.doc.lineAt(from).number;
151 const endLine =
152 to === undefined ? startLine : state.doc.lineAt(to).number;
153 const top = (startLine - 1) * lineHeight;
154 const height = Math.max(
155 MIN_ANNOTATION_HEIGHT,
156 Math.max(1, endLine - startLine) * lineHeight,
157 );
158
159 let annotation: HTMLDivElement | undefined;
160 if (i < annotations.length) {
161 annotation = annotations[i];
162 }
163 if (annotation === undefined) {
164 annotation = ownerDocument.createElement('div');
165 annotations.push(annotation);
166 holder.appendChild(annotation);
167 }
168 i += 1;
169
170 annotation.style.top = `${top}px`;
171 annotation.style.height = `${height}px`;
172
173 return annotation;
174 }
175
176 state.selection.ranges.forEach(({ head }) => {
177 const selectionAnnotation = getOrCreateAnnotation(head);
178 selectionAnnotation.className = ANNOTATION_SELECTION_CLASS;
179 selectionAnnotation.style.width = `${SCROLLBAR_WIDTH}px`;
180 });
181
182 const diagnosticsIter = getDiagnostics(state).iter();
183 while (diagnosticsIter.value !== null) {
184 const diagnosticAnnotation = getOrCreateAnnotation(
185 diagnosticsIter.from,
186 diagnosticsIter.to,
187 );
188 diagnosticAnnotation.className = `${ANNOTATION_DIAGNOSTIC_CLASS} ${ANNOTATION_DIAGNOSTIC_CLASS}-${diagnosticsIter.value.severity}`;
189 diagnosticAnnotation.style.width = `${ANNOTATION_WIDTH}px`;
190 diagnosticsIter.next();
191 }
192
193 const occurrences = view.state.field(findOccurrences);
194 const occurrencesIter = occurrences.iter();
195 while (occurrencesIter.value !== null) {
196 const occurrenceAnnotation = getOrCreateAnnotation(
197 occurrencesIter.from,
198 occurrencesIter.to,
199 );
200 occurrenceAnnotation.className = ANNOTATION_OCCURRENCE_CLASS;
201 occurrenceAnnotation.style.width = `${ANNOTATION_WIDTH}px`;
202 occurrenceAnnotation.style.right = `${ANNOTATION_WIDTH}px`;
203 occurrencesIter.next();
204 }
205
206 annotations
207 .splice(i)
208 .forEach((staleAnnotation) => holder.removeChild(staleAnnotation));
209 }
210
211 let observer: ResizeObserver | undefined; 246 let observer: ResizeObserver | undefined;
212 247
213 function update() { 248 function update() {
@@ -237,13 +272,15 @@ export default function scrollbarViewPlugin(
237 const trackXWidth = scrollerWidth - gutterWidth - SCROLLBAR_WIDTH; 272 const trackXWidth = scrollerWidth - gutterWidth - SCROLLBAR_WIDTH;
238 const thumbWidth = trackXWidth * (scrollerWidth / scrollWidth); 273 const thumbWidth = trackXWidth * (scrollerWidth / scrollWidth);
239 factorX = (trackXWidth - thumbWidth) / (scrollWidth - scrollerWidth); 274 factorX = (trackXWidth - thumbWidth) / (scrollWidth - scrollerWidth);
240 thumbX.style.display = 'block'; 275 trackY.style.bottom = `${SCROLLBAR_WIDTH}px`;
241 thumbX.style.height = `${SCROLLBAR_WIDTH}px`; 276 trackX.style.display = 'block';
277 trackX.style.left = `${gutterWidth}px`;
242 thumbX.style.width = `${thumbWidth}px`; 278 thumbX.style.width = `${thumbWidth}px`;
243 thumbX.style.left = `${gutterWidth + scrollLeft * factorX}px`; 279 thumbX.style.left = `${scrollLeft * factorX}px`;
244 scrollDOM.style.overflowX = 'scroll'; 280 scrollDOM.style.overflowX = 'scroll';
245 } else { 281 } else {
246 thumbX.style.display = 'none'; 282 trackY.style.bottom = '0px';
283 trackX.style.display = 'none';
247 scrollDOM.style.overflowX = 'hidden'; 284 scrollDOM.style.overflowX = 'hidden';
248 } 285 }
249 286
@@ -251,7 +288,6 @@ export default function scrollbarViewPlugin(
251 factorY = (trackYHeight - thumbHeight) / (scrollHeight - scrollerHeight); 288 factorY = (trackYHeight - thumbHeight) / (scrollHeight - scrollerHeight);
252 thumbY.style.display = 'block'; 289 thumbY.style.display = 'block';
253 thumbY.style.height = `${thumbHeight}px`; 290 thumbY.style.height = `${thumbHeight}px`;
254 thumbY.style.width = `${SCROLLBAR_WIDTH}px`;
255 thumbY.style.top = `${scrollTop * factorY}px`; 291 thumbY.style.top = `${scrollTop * factorY}px`;
256 292
257 gutterDecoration.style.left = `${gutterWidth}px`; 293 gutterDecoration.style.left = `${gutterWidth}px`;
@@ -266,7 +302,13 @@ export default function scrollbarViewPlugin(
266 )}px`; 302 )}px`;
267 303
268 if (rebuildRequested) { 304 if (rebuildRequested) {
269 rebuildAnnotations(scrollHeight, trackYHeight); 305 rebuildAnnotations(
306 view,
307 scrollHeight,
308 trackYHeight,
309 holder,
310 annotations,
311 );
270 rebuildRequested = false; 312 rebuildRequested = false;
271 } 313 }
272 } 314 }