Skip to content

Commit

Permalink
Markdown support in TextDocument (#3343)
Browse files Browse the repository at this point in the history
### What
```rs
    rec.log(
        "markdown",
        &TextDocument::new(
            "# Hello\n\
             Markdown with `code`!\n\
             \n\
             A random image:\n\
             \n\
             ![A random image](https://picsum.photos/640/480)",
        )
        .with_media_type(MediaType::markdown()),
    )?;
```

<img width="809" alt="image"
src="https://github.com/rerun-io/rerun/assets/1148717/d4cbf9a3-e3c9-4aba-be35-8921dae001e7">

This will let us add nice documentation explaining our examples.

### Not yet supported
* Syntax highlighting of code blocks
* `egui_commomark` uses `syntect` which is too big of a dependency imho,
see lampsitter/egui_commonmark#13
* Embedding references to images in the data store
* We should support something like `![](entity://my/image/entity)` where
`my/image/entity` is the entity path to an image you have logged.

### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested [demo.rerun.io](https://demo.rerun.io/pr/3343) (if
applicable)

- [PR Build Summary](https://build.rerun.io/pr/3343)
- [Docs
preview](https://rerun.io/preview/8b743c5708382aef1841a364442762a2abc80db3/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/8b743c5708382aef1841a364442762a2abc80db3/examples)
<!--EXAMPLES-PREVIEW-->
- [Recent benchmark results](https://ref.rerun.io/dev/bench/)
- [Wasm size tracking](https://ref.rerun.io/dev/sizes/)
  • Loading branch information
emilk committed Sep 18, 2023
1 parent 01f8ee5 commit de31501
Show file tree
Hide file tree
Showing 36 changed files with 832 additions and 77 deletions.
46 changes: 35 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 13 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ egui = { version = "0.22.0", features = [
"log",
"puffin",
] }
egui_commonmark = { version = "0.7", default-features = false }
egui_extras = { version = "0.22.0", features = ["http", "image", "puffin"] }
egui_plot = { git = "https://github.com/emilk/egui", rev = "2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" }
egui_plot = { git = "https://github.com/emilk/egui", rev = "d949eaf" } # egui_lot is not yet published on crates.io
egui_tiles = { version = "0.2" }
egui-wgpu = "0.22.0"
ehttp = { version = "0.3" }
Expand Down Expand Up @@ -165,14 +166,17 @@ debug = true
# ALWAYS document what PR the commit hash is part of, or when it was merged into the upstream trunk.

# Temporary patch until next egui release
ecolor = { git = "https://github.com/emilk/egui", rev = "2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" }
eframe = { git = "https://github.com/emilk/egui", rev = "2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" }
egui-wgpu = { git = "https://github.com/emilk/egui", rev = "2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" }
egui-winit = { git = "https://github.com/emilk/egui", rev = "2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" }
egui = { git = "https://github.com/emilk/egui", rev = "2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" }
egui_extras = { git = "https://github.com/emilk/egui", rev = "2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" }
emath = { git = "https://github.com/emilk/egui", rev = "2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" }
epaint = { git = "https://github.com/emilk/egui", rev = "2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" }
ecolor = { git = "https://github.com/emilk/egui", rev = "d949eaf" }
eframe = { git = "https://github.com/emilk/egui", rev = "d949eaf" }
egui-wgpu = { git = "https://github.com/emilk/egui", rev = "d949eaf" }
egui-winit = { git = "https://github.com/emilk/egui", rev = "d949eaf" }
egui = { git = "https://github.com/emilk/egui", rev = "d949eaf" }
egui_extras = { git = "https://github.com/emilk/egui", rev = "d949eaf" }
emath = { git = "https://github.com/emilk/egui", rev = "d949eaf" }
epaint = { git = "https://github.com/emilk/egui", rev = "d949eaf" }

# Temporary patch until next egui_commonmark release
egui_commonmark = { git = "https://github.com/lampsitter/egui_commonmark.git", rev = "a133564f26a95672e756079ac5583817e0cdaa1f" }

# Temporary patch until next egui_tiles release
egui_tiles = { git = "https://github.com/rerun-io/egui_tiles", rev = "c66d6cba7ddb5b236be614d1816be4561260274e" }
14 changes: 14 additions & 0 deletions crates/re_space_view_text_document/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,28 @@ include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[package.metadata.docs.rs]
all-features = true

[features]
default = ["markdown"]
## Show `text/markdown` as Markdown.
markdown = ["dep:egui_commonmark"]

[dependencies]
re_arrow_store.workspace = true
re_log.workspace = true
re_query.workspace = true
re_renderer.workspace = true
re_tracing.workspace = true
re_types.workspace = true
re_ui.workspace = true
re_viewer_context.workspace = true

egui.workspace = true
itertools.workspace = true
vec1.workspace = true

# Optional dependencies:

# egui_commonmark is a 3rd party crate.
# By making it an optional dependency we can easily drop it if we need to,
# .e.g if isn't release a new version quickly enough after an egui release.
egui_commonmark = { workspace = true, optional = true, default-features = false }
39 changes: 33 additions & 6 deletions crates/re_space_view_text_document/src/space_view_class.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
use egui::Label;

use re_viewer_context::{
external::re_log_types::EntityPath, SpaceViewClass, SpaceViewClassName,
SpaceViewClassRegistryError, SpaceViewId, SpaceViewState, SpaceViewSystemExecutionError,
ViewContextCollection, ViewPartCollection, ViewQuery, ViewerContext,
};

use crate::view_part_system::TextDocumentEntry;

use super::view_part_system::TextDocumentSystem;

// TODO(andreas): This should be a blueprint component.
#[derive(Clone, PartialEq, Eq)]

pub struct TextDocumentSpaceViewState {
monospace: bool,
word_wrap: bool,

#[cfg(feature = "markdown")]
commonmark_cache: egui_commonmark::CommonMarkCache,
}

impl Default for TextDocumentSpaceViewState {
fn default() -> Self {
Self {
monospace: false,
word_wrap: true,

#[cfg(feature = "markdown")]
commonmark_cache: Default::default(),
}
}
}
Expand Down Expand Up @@ -104,21 +113,39 @@ impl SpaceViewClass for TextDocumentSpaceView {
egui::ScrollArea::both()
.auto_shrink([false, false])
.show(ui, |ui| {
// TODO(jleibs): better handling for multiple results
if text_document.text_entries.is_empty() {
ui.label("No TextDocument entries found.");
// We get here if we scroll back time to before the first text document was logged.
ui.weak("(empty)");
} else if text_document.text_entries.len() == 1 {
let mut text =
egui::RichText::new(text_document.text_entries[0].body.as_str());
let TextDocumentEntry { body, media_type } =
&text_document.text_entries[0];

#[cfg(feature = "markdown")]
{
if media_type == &re_types::components::MediaType::markdown() {
re_tracing::profile_scope!("egui_commonmark");
egui_commonmark::CommonMarkViewer::new("markdown_viewer")
.max_image_width(Some(ui.available_width().floor() as _))
.show(ui, &mut state.commonmark_cache, body);
return;
}
}
#[cfg(not(feature = "markdown"))]
{
_ = media_type;
}

let mut text = egui::RichText::new(body.as_str());

if state.monospace {
text = text.monospace();
}

ui.add(Label::new(text).wrap(state.word_wrap));
} else {
// TODO(jleibs): better handling for multiple results
ui.label(format!(
"Unexpected number of text entries: {}. Limit your query to 1.",
"Can only show one text document at a time; was given {}.",
text_document.text_entries.len()
));
}
Expand Down
27 changes: 15 additions & 12 deletions crates/re_space_view_text_document/src/view_part_system.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use re_arrow_store::LatestAtQuery;
use re_query::{query_archetype, QueryError};
use re_types::{archetypes::TextDocument, Archetype as _, ComponentNameSet};
use re_types::{
archetypes::{self, TextDocument},
components, Archetype as _, ComponentNameSet,
};
use re_viewer_context::{
NamedViewSystem, SpaceViewSystemExecutionError, ViewContextCollection, ViewPartSystem,
ViewQuery, ViewerContext,
Expand All @@ -10,7 +13,8 @@ use re_viewer_context::{

#[derive(Debug, Clone)]
pub struct TextDocumentEntry {
pub body: re_types::datatypes::Utf8,
pub body: components::Text,
pub media_type: components::MediaType,
}

/// A text scene, with everything needed to render it.
Expand Down Expand Up @@ -50,17 +54,16 @@ impl ViewPartSystem for TextDocumentSystem {
for (ent_path, _props) in query.iter_entities_for_system(Self::name()) {
// TODO(jleibs): this match can go away once we resolve:
// https://github.com/rerun-io/rerun/issues/3320
match query_archetype::<re_types::archetypes::TextDocument>(
store,
&timeline_query,
ent_path,
) {
match query_archetype::<archetypes::TextDocument>(store, &timeline_query, ent_path) {
Ok(arch_view) => {
for text_entry in
arch_view.iter_required_component::<re_types::components::Text>()?
{
let re_types::components::Text(text) = text_entry;
self.text_entries.push(TextDocumentEntry { body: text });
let bodies = arch_view.iter_required_component::<components::Text>()?;
let media_types =
arch_view.iter_optional_component::<components::MediaType>()?;

for (body, media_type) in itertools::izip!(bodies, media_types) {
let media_type = media_type.unwrap_or(components::MediaType::plain_text());
self.text_entries
.push(TextDocumentEntry { body, media_type });
}
}
Err(QueryError::PrimaryNotFound(_)) => {}
Expand Down
11 changes: 11 additions & 0 deletions crates/re_string_interner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,24 +260,35 @@ macro_rules! declare_new_type {
}

impl std::fmt::Debug for $StructName {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_str().fmt(f)
}
}

impl std::fmt::Display for $StructName {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_str().fmt(f)
}
}

impl<'a> PartialEq<&'a str> for $StructName {
#[inline]
fn eq(&self, other: &&'a str) -> bool {
self.as_str() == *other
}
}

impl<'a> PartialEq<&'a str> for &$StructName {
#[inline]
fn eq(&self, other: &&'a str) -> bool {
self.as_str() == *other
}
}

impl<'a> PartialEq<$StructName> for &'a str {
#[inline]
fn eq(&self, other: &$StructName) -> bool {
*self == other.as_str()
}
Expand Down
1 change: 1 addition & 0 deletions crates/re_types/.gitattributes

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,12 @@ table TextDocument (
) {
body: rerun.components.Text ("attr.rerun.component_required", order: 100);

// TODO(emilk): text_format: rerun.components.TextFormat # (txt, md, …)
/// The Media Type of the text.
///
/// For instance:
/// * `text/plain`
/// * `text/markdown`
///
/// If omitted, `text/plain` is assumed.
media_type: rerun.components.MediaType ("attr.rerun.component_optional", nullable, order: 100);
}
Loading

0 comments on commit de31501

Please sign in to comment.