Merge "ui: relocate registerXXX methods under App{tracks,tabs}" into main
diff --git a/test/data/ui-screenshots/ui-features_track_debuggable_chip.png.sha256 b/test/data/ui-screenshots/ui-features_track_debuggable_chip.png.sha256
index 0b58e25..31c9292 100644
--- a/test/data/ui-screenshots/ui-features_track_debuggable_chip.png.sha256
+++ b/test/data/ui-screenshots/ui-features_track_debuggable_chip.png.sha256
@@ -1 +1 @@
-c501656f081271470473be88f4769ee0cbc1b64da59de309012df79d30469ac8
\ No newline at end of file
+a822d1ac988d4523b96e44230784c52393d3418ecb055d311dde7a34a83c5c18
\ No newline at end of file
diff --git a/ui/src/assets/track_panel.scss b/ui/src/assets/track_panel.scss
index d777ac7..31f7717 100644
--- a/ui/src/assets/track_panel.scss
+++ b/ui/src/assets/track_panel.scss
@@ -24,6 +24,21 @@
   line-break: anywhere;
   white-space: nowrap;
   text-overflow: ellipsis;
+
+  .popup {
+    visibility: hidden;
+    box-shadow: 1px 1px 2px 2px var(--track-border-color);
+    border-radius: 2px;
+    background: white;
+    color: black;
+    position: absolute;
+    text-overflow: unset;
+    pointer-events: none;
+  }
+
+  &:hover .popup.show-popup {
+    visibility: visible;
+  }
 }
 
 .track-content.pf-track-content-error {
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index 10bc73f..9726a12 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -44,7 +44,6 @@
 interface Attrs {
   readonly groupNode: GroupNode;
   readonly title: string;
-  readonly tooltip: string;
   readonly collapsed: boolean;
   readonly collapsable: boolean;
   readonly trackRenderer?: TrackRenderer;
@@ -63,8 +62,7 @@
   }
 
   render(): m.Children {
-    const {title, subtitle, chips, collapsed, trackRenderer, tooltip} =
-      this.attrs;
+    const {title, subtitle, collapsed, trackRenderer} = this.attrs;
 
     // The shell should be highlighted if the current search result is inside
     // this track group.
@@ -98,13 +96,14 @@
     }
 
     const error = trackRenderer?.getError();
+    const chips = this.attrs.chips && renderChips(this.attrs.chips);
 
     return m(
       `.track-group-panel[collapsed=${collapsed}]`,
       {
         id: 'track_' + this.groupUri,
-        oncreate: () => this.onupdate(),
-        onupdate: () => this.onupdate(),
+        oncreate: (vnode) => this.onupdate(vnode),
+        onupdate: (vnode) => this.onupdate(vnode),
       },
       m(
         `.shell`,
@@ -134,8 +133,11 @@
           '.title-wrapper',
           m(
             'h1.track-title',
-            {title: tooltip},
-            m(MiddleEllipsis, {text: title}, chips && renderChips(chips)),
+            {
+              ref: this.attrs.title,
+            },
+            m('.popup', title, chips),
+            m(MiddleEllipsis, {text: title}, chips),
           ),
           collapsed && exists(subtitle) && m('h2.track-subtitle', subtitle),
         ),
@@ -172,12 +174,29 @@
     );
   }
 
-  private onupdate() {
+  private onupdate({dom}: m.VnodeDOM) {
+    this.decidePopupRequired(dom);
+
     if (this.attrs.trackRenderer !== undefined) {
       this.attrs.trackRenderer.track.onFullRedraw?.();
     }
   }
 
+  // Works out whether to display a title popup on hover, based on whether the
+  // current title is truncated.
+  private decidePopupRequired(dom: Element) {
+    const popupElement = dom.querySelector('.popup') as HTMLElement;
+    const titleElement = dom.querySelector(
+      '.pf-middle-ellipsis',
+    ) as HTMLElement;
+
+    if (popupElement.clientWidth >= titleElement.clientWidth) {
+      popupElement.classList.add('show-popup');
+    } else {
+      popupElement.classList.remove('show-popup');
+    }
+  }
+
   highlightIfTrackSelected(
     ctx: CanvasRenderingContext2D,
     timescale: TimeScale,
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 6b8383e..6d03ea5 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -155,6 +155,7 @@
 
     const currentSelection = globals.selectionManager.selection;
     const pinned = attrs.track.isPinned;
+    const chips = attrs.chips && renderChips(attrs.chips);
 
     return m(
       `.track-shell[draggable=true]`,
@@ -175,13 +176,10 @@
         m(
           'h1',
           {
-            title: attrs.track.displayName,
+            ref: attrs.title,
           },
-          m(
-            MiddleEllipsis,
-            {text: attrs.title},
-            attrs.chips && renderChips(attrs.chips),
-          ),
+          m('.popup', attrs.title, chips),
+          m(MiddleEllipsis, {text: attrs.title}, chips),
         ),
         m(
           ButtonBar,
@@ -482,6 +480,22 @@
 
   onupdate(vnode: m.VnodeDOM<TrackComponentAttrs>) {
     vnode.attrs.track?.onFullRedraw?.();
+    this.decidePopupRequired(vnode.dom);
+  }
+
+  // Works out whether to display a title popup on hover, based on whether the
+  // current title is truncated.
+  private decidePopupRequired(dom: Element) {
+    const popupElement = dom.querySelector('.popup') as HTMLElement;
+    const titleElement = dom.querySelector(
+      '.pf-middle-ellipsis',
+    ) as HTMLElement;
+
+    if (popupElement.clientWidth >= titleElement.clientWidth) {
+      popupElement.classList.add('show-popup');
+    } else {
+      popupElement.classList.remove('show-popup');
+    }
   }
 }
 
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 1569a92..93f805f 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -432,7 +432,6 @@
       chips: tr?.desc.chips,
       collapsed,
       title: group.displayName,
-      tooltip: group.displayName,
       collapsable,
     });
   } else {
@@ -440,7 +439,6 @@
       groupNode: group,
       collapsed,
       title: group.displayName,
-      tooltip: group.displayName,
       collapsable,
     });
   }
diff --git a/ui/src/test/ui_integrationtest.ts b/ui/src/test/ui_integrationtest.ts
index 6a63ad4..2dfbc56 100644
--- a/ui/src/test/ui_integrationtest.ts
+++ b/ui/src/test/ui_integrationtest.ts
@@ -86,7 +86,7 @@
 
   test('expand_camera', async () => {
     await page.click('.pf-overlay');
-    await page.click('h1[title="com.google.android.GoogleCamera 5506"]');
+    await page.click('h1[ref="com.google.android.GoogleCamera 5506"]');
     await page.evaluate(() => {
       document.querySelector('.scrolling-panel-container')!.scrollTo(0, 400);
     });
@@ -114,7 +114,7 @@
   test('expand_browser_proc', async () => {
     const page = await getPage();
     await page.click('.pf-overlay');
-    await page.click('h1[title="Browser 12685"]');
+    await page.click('h1[ref="Browser 12685"]');
     await waitForPerfettoIdle(page);
   });
 
@@ -347,7 +347,7 @@
     );
     await waitForPerfettoIdle(page);
     await page.hover(
-      'h1[title="androidx.benchmark.integration.macrobenchmark.test 7527"]',
+      'h1[ref="androidx.benchmark.integration.macrobenchmark.test 7527"]',
     );
     await waitForPerfettoIdle(page);
   });