Skip to content

Commit 8ff82fe

Browse files
authored
Add timeline grabbing interaction (#479)
1 parent 3efead9 commit 8ff82fe

File tree

4 files changed

+74
-4
lines changed

4 files changed

+74
-4
lines changed

web/src/app/timeline/components/timeline-frame.component.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<div
1818
khiCaptureShiftKey
1919
class="container"
20+
[class.is-grabbing-and-moving]="isGrabbingAndMoving()"
2021
[style.--header-height.px]="HEADER_HEIGHT"
2122
[style.--gutter-width.px]="GUTTER_WIDTH"
2223
[style.--index-pane-width.px]="indexAreaWidthPixels()"
@@ -28,6 +29,9 @@
2829
[style.--viewport-height.px]="viewportHeight()"
2930
[style.--viewport-width.px]="viewportWidth()"
3031
(shiftStatusChange)="shiftStatus.set($event)"
32+
(mousedown)="handleMouseDown($event)"
33+
(mouseup)="handleMouseUp()"
34+
(mouseleave)="handleMouseLeave()"
3135
>
3236
<div class="virtual-scroll-container" #container>
3337
<div class="corner">

web/src/app/timeline/components/timeline-frame.component.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ $z-index-resizer: 1000;
102102
grid-template-areas: "fill";
103103
grid-template-columns: 1fr;
104104
grid-template-rows: 1fr;
105+
&.is-grabbing-and-moving {
106+
user-select: none;
107+
cursor: grab;
108+
.virtual-scroll-container .chart {
109+
pointer-events: none;
110+
}
111+
}
105112
}
106113

107114
.virtual-scroll-container {

web/src/app/timeline/components/timeline-frame.component.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,25 @@ export class TimelineFrameComponent implements AfterViewInit {
593593
);
594594
});
595595

596+
/**
597+
* Whether the user is currently grabbing the chart or not.
598+
*/
599+
private readonly isGrabbing = signal(false);
600+
601+
/**
602+
* Whether the user is currently grabbing and moving the chart or not.
603+
* This is needed in addition to isGrabbing not to prevent click event by applying pointer-events: none to the chart area just by mouse down event.
604+
*/
605+
protected readonly isGrabbingAndMoving = signal(false);
606+
607+
/**
608+
* The position of the last mouse down event.
609+
*/
610+
private readonly lastMouseDownPosition: { x: number; y: number } = {
611+
x: 0,
612+
y: 0,
613+
};
614+
596615
/**
597616
* The current action that is being performed.
598617
* This is defined not to move and scale at the same frame.
@@ -720,6 +739,30 @@ export class TimelineFrameComponent implements AfterViewInit {
720739
outputRef.emit(e);
721740
}
722741

742+
handleMouseDown(e: MouseEvent) {
743+
const indexArea = this.indexSplitArea()?.nativeElement;
744+
if (!indexArea) {
745+
return;
746+
}
747+
const indexAreaRect = indexArea.getBoundingClientRect();
748+
const isChartArea = e.clientX > indexAreaRect.right + this.GUTTER_WIDTH;
749+
if (isChartArea) {
750+
this.isGrabbing.set(true);
751+
this.lastMouseDownPosition.x = e.clientX;
752+
this.lastMouseDownPosition.y = e.clientY;
753+
}
754+
}
755+
756+
handleMouseUp() {
757+
this.isGrabbing.set(false);
758+
this.isGrabbingAndMoving.set(false);
759+
}
760+
761+
handleMouseLeave() {
762+
this.isGrabbing.set(false);
763+
this.isGrabbingAndMoving.set(false);
764+
}
765+
723766
ngAfterViewInit(): void {
724767
// Run outside of Angular zone to avoid unnecessary change detection by size changing or scrolls..
725768
// Frequent scroll events or resize events can trigger Angular's change detection if processed within the zone, leading to performance issues.
@@ -786,11 +829,30 @@ export class TimelineFrameComponent implements AfterViewInit {
786829
container.nativeElement.addEventListener('scroll', onContainerScroll, {
787830
passive: true,
788831
});
832+
789833
const onScrollEnd = () => {
790834
this.horizontalScrollSourceOfTruth = 'property';
791835
};
792836
container.nativeElement.addEventListener('scrollend', onScrollEnd);
793837

838+
const onMouseMove = (e: MouseEvent) => {
839+
if (!this.isGrabbing()) {
840+
return;
841+
}
842+
const dx = e.clientX - this.lastMouseDownPosition.x;
843+
const dy = e.clientY - this.lastMouseDownPosition.y;
844+
this.lastMouseDownPosition.x = e.clientX;
845+
this.lastMouseDownPosition.y = e.clientY;
846+
this.isGrabbingAndMoving.set(true);
847+
this.renderingLoopManager.registerOnceBeforeRenderHandler(() => {
848+
container.nativeElement.scrollBy({
849+
left: -dx,
850+
top: -dy,
851+
});
852+
});
853+
};
854+
window.addEventListener('mousemove', onMouseMove, { passive: true });
855+
794856
this.destroyRef.onDestroy(() => {
795857
resizeObserver.disconnect();
796858
containerResizeObserver.disconnect();
@@ -800,6 +862,7 @@ export class TimelineFrameComponent implements AfterViewInit {
800862
onContainerScroll,
801863
);
802864
container.nativeElement.removeEventListener('scrollend', onScrollEnd);
865+
window.removeEventListener('mousemove', onMouseMove);
803866
});
804867
});
805868

web/src/app/timeline/timeline-smart.component.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,6 @@ export class TimelineSmartComponent {
229229
if (!timeline) {
230230
return null;
231231
}
232-
const log = this.selectedLog();
233-
if (!log) {
234-
return null;
235-
}
236232
const lastClickedTimeMs = this.lastClickedTimeMs();
237233

238234
const maxT = this.HOVER_VIEW_SELECTABLE_RANGE_IN_PX / this.pixelsPerMs();

0 commit comments

Comments
 (0)