re_ui/
lib.rs

1//! Rerun GUI theme and helpers, built around [`egui`](https://www.egui.rs/).
2
3mod color_table;
4mod command;
5mod command_palette;
6mod context_ext;
7mod design_tokens;
8pub mod drag_and_drop;
9pub mod filter_widget;
10mod help;
11mod icon_text;
12pub mod icons;
13pub mod list_item;
14mod markdown_utils;
15pub mod modal;
16pub mod notifications;
17mod section_collapsing_header;
18pub mod syntax_highlighting;
19mod ui_ext;
20mod ui_layout;
21
22use egui::Color32;
23use egui::NumExt as _;
24
25pub use self::{
26    color_table::{ColorTable, ColorToken, Hue, Scale},
27    command::{UICommand, UICommandSender},
28    command_palette::CommandPalette,
29    context_ext::ContextExt,
30    design_tokens::DesignTokens,
31    help::*,
32    icon_text::*,
33    icons::Icon,
34    markdown_utils::*,
35    section_collapsing_header::SectionCollapsingHeader,
36    syntax_highlighting::SyntaxHighlighting,
37    ui_ext::UiExt,
38    ui_layout::UiLayout,
39};
40
41#[cfg(feature = "arrow")]
42mod arrow_ui;
43
44#[cfg(feature = "arrow")]
45pub use self::arrow_ui::arrow_ui;
46
47// ---------------------------------------------------------------------------
48
49/// If true, we fill the entire window, except for the close/maximize/minimize buttons in the top-left.
50/// See <https://github.com/emilk/egui/pull/2049>
51pub const FULLSIZE_CONTENT: bool = cfg!(target_os = "macos");
52
53/// If true, we hide the native window decoration
54/// (the top bar with app title, close button etc),
55/// and instead paint our own close/maximize/minimize buttons.
56pub const CUSTOM_WINDOW_DECORATIONS: bool = false; // !FULLSIZE_CONTENT; // TODO(emilk): https://github.com/rerun-io/rerun/issues/1063
57
58/// If true, we show the native window decorations/chrome with the
59/// close/maximize/minimize buttons and app title.
60pub const NATIVE_WINDOW_BAR: bool = !FULLSIZE_CONTENT && !CUSTOM_WINDOW_DECORATIONS;
61
62pub const INFO_COLOR: Color32 = Color32::from_rgb(0, 155, 255);
63pub const SUCCESS_COLOR: Color32 = Color32::from_rgb(0, 240, 32);
64
65// ----------------------------------------------------------------------------
66
67pub struct TopBarStyle {
68    /// Height of the top bar
69    pub height: f32,
70
71    /// Extra horizontal space in the top left corner to make room for
72    /// close/minimize/maximize buttons (on Mac)
73    pub indent: f32,
74}
75
76/// The style of a label.
77///
78/// This should be used for all UI widgets that support these styles.
79#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
80pub enum LabelStyle {
81    /// Regular style for a label.
82    #[default]
83    Normal,
84
85    /// Label displaying the placeholder text for a yet unnamed item (e.g. an unnamed view).
86    Unnamed,
87}
88
89// ----------------------------------------------------------------------------
90
91/// Return a reference to the global design tokens structure.
92pub fn design_tokens() -> &'static DesignTokens {
93    use once_cell::sync::OnceCell;
94    static DESIGN_TOKENS: OnceCell<DesignTokens> = OnceCell::new();
95    DESIGN_TOKENS.get_or_init(DesignTokens::load)
96}
97
98/// Apply the Rerun design tokens to the given egui context and install image loaders.
99pub fn apply_style_and_install_loaders(egui_ctx: &egui::Context) {
100    egui_extras::install_image_loaders(egui_ctx);
101
102    egui_ctx.include_bytes(
103        "bytes://logo_dark_mode",
104        include_bytes!("../data/logo_dark_mode.png"),
105    );
106    egui_ctx.include_bytes(
107        "bytes://logo_light_mode",
108        include_bytes!("../data/logo_light_mode.png"),
109    );
110
111    egui_ctx.options_mut(|o| {
112        o.theme_preference = egui::ThemePreference::Dark;
113        o.fallback_theme = egui::Theme::Dark;
114    });
115
116    design_tokens().apply(egui_ctx);
117
118    egui_ctx.style_mut(|style| {
119        style.number_formatter = egui::style::NumberFormatter::new(format_with_decimals_in_range);
120    });
121}
122
123fn format_with_decimals_in_range(
124    value: f64,
125    decimal_range: std::ops::RangeInclusive<usize>,
126) -> String {
127    fn format_with_decimals(value: f64, decimals: usize) -> String {
128        re_format::FloatFormatOptions::DEFAULT_f64
129            .with_decimals(decimals)
130            .with_strip_trailing_zeros(false)
131            .format(value)
132    }
133
134    let epsilon = 16.0 * f32::EPSILON; // margin large enough to handle most peoples round-tripping needs
135
136    let min_decimals = *decimal_range.start();
137    let max_decimals = *decimal_range.end();
138    debug_assert!(min_decimals <= max_decimals);
139    debug_assert!(max_decimals < 100);
140    let max_decimals = max_decimals.at_most(16);
141    let min_decimals = min_decimals.at_most(max_decimals);
142
143    if min_decimals < max_decimals {
144        // Try using a few decimals as possible, and then add more until we have enough precision
145        // to round-trip the number.
146        for decimals in min_decimals..max_decimals {
147            let text = format_with_decimals(value, decimals);
148            if let Some(parsed) = re_format::parse_f64(&text) {
149                if egui::emath::almost_equal(parsed as f32, value as f32, epsilon) {
150                    // Enough precision to show the value accurately - good!
151                    return text;
152                }
153            }
154        }
155        // The value has more precision than we expected.
156        // Probably the value was set not by the slider, but from outside.
157        // In any case: show the full value
158    }
159
160    // Use max decimals
161    format_with_decimals(value, max_decimals)
162}
163
164/// Is this Ui in a resizable panel?
165///
166/// Used as a heuristic to figure out if it is safe to truncate text.
167///
168/// In a resizable panel, it is safe to truncate text if it doesn't fit,
169/// because the user can just make the panel wider to see the full text.
170///
171/// In other places, we should never truncate text, because then the user
172/// cannot read it all. In those places (when this functions returns `false`)
173/// you should either wrap the text or let it grow the Ui it is in.
174fn is_in_resizable_panel(ui: &egui::Ui) -> bool {
175    re_tracing::profile_function!();
176
177    let mut is_in_side_panel = false;
178
179    for frame in ui.stack().iter() {
180        if let Some(kind) = frame.kind() {
181            if kind.is_area() {
182                return false; // Our popups (tooltips etc) aren't resizable
183            }
184            if matches!(kind, egui::UiKind::LeftPanel | egui::UiKind::RightPanel) {
185                is_in_side_panel = true;
186            }
187        }
188    }
189
190    if is_in_side_panel {
191        true // Our side-panels are resizable
192    } else {
193        false // Safe fallback
194    }
195}