diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts index 1e277f2ae5..ca8b5a59cd 100644 --- a/ui/src/common/actions.ts +++ b/ui/src/common/actions.ts @@ -704,19 +704,29 @@ export const StateActions = { srcParent = trackLikeSrc.parentGroup; } else { trackLikeSrc = state.tracks[args.srcId]; - srcParent = trackLikeSrc.trackGroup; + if (state.pinnedTracks.includes(trackLikeSrc.id)) { + srcParent = 'Pinned'; + } else { + srcParent = trackLikeSrc.trackGroup; + } } if (trackLikeDst) { dstParent = trackLikeDst.parentGroup; } else { trackLikeDst = state.tracks[args.dstId]; - dstParent = trackLikeDst.trackGroup; + if (state.pinnedTracks.includes(trackLikeDst.id)) { + dstParent = 'Pinned'; + } else { + dstParent = trackLikeDst.trackGroup; + } } if (srcParent && srcParent === dstParent) { if (srcParent === SCROLLING_TRACK_GROUP) { moveWithinTrackList(state.scrollingTracks); + } else if (srcParent === 'Pinned') { + moveWithinTrackList(state.pinnedTracks); } else { moveWithinTrackList(state.trackGroups[srcParent].sortOrder); } @@ -1543,6 +1553,11 @@ export const StateActions = { state.filteredTracks = [...args.filteredTracks]; }, + togglePinnedGroupCollapsed( + state: StateDraft, + _: {}) { + state.pinnedGroupCollapsed = !state.pinnedGroupCollapsed; + }, clearTrackAndGroupSelection( state: StateDraft, _: {}, diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts index 4a00bf6b90..ecd8964daf 100644 --- a/ui/src/common/empty_state.ts +++ b/ui/src/common/empty_state.ts @@ -171,6 +171,7 @@ export function createEmptyState(): State { }, filteredTracks: [], + pinnedGroupCollapsed: false, selectedTrackGroupIds: new Set(), selectedTrackIds: new Set(), }; diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts index b19201f95c..5bda6205aa 100644 --- a/ui/src/common/state.ts +++ b/ui/src/common/state.ts @@ -579,6 +579,7 @@ export interface State { traceConversionInProgress: boolean; visualisedArgs: string[]; filteredTracks: AddTrackLikeArgs[]; + pinnedGroupCollapsed: boolean; /** * This state is updated on the frontend at 60Hz and eventually syncronised to diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts index b8b970c356..b5266283be 100644 --- a/ui/src/frontend/track_group_panel.ts +++ b/ui/src/frontend/track_group_panel.ts @@ -511,6 +511,122 @@ export class TrackGroupPanel extends Panel { } } +interface MinimalGroupAttrs { + name: string; +} +export class MinimalTrackGroup extends Panel { + private shellWidth = 0; + renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) { + // If we have vsync data, render columns under the track group + const vsync = getActiveVsyncData(); + if (vsync) { + ctx.save(); + ctx.translate(this.shellWidth, 0); + renderVsyncColumns(ctx, size.height, vsync); + ctx.restore(); + } + + drawGridLines( + ctx, + size.width, + size.height); + + ctx.save(); + ctx.translate(this.shellWidth, 0); + ctx.restore(); + + const {visibleTimeScale} = globals.frontendLocalState; + // Draw vertical line when hovering on the notes panel. + if (globals.state.hoveredNoteTimestamp !== -1n) { + drawVerticalLineAtTime( + ctx, + visibleTimeScale, + globals.state.hoveredNoteTimestamp, + size.height, + `#aaa`); + } + if (globals.state.hoverCursorTimestamp !== -1n) { + drawVerticalLineAtTime( + ctx, + visibleTimeScale, + globals.state.hoverCursorTimestamp, + size.height, + `#344596`); + } + + if (globals.state.currentSelection !== null) { + if (globals.state.currentSelection.kind === 'SLICE' && + globals.sliceDetails.wakeupTs !== undefined) { + drawVerticalLineAtTime( + ctx, + visibleTimeScale, + globals.sliceDetails.wakeupTs, + size.height, + getCssStr('--main-foreground-color')); + } + } + // All marked areas should have semi-transparent vertical lines + // marking the start and end. + for (const note of Object.values(globals.state.notes)) { + if (note.noteType === 'AREA') { + const transparentNoteColor = + 'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)'; + drawVerticalLineAtTime( + ctx, + visibleTimeScale, + globals.state.areas[note.areaId].start, + size.height, + transparentNoteColor, + 1); + drawVerticalLineAtTime( + ctx, + visibleTimeScale, + globals.state.areas[note.areaId].end, + size.height, + transparentNoteColor, + 1); + } else if (note.noteType === 'DEFAULT') { + drawVerticalLineAtTime( + ctx, visibleTimeScale, note.timestamp, size.height, note.color); + } + } + } + + view({attrs}: m.Vnode) { + let name = attrs.name; + if (name[0] === '/') { + name = StripPathFromExecutable(name); + } + return m( + `.track-group-panel[collapsed=${globals.state.pinnedGroupCollapsed}]`, + {style: { + height: '18px', + }}, + m(`.shell`, + m('.fold-button', + { + style: { + marginLeft: '.5rem', + }, + onclick: (e: MouseEvent) => { + // Toggle Collapsing + e.stopPropagation(); + globals.dispatch(Actions.togglePinnedGroupCollapsed({})); + }, + }, + m('i.material-icons', + globals.state.pinnedGroupCollapsed ? + CHEVRON_RIGHT : EXPAND_DOWN)), + m('h1.track-title', + {style: { + marginLeft: '.5rem', + }}, + name, + ), + + )); + } +} function StripPathFromExecutable(path: string) { return path.split('/').slice(-1)[0]; } diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts index c891f45c16..284d48a898 100644 --- a/ui/src/frontend/track_panel.ts +++ b/ui/src/frontend/track_panel.ts @@ -339,8 +339,11 @@ class TrackShell implements m.ClassComponent { const trackLike : TrackState | TrackGroupState = globals.state.trackGroups[trackLikeId] ?? globals.state.tracks[trackLikeId]; - if (('trackGroup' in trackLike && this.attrs!.trackState.trackGroup === trackLike.trackGroup) || - 'parentGroup' in trackLike && this.attrs!.trackState.trackGroup === trackLike.parentGroup) { + if ( + ('trackGroup' in trackLike && this.attrs!.trackState.trackGroup === trackLike.trackGroup) || + 'parentGroup' in trackLike && this.attrs!.trackState.trackGroup === trackLike.parentGroup || + (globals.state.pinnedTracks.includes(this.attrs!.trackState.id) && + trackLike.id)) { // 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) { diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts index c545d807f8..6a68de4254 100644 --- a/ui/src/frontend/viewer_page.ts +++ b/ui/src/frontend/viewer_page.ts @@ -31,7 +31,7 @@ import {TickmarkPanel} from './tickmark_panel'; import {TimeAxisPanel} from './time_axis_panel'; import {TimeSelectionPanel} from './time_selection_panel'; import {DISMISSED_PANNING_HINT_KEY} from './topbar'; -import {TrackGroupPanel} from './track_group_panel'; +import {MinimalTrackGroup, TrackGroupPanel} from './track_group_panel'; import {TrackPanel} from './track_panel'; import {TrackGroupState, TrackState} from '../common/state'; @@ -290,6 +290,29 @@ class TraceViewer implements m.ClassComponent { if (OVERVIEW_PANEL_FLAG.get()) { overviewPanel.push(m(OverviewTimelinePanel, {key: 'overview'})); } + const pinnedPanels: AnyAttrsVnode[] = [ + ...overviewPanel, + m(TimeAxisPanel, {key: 'timeaxis'}), + m(TimeSelectionPanel, {key: 'timeselection'}), + m(NotesPanel, {key: 'notes'}), + m(TickmarkPanel, {key: 'searchTickmarks'}), + ]; + if (globals.state.pinnedTracks.length > 0) { + pinnedPanels.push(m(TrackGroup, { + header: m(MinimalTrackGroup, { + name: 'Pinned Tracks', + key: 'trackgroup-something', + }), + collapsed: globals.state.pinnedGroupCollapsed, + childTracks: !globals.state.pinnedGroupCollapsed ? + globals.state.pinnedTracks.map( + (id) => m(TrackPanel, { + key: id, + id, + selectable: true, + pinnedCopy: true})): [], + } as TrackGroupAttrs)); + } return m( '.page', @@ -307,19 +330,7 @@ class TraceViewer implements m.ClassComponent { }, m('.pinned-panel-container', m(PanelContainer, { doesScroll: false, - panels: [ - ...overviewPanel, - m(TimeAxisPanel, {key: 'timeaxis'}), - m(TimeSelectionPanel, {key: 'timeselection'}), - m(NotesPanel, {key: 'notes'}), - m(TickmarkPanel, {key: 'searchTickmarks'}), - ...globals.state.pinnedTracks.map( - (id) => m(TrackPanel, { - key: id, - id, - selectable: true, - pinnedCopy: true})), - ], + panels: pinnedPanels, kind: 'OVERVIEW', })), m('.scrolling-panel-container', m(PanelContainer, {