1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
use std::sync::Arc;
use egui::{
NumExt as _, TextWrapMode,
text::{LayoutJob, TextWrapping},
};
use crate::{UiExt as _, syntax_highlighting::SyntaxHighlightedBuilder};
/// Specifies the context in which the UI is used and the constraints it should follow.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UiLayout {
/// Display a short summary. Used in lists.
///
/// Keep it small enough to fit on half a row (i.e. the second column of a
/// [`crate::list_item::ListItem`] with [`crate::list_item::PropertyContent`]. Text should
/// truncate.
List,
/// Display as much information as possible in a compact way. Used for hovering/tooltips.
///
/// Keep it under a half-dozen lines. Text may wrap. Avoid interactive UI. When using a table,
/// use the [`Self::table`] function.
Tooltip,
/// Display everything as wide as available, without height restriction. Used in the selection
/// panel when a single item is selected.
///
/// The UI will be wrapped in a [`egui::ScrollArea`], so data should be fully displayed with no
/// restriction. When using a table, use the [`Self::table`] function.
SelectionPanel,
}
impl UiLayout {
/// Should the UI fit on one line?
#[inline]
pub fn is_single_line(&self) -> bool {
match self {
Self::List => true,
Self::Tooltip | Self::SelectionPanel => false,
}
}
/// Do we have a lot of vertical space?
#[inline]
pub fn is_selection_panel(self) -> bool {
match self {
Self::List | Self::Tooltip => false,
Self::SelectionPanel => true,
}
}
/// Build an egui table and configure it for the given UI layout.
///
/// Note that the caller is responsible for strictly limiting the number of displayed rows for
/// [`Self::List`] and [`Self::Tooltip`], as the table will not scroll.
pub fn table(self, ui: &mut egui::Ui) -> egui_extras::TableBuilder<'_> {
let table = egui_extras::TableBuilder::new(ui);
match self {
Self::List | Self::Tooltip => {
// Be as small as possible in the hover tooltips. No scrolling related configuration, as
// the content itself must be limited (scrolling is not possible in tooltips).
table.auto_shrink([true, true])
}
Self::SelectionPanel => {
// We're alone in the selection panel. Let the outer ScrollArea do the work.
table.auto_shrink([false, true]).vscroll(false)
}
}
}
/// Show a label while respecting the given UI layout.
///
/// Important: for label only, data should use [`UiLayout::data_label`] instead.
// TODO(#6315): must be merged with `Self::data_label` and have an improved API
pub fn label(self, ui: &mut egui::Ui, text: impl Into<egui::WidgetText>) -> egui::Response {
let mut label = egui::Label::new(text);
// Respect set wrap_mode if already set
if ui.style().wrap_mode.is_none() {
let wrap_mode = match self {
Self::List => {
if ui.is_sizing_pass() {
if ui.is_tooltip() {
TextWrapMode::Truncate // Dangerous to let this grow without bounds. TODO(emilk): let it grow up to `tooltip_width`
} else {
// grow parent if needed - that's the point of a sizing pass
TextWrapMode::Extend
}
} else {
TextWrapMode::Truncate
}
}
Self::Tooltip | Self::SelectionPanel => TextWrapMode::Wrap,
};
label = label.wrap_mode(wrap_mode);
}
ui.add(label)
}
/// Show data while respecting the given UI layout.
///
/// Import: for data only, labels should use [`UiLayout::label`] instead.
///
/// Make sure to use the right syntax highlighting. Check [`SyntaxHighlightedBuilder`] docs
/// for details.
// TODO(#6315): must be merged with `Self::label` and have an improved API
pub fn data_label(
self,
ui: &mut egui::Ui,
data: impl Into<SyntaxHighlightedBuilder>,
) -> egui::Response {
self.data_label_impl(ui, data.into().into_job(ui.style()))
}
fn decorate_url(ui: &mut egui::Ui, mut galley: Arc<egui::Galley>) -> egui::Response {
ui.sanity_check();
if ui.layer_id().order == egui::Order::Tooltip
&& ui.spacing().tooltip_width < galley.size().x
{
// This will make the tooltip too wide.
// TODO(#11211): do proper fix
debug_assert!(
galley.size().x < ui.spacing().tooltip_width + 1000.0,
"DEBUG ASSERT: adding huge galley with width: {} to a tooltip.",
galley.size().x
);
// Ugly hack that may or may not work correctly.
let mut layout_job = Arc::unwrap_or_clone(galley.job.clone());
layout_job.wrap.max_width = ui.spacing().tooltip_width;
galley = ui.fonts_mut(|f| f.layout_job(layout_job));
}
let text = galley.text();
// By default e.g., "droid:full" would be considered a valid URL. We decided we only care
// about sane URL formats that include "://". This means e.g., "mailto:hello@world" won't
// be considered a URL, but that is preferable to showing links for anything with a colon.
if text.contains("://") {
// Syntax highlighting may add quotes around strings.
let stripped = text.trim_matches(SyntaxHighlightedBuilder::QUOTE_CHAR);
if url::Url::parse(stripped).is_ok() {
// This is a general link and should not open a new tab unless desired by the user.
return ui.re_hyperlink(galley.clone(), stripped, false);
}
}
let response = ui.label(galley);
ui.sanity_check();
response
}
fn data_label_impl(self, ui: &mut egui::Ui, mut layout_job: LayoutJob) -> egui::Response {
let wrap_width = ui.available_width();
layout_job.wrap = TextWrapping::wrap_at_width(wrap_width);
match self {
Self::List => {
layout_job.wrap.max_rows = 1; // We must fit on one line
// Show the whole text; not just the first line.
// See https://github.com/rerun-io/rerun/issues/10653
// Ideally egui would allow us to configure what replacement character to use
// instead of newline, but for now it doesn't, so `\n` will show up as a square.
layout_job.break_on_newline = false;
if ui.is_sizing_pass() {
if ui.is_tooltip() {
// We should only allow this to grow up to the width of the tooltip:
let max_tooltip_width = ui.style().spacing.tooltip_width;
let growth_margin = max_tooltip_width - ui.max_rect().width();
layout_job.wrap.max_width += growth_margin;
// There are limits to how small we shrink this though,
// even at the cost of making the tooltip too wide.
layout_job.wrap.max_width = layout_job.wrap.max_width.at_least(10.0);
} else {
// grow parent if needed - that's the point of a sizing pass
layout_job.wrap.max_width = f32::INFINITY;
}
} else {
// Truncate
layout_job.wrap.break_anywhere = true;
}
}
Self::Tooltip => {
layout_job.wrap.max_rows = 3;
}
Self::SelectionPanel => {}
}
let galley = ui.fonts_mut(|f| f.layout_job(layout_job)); // We control the text layout; not the label
Self::decorate_url(ui, galley)
}
}