UI: tracks drag & drop + minor fixes
Allows tracks to be reordered by drag&drop, supporting
also D&D between scrolling and pinned containers.
Furthermore bunch of small fixes:
- reduce the track shell width to 250px, was taking
too much real estate.
- fix off-by-one pixel caused by the track bottom
border. The border was being over-painted by the
track shell having a bg:white. Now it's a border
shifted back by 1px, so the track stays at full
height but the border is always overlaid on top.
- Reduces the font size of the track shell.
- Fixes ui_unittests.
Demo: https://primiano-dot-perfetto-ui.appspot.com
Change-Id: I5e99fbc1d7f8118c166ff1e677a7acf805469599
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 9386985..01ee096 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -25,7 +25,7 @@
// TODO(hjd): We should remove the constant where possible.
// If any uses can't be removed we should read this constant from CSS.
-export const TRACK_SHELL_WIDTH = 300;
+export const TRACK_SHELL_WIDTH = 250;
function isPinned(id: string) {
return globals.state.pinnedTracks.indexOf(id) !== -1;
@@ -34,26 +34,86 @@
interface TrackShellAttrs {
trackState: TrackState;
}
+
class TrackShell implements m.ClassComponent<TrackShellAttrs> {
+ // Set to true when we click down and drag the
+ private dragging = false;
+ private dropping: 'before'|'after'|undefined = undefined;
+ private attrs?: TrackShellAttrs;
+
+ oninit(vnode: m.Vnode<TrackShellAttrs>) {
+ this.attrs = vnode.attrs;
+ }
+
view({attrs}: m.CVnode<TrackShellAttrs>) {
+ const dragClass = this.dragging ? `.drag` : '';
+ const dropClass = this.dropping ? `.drop-${this.dropping}` : '';
return m(
- '.track-shell',
+ `.track-shell${dragClass}${dropClass}[draggable=true]`,
+ {
+ onmousedown: this.onmousedown.bind(this),
+ ondragstart: this.ondragstart.bind(this),
+ ondragend: this.ondragend.bind(this),
+ ondragover: this.ondragover.bind(this),
+ ondragleave: this.ondragleave.bind(this),
+ ondrop: this.ondrop.bind(this),
+ },
m('h1', attrs.trackState.name),
m(TrackButton, {
- action: Actions.moveTrack(
- {trackId: attrs.trackState.id, direction: 'up'}),
- i: 'arrow_upward_alt',
- }),
- m(TrackButton, {
- action: Actions.moveTrack(
- {trackId: attrs.trackState.id, direction: 'down'}),
- i: 'arrow_downward_alt',
- }),
- m(TrackButton, {
action: Actions.toggleTrackPinned({trackId: attrs.trackState.id}),
i: isPinned(attrs.trackState.id) ? 'star' : 'star_border',
}));
}
+
+ onmousedown(e: MouseEvent) {
+ // Prevent that the click is intercepted by the PanAndZoomHandler and that
+ // we start panning while dragging.
+ e.stopPropagation();
+ }
+
+ ondragstart(e: DragEvent) {
+ this.dragging = true;
+ globals.rafScheduler.scheduleFullRedraw();
+ e.dataTransfer.setData('perfetto/track', `${this.attrs!.trackState.id}`);
+ e.dataTransfer.setDragImage(new Image(), 0, 0);
+ e.stopImmediatePropagation();
+ }
+
+ ondragend() {
+ this.dragging = false;
+ globals.rafScheduler.scheduleFullRedraw();
+ }
+
+ ondragover(e: DragEvent) {
+ if (this.dragging) return;
+ if (!(e.target instanceof HTMLElement)) return;
+ if (!e.dataTransfer.types.includes('perfetto/track')) return;
+ e.dataTransfer.dropEffect = 'move';
+ e.preventDefault();
+
+ // Apply some hysteresis to the drop logic so that the lightened border
+ // changes only when we get close enough to the border.
+ if (e.offsetY < e.target.scrollHeight / 3) {
+ this.dropping = 'before';
+ } else if (e.offsetY > e.target.scrollHeight / 3 * 2) {
+ this.dropping = 'after';
+ }
+ globals.rafScheduler.scheduleFullRedraw();
+ }
+
+ ondragleave() {
+ this.dropping = undefined;
+ globals.rafScheduler.scheduleFullRedraw();
+ }
+
+ ondrop(e: DragEvent) {
+ if (this.dropping === undefined) return;
+ globals.rafScheduler.scheduleFullRedraw();
+ const srcId = e.dataTransfer.getData('perfetto/track');
+ const dstId = this.attrs!.trackState.id;
+ globals.dispatch(Actions.moveTrack({srcId, op: this.dropping, dstId}));
+ this.dropping = undefined;
+ }
}
interface TrackContentAttrs {