From fbd04fba4c84bae9a4fbd4144c2f55682028b946 Mon Sep 17 00:00:00 2001 From: Steve Golton Date: Thu, 5 Sep 2024 10:29:44 +0100 Subject: [PATCH] ui: Put ellipsis towards the middle of track titles The start and end of track titles are usually more important than the middle, so put the ellipsis in the middle instead of at the end when they get too long. In order to get an implementation working using only CSS, (i.e. without using a ResizeObserver on every track title element), this impl uses a fixed character offset from the end of the title, thus the position of the offsets are not entirely uniform. However, this CL is aimed at readability so beauty can take a back seat. Before: https://screenshot.googleplex.com/4cyVSahb49BtXbt.png After: https://screenshot.googleplex.com/9Ya7U7WvDroyChp.png Change-Id: Id50c7da459fceb571dd716ce186bf66c6b9cf786 --- ...android_trace_30s_expand_camera.png.sha256 | 2 +- .../ui-android_trace_30s_load.png.sha256 | 2 +- ...-features_track_debuggable_chip.png.sha256 | 2 +- ...ng_navigate_open_trace_from_url.png.sha256 | 2 +- ..._back_open_first_trace_from_url.png.sha256 | 2 +- ...from_no_trace_open_second_trace.png.sha256 | 2 +- ui/src/assets/perfetto.scss | 1 + ui/src/assets/track_panel.scss | 2 +- ui/src/assets/widgets/middle_ellipsis.scss | 36 ++++++++++++++++ ui/src/frontend/track_group_panel.ts | 6 +-- ui/src/frontend/track_panel.ts | 14 ++++--- ui/src/frontend/viewer_page.ts | 42 ++++--------------- ui/src/frontend/widgets_page.ts | 19 +++++++++ ui/src/widgets/middle_ellipsis.ts | 40 ++++++++++++++++++ 14 files changed, 124 insertions(+), 48 deletions(-) create mode 100644 ui/src/assets/widgets/middle_ellipsis.scss create mode 100644 ui/src/widgets/middle_ellipsis.ts diff --git a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 index ed650b79cf..0c5a765366 100644 --- a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 +++ b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 @@ -1 +1 @@ -f537ab6edc0f3bc5b493deec0f2f05b219101744d3005fedff1921c9f721c5f9 \ No newline at end of file +f8be4aa38a1cc13d73d4a1a42a7a6e8764f124715d27513ceae6d3d229cf446d \ No newline at end of file diff --git a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256 index 5f3da429f4..cda45370ee 100644 --- a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256 +++ b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256 @@ -1 +1 @@ -d2d73470ccc7263044234a81acd50bb46581fe94d733ac9d29a4b485c7737f50 \ No newline at end of file +c29194bd61b79ac57e5684290f97978c210b60b4861d853a73608612cf0e604f \ No newline at end of file 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 cbfc381265..0b58e25ac7 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 @@ -8d4447efdb31ca493a14247df23abb80367914ca96a9a4b59ae47773766201a6 \ No newline at end of file +c501656f081271470473be88f4769ee0cbc1b64da59de309012df79d30469ac8 \ No newline at end of file diff --git a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256 index 6262d96059..edf8d4f53c 100644 --- a/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256 +++ b/test/data/ui-screenshots/ui-routing_navigate_open_trace_from_url.png.sha256 @@ -1 +1 @@ -7f9538f60c132ad6e91775a376f39b9d4f2064172d0b80105159c89d9e53d9a1 \ No newline at end of file +1ce2f3aa68f01a7e7faf0fd9f49059ad81ab910d80144e679f077e498dc0e96d \ No newline at end of file diff --git a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256 b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256 index 6262d96059..edf8d4f53c 100644 --- a/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256 +++ b/test/data/ui-screenshots/ui-routing_open_two_traces_then_go_back_open_first_trace_from_url.png.sha256 @@ -1 +1 @@ -7f9538f60c132ad6e91775a376f39b9d4f2064172d0b80105159c89d9e53d9a1 \ No newline at end of file +1ce2f3aa68f01a7e7faf0fd9f49059ad81ab910d80144e679f077e498dc0e96d \ No newline at end of file diff --git a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256 b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256 index 6262d96059..edf8d4f53c 100644 --- a/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256 +++ b/test/data/ui-screenshots/ui-routing_start_from_no_trace_open_second_trace.png.sha256 @@ -1 +1 @@ -7f9538f60c132ad6e91775a376f39b9d4f2064172d0b80105159c89d9e53d9a1 \ No newline at end of file +1ce2f3aa68f01a7e7faf0fd9f49059ad81ab910d80144e679f077e498dc0e96d \ No newline at end of file diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss index fdf03d2092..d47b1cd866 100644 --- a/ui/src/assets/perfetto.scss +++ b/ui/src/assets/perfetto.scss @@ -46,6 +46,7 @@ @import "widgets/grid_layout"; @import "widgets/hotkey"; @import "widgets/menu"; +@import "widgets/middle_ellipsis"; @import "widgets/multiselect"; @import "widgets/popup"; @import "widgets/section"; diff --git a/ui/src/assets/track_panel.scss b/ui/src/assets/track_panel.scss index 05746f3e94..d777ac7794 100644 --- a/ui/src/assets/track_panel.scss +++ b/ui/src/assets/track_panel.scss @@ -44,7 +44,7 @@ .track { display: grid; grid-template-columns: auto 1fr; - grid-template-rows: 1fr 0; + grid-template-rows: 1fr; container-type: size; &::after { diff --git a/ui/src/assets/widgets/middle_ellipsis.scss b/ui/src/assets/widgets/middle_ellipsis.scss new file mode 100644 index 0000000000..b1d49f6079 --- /dev/null +++ b/ui/src/assets/widgets/middle_ellipsis.scss @@ -0,0 +1,36 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +.pf-middle-ellipsis { + display: flex; + flex-direction: row; + + .pf-middle-ellipsis-left { + overflow: hidden; + white-space: pre; + text-overflow: ellipsis; + } + + .pf-middle-ellipsis-right { + overflow: hidden; + white-space: pre; + flex-shrink: 0; + } + + .pf-middle-ellipsis-extras { + overflow: hidden; + white-space: nowrap; + flex-shrink: 0; + } +} diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts index c5d3596819..0ece88584f 100644 --- a/ui/src/frontend/track_group_panel.ts +++ b/ui/src/frontend/track_group_panel.ts @@ -40,10 +40,11 @@ import {classNames} from '../base/classnames'; import {GroupNode} from '../public/workspace'; import {raf} from '../core/raf_scheduler'; import {Actions} from '../common/actions'; +import {MiddleEllipsis} from '../widgets/middle_ellipsis'; interface Attrs { readonly groupNode: GroupNode; - readonly title: m.Children; + readonly title: string; readonly tooltip: string; readonly collapsed: boolean; readonly collapsable: boolean; @@ -135,8 +136,7 @@ export class TrackGroupPanel implements Panel { m( 'h1.track-title', {title: tooltip}, - title, - chips && renderChips(chips), + m(MiddleEllipsis, {text: title}, chips && renderChips(chips)), ), collapsed && exists(subtitle) && m('h2.track-subtitle', subtitle), ), diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts index f902939f99..33e160f1da 100644 --- a/ui/src/frontend/track_panel.ts +++ b/ui/src/frontend/track_panel.ts @@ -45,6 +45,7 @@ import {calculateResolution} from '../common/resolution'; import {featureFlags} from '../core/feature_flags'; import {Tree, TreeNode} from '../widgets/tree'; import {TrackNode} from '../public/workspace'; +import {MiddleEllipsis} from '../widgets/middle_ellipsis'; export const SHOW_TRACK_DETAILS_BUTTON = featureFlags.register({ id: 'showTrackDetailsButton', @@ -128,7 +129,7 @@ export class CrashButton implements m.ClassComponent { } interface TrackShellAttrs { - readonly title: m.Children; + readonly title: string; readonly buttons: m.Children; readonly tags?: TrackTags; readonly chips?: ReadonlyArray; @@ -178,8 +179,11 @@ class TrackShell implements m.ClassComponent { { title: attrs.track.displayName, }, - attrs.title, - attrs.chips && renderChips(attrs.chips), + m( + MiddleEllipsis, + {text: attrs.title}, + attrs.chips && renderChips(attrs.chips), + ), ), m( ButtonBar, @@ -412,7 +416,7 @@ export class TrackContent implements m.ClassComponent { interface TrackComponentAttrs { readonly heightPx?: number; - readonly title: m.Children; + readonly title: string; readonly buttons?: m.Children; readonly tags?: TrackTags; readonly chips?: ReadonlyArray; @@ -486,7 +490,7 @@ class TrackComponent implements m.ClassComponent { } interface TrackPanelAttrs { - readonly title: m.Children; + readonly title: string; readonly tags?: TrackTags; readonly chips?: ReadonlyArray; readonly trackRenderer?: TrackRenderer; diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts index 9ee293b6e3..ff4c8fcd6d 100644 --- a/ui/src/frontend/viewer_page.ts +++ b/ui/src/frontend/viewer_page.ts @@ -37,12 +37,12 @@ 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 {TrackPanel, getTitleFontSize} from './track_panel'; +import {TrackPanel} from './track_panel'; import {assertExists} from '../base/logging'; import {TimeScale} from '../base/time_scale'; import {GroupNode, Node, TrackNode} from '../public/workspace'; -import {fuzzyMatch, FuzzySegment} from '../base/fuzzy'; -import {exists, Optional} from '../base/utils'; +import {fuzzyMatch} from '../base/fuzzy'; +import {Optional} from '../base/utils'; import {EmptyState} from '../widgets/empty_state'; import {removeFalsyValues} from '../base/array_utils'; import {renderFlows} from './flow_events_renderer'; @@ -325,13 +325,6 @@ function renderOverlay( renderFlows(ctx, size, panels); } -// Given a set of fuzzy matched results, render the matching segments in bold -function renderFuzzyMatchedTrackTitle(title: FuzzySegment[]): m.Children { - return title.map(({matching, value}) => { - return matching ? m('b', value) : value; - }); -} - function filterTermIsValid( filterTerm: undefined | string, ): filterTerm is string { @@ -371,12 +364,7 @@ function renderNodes( return { kind: 'group', collapsed: node.collapsed, - header: renderGroupHeaderPanel( - node, - true, - node.collapsed, - renderFuzzyMatchedTrackTitle(match.segments), - ), + header: renderGroupHeaderPanel(node, true, node.collapsed), childPanels: node.collapsed ? [] : renderNodes(node.children), }; } else { @@ -407,10 +395,7 @@ function renderNodes( const tokens = tokenizeFilterTerm(filterTerm); const match = fuzzyMatch(node.displayName, ...tokens); if (match.matches) { - return renderTrackPanel( - node, - renderFuzzyMatchedTrackTitle(match.segments), - ); + return renderTrackPanel(node); } else { return []; } @@ -421,19 +406,11 @@ function renderNodes( }); } -function renderTrackPanel(track: TrackNode, title?: m.Children) { +function renderTrackPanel(track: TrackNode) { const tr = globals.trackManager.getTrackRenderer(track.uri); return new TrackPanel({ track: track, - title: m( - 'span', - { - style: { - 'font-size': getTitleFontSize(track.displayName), - }, - }, - Boolean(title) ? title : track.displayName, - ), + title: track.displayName, tags: tr?.desc.tags, trackRenderer: tr, chips: tr?.desc.chips, @@ -445,7 +422,6 @@ function renderGroupHeaderPanel( group: GroupNode, collapsable: boolean, collapsed: boolean, - title?: m.Children, ): TrackGroupPanel { if (group.headerTrackUri !== undefined) { const tr = globals.trackManager.getTrackRenderer(group.headerTrackUri); @@ -456,7 +432,7 @@ function renderGroupHeaderPanel( tags: tr?.desc.tags, chips: tr?.desc.chips, collapsed, - title: exists(title) ? title : group.displayName, + title: group.displayName, tooltip: group.displayName, collapsable, }); @@ -464,7 +440,7 @@ function renderGroupHeaderPanel( return new TrackGroupPanel({ groupNode: group, collapsed, - title: exists(title) ? title : group.displayName, + title: group.displayName, tooltip: group.displayName, collapsable, }); diff --git a/ui/src/frontend/widgets_page.ts b/ui/src/frontend/widgets_page.ts index 811be83b97..391e38dc74 100644 --- a/ui/src/frontend/widgets_page.ts +++ b/ui/src/frontend/widgets_page.ts @@ -55,6 +55,7 @@ import { } from '../widgets/virtual_table'; import {TagInput} from '../widgets/tag_input'; import {SegmentedButtons} from '../widgets/segmented_buttons'; +import {MiddleEllipsis} from '../widgets/middle_ellipsis'; const DATA_ENGLISH_LETTER_FREQUENCY = { table: [ @@ -1271,6 +1272,24 @@ export const WidgetsPage = createPage({ Clicking anywhere on the container will focus the text input.`, renderWidget: () => m(TagInputDemo), }), + m(WidgetShowcase, { + label: 'Middle Ellipsis', + description: ` + Sometimes the start and end of a bit of text are more important than + the middle. This element puts the ellipsis in the midde if the content + is too wide for its container.`, + renderWidget: (opts) => + m( + 'div', + {style: {width: Boolean(opts.squeeze) ? '150px' : '450px'}}, + m(MiddleEllipsis, { + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', + }), + ), + initialOpts: { + squeeze: false, + }, + }), ); }, }); diff --git a/ui/src/widgets/middle_ellipsis.ts b/ui/src/widgets/middle_ellipsis.ts new file mode 100644 index 0000000000..9c58be7dd9 --- /dev/null +++ b/ui/src/widgets/middle_ellipsis.ts @@ -0,0 +1,40 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import m from 'mithril'; + +export interface MiddleEllipsisAttrs { + text: string; + endChars?: number; +} + +/** + * Puts ellipsis in the middle of a long string, rather than putting them at + * either end, for occasions where the start and end of the text are more + * important than the middle. + */ +export class MiddleEllipsis implements m.ClassComponent { + view({attrs, children}: m.Vnode): m.Children { + const {text, endChars = text.length > 16 ? 10 : 0} = attrs; + const index = text.length - endChars; + const left = text.substring(0, index); + const right = text.substring(index); + return m( + '.pf-middle-ellipsis', + m('span.pf-middle-ellipsis-left', left), + m('span.pf-middle-ellipsis-right', right), + m('span.pf-middle-ellipsis-extras', children), + ); + } +}