From de315019258d153d1200a0fc6fba593572802756 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 18 Sep 2023 15:17:56 +0200 Subject: [PATCH] Markdown support in `TextDocument` (#3343) ### 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()), )?; ``` image 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 https://github.com/lampsitter/egui_commonmark/issues/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) - [Examples preview](https://rerun.io/preview/8b743c5708382aef1841a364442762a2abc80db3/examples) - [Recent benchmark results](https://ref.rerun.io/dev/bench/) - [Wasm size tracking](https://ref.rerun.io/dev/sizes/) --- Cargo.lock | 46 ++++- Cargo.toml | 22 +- crates/re_space_view_text_document/Cargo.toml | 14 ++ .../src/space_view_class.rs | 39 +++- .../src/view_part_system.rs | 27 +-- crates/re_string_interner/src/lib.rs | 11 + crates/re_types/.gitattributes | 1 + .../rerun/archetypes/text_document.fbs | 9 +- .../re_types/definitions/rerun/components.fbs | 1 + .../rerun/components/media_type.fbs | 26 +++ crates/re_types/source_hash.txt | 2 +- .../re_types/src/archetypes/text_document.rs | 107 +++++++--- crates/re_types/src/components/media_type.rs | 194 ++++++++++++++++++ .../re_types/src/components/media_type_ext.rs | 15 ++ crates/re_types/src/components/mod.rs | 3 + crates/re_types/tests/text_document.rs | 12 +- crates/re_viewer/Cargo.toml | 2 +- crates/re_viewport/src/space_info.rs | 3 +- .../re_viewport/src/space_view_heuristics.rs | 9 +- docs/code-examples/text_document.cpp | 10 + docs/code-examples/text_document.py | 7 + docs/code-examples/text_document.rs | 16 +- rerun_cpp/.gitattributes | 2 + .../src/rerun/archetypes/text_document.cpp | 5 +- .../src/rerun/archetypes/text_document.hpp | 23 +++ rerun_cpp/src/rerun/components.hpp | 1 + rerun_cpp/src/rerun/components/media_type.cpp | 84 ++++++++ rerun_cpp/src/rerun/components/media_type.hpp | 80 ++++++++ .../src/rerun/components/media_type_ext.cpp | 32 +++ .../rerun_sdk/rerun/_rerun2/.gitattributes | 1 + .../rerun/_rerun2/archetypes/text_document.py | 15 ++ .../rerun/_rerun2/components/__init__.py | 4 + .../rerun/_rerun2/components/media_type.py | 46 +++++ tests/cpp/roundtrips/text_document/main.cpp | 11 + tests/python/roundtrips/text_document/main.py | 7 + .../rust/roundtrips/text_document/src/main.rs | 22 +- 36 files changed, 832 insertions(+), 77 deletions(-) create mode 100644 crates/re_types/definitions/rerun/components/media_type.fbs create mode 100644 crates/re_types/src/components/media_type.rs create mode 100644 crates/re_types/src/components/media_type_ext.rs create mode 100644 rerun_cpp/src/rerun/components/media_type.cpp create mode 100644 rerun_cpp/src/rerun/components/media_type.hpp create mode 100644 rerun_cpp/src/rerun/components/media_type_ext.cpp create mode 100644 rerun_py/rerun_sdk/rerun/_rerun2/components/media_type.py diff --git a/Cargo.lock b/Cargo.lock index 330bc3a829c5..6ab3e3be7d30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1363,7 +1363,7 @@ checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" [[package]] name = "ecolor" version = "0.22.0" -source = "git+https://github.com/emilk/egui?rev=2bbceb856b8c26cd7d7a749baba13cb7ffe41d89#2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" +source = "git+https://github.com/emilk/egui?rev=d949eaf#d949eaf682ea96784afb0f3463e11387db610646" dependencies = [ "bytemuck", "serde", @@ -1372,7 +1372,7 @@ dependencies = [ [[package]] name = "eframe" version = "0.22.0" -source = "git+https://github.com/emilk/egui?rev=2bbceb856b8c26cd7d7a749baba13cb7ffe41d89#2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" +source = "git+https://github.com/emilk/egui?rev=d949eaf#d949eaf682ea96784afb0f3463e11387db610646" dependencies = [ "bytemuck", "cocoa", @@ -1405,7 +1405,7 @@ dependencies = [ [[package]] name = "egui" version = "0.22.0" -source = "git+https://github.com/emilk/egui?rev=2bbceb856b8c26cd7d7a749baba13cb7ffe41d89#2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" +source = "git+https://github.com/emilk/egui?rev=d949eaf#d949eaf682ea96784afb0f3463e11387db610646" dependencies = [ "accesskit", "ahash 0.8.3", @@ -1420,7 +1420,7 @@ dependencies = [ [[package]] name = "egui-wgpu" version = "0.22.0" -source = "git+https://github.com/emilk/egui?rev=2bbceb856b8c26cd7d7a749baba13cb7ffe41d89#2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" +source = "git+https://github.com/emilk/egui?rev=d949eaf#d949eaf682ea96784afb0f3463e11387db610646" dependencies = [ "bytemuck", "epaint", @@ -1435,7 +1435,7 @@ dependencies = [ [[package]] name = "egui-winit" version = "0.22.0" -source = "git+https://github.com/emilk/egui?rev=2bbceb856b8c26cd7d7a749baba13cb7ffe41d89#2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" +source = "git+https://github.com/emilk/egui?rev=d949eaf#d949eaf682ea96784afb0f3463e11387db610646" dependencies = [ "arboard", "egui", @@ -1449,10 +1449,20 @@ dependencies = [ "winit", ] +[[package]] +name = "egui_commonmark" +version = "0.7.4" +source = "git+https://github.com/lampsitter/egui_commonmark.git?rev=a133564f26a95672e756079ac5583817e0cdaa1f#a133564f26a95672e756079ac5583817e0cdaa1f" +dependencies = [ + "egui", + "egui_extras", + "pulldown-cmark", +] + [[package]] name = "egui_extras" version = "0.22.0" -source = "git+https://github.com/emilk/egui?rev=2bbceb856b8c26cd7d7a749baba13cb7ffe41d89#2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" +source = "git+https://github.com/emilk/egui?rev=d949eaf#d949eaf682ea96784afb0f3463e11387db610646" dependencies = [ "egui", "ehttp", @@ -1467,7 +1477,7 @@ dependencies = [ [[package]] name = "egui_glow" version = "0.22.0" -source = "git+https://github.com/emilk/egui?rev=2bbceb856b8c26cd7d7a749baba13cb7ffe41d89#2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" +source = "git+https://github.com/emilk/egui?rev=d949eaf#d949eaf682ea96784afb0f3463e11387db610646" dependencies = [ "bytemuck", "egui", @@ -1483,7 +1493,7 @@ dependencies = [ [[package]] name = "egui_plot" version = "0.22.0" -source = "git+https://github.com/emilk/egui?rev=2bbceb856b8c26cd7d7a749baba13cb7ffe41d89#2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" +source = "git+https://github.com/emilk/egui?rev=d949eaf#d949eaf682ea96784afb0f3463e11387db610646" dependencies = [ "egui", ] @@ -1525,7 +1535,7 @@ checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "emath" version = "0.22.0" -source = "git+https://github.com/emilk/egui?rev=2bbceb856b8c26cd7d7a749baba13cb7ffe41d89#2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" +source = "git+https://github.com/emilk/egui?rev=d949eaf#d949eaf682ea96784afb0f3463e11387db610646" dependencies = [ "bytemuck", "serde", @@ -1627,7 +1637,7 @@ dependencies = [ [[package]] name = "epaint" version = "0.22.0" -source = "git+https://github.com/emilk/egui?rev=2bbceb856b8c26cd7d7a749baba13cb7ffe41d89#2bbceb856b8c26cd7d7a749baba13cb7ffe41d89" +source = "git+https://github.com/emilk/egui?rev=d949eaf#d949eaf682ea96784afb0f3463e11387db610646" dependencies = [ "ab_glyph", "ahash 0.8.3", @@ -1765,7 +1775,7 @@ checksum = "79386fdcec5e0fde91b1a6a5bcd89677d1f9304f7f986b154a1b9109038854d9" dependencies = [ "az", "bytemuck", - "half 1.8.2", + "half 2.3.1", "serde", "typenum", ] @@ -3809,6 +3819,17 @@ dependencies = [ "puffin", ] +[[package]] +name = "pulldown-cmark" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase", +] + [[package]] name = "pyo3" version = "0.19.0" @@ -4518,10 +4539,13 @@ name = "re_space_view_text_document" version = "0.9.0-alpha.4" dependencies = [ "egui", + "egui_commonmark", + "itertools 0.11.0", "re_arrow_store", "re_log", "re_query", "re_renderer", + "re_tracing", "re_types", "re_ui", "re_viewer_context", diff --git a/Cargo.toml b/Cargo.toml index b2b9e25d8803..dfd9b34dedc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } @@ -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" } diff --git a/crates/re_space_view_text_document/Cargo.toml b/crates/re_space_view_text_document/Cargo.toml index d164c887d199..8d9ed1522c02 100644 --- a/crates/re_space_view_text_document/Cargo.toml +++ b/crates/re_space_view_text_document/Cargo.toml @@ -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 } diff --git a/crates/re_space_view_text_document/src/space_view_class.rs b/crates/re_space_view_text_document/src/space_view_class.rs index d7df720ce9fd..f4b9d71cbf8a 100644 --- a/crates/re_space_view_text_document/src/space_view_class.rs +++ b/crates/re_space_view_text_document/src/space_view_class.rs @@ -1,17 +1,23 @@ 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 { @@ -19,6 +25,9 @@ impl Default for TextDocumentSpaceViewState { Self { monospace: false, word_wrap: true, + + #[cfg(feature = "markdown")] + commonmark_cache: Default::default(), } } } @@ -104,12 +113,29 @@ 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(); @@ -117,8 +143,9 @@ impl SpaceViewClass for TextDocumentSpaceView { 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() )); } diff --git a/crates/re_space_view_text_document/src/view_part_system.rs b/crates/re_space_view_text_document/src/view_part_system.rs index 434c825634e8..00911bd248df 100644 --- a/crates/re_space_view_text_document/src/view_part_system.rs +++ b/crates/re_space_view_text_document/src/view_part_system.rs @@ -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, @@ -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. @@ -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::( - store, - &timeline_query, - ent_path, - ) { + match query_archetype::(store, &timeline_query, ent_path) { Ok(arch_view) => { - for text_entry in - arch_view.iter_required_component::()? - { - let re_types::components::Text(text) = text_entry; - self.text_entries.push(TextDocumentEntry { body: text }); + let bodies = arch_view.iter_required_component::()?; + let media_types = + arch_view.iter_optional_component::()?; + + 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(_)) => {} diff --git a/crates/re_string_interner/src/lib.rs b/crates/re_string_interner/src/lib.rs index cd30a7eb8360..7ccf97af9ebb 100644 --- a/crates/re_string_interner/src/lib.rs +++ b/crates/re_string_interner/src/lib.rs @@ -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() } diff --git a/crates/re_types/.gitattributes b/crates/re_types/.gitattributes index 96af3f9b9db4..13f0211da0c6 100644 --- a/crates/re_types/.gitattributes +++ b/crates/re_types/.gitattributes @@ -32,6 +32,7 @@ src/components/instance_key.rs linguist-generated=true src/components/keypoint_id.rs linguist-generated=true src/components/line_strip2d.rs linguist-generated=true src/components/line_strip3d.rs linguist-generated=true +src/components/media_type.rs linguist-generated=true src/components/mod.rs linguist-generated=true src/components/origin2d.rs linguist-generated=true src/components/origin3d.rs linguist-generated=true diff --git a/crates/re_types/definitions/rerun/archetypes/text_document.fbs b/crates/re_types/definitions/rerun/archetypes/text_document.fbs index b6bf634c5a0c..23c5005c7c82 100644 --- a/crates/re_types/definitions/rerun/archetypes/text_document.fbs +++ b/crates/re_types/definitions/rerun/archetypes/text_document.fbs @@ -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); } diff --git a/crates/re_types/definitions/rerun/components.fbs b/crates/re_types/definitions/rerun/components.fbs index f5d965401d4e..8f6954d13c26 100644 --- a/crates/re_types/definitions/rerun/components.fbs +++ b/crates/re_types/definitions/rerun/components.fbs @@ -11,6 +11,7 @@ include "./components/instance_key.fbs"; include "./components/keypoint_id.fbs"; include "./components/line_strip2d.fbs"; include "./components/line_strip3d.fbs"; +include "./components/media_type.fbs"; include "./components/origin2d.fbs"; include "./components/origin3d.fbs"; include "./components/position2d.fbs"; diff --git a/crates/re_types/definitions/rerun/components/media_type.fbs b/crates/re_types/definitions/rerun/components/media_type.fbs new file mode 100644 index 000000000000..707e1f55e63c --- /dev/null +++ b/crates/re_types/definitions/rerun/components/media_type.fbs @@ -0,0 +1,26 @@ +include "arrow/attributes.fbs"; +include "python/attributes.fbs"; +include "rust/attributes.fbs"; + +include "rerun/datatypes.fbs"; +include "rerun/attributes.fbs"; + +namespace rerun.components; + +// --- + +/// [MIME-type](https://en.wikipedia.org/wiki/Media_type) of an entity. +/// +/// For instance: +/// * `text/plain` +/// * `text/markdown` +table MediaType ( + "attr.arrow.transparent", + "attr.python.aliases": "str", + "attr.python.array_aliases": "str, Sequence[str]", + "attr.rust.derive": "PartialEq, Eq, PartialOrd, Ord", + "attr.rust.repr": "transparent", + order: 100 +) { + value: rerun.datatypes.Utf8 (order: 100); +} diff --git a/crates/re_types/source_hash.txt b/crates/re_types/source_hash.txt index d751ac94fb76..4a9d8149b83e 100644 --- a/crates/re_types/source_hash.txt +++ b/crates/re_types/source_hash.txt @@ -1,4 +1,4 @@ # This is a sha256 hash for all direct and indirect dependencies of this crate's build script. # It can be safely removed at anytime to force the build script to run again. # Check out build.rs to see how it's computed. -ab3878809658844ace411b8bee81eafa8ac8a4382695a78dc89dd281113a2190 +8f5d0b67f16163c142d34d2af8153e2c531c6637600f0101865707f4fe0581dc diff --git a/crates/re_types/src/archetypes/text_document.rs b/crates/re_types/src/archetypes/text_document.rs index 175723af930f..3d4e8f24b683 100644 --- a/crates/re_types/src/archetypes/text_document.rs +++ b/crates/re_types/src/archetypes/text_document.rs @@ -17,6 +17,15 @@ #[derive(Clone, Debug, PartialEq, Eq)] pub struct TextDocument { pub body: crate::components::Text, + + /// The Media Type of the text. + /// + /// For instance: + /// * `text/plain` + /// * `text/markdown` + /// + /// If omitted, `text/plain` is assumed. + pub media_type: Option, } static REQUIRED_COMPONENTS: once_cell::sync::Lazy<[crate::ComponentName; 1usize]> = @@ -25,20 +34,26 @@ static REQUIRED_COMPONENTS: once_cell::sync::Lazy<[crate::ComponentName; 1usize] static RECOMMENDED_COMPONENTS: once_cell::sync::Lazy<[crate::ComponentName; 1usize]> = once_cell::sync::Lazy::new(|| ["rerun.components.TextDocumentIndicator".into()]); -static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[crate::ComponentName; 1usize]> = - once_cell::sync::Lazy::new(|| ["rerun.components.InstanceKey".into()]); +static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[crate::ComponentName; 2usize]> = + once_cell::sync::Lazy::new(|| { + [ + "rerun.components.InstanceKey".into(), + "rerun.components.MediaType".into(), + ] + }); -static ALL_COMPONENTS: once_cell::sync::Lazy<[crate::ComponentName; 3usize]> = +static ALL_COMPONENTS: once_cell::sync::Lazy<[crate::ComponentName; 4usize]> = once_cell::sync::Lazy::new(|| { [ "rerun.components.Text".into(), "rerun.components.TextDocumentIndicator".into(), "rerun.components.InstanceKey".into(), + "rerun.components.MediaType".into(), ] }); impl TextDocument { - pub const NUM_COMPONENTS: usize = 3usize; + pub const NUM_COMPONENTS: usize = 4usize; } /// Indicator component for the [`TextDocument`] [`crate::Archetype`] @@ -81,6 +96,9 @@ impl crate::Archetype for TextDocument { [ Some(Self::Indicator::batch(self.num_instances() as _).into()), Some((&self.body as &dyn crate::ComponentBatch).into()), + self.media_type + .as_ref() + .map(|comp| (comp as &dyn crate::ComponentBatch).into()), ] .into_iter() .flatten() @@ -94,24 +112,46 @@ impl crate::Archetype for TextDocument { Vec<(::arrow2::datatypes::Field, Box)>, > { use crate::{Loggable as _, ResultExt as _}; - Ok([{ - Some({ - let array = ::try_to_arrow([&self.body]); - array.map(|array| { - let datatype = ::arrow2::datatypes::DataType::Extension( - "rerun.components.Text".into(), - Box::new(array.data_type().clone()), - None, - ); - ( - ::arrow2::datatypes::Field::new("body", datatype, false), - array, - ) + Ok([ + { + Some({ + let array = ::try_to_arrow([&self.body]); + array.map(|array| { + let datatype = ::arrow2::datatypes::DataType::Extension( + "rerun.components.Text".into(), + Box::new(array.data_type().clone()), + None, + ); + ( + ::arrow2::datatypes::Field::new("body", datatype, false), + array, + ) + }) }) - }) - .transpose() - .with_context("rerun.archetypes.TextDocument#body")? - }] + .transpose() + .with_context("rerun.archetypes.TextDocument#body")? + }, + { + self.media_type + .as_ref() + .map(|single| { + let array = ::try_to_arrow([single]); + array.map(|array| { + let datatype = ::arrow2::datatypes::DataType::Extension( + "rerun.components.MediaType".into(), + Box::new(array.data_type().clone()), + None, + ); + ( + ::arrow2::datatypes::Field::new("media_type", datatype, false), + array, + ) + }) + }) + .transpose() + .with_context("rerun.archetypes.TextDocument#media_type")? + }, + ] .into_iter() .flatten() .collect()) @@ -141,12 +181,33 @@ impl crate::Archetype for TextDocument { .ok_or_else(crate::DeserializationError::missing_data) .with_context("rerun.archetypes.TextDocument#body")? }; - Ok(Self { body }) + let media_type = if let Some(array) = arrays_by_name.get("media_type") { + Some({ + ::try_from_arrow_opt(&**array) + .with_context("rerun.archetypes.TextDocument#media_type")? + .into_iter() + .next() + .flatten() + .ok_or_else(crate::DeserializationError::missing_data) + .with_context("rerun.archetypes.TextDocument#media_type")? + }) + } else { + None + }; + Ok(Self { body, media_type }) } } impl TextDocument { pub fn new(body: impl Into) -> Self { - Self { body: body.into() } + Self { + body: body.into(), + media_type: None, + } + } + + pub fn with_media_type(mut self, media_type: impl Into) -> Self { + self.media_type = Some(media_type.into()); + self } } diff --git a/crates/re_types/src/components/media_type.rs b/crates/re_types/src/components/media_type.rs new file mode 100644 index 000000000000..1e2d4ca75f7b --- /dev/null +++ b/crates/re_types/src/components/media_type.rs @@ -0,0 +1,194 @@ +// DO NOT EDIT! This file was auto-generated by crates/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/re_types/definitions/rerun/components/media_type.fbs". + +#![allow(trivial_numeric_casts)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::iter_on_single_items)] +#![allow(clippy::map_flatten)] +#![allow(clippy::match_wildcard_for_single_variants)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::unnecessary_cast)] + +/// [MIME-type](https://en.wikipedia.org/wiki/Media_type) of an entity. +/// +/// For instance: +/// * `text/plain` +/// * `text/markdown` +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +pub struct MediaType(pub crate::datatypes::Utf8); + +impl> From for MediaType { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow for MediaType { + #[inline] + fn borrow(&self) -> &crate::datatypes::Utf8 { + &self.0 + } +} + +impl std::ops::Deref for MediaType { + type Target = crate::datatypes::Utf8; + + #[inline] + fn deref(&self) -> &crate::datatypes::Utf8 { + &self.0 + } +} + +impl<'a> From for ::std::borrow::Cow<'a, MediaType> { + #[inline] + fn from(value: MediaType) -> Self { + std::borrow::Cow::Owned(value) + } +} + +impl<'a> From<&'a MediaType> for ::std::borrow::Cow<'a, MediaType> { + #[inline] + fn from(value: &'a MediaType) -> Self { + std::borrow::Cow::Borrowed(value) + } +} + +impl crate::Loggable for MediaType { + type Name = crate::ComponentName; + + #[inline] + fn name() -> Self::Name { + "rerun.components.MediaType".into() + } + + #[allow(unused_imports, clippy::wildcard_imports)] + #[inline] + fn arrow_datatype() -> arrow2::datatypes::DataType { + use ::arrow2::datatypes::*; + DataType::Utf8 + } + + #[allow(unused_imports, clippy::wildcard_imports)] + fn try_to_arrow_opt<'a>( + data: impl IntoIterator>>>, + ) -> crate::SerializationResult> + where + Self: Clone + 'a, + { + use crate::{Loggable as _, ResultExt as _}; + use ::arrow2::{array::*, datatypes::*}; + Ok({ + let (somes, data0): (Vec<_>, Vec<_>) = data + .into_iter() + .map(|datum| { + let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); + let datum = datum.map(|datum| { + let Self(data0) = datum.into_owned(); + data0 + }); + (datum.is_some(), datum) + }) + .unzip(); + let data0_bitmap: Option<::arrow2::bitmap::Bitmap> = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + { + let inner_data: ::arrow2::buffer::Buffer = data0 + .iter() + .flatten() + .flat_map(|datum| { + let crate::datatypes::Utf8(data0) = datum; + data0.0.clone() + }) + .collect(); + let offsets = + ::arrow2::offset::Offsets::::try_from_lengths(data0.iter().map(|opt| { + opt.as_ref() + .map(|datum| { + let crate::datatypes::Utf8(data0) = datum; + data0.0.len() + }) + .unwrap_or_default() + })) + .unwrap() + .into(); + + #[allow(unsafe_code, clippy::undocumented_unsafe_blocks)] + unsafe { + Utf8Array::::new_unchecked( + Self::arrow_datatype(), + offsets, + inner_data, + data0_bitmap, + ) + } + .boxed() + } + }) + } + + #[allow(unused_imports, clippy::wildcard_imports)] + fn try_from_arrow_opt( + arrow_data: &dyn ::arrow2::array::Array, + ) -> crate::DeserializationResult>> + where + Self: Sized, + { + use crate::{Loggable as _, ResultExt as _}; + use ::arrow2::{array::*, buffer::*, datatypes::*}; + Ok({ + let arrow_data = arrow_data + .as_any() + .downcast_ref::<::arrow2::array::Utf8Array>() + .ok_or_else(|| { + crate::DeserializationError::datatype_mismatch( + DataType::Utf8, + arrow_data.data_type().clone(), + ) + }) + .with_context("rerun.components.MediaType#value")?; + let arrow_data_buf = arrow_data.values(); + let offsets = arrow_data.offsets(); + arrow2::bitmap::utils::ZipValidity::new_with_validity( + offsets.iter().zip(offsets.lengths()), + arrow_data.validity(), + ) + .map(|elem| { + elem.map(|(start, len)| { + let start = *start as usize; + let end = start + len; + if end as usize > arrow_data_buf.len() { + return Err(crate::DeserializationError::offset_slice_oob( + (start, end), + arrow_data_buf.len(), + )); + } + + #[allow(unsafe_code, clippy::undocumented_unsafe_blocks)] + let data = unsafe { arrow_data_buf.clone().sliced_unchecked(start, len) }; + Ok(data) + }) + .transpose() + }) + .map(|res_or_opt| { + res_or_opt.map(|res_or_opt| { + res_or_opt.map(|v| crate::datatypes::Utf8(crate::ArrowString(v))) + }) + }) + .collect::>>>() + .with_context("rerun.components.MediaType#value")? + .into_iter() + } + .map(|v| v.ok_or_else(crate::DeserializationError::missing_data)) + .map(|res| res.map(|v| Some(Self(v)))) + .collect::>>>() + .with_context("rerun.components.MediaType#value") + .with_context("rerun.components.MediaType")?) + } +} diff --git a/crates/re_types/src/components/media_type_ext.rs b/crates/re_types/src/components/media_type_ext.rs new file mode 100644 index 000000000000..689372624ba6 --- /dev/null +++ b/crates/re_types/src/components/media_type_ext.rs @@ -0,0 +1,15 @@ +use super::MediaType; + +impl MediaType { + /// `text/plain` + #[inline] + pub fn plain_text() -> Self { + Self("text/plain".into()) + } + + /// `text/markdown` + #[inline] + pub fn markdown() -> Self { + Self("text/markdown".into()) + } +} diff --git a/crates/re_types/src/components/mod.rs b/crates/re_types/src/components/mod.rs index 5842819ea3f3..d590ad7ab3ea 100644 --- a/crates/re_types/src/components/mod.rs +++ b/crates/re_types/src/components/mod.rs @@ -24,6 +24,8 @@ mod line_strip2d; mod line_strip2d_ext; mod line_strip3d; mod line_strip3d_ext; +mod media_type; +mod media_type_ext; mod origin2d; mod origin2d_ext; mod origin3d; @@ -58,6 +60,7 @@ pub use self::instance_key::InstanceKey; pub use self::keypoint_id::KeypointId; pub use self::line_strip2d::LineStrip2D; pub use self::line_strip3d::LineStrip3D; +pub use self::media_type::MediaType; pub use self::origin2d::Origin2D; pub use self::origin3d::Origin3D; pub use self::position2d::Position2D; diff --git a/crates/re_types/tests/text_document.rs b/crates/re_types/tests/text_document.rs index bd43f44e29f6..c59ae0bcbb30 100644 --- a/crates/re_types/tests/text_document.rs +++ b/crates/re_types/tests/text_document.rs @@ -1,17 +1,23 @@ use std::collections::HashMap; -use re_types::{archetypes::TextDocument, Archetype as _}; +use re_types::{archetypes::TextDocument, components::MediaType, Archetype as _}; #[test] fn roundtrip() { let expected = TextDocument { body: "This is the contents of the text document.".into(), + media_type: Some(MediaType::markdown()), }; - let arch = TextDocument::new("This is the contents of the text document."); + let arch = TextDocument::new("This is the contents of the text document.") + .with_media_type(MediaType::markdown()); similar_asserts::assert_eq!(expected, arch); - let expected_extensions: HashMap<_, _> = [("body", vec!["rerun.components.Text"])].into(); + let expected_extensions: HashMap<_, _> = [ + ("body", vec!["rerun.components.Text"]), + ("media_type", vec!["rerun.components.MediaType"]), + ] + .into(); eprintln!("arch = {arch:#?}"); let serialized = arch.to_arrow(); diff --git a/crates/re_viewer/Cargo.toml b/crates/re_viewer/Cargo.toml index 7c987b6dd78c..014cd5b0a479 100644 --- a/crates/re_viewer/Cargo.toml +++ b/crates/re_viewer/Cargo.toml @@ -55,7 +55,7 @@ re_smart_channel.workspace = true re_space_view_bar_chart.workspace = true re_space_view_spatial.workspace = true re_space_view_tensor.workspace = true -re_space_view_text_document.workspace = true +re_space_view_text_document = { workspace = true, features = ["markdown"] } re_space_view_text_log.workspace = true re_space_view_time_series.workspace = true re_time_panel.workspace = true diff --git a/crates/re_viewport/src/space_info.rs b/crates/re_viewport/src/space_info.rs index 4f1a923812ec..b25085725c39 100644 --- a/crates/re_viewport/src/space_info.rs +++ b/crates/re_viewport/src/space_info.rs @@ -25,6 +25,7 @@ pub enum SpaceInfoConnection { /// ⚠️ Transforms used for this are latest known, i.e. the "right most location in the timeline" ⚠️ /// /// Expected to be recreated every frame (or whenever new data is available). +#[derive(Debug)] pub struct SpaceInfo { pub path: EntityPath, @@ -106,7 +107,7 @@ impl SpaceInfo { /// Each of these we walk down recursively, every time a transform is encountered, we create another space info. /// /// Expected to be recreated every frame (or whenever new data is available). -#[derive(Default)] +#[derive(Debug, Default)] pub struct SpaceInfoCollection { spaces: BTreeMap, } diff --git a/crates/re_viewport/src/space_view_heuristics.rs b/crates/re_viewport/src/space_view_heuristics.rs index 55130398a255..81ce2671c8eb 100644 --- a/crates/re_viewport/src/space_view_heuristics.rs +++ b/crates/re_viewport/src/space_view_heuristics.rs @@ -27,8 +27,10 @@ fn is_spatial_class(class: &SpaceViewClassName) -> bool { class.as_str() == "3D" || class.as_str() == "2D" } -fn is_tensor_class(class: &SpaceViewClassName) -> bool { - class.as_str() == "Tensor" +fn spawn_one_space_view_per_entity(class: &SpaceViewClassName) -> bool { + // For tensors create one space view for each tensor (even though we're able to stack them in one view) + // TODO(emilk): query the actual [`ViewPartSystem`] instead. + class == "Tensor" || class == "Text Document" } // --------------------------------------------------------------------------- @@ -252,8 +254,7 @@ pub fn default_created_space_views( } } - // For tensors create one space view for each tensor (even though we're able to stack them in one view) - if is_tensor_class(candidate.class_name()) { + if spawn_one_space_view_per_entity(candidate.class_name()) { for entity_path in candidate.contents.entity_paths() { let mut space_view = SpaceViewBlueprint::new( *candidate.class_name(), diff --git a/docs/code-examples/text_document.cpp b/docs/code-examples/text_document.cpp index c5cd1408d0ec..12009bbad956 100644 --- a/docs/code-examples/text_document.cpp +++ b/docs/code-examples/text_document.cpp @@ -12,4 +12,14 @@ int main() { rr_stream.connect("127.0.0.1:9876").throw_on_failure(); rr_stream.log("text_document", rr::archetypes::TextDocument("Hello, TextDocument!")); + rr_stream.log( + "markdown", + rr::archetypes::TextDocument("# Hello\n" + "Markdown with `code`!\n" + "\n" + "A random image:\n" + "\n" + "![A random image](https://picsum.photos/640/480)") + .with_media_type(rr::components::MediaType::markdown()) + ); } diff --git a/docs/code-examples/text_document.py b/docs/code-examples/text_document.py index 692d06189cfb..058cefca08ab 100755 --- a/docs/code-examples/text_document.py +++ b/docs/code-examples/text_document.py @@ -7,3 +7,10 @@ rr.init("rerun_example_text_document", spawn=True) rr2.log("text_document", rr2.TextDocument(body="Hello, TextDocument!")) +rr2.log( + "markdown", + rr2.TextDocument( + body="# Hello\nMarkdown with `code`!\n\nA random image:\n\n![A random image](https://picsum.photos/640/480)", + media_type="text/markdown", + ), +) diff --git a/docs/code-examples/text_document.rs b/docs/code-examples/text_document.rs index 26edc540984d..ce5fdc932a04 100644 --- a/docs/code-examples/text_document.rs +++ b/docs/code-examples/text_document.rs @@ -1,11 +1,25 @@ //! Log a `TextDocument` -use rerun::{archetypes::TextDocument, RecordingStreamBuilder}; +use rerun::{ + archetypes::TextDocument, external::re_types::components::MediaType, RecordingStreamBuilder, +}; fn main() -> Result<(), Box> { let (rec, storage) = RecordingStreamBuilder::new("rerun_example_text_document").memory()?; rec.log("text_document", &TextDocument::new("Hello, TextDocument!"))?; + 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()), + )?; rerun::native_viewer::show(storage.take())?; Ok(()) diff --git a/rerun_cpp/.gitattributes b/rerun_cpp/.gitattributes index 6d773bc3863e..bf42c0170c48 100644 --- a/rerun_cpp/.gitattributes +++ b/rerun_cpp/.gitattributes @@ -62,6 +62,8 @@ src/rerun/components/line_strip2d.cpp linguist-generated=true src/rerun/components/line_strip2d.hpp linguist-generated=true src/rerun/components/line_strip3d.cpp linguist-generated=true src/rerun/components/line_strip3d.hpp linguist-generated=true +src/rerun/components/media_type.cpp linguist-generated=true +src/rerun/components/media_type.hpp linguist-generated=true src/rerun/components/origin2d.cpp linguist-generated=true src/rerun/components/origin2d.hpp linguist-generated=true src/rerun/components/origin3d.cpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/archetypes/text_document.cpp b/rerun_cpp/src/rerun/archetypes/text_document.cpp index 8ba31d7f2f12..e2f288ebd0a8 100644 --- a/rerun_cpp/src/rerun/archetypes/text_document.cpp +++ b/rerun_cpp/src/rerun/archetypes/text_document.cpp @@ -12,9 +12,12 @@ namespace rerun { std::vector TextDocument::as_component_batches() const { std::vector comp_batches; - comp_batches.reserve(1); + comp_batches.reserve(2); comp_batches.emplace_back(body); + if (media_type.has_value()) { + comp_batches.emplace_back(media_type.value()); + } comp_batches.emplace_back( ComponentBatch< components::IndicatorComponent>( diff --git a/rerun_cpp/src/rerun/archetypes/text_document.hpp b/rerun_cpp/src/rerun/archetypes/text_document.hpp index b7c3f8a66500..43191aec5ac6 100644 --- a/rerun_cpp/src/rerun/archetypes/text_document.hpp +++ b/rerun_cpp/src/rerun/archetypes/text_document.hpp @@ -5,11 +5,13 @@ #include "../arrow.hpp" #include "../component_batch.hpp" +#include "../components/media_type.hpp" #include "../components/text.hpp" #include "../data_cell.hpp" #include "../result.hpp" #include +#include #include #include @@ -19,6 +21,15 @@ namespace rerun { struct TextDocument { rerun::components::Text body; + /// The Media Type of the text. + /// + /// For instance: + ///* `text/plain` + ///* `text/markdown` + /// + /// If omitted, `text/plain` is assumed. + std::optional media_type; + /// Name of the indicator component, used to identify the archetype when converting to a /// list of components. static const char INDICATOR_COMPONENT_NAME[]; @@ -28,6 +39,18 @@ namespace rerun { TextDocument(rerun::components::Text _body) : body(std::move(_body)) {} + /// The Media Type of the text. + /// + /// For instance: + ///* `text/plain` + ///* `text/markdown` + /// + /// If omitted, `text/plain` is assumed. + TextDocument& with_media_type(rerun::components::MediaType _media_type) { + media_type = std::move(_media_type); + return *this; + } + /// Returns the number of primary instances of this archetype. size_t num_instances() const { return 1; diff --git a/rerun_cpp/src/rerun/components.hpp b/rerun_cpp/src/rerun/components.hpp index c1e8a07e7574..0b891a137e88 100644 --- a/rerun_cpp/src/rerun/components.hpp +++ b/rerun_cpp/src/rerun/components.hpp @@ -15,6 +15,7 @@ #include "components/keypoint_id.hpp" #include "components/line_strip2d.hpp" #include "components/line_strip3d.hpp" +#include "components/media_type.hpp" #include "components/origin2d.hpp" #include "components/origin3d.hpp" #include "components/position2d.hpp" diff --git a/rerun_cpp/src/rerun/components/media_type.cpp b/rerun_cpp/src/rerun/components/media_type.cpp new file mode 100644 index 000000000000..67468cf8a5cb --- /dev/null +++ b/rerun_cpp/src/rerun/components/media_type.cpp @@ -0,0 +1,84 @@ +// DO NOT EDIT! This file was auto-generated by crates/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/re_types/definitions/rerun/components/media_type.fbs". + +#include "media_type.hpp" + +#include "../arrow.hpp" +#include "../datatypes/utf8.hpp" + +#include +#include +#include + +namespace rerun { + namespace components { + const char MediaType::NAME[] = "rerun.components.MediaType"; + + const std::shared_ptr &MediaType::arrow_datatype() { + static const auto datatype = rerun::datatypes::Utf8::arrow_datatype(); + return datatype; + } + + Result> MediaType::new_arrow_array_builder( + arrow::MemoryPool *memory_pool + ) { + if (!memory_pool) { + return Error(ErrorCode::UnexpectedNullArgument, "Memory pool is null."); + } + + return Result(rerun::datatypes::Utf8::new_arrow_array_builder(memory_pool).value); + } + + Error MediaType::fill_arrow_array_builder( + arrow::StringBuilder *builder, const MediaType *elements, size_t num_elements + ) { + if (!builder) { + return Error(ErrorCode::UnexpectedNullArgument, "Passed array builder is null."); + } + if (!elements) { + return Error( + ErrorCode::UnexpectedNullArgument, + "Cannot serialize null pointer to arrow array." + ); + } + + static_assert(sizeof(rerun::datatypes::Utf8) == sizeof(MediaType)); + RR_RETURN_NOT_OK(rerun::datatypes::Utf8::fill_arrow_array_builder( + builder, + reinterpret_cast(elements), + num_elements + )); + + return Error::ok(); + } + + Result MediaType::to_data_cell( + const MediaType *instances, size_t num_instances + ) { + // TODO(andreas): Allow configuring the memory pool. + arrow::MemoryPool *pool = arrow::default_memory_pool(); + + auto builder_result = MediaType::new_arrow_array_builder(pool); + RR_RETURN_NOT_OK(builder_result.error); + auto builder = std::move(builder_result.value); + if (instances && num_instances > 0) { + RR_RETURN_NOT_OK( + MediaType::fill_arrow_array_builder(builder.get(), instances, num_instances) + ); + } + std::shared_ptr array; + ARROW_RETURN_NOT_OK(builder->Finish(&array)); + + auto schema = + arrow::schema({arrow::field(MediaType::NAME, MediaType::arrow_datatype(), false)}); + + rerun::DataCell cell; + cell.component_name = MediaType::NAME; + const auto ipc_result = rerun::ipc_from_table(*arrow::Table::Make(schema, {array})); + RR_RETURN_NOT_OK(ipc_result.error); + cell.buffer = std::move(ipc_result.value); + + return cell; + } + } // namespace components +} // namespace rerun diff --git a/rerun_cpp/src/rerun/components/media_type.hpp b/rerun_cpp/src/rerun/components/media_type.hpp new file mode 100644 index 000000000000..a2ab3bfef6f2 --- /dev/null +++ b/rerun_cpp/src/rerun/components/media_type.hpp @@ -0,0 +1,80 @@ +// DO NOT EDIT! This file was auto-generated by crates/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/re_types/definitions/rerun/components/media_type.fbs". + +#pragma once + +#include "../data_cell.hpp" +#include "../datatypes/utf8.hpp" +#include "../result.hpp" + +#include +#include +#include +#include + +namespace arrow { + class DataType; + class MemoryPool; + class StringBuilder; +} // namespace arrow + +namespace rerun { + namespace components { + ///[MIME-type](https://en.wikipedia.org/wiki/Media_type) of an entity. + /// + /// For instance: + ///* `text/plain` + ///* `text/markdown` + struct MediaType { + rerun::datatypes::Utf8 value; + + /// Name of the component, used for serialization. + static const char NAME[]; + + public: + // Extensions to generated type defined in 'media_type_ext.cpp' + + MediaType(const char* media_type) : value(media_type) {} + + /// `text/plain` + static MediaType plain_text() { + return "text/plain"; + } + + /// `text/markdown` + static MediaType markdown() { + return "text/markdown"; + } + + public: + MediaType() = default; + + MediaType(rerun::datatypes::Utf8 _value) : value(std::move(_value)) {} + + MediaType& operator=(rerun::datatypes::Utf8 _value) { + value = std::move(_value); + return *this; + } + + MediaType(std::string arg) : value(std::move(arg)) {} + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype(); + + /// Creates a new array builder with an array of this type. + static Result> new_arrow_array_builder( + arrow::MemoryPool* memory_pool + ); + + /// Fills an arrow array builder with an array of this type. + static Error fill_arrow_array_builder( + arrow::StringBuilder* builder, const MediaType* elements, size_t num_elements + ); + + /// Creates a Rerun DataCell from an array of MediaType components. + static Result to_data_cell( + const MediaType* instances, size_t num_instances + ); + }; + } // namespace components +} // namespace rerun diff --git a/rerun_cpp/src/rerun/components/media_type_ext.cpp b/rerun_cpp/src/rerun/components/media_type_ext.cpp new file mode 100644 index 000000000000..f7b5be2f10cf --- /dev/null +++ b/rerun_cpp/src/rerun/components/media_type_ext.cpp @@ -0,0 +1,32 @@ +#include "media_type.hpp" + +// Uncomment for better auto-complete while editing the extension. +// #define EDIT_EXTENSION + +namespace rerun { + namespace components { + +#ifdef EDIT_EXTENSION + struct MediaTypeExt { +#define MediaType MediaTypeExt + + // [CODEGEN COPY TO HEADER START] + + MediaType(const char* media_type) : value(media_type) {} + + /// `text/plain` + static MediaType plain_text() { + return "text/plain"; + } + + /// `text/markdown` + static MediaType markdown() { + return "text/markdown"; + } + + // [CODEGEN COPY TO HEADER END] + } + }; +#endif +} // namespace components +} // namespace rerun diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/.gitattributes b/rerun_py/rerun_sdk/rerun/_rerun2/.gitattributes index 417e71c61bd4..1135a4d08006 100644 --- a/rerun_py/rerun_sdk/rerun/_rerun2/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/_rerun2/.gitattributes @@ -55,6 +55,7 @@ components/instance_key.py linguist-generated=true components/keypoint_id.py linguist-generated=true components/line_strip2d.py linguist-generated=true components/line_strip3d.py linguist-generated=true +components/media_type.py linguist-generated=true components/origin2d.py linguist-generated=true components/origin3d.py linguist-generated=true components/position2d.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/archetypes/text_document.py b/rerun_py/rerun_sdk/rerun/_rerun2/archetypes/text_document.py index b26431968371..47a390b84fa3 100644 --- a/rerun_py/rerun_sdk/rerun/_rerun2/archetypes/text_document.py +++ b/rerun_py/rerun_sdk/rerun/_rerun2/archetypes/text_document.py @@ -25,5 +25,20 @@ class TextDocument(Archetype): metadata={"component": "primary"}, converter=components.TextArray.from_similar, # type: ignore[misc] ) + media_type: components.MediaTypeArray | None = field( + metadata={"component": "secondary"}, + default=None, + converter=components.MediaTypeArray.from_similar, # type: ignore[misc] + ) + """ + The Media Type of the text. + + For instance: + * `text/plain` + * `text/markdown` + + If omitted, `text/plain` is assumed. + """ + __str__ = Archetype.__str__ __repr__ = Archetype.__repr__ diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/components/__init__.py b/rerun_py/rerun_sdk/rerun/_rerun2/components/__init__.py index 4e9e5d400cd2..1978390aa300 100644 --- a/rerun_py/rerun_sdk/rerun/_rerun2/components/__init__.py +++ b/rerun_py/rerun_sdk/rerun/_rerun2/components/__init__.py @@ -95,6 +95,7 @@ from .keypoint_id import KeypointId, KeypointIdArray, KeypointIdType from .line_strip2d import LineStrip2D, LineStrip2DArray, LineStrip2DArrayLike, LineStrip2DLike, LineStrip2DType from .line_strip3d import LineStrip3D, LineStrip3DArray, LineStrip3DArrayLike, LineStrip3DLike, LineStrip3DType +from .media_type import MediaType, MediaTypeArray, MediaTypeType from .origin2d import Origin2D, Origin2DArray, Origin2DType from .origin3d import Origin3D, Origin3DArray, Origin3DType from .position2d import Position2D, Position2DArray, Position2DType @@ -243,6 +244,9 @@ "LineStrip3DArrayLike", "LineStrip3DLike", "LineStrip3DType", + "MediaType", + "MediaTypeArray", + "MediaTypeType", "Origin2D", "Origin2DArray", "Origin2DType", diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/components/media_type.py b/rerun_py/rerun_sdk/rerun/_rerun2/components/media_type.py new file mode 100644 index 000000000000..8a719d3c3492 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/_rerun2/components/media_type.py @@ -0,0 +1,46 @@ +# DO NOT EDIT! This file was auto-generated by crates/re_types_builder/src/codegen/python.rs +# Based on "crates/re_types/definitions/rerun/components/media_type.fbs". + +# You can extend this class by creating a "MediaTypeExt" class in "media_type_ext.py". + +from __future__ import annotations + +from .. import datatypes +from .._baseclasses import ( + BaseDelegatingExtensionArray, + BaseDelegatingExtensionType, +) + +__all__ = ["MediaType", "MediaTypeArray", "MediaTypeType"] + + +class MediaType(datatypes.Utf8): + """ + [MIME-type](https://en.wikipedia.org/wiki/Media_type) of an entity. + + For instance: + * `text/plain` + * `text/markdown` + """ + + # You can define your own __init__ function as a member of MediaTypeExt in media_type_ext.py + + # Note: there are no fields here because MediaType delegates to datatypes.Utf8 + pass + + +class MediaTypeType(BaseDelegatingExtensionType): + _TYPE_NAME = "rerun.components.MediaType" + _DELEGATED_EXTENSION_TYPE = datatypes.Utf8Type + + +class MediaTypeArray(BaseDelegatingExtensionArray[datatypes.Utf8ArrayLike]): + _EXTENSION_NAME = "rerun.components.MediaType" + _EXTENSION_TYPE = MediaTypeType + _DELEGATED_ARRAY_TYPE = datatypes.Utf8Array + + +MediaTypeType._ARRAY_TYPE = MediaTypeArray + +# TODO(cmc): bring back registration to pyarrow once legacy types are gone +# pa.register_extension_type(MediaTypeType()) diff --git a/tests/cpp/roundtrips/text_document/main.cpp b/tests/cpp/roundtrips/text_document/main.cpp index 7dca5f3522c3..769083c889b4 100644 --- a/tests/cpp/roundtrips/text_document/main.cpp +++ b/tests/cpp/roundtrips/text_document/main.cpp @@ -6,4 +6,15 @@ int main(int argc, char** argv) { auto rr_stream = rr::RecordingStream("rerun_example_text_document"); rr_stream.save(argv[1]).throw_on_failure(); rr_stream.log("text_document", rr::archetypes::TextDocument("Hello, TextDocument!")); + rr_stream.log( + "markdown", + rr::archetypes::TextDocument("# Hello\n" + "Markdown with `code`!\n" + "\n" + "A random image:\n" + "\n" + "" + "![A random image](https://picsum.photos/640/480)") + .with_media_type(rr::components::MediaType::markdown()) + ); } diff --git a/tests/python/roundtrips/text_document/main.py b/tests/python/roundtrips/text_document/main.py index 2d682eb6fbb2..8e60258fb7e3 100755 --- a/tests/python/roundtrips/text_document/main.py +++ b/tests/python/roundtrips/text_document/main.py @@ -18,6 +18,13 @@ def main() -> None: rr.script_setup(args, "rerun_example_roundtrip_text_document") rr2.log("text_document", rr2.TextDocument(body="Hello, TextDocument!")) + rr2.log( + "markdown", + rr2.TextDocument( + body="# Hello\nMarkdown with `code`!\n\nA random image:\n\n![A random image](https://picsum.photos/640/480)", + media_type="text/markdown", + ), + ) rr.script_teardown(args) diff --git a/tests/rust/roundtrips/text_document/src/main.rs b/tests/rust/roundtrips/text_document/src/main.rs index 7ffb462e66d7..d63282fbd288 100644 --- a/tests/rust/roundtrips/text_document/src/main.rs +++ b/tests/rust/roundtrips/text_document/src/main.rs @@ -1,6 +1,10 @@ //! Logs a `Tensor` archetype for roundtrip checks. -use rerun::{archetypes::TextDocument, external::re_log, RecordingStream}; +use rerun::{ + archetypes::TextDocument, + external::{re_log, re_types::components::MediaType}, + RecordingStream, +}; #[derive(Debug, clap::Parser)] #[clap(author, version, about)] @@ -10,8 +14,20 @@ struct Args { } fn run(rec: &RecordingStream, _args: &Args) -> anyhow::Result<()> { - rec.log("text_document", &TextDocument::new("Hello, TextDocument!")) - .map_err(Into::into) + rec.log("text_document", &TextDocument::new("Hello, TextDocument!"))?; + 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()), + )?; + Ok(()) } fn main() -> anyhow::Result<()> {