Skip to content

Commit c2d05fc

Browse files
committed
Fix selection overlay sizing, draw-selection scroll anchoring, and granularity precedence
- Expand selection overlays by 2 px left/right so they fully cover grid cells - Match selection overlay border-radius to note-viewer cells (8 px) - Anchor draw-selection start point to scroll host content so the rect does not jump when the user scrolls during a drag - Auto-scroll the track viewer when the pointer is near the host edge during draw selection; speed scales with distance beyond the edge - Always resolve entries by dominant granularity instead of giving Track entries unconditional priority — the lowest granularity level wins, preventing full-track selection from overriding individual cell selections Signed-off-by: Mike Lischke <mike@lischke-online.de>
1 parent 2568d39 commit c2d05fc

4 files changed

Lines changed: 132 additions & 11 deletions

File tree

src/components/styles.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,7 @@
10911091
position: absolute;
10921092
background-color: color-mix(in srgb, var(--color-primary) 25%, transparent);
10931093
border: 1px solid color-mix(in srgb, var(--color-primary) 60%, transparent);
1094-
border-radius: 6px;
1094+
border-radius: 8px;
10951095
pointer-events: none;
10961096
}
10971097

src/components/ui/Arrangement/ArrangementViewer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export class ArrangementViewer extends UIComponent<IArrangementViewerProps, IArr
9494
const { autoFollowIsOn, viewerZoom } = this.state;
9595

9696
services.selectionManager.setEventContainer(this.arrangementViewerRef.current!);
97+
services.selectionManager.setScrollHost(this.viewerRef.current!);
9798

9899
requisitions.register("settingsChanged", this.handleSettingsChanged);
99100
requisitions.register("trackViewModeToggled", this.handleTrackViewModeToggled);

src/ui/SelectionManager.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ export class SelectionManager {
101101
this.view = new SelectionView(this, container);
102102
}
103103

104+
/**
105+
* Sets the scroll host element for the selection view.
106+
* Must be called after {@link setEventContainer}.
107+
*
108+
* @param host The scrollable container element (typically `#trackViewerHost`).
109+
*/
110+
public setScrollHost(host: HTMLElement): void {
111+
this.view?.setScrollHost(host);
112+
}
113+
104114
public registerHitTester(tester: ISelectionHitTester): void {
105115
this.hitTesters.add(tester);
106116
}
@@ -454,14 +464,6 @@ export class SelectionManager {
454464
rawEntries.push(...tester.hitTest(rect));
455465
}
456466

457-
if (rawEntries.some((e) => {
458-
return e.granularity === SelectionGranularity.Track;
459-
})) {
460-
return rawEntries.filter((e) => {
461-
return e.granularity === SelectionGranularity.Track;
462-
});
463-
}
464-
465467
return this.filterToDominantGranularity(rawEntries);
466468
}
467469

src/ui/SelectionView.ts

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,19 @@ const formElementNames = new Set(["BUTTON", "INPUT", "SELECT", "TEXTAREA"]);
2626
export class SelectionView {
2727
private rectElement?: HTMLDivElement;
2828
private captureElement?: HTMLElement;
29+
private scrollHost?: HTMLElement;
2930

3031
private dragPending = false;
3132
private isDragging = false;
3233
private startX = 0;
3334
private startY = 0;
35+
private startScrollLeft = 0;
36+
private startScrollTop = 0;
37+
private lastPointerX = 0;
38+
private lastPointerY = 0;
39+
private autoScrollTimer?: ReturnType<typeof setInterval>;
40+
private autoScrollDX = 0;
41+
private autoScrollDY = 0;
3442

3543
/**
3644
* Ratio of viewport pixels to CSS pixels inside #trackViewerContainer.
@@ -54,6 +62,16 @@ export class SelectionView {
5462
requisitions.unregister("selectionChanged", this.handleSelectionChanged);
5563
}
5664

65+
/**
66+
* Sets the scroll host element whose scroll position is tracked during drag selection
67+
* so the selection rectangle stays anchored to the content. Also enables edge auto-scroll.
68+
*
69+
* @param host The scrollable container element (typically `#trackViewerHost`).
70+
*/
71+
public setScrollHost(host: HTMLElement): void {
72+
this.scrollHost = host;
73+
}
74+
5775
private handlePointerDown = (event: PointerEvent): void => {
5876
if (event.defaultPrevented || this.isFormElement(event.target)) {
5977
return;
@@ -69,6 +87,15 @@ export class SelectionView {
6987
this.dragPending = true;
7088
this.startX = event.clientX;
7189
this.startY = event.clientY;
90+
this.lastPointerX = event.clientX;
91+
this.lastPointerY = event.clientY;
92+
93+
if (this.scrollHost) {
94+
this.startScrollLeft = this.scrollHost.scrollLeft;
95+
this.startScrollTop = this.scrollHost.scrollTop;
96+
this.scrollHost.addEventListener("scroll", this.handleScroll, { passive: true });
97+
}
98+
7299
this.createRectElement(event.clientX, event.clientY);
73100

74101
event.preventDefault();
@@ -79,7 +106,11 @@ export class SelectionView {
79106
return;
80107
}
81108

109+
this.lastPointerX = event.clientX;
110+
this.lastPointerY = event.clientY;
111+
82112
this.updateRectElement(event.clientX, event.clientY);
113+
this.updateAutoScroll(event.clientX, event.clientY);
83114

84115
const rect = this.rectElement.getBoundingClientRect();
85116
if (rect.width > 2 || rect.height > 2) {
@@ -116,6 +147,31 @@ export class SelectionView {
116147
this.cancelDrag();
117148
};
118149

150+
/**
151+
* Adjusts the selection start point when the scroll host scrolls during a drag,
152+
* keeping the anchor pinned to the content rather than the viewport.
153+
*/
154+
private handleScroll = (): void => {
155+
if (!this.scrollHost || !this.rectElement) {
156+
return;
157+
}
158+
159+
const deltaX = this.scrollHost.scrollLeft - this.startScrollLeft;
160+
const deltaY = this.scrollHost.scrollTop - this.startScrollTop;
161+
162+
this.startX -= deltaX;
163+
this.startY -= deltaY;
164+
this.startScrollLeft = this.scrollHost.scrollLeft;
165+
this.startScrollTop = this.scrollHost.scrollTop;
166+
167+
this.updateRectElement(this.lastPointerX, this.lastPointerY);
168+
169+
if (this.isDragging) {
170+
const rect = this.rectElement.getBoundingClientRect();
171+
void requisitions.execute("selectionRectChanged", { rect });
172+
}
173+
};
174+
119175
private selectionModeFromEvent(event: KeyboardEvent | MouseEvent): SelectionMode {
120176
if (event.shiftKey) {
121177
return SelectionMode.Add;
@@ -144,8 +200,15 @@ export class SelectionView {
144200
private cancelDrag(): void {
145201
this.isDragging = false;
146202
this.dragPending = false;
203+
this.stopAutoScroll();
147204
this.removeRectElement();
148205

206+
if (this.scrollHost) {
207+
this.scrollHost.removeEventListener("scroll", this.handleScroll);
208+
this.startScrollLeft = 0;
209+
this.startScrollTop = 0;
210+
}
211+
149212
if (this.captureElement) {
150213
this.captureElement.removeEventListener("pointermove", this.handlePointerMove);
151214
this.captureElement.removeEventListener("pointerup", this.handlePointerUp);
@@ -154,6 +217,61 @@ export class SelectionView {
154217
}
155218
}
156219

220+
/**
221+
* Starts, updates or stops auto-scroll based on the pointer's distance from the scroll host edges.
222+
* The further the pointer is outside the host, the faster the scroll speed.
223+
*
224+
* @param clientX The current pointer X position in viewport coordinates.
225+
* @param clientY The current pointer Y position in viewport coordinates.
226+
*/
227+
private updateAutoScroll(clientX: number, clientY: number): void {
228+
if (!this.scrollHost) {
229+
return;
230+
}
231+
232+
const hostRect = this.scrollHost.getBoundingClientRect();
233+
const edgeThreshold = 10;
234+
let scrollDX = 0;
235+
let scrollDY = 0;
236+
237+
const distLeft = clientX - hostRect.left;
238+
const distRight = hostRect.right - clientX;
239+
const distTop = clientY - hostRect.top;
240+
const distBottom = hostRect.bottom - clientY;
241+
242+
if (distLeft < edgeThreshold) {
243+
scrollDX = -Math.max(1, Math.ceil((edgeThreshold - distLeft) / 2));
244+
} else if (distRight < edgeThreshold) {
245+
scrollDX = Math.max(1, Math.ceil((edgeThreshold - distRight) / 2));
246+
}
247+
248+
if (distTop < edgeThreshold) {
249+
scrollDY = -Math.max(1, Math.ceil((edgeThreshold - distTop) / 2));
250+
} else if (distBottom < edgeThreshold) {
251+
scrollDY = Math.max(1, Math.ceil((edgeThreshold - distBottom) / 2));
252+
}
253+
254+
this.autoScrollDX = scrollDX;
255+
this.autoScrollDY = scrollDY;
256+
257+
if (scrollDX !== 0 || scrollDY !== 0) {
258+
this.autoScrollTimer ??= setInterval(() => {
259+
this.scrollHost!.scrollBy(this.autoScrollDX, this.autoScrollDY);
260+
}, 16);
261+
} else {
262+
this.stopAutoScroll();
263+
}
264+
}
265+
266+
private stopAutoScroll(): void {
267+
if (this.autoScrollTimer) {
268+
clearInterval(this.autoScrollTimer);
269+
this.autoScrollTimer = undefined;
270+
this.autoScrollDX = 0;
271+
this.autoScrollDY = 0;
272+
}
273+
}
274+
157275
private createRectElement(x: number, y: number): void {
158276
this.removeRectElement();
159277

@@ -906,9 +1024,9 @@ export class SelectionView {
9061024
const overlay = document.createElement("div");
9071025
overlay.className = selectionOverlayClass;
9081026
overlay.style.position = "absolute";
909-
overlay.style.left = `${rect.x}px`;
1027+
overlay.style.left = `${rect.x - 2}px`;
9101028
overlay.style.top = `${rect.y}px`;
911-
overlay.style.width = `${rect.width}px`;
1029+
overlay.style.width = `${rect.width + 4}px`;
9121030
overlay.style.height = `${rect.height}px`;
9131031
container.appendChild(overlay);
9141032
}

0 commit comments

Comments
 (0)