Skip to main content

re_view_text_document/
view_class.rs

1use egui::{Label, Sense};
2use re_sdk_types::{View as _, ViewClassIdentifier};
3use re_ui::{Help, UiExt as _};
4use re_viewer_context::external::re_log_types::EntityPath;
5use re_viewer_context::{
6    Item, SystemCommand, SystemCommandSender as _, ViewClass, ViewClassRegistryError, ViewId,
7    ViewQuery, ViewState, ViewStateExt as _, ViewSystemExecutionError, ViewerContext,
8    suggest_view_for_each_entity,
9};
10
11use crate::visualizer_system::{TextDocumentEntry, TextDocumentSystem};
12
13// TODO(andreas): This should be a blueprint component.
14
15pub struct TextDocumentViewState {
16    monospace: bool,
17    word_wrap: bool,
18    commonmark_cache: egui_commonmark::CommonMarkCache,
19}
20
21impl Default for TextDocumentViewState {
22    fn default() -> Self {
23        Self {
24            monospace: false,
25            word_wrap: true,
26            commonmark_cache: Default::default(),
27        }
28    }
29}
30
31impl ViewState for TextDocumentViewState {
32    fn as_any(&self) -> &dyn std::any::Any {
33        self
34    }
35
36    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
37        self
38    }
39}
40
41#[derive(Default)]
42pub struct TextDocumentView;
43
44type ViewType = re_sdk_types::blueprint::views::TextDocumentView;
45
46impl ViewClass for TextDocumentView {
47    fn identifier() -> ViewClassIdentifier {
48        ViewType::identifier()
49    }
50
51    fn display_name(&self) -> &'static str {
52        "Text document"
53    }
54
55    fn icon(&self) -> &'static re_ui::Icon {
56        &re_ui::icons::VIEW_TEXT
57    }
58
59    fn help(&self, _os: egui::os::OperatingSystem) -> Help {
60        Help::new("Text document view")
61            .docs_link("https://rerun.io/docs/reference/types/views/text_document_view")
62            .markdown("Supports raw text and markdown.")
63    }
64
65    fn on_register(
66        &self,
67        system_registry: &mut re_viewer_context::ViewSystemRegistrator<'_>,
68    ) -> Result<(), ViewClassRegistryError> {
69        system_registry.register_visualizer::<TextDocumentSystem>()
70    }
71
72    fn new_state(&self) -> Box<dyn ViewState> {
73        Box::<TextDocumentViewState>::default()
74    }
75
76    fn layout_priority(&self) -> re_viewer_context::ViewClassLayoutPriority {
77        re_viewer_context::ViewClassLayoutPriority::Low
78    }
79
80    fn selection_ui(
81        &self,
82        _ctx: &ViewerContext<'_>,
83        ui: &mut egui::Ui,
84        state: &mut dyn ViewState,
85        _space_origin: &EntityPath,
86        _view_id: ViewId,
87    ) -> Result<(), ViewSystemExecutionError> {
88        let state = state.downcast_mut::<TextDocumentViewState>()?;
89
90        ui.selection_grid("text_config").show(ui, |ui| {
91            ui.grid_left_hand_label("Text style");
92            ui.vertical(|ui| {
93                ui.re_radio_value(&mut state.monospace, false, "Proportional");
94                ui.re_radio_value(&mut state.monospace, true, "Monospace");
95                ui.re_checkbox(&mut state.word_wrap, "Word Wrap");
96            });
97            ui.end_row();
98        });
99
100        Ok(())
101    }
102
103    fn spawn_heuristics(
104        &self,
105        ctx: &ViewerContext<'_>,
106        include_entity: &dyn Fn(&EntityPath) -> bool,
107    ) -> re_viewer_context::ViewSpawnHeuristics {
108        re_tracing::profile_function!();
109        // By default spawn a view for every text document.
110        suggest_view_for_each_entity::<TextDocumentSystem>(ctx, include_entity)
111    }
112
113    fn ui(
114        &self,
115        ctx: &ViewerContext<'_>,
116        _missing_chunk_reporter: &re_viewer_context::MissingChunkReporter,
117        ui: &mut egui::Ui,
118        state: &mut dyn ViewState,
119        query: &ViewQuery<'_>,
120        system_output: re_viewer_context::SystemExecutionOutput,
121    ) -> Result<(), ViewSystemExecutionError> {
122        let tokens = ui.tokens();
123        let state = state.downcast_mut::<TextDocumentViewState>()?;
124        let text_document = system_output.view_systems.get::<TextDocumentSystem>()?;
125
126        let frame = egui::Frame::new().inner_margin(tokens.view_padding());
127        let response = frame
128            .show(ui, |ui| {
129                let inner_ui_builder = egui::UiBuilder::new()
130                    .layout(egui::Layout::top_down(egui::Align::LEFT))
131                    .sense(Sense::click());
132                ui.scope_builder(inner_ui_builder, |ui| {
133                    egui::ScrollArea::both()
134                        .auto_shrink([false, false])
135                        .show(ui, |ui| text_document_ui(ui, state, text_document));
136
137                    ui.response()
138                })
139                .inner
140            })
141            .inner;
142
143        // Since we want the view to be hoverable / clickable when the pointer is over a label
144        // (and we want selectable labels), we need to work around egui's interactions here.
145        // Since `rect_contains_pointer` checks for the layer id, this shouldn't cause any problems
146        // with popups / modals.
147        let hovered = ui.ctx().rect_contains_pointer(ui.layer_id(), response.rect);
148        let clicked = hovered && ui.input(|i| i.pointer.primary_pressed());
149
150        if hovered {
151            ctx.selection_state().set_hovered(Item::View(query.view_id));
152        }
153
154        if clicked {
155            ctx.command_sender()
156                .send_system(SystemCommand::set_selection(Item::View(query.view_id)));
157        }
158
159        Ok(())
160    }
161}
162
163fn text_document_ui(
164    ui: &mut egui::Ui,
165    state: &mut TextDocumentViewState,
166    text_document: &TextDocumentSystem,
167) {
168    if text_document.text_entries.is_empty() {
169        // We get here if we scroll back time to before the first text document was logged.
170        ui.weak("(empty)");
171    } else if text_document.text_entries.len() == 1 {
172        let TextDocumentEntry { body, media_type } = &text_document.text_entries[0];
173
174        if media_type == &re_sdk_types::components::MediaType::markdown() {
175            re_tracing::profile_scope!("egui_commonmark");
176
177            // Make sure headers are big:
178            ui.style_mut()
179                .text_styles
180                .entry(egui::TextStyle::Heading)
181                .or_insert(egui::FontId::proportional(32.0))
182                .size = 24.0;
183
184            egui_commonmark::CommonMarkViewer::new()
185                .max_image_width(Some(ui.available_width().floor() as _))
186                .show(ui, &mut state.commonmark_cache, body);
187        } else {
188            let mut text = egui::RichText::new(body.as_str());
189
190            if state.monospace {
191                text = text.monospace();
192            }
193
194            ui.add(Label::new(text).wrap_mode(if state.word_wrap {
195                egui::TextWrapMode::Wrap
196            } else {
197                egui::TextWrapMode::Extend
198            }));
199        }
200    } else {
201        // TODO(jleibs): better handling for multiple results
202        ui.error_label(format!(
203            "Can only show one text document at a time; was given {}. Update \
204                                    the query so that it returns a single text document and create \
205                                    additional views for the others.",
206            text_document.text_entries.len()
207        ));
208    }
209}
210
211#[test]
212fn test_help_view() {
213    re_test_context::TestContext::test_help_view(|ctx| TextDocumentView.help(ctx));
214}