diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts index c0b3a34179..a1e813fbc4 100644 --- a/ui/src/frontend/panel_container.ts +++ b/ui/src/frontend/panel_container.ts @@ -392,6 +392,12 @@ export class PanelContainer implements m.ClassComponent { let totalOnCanvas = 0; const flowEventsRendererArgs = new FlowEventsRendererArgs(this.parentWidth, this.canvasHeight); + + const stickyPanels: { + panel: AnyAttrsVnode & m.Vnode<{ trackGroupId: string }>; + yOffset: number; + }[] = []; + for (let i = 0; i < this.panelInfos.length; i++) { const panel = this.panelInfos[i].vnode; const panelHeight = this.panelInfos[i].height; @@ -405,23 +411,50 @@ export class PanelContainer implements m.ClassComponent { if (!this.overlapsCanvas(yStartOnCanvas, yStartOnCanvas + panelHeight)) { panelYStart += panelHeight; + + // If it's a sticky header for a track-group that is expanded, it + // will be stuck showing as long as any of its member tracks is + // in view, so keep track of it to redraw it later when we find + // overlapping panels + if (panel.attrs.trackGroupId !== undefined) { + const trackGroup = + globals.state.trackGroups[panel.attrs.trackGroupId]; + if (!trackGroup.collapsed) { + stickyPanels.push({panel, yOffset: yStartOnCanvas}); + } + } + continue; } totalOnCanvas++; - this.ctx.save(); - this.ctx.translate(0, yStartOnCanvas); - const clipRect = new Path2D(); - const size = {width: this.parentWidth, height: panelHeight}; - clipRect.rect(0, 0, size.width, size.height); - this.ctx.clip(clipRect); - const beforeRender = debugNow(); - panel.state.renderCanvas(this.ctx, size, panel); - this.updatePanelStats( - i, panel.state, debugNow() - beforeRender, this.ctx, size); - this.ctx.restore(); + const draw = ( + ctx: CanvasRenderingContext2D, + panel: AnyAttrsVnode, + yOffset: number): void => { + ctx.save(); + ctx.translate(0, yOffset); + const clipRect = new Path2D(); + const size = {width: this.parentWidth, height: panelHeight}; + clipRect.rect(0, 0, size.width, size.height); + ctx.clip(clipRect); + const beforeRender = debugNow(); + panel.state.renderCanvas(this.ctx, size, panel); + this.updatePanelStats( + i, panel.state, debugNow() - beforeRender, ctx, size); + ctx.restore(); + }; + panelYStart += panelHeight; + + // Draw all of the sticky panels encountered so far + for (const sticky of stickyPanels) { + totalOnCanvas++; + draw(this.ctx, sticky.panel, sticky.yOffset); + } + + draw(this.ctx, panel, yStartOnCanvas); } this.drawTopLayerOnCanvas(); diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts index 6525a3404d..fe857b2419 100644 --- a/ui/src/frontend/track_group_panel.ts +++ b/ui/src/frontend/track_group_panel.ts @@ -34,7 +34,7 @@ import { } from './icons'; import {Panel, PanelSize} from './panel'; import {Track} from './track'; -import {TrackButton, TrackContent} from './track_panel'; +import {TrackButton, TrackContent, TrackContentAttrs} from './track_panel'; import {trackRegistry} from './track_registry'; import { drawVerticalLineAtTime, @@ -55,6 +55,7 @@ export class TrackGroupPanel extends Panel { private summaryTrack: Track|undefined; private dragging = false; private dropping: 'before'|'after'|undefined = undefined; + private privateCtx: CanvasRenderingContext2D | null = null; // Caches the last state.trackGroups[this.trackGroupId]. // This is to deal with track group deletion. See comments @@ -172,6 +173,21 @@ export class TrackGroupPanel extends Panel { const titleStyling = indent(depth(trackGroup)); const dragClass = this.dragging ? `drag` : ''; const dropClass = this.dropping ? `drop-${this.dropping}` : ''; + + const trackContentAttrs: TrackContentAttrs | undefined = this.summaryTrack ? + {track: this.summaryTrack} : + undefined; + + // Need a canvas to draw on because our positioning is relative, + // not static like the shared canvas, and we can stick to the top + // of the track scroll area + if (!collapsed && trackContentAttrs) { + trackContentAttrs.tagname = 'canvas'; + trackContentAttrs.attrs = { + height: this.summaryTrack!.getHeight(), + }; + } + return m( `.track-group-panel[collapsed=${collapsed}]`, { @@ -232,9 +248,7 @@ export class TrackGroupPanel extends Panel { this.summaryTrack ? m(TrackContent, - { - track: this.summaryTrack, - }, + trackContentAttrs!, (!this.trackGroupState.collapsed && child !== null) ? m('span', child) : null) : @@ -358,6 +372,17 @@ export class TrackGroupPanel extends Panel { // TODO(andrewbb): move this to css_constants this.backgroundColor = getComputedStyle(dom).getPropertyValue('--collapsed-background'); + const canvas = dom.querySelector('canvas.track-content'); + if (!canvas) { + this.privateCtx = null; + } else { + this.privateCtx = canvas.getContext('2d'); + const canvasWidth = dom.clientWidth - this.shellWidth; + if (canvas.width !== canvasWidth) { + canvas.width = canvasWidth; + } + } + if (this.summaryTrack !== undefined) { this.summaryTrack.onFullRedraw(); } @@ -449,35 +474,51 @@ export class TrackGroupPanel extends Panel { } renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) { + if (this.privateCtx) { + // Render on our own canvas. It's already offset by the + // track shell width, so offset the rendering's translation + this.privateCtx.save(); + this.privateCtx.translate(-this.shellWidth, 0); + this.doRenderOn(this.privateCtx, size); + this.privateCtx.restore(); + } else { + // Shared canvas + this.doRenderOn(this.privateCtx ?? ctx, size); + } + } + + protected doRenderOn(ctx: CanvasRenderingContext2D, size: PanelSize) { const collapsed = this.trackGroupState.collapsed; ctx.fillStyle = this.backgroundColor; ctx.fillRect(0, 0, size.width, size.height); - if (!collapsed) return; + // Only draw the grid lines, vsync highlights, and summary + // (like a track) when collapsed. + if (collapsed) { + // 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); - // 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); + if (this.summaryTrack) { + this.summaryTrack.render(ctx); + } ctx.restore(); - } - - drawGridLines( - ctx, - size.width, - size.height); - ctx.save(); - ctx.translate(this.shellWidth, 0); - if (this.summaryTrack) { - this.summaryTrack.render(ctx); + this.highlightIfTrackSelected(ctx, size); } - ctx.restore(); - - this.highlightIfTrackSelected(ctx, size); const {visibleTimeScale} = globals.frontendLocalState; // Draw vertical line when hovering on the notes panel. @@ -498,7 +539,7 @@ export class TrackGroupPanel extends Panel { `#344596`); } - if (globals.state.currentSelection !== null) { + if (collapsed && globals.state.currentSelection !== null) { if (globals.state.currentSelection.kind === 'SLICE' && globals.sliceDetails.wakeupTs !== undefined) { drawVerticalLineAtTime( @@ -509,6 +550,7 @@ export class TrackGroupPanel extends Panel { 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)) { diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts index 50f7949607..49b6247c15 100644 --- a/ui/src/frontend/track_panel.ts +++ b/ui/src/frontend/track_panel.ts @@ -308,7 +308,16 @@ class TrackShell implements m.ClassComponent { } } -export interface TrackContentAttrs { track: Track; } +export interface TrackContentAttrs { + track: Track; + /** Optional tag name to instantiate instead of the default `
`. */ + tagname?: keyof HTMLElementTagNameMap; + /** + * Optional attributes to add to the track content element, especially + * when using the custom `tagname`. + */ + attrs?: m.Attributes; +} export class TrackContent implements m.ClassComponent { private mouseDownX?: number; private mouseDownY?: number; @@ -317,8 +326,9 @@ export class TrackContent implements m.ClassComponent { view(node: m.CVnode) { const attrs = node.attrs; return m( - '.track-content', + `${attrs.tagname ?? ''}.track-content`, { + ...(attrs.attrs ?? {}), onmousemove: (e: PerfettoMouseEvent) => { attrs.track.onMouseMove( {x: e.layerX - TRACK_SHELL_WIDTH, y: e.layerY});