re_view_text_document/
view_class.rs1use 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
13pub 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 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 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 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 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 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}