Skip to content

Commit b9f1a66

Browse files
lucasmerlinWumpf
andauthored
Move copy / save buttons inline with the relevant component (#11181)
Co-authored-by: Andreas Reich <[email protected]>
1 parent 4666ea1 commit b9f1a66

File tree

8 files changed

+742
-582
lines changed

8 files changed

+742
-582
lines changed
Lines changed: 185 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
1-
use std::sync::Arc;
2-
1+
use crate::image::ImageUi;
2+
use crate::video::VideoUi;
3+
use crate::{EntityDataUi, find_and_deserialize_archetype_mono_component};
4+
use re_chunk_store::UnitChunkShared;
35
use re_log_types::EntityPath;
46
use re_types::{
5-
ComponentDescriptor, RowId,
7+
ComponentDescriptor, RowId, archetypes, components,
68
components::{Blob, MediaType, VideoTimestamp},
79
};
10+
use re_types_core::Component as _;
11+
use re_ui::list_item::ListItemContentButtonsExt as _;
812
use re_ui::{
913
UiExt as _, icons,
1014
list_item::{self, PropertyContent},
1115
};
1216
use re_viewer_context::{StoredBlobCacheKey, UiLayout, ViewerContext};
13-
14-
use crate::{
15-
EntityDataUi,
16-
image::image_preview_ui,
17-
video::{show_decoded_frame_info, video_asset_result_ui},
18-
};
17+
use std::sync::Arc;
1918

2019
impl EntityDataUi for Blob {
2120
fn entity_data_ui(
@@ -39,20 +38,20 @@ impl EntityDataUi for Blob {
3938
// This can also help a user debug if they log the contents of `.png` file with a `image/jpeg` `MediaType`.
4039
let media_type = MediaType::guess_from_data(self);
4140

41+
let blob_ui = BlobUi::new(
42+
ctx,
43+
entity_path,
44+
component_descriptor,
45+
row_id,
46+
self.0.clone(),
47+
media_type.as_ref(),
48+
None,
49+
);
50+
4251
if ui_layout.is_single_line() {
4352
ui.horizontal(|ui| {
44-
blob_preview_and_save_ui(
45-
ctx,
46-
ui,
47-
ui_layout,
48-
query,
49-
entity_path,
50-
component_descriptor,
51-
row_id,
52-
self,
53-
media_type.as_ref(),
54-
None,
55-
);
53+
ui.set_truncate_style();
54+
blob_ui.data_ui(ctx, ui, ui_layout, query, entity_path);
5655

5756
ui.label(compact_size_string);
5857

@@ -85,167 +84,12 @@ impl EntityDataUi for Blob {
8584
)
8685
.on_hover_text("Failed to detect media type (Mime) from magic header bytes");
8786
}
88-
89-
blob_preview_and_save_ui(
90-
ctx,
91-
ui,
92-
ui_layout,
93-
query,
94-
entity_path,
95-
component_descriptor,
96-
row_id,
97-
self,
98-
media_type.as_ref(),
99-
None,
100-
);
87+
blob_ui.data_ui(ctx, ui, ui_layout, query, entity_path);
10188
});
10289
}
10390
}
10491
}
10592

106-
#[allow(clippy::too_many_arguments)]
107-
pub fn blob_preview_and_save_ui(
108-
ctx: &re_viewer_context::ViewerContext<'_>,
109-
ui: &mut egui::Ui,
110-
ui_layout: UiLayout,
111-
query: &re_chunk_store::LatestAtQuery,
112-
entity_path: &re_log_types::EntityPath,
113-
blob_component_descriptor: &ComponentDescriptor,
114-
blob_row_id: Option<RowId>,
115-
blob: &re_types::datatypes::Blob,
116-
media_type: Option<&MediaType>,
117-
video_timestamp: Option<VideoTimestamp>,
118-
) {
119-
#[allow(unused_assignments)] // Not used when targeting web.
120-
let mut image = None;
121-
let mut video_result_for_frame_preview = None;
122-
123-
if let Some(blob_row_id) = blob_row_id {
124-
if !ui_layout.is_single_line() && ui_layout != UiLayout::Tooltip {
125-
exif_ui(
126-
ui,
127-
StoredBlobCacheKey::new(blob_row_id, blob_component_descriptor),
128-
blob,
129-
);
130-
}
131-
132-
// Try to treat it as an image:
133-
image = ctx
134-
.store_context
135-
.caches
136-
.entry(|c: &mut re_viewer_context::ImageDecodeCache| {
137-
c.entry(blob_row_id, blob_component_descriptor, blob, media_type)
138-
})
139-
.ok();
140-
141-
if let Some(image) = &image {
142-
if !ui_layout.is_single_line() {
143-
ui.list_item_flat_noninteractive(
144-
PropertyContent::new("Image format").value_text(image.format.to_string()),
145-
);
146-
}
147-
148-
let colormap = None; // TODO(andreas): Rely on default here for now.
149-
image_preview_ui(ctx, ui, ui_layout, query, entity_path, image, colormap);
150-
} else {
151-
// Try to treat it as a video.
152-
let video_result =
153-
ctx.store_context
154-
.caches
155-
.entry(|c: &mut re_viewer_context::VideoAssetCache| {
156-
let debug_name = entity_path.to_string();
157-
c.entry(
158-
debug_name,
159-
blob_row_id,
160-
blob_component_descriptor,
161-
blob,
162-
media_type,
163-
ctx.app_options().video_decoder_settings(),
164-
)
165-
});
166-
video_asset_result_ui(ui, ui_layout, &video_result);
167-
video_result_for_frame_preview = Some(video_result);
168-
}
169-
}
170-
171-
if !ui_layout.is_single_line() && ui_layout != UiLayout::Tooltip {
172-
ui.horizontal(|ui| {
173-
let text = if cfg!(target_arch = "wasm32") {
174-
"Download blob…"
175-
} else {
176-
"Save blob…"
177-
};
178-
if ui
179-
.add(egui::Button::image_and_text(
180-
icons::DOWNLOAD.as_image(),
181-
text,
182-
))
183-
.clicked()
184-
{
185-
let mut file_name = entity_path
186-
.last()
187-
.map_or("blob", |name| name.unescaped_str())
188-
.to_owned();
189-
190-
if let Some(file_extension) = media_type.as_ref().and_then(|mt| mt.file_extension())
191-
{
192-
file_name.push('.');
193-
file_name.push_str(file_extension);
194-
}
195-
196-
ctx.command_sender().save_file_dialog(
197-
re_capabilities::MainThreadToken::from_egui_ui(ui),
198-
&file_name,
199-
"Save blob".to_owned(),
200-
blob.to_vec(),
201-
);
202-
}
203-
204-
if let Some(image) = image {
205-
let image_stats = ctx
206-
.store_context
207-
.caches
208-
.entry(|c: &mut re_viewer_context::ImageStatsCache| c.entry(&image));
209-
let data_range = re_viewer_context::gpu_bridge::image_data_range_heuristic(
210-
&image_stats,
211-
&image.format,
212-
);
213-
crate::image::copy_image_button_ui(ui, &image, data_range);
214-
}
215-
});
216-
217-
// Show a mini video player for video blobs:
218-
if let Some(video_result) = &video_result_for_frame_preview
219-
&& let Ok(video) = video_result.as_ref()
220-
{
221-
ui.separator();
222-
223-
let video_timestamp = video_timestamp.unwrap_or_else(|| {
224-
// TODO(emilk): Some time controls would be nice,
225-
// but the point here is not to have a nice viewer,
226-
// but to show the user what they have selected
227-
ui.ctx().request_repaint(); // TODO(emilk): schedule a repaint just in time for the next frame of video
228-
let time = ui.input(|i| i.time);
229-
230-
if let Some(duration) = video.data_descr().duration() {
231-
VideoTimestamp::from_secs(time % duration.as_secs_f64())
232-
} else {
233-
// Invalid video or unknown timescale.
234-
VideoTimestamp::from_nanos(0)
235-
}
236-
});
237-
let video_time = re_viewer_context::video_timestamp_component_to_video_time(
238-
ctx,
239-
video_timestamp,
240-
video.data_descr().timescale,
241-
);
242-
let video_buffers = std::iter::once(blob.as_ref()).collect();
243-
244-
show_decoded_frame_info(ctx, ui, ui_layout, video, video_time, &video_buffers);
245-
}
246-
}
247-
}
248-
24993
/// Show EXIF data about the given blob (image), if possible.
25094
fn exif_ui(ui: &mut egui::Ui, key: StoredBlobCacheKey, blob: &re_types::datatypes::Blob) {
25195
let exif_result = ui.ctx().memory_mut(|mem| {
@@ -281,3 +125,167 @@ fn exif_ui(ui: &mut egui::Ui, key: StoredBlobCacheKey, blob: &re_types::datatype
281125
});
282126
}
283127
}
128+
129+
/// Utility for displaying additional UI for blobs.
130+
pub struct BlobUi {
131+
descr: ComponentDescriptor,
132+
blob: re_types::datatypes::Blob,
133+
134+
/// Additional image ui if any.
135+
image: Option<ImageUi>,
136+
137+
/// Additional video ui if the blob is a video.
138+
video: Option<VideoUi>,
139+
140+
/// The row id of the blob.
141+
row_id: Option<RowId>,
142+
143+
/// The media type of the blob if known (used to inform image and video uis).
144+
media_type: Option<MediaType>,
145+
}
146+
147+
impl BlobUi {
148+
pub fn from_components(
149+
ctx: &ViewerContext<'_>,
150+
entity_path: &re_log_types::EntityPath,
151+
blob_descr: &ComponentDescriptor,
152+
blob_chunk: &UnitChunkShared,
153+
components: &[(ComponentDescriptor, UnitChunkShared)],
154+
) -> Option<Self> {
155+
if blob_descr.component_type != Some(components::Blob::name()) {
156+
return None;
157+
}
158+
159+
let blob = blob_chunk
160+
.component_mono::<components::Blob>(blob_descr)?
161+
.ok()?;
162+
163+
// Media type comes typically alongside the blob in various different archetypes.
164+
// Look for the one that matches the blob's archetype.
165+
let media_type = find_and_deserialize_archetype_mono_component::<components::MediaType>(
166+
components,
167+
blob_descr.archetype,
168+
)
169+
.or_else(|| components::MediaType::guess_from_data(&blob));
170+
171+
// Video timestamp is only relevant here if it comes from a VideoFrameReference archetype.
172+
// It doesn't show up in the blob's archetype.
173+
let video_timestamp_descr = archetypes::VideoFrameReference::descriptor_timestamp();
174+
let video_timestamp = components
175+
.iter()
176+
.find_map(|(descr, chunk)| {
177+
(descr == &video_timestamp_descr).then(|| {
178+
chunk
179+
.component_mono::<components::VideoTimestamp>(&video_timestamp_descr)?
180+
.ok()
181+
})
182+
})
183+
.flatten();
184+
185+
Some(Self::new(
186+
ctx,
187+
entity_path,
188+
blob_descr,
189+
blob_chunk.row_id(),
190+
blob.0,
191+
media_type.as_ref(),
192+
video_timestamp,
193+
))
194+
}
195+
196+
pub fn new(
197+
ctx: &re_viewer_context::ViewerContext<'_>,
198+
entity_path: &re_log_types::EntityPath,
199+
blob_component_descriptor: &ComponentDescriptor,
200+
blob_row_id: Option<RowId>,
201+
blob: re_types::datatypes::Blob,
202+
media_type: Option<&MediaType>,
203+
video_timestamp: Option<VideoTimestamp>,
204+
) -> Self {
205+
let mut image = None;
206+
let mut video = None;
207+
208+
if let Some(blob_row_id) = blob_row_id {
209+
image = ImageUi::from_blob(
210+
ctx,
211+
blob_row_id,
212+
blob_component_descriptor,
213+
&blob,
214+
media_type,
215+
);
216+
217+
video = VideoUi::from_blob(
218+
ctx,
219+
entity_path,
220+
blob_row_id,
221+
blob_component_descriptor,
222+
&blob,
223+
media_type,
224+
video_timestamp,
225+
);
226+
}
227+
Self {
228+
image,
229+
video,
230+
row_id: blob_row_id,
231+
descr: blob_component_descriptor.clone(),
232+
blob,
233+
media_type: media_type.cloned(),
234+
}
235+
}
236+
237+
pub fn inline_download_button<'a>(
238+
&'a self,
239+
ctx: &'a ViewerContext<'_>,
240+
entity_path: &'a EntityPath,
241+
mut property_content: list_item::PropertyContent<'a>,
242+
) -> list_item::PropertyContent<'a> {
243+
if let Some(image) = &self.image {
244+
property_content = image.inline_copy_button(ctx, property_content);
245+
}
246+
property_content.with_action_button(&icons::DOWNLOAD, "Save blob…", || {
247+
let mut file_name = entity_path
248+
.last()
249+
.map_or("blob", |name| name.unescaped_str())
250+
.to_owned();
251+
252+
if let Some(file_extension) =
253+
self.media_type.as_ref().and_then(|mt| mt.file_extension())
254+
{
255+
file_name.push('.');
256+
file_name.push_str(file_extension);
257+
}
258+
259+
ctx.command_sender().save_file_dialog(
260+
re_capabilities::MainThreadToken::i_promise_i_am_on_the_main_thread(),
261+
&file_name,
262+
"Save blob".to_owned(),
263+
self.blob.to_vec(),
264+
);
265+
})
266+
}
267+
268+
pub fn data_ui(
269+
&self,
270+
ctx: &ViewerContext<'_>,
271+
ui: &mut egui::Ui,
272+
ui_layout: UiLayout,
273+
query: &re_chunk_store::LatestAtQuery,
274+
entity_path: &EntityPath,
275+
) {
276+
if let Some(row_id) = self.row_id
277+
&& !ui_layout.is_single_line()
278+
&& ui_layout != UiLayout::Tooltip
279+
{
280+
exif_ui(ui, StoredBlobCacheKey::new(row_id, &self.descr), &self.blob);
281+
}
282+
283+
if let Some(image) = &self.image {
284+
image.data_ui(ctx, ui, ui_layout, query, entity_path);
285+
}
286+
287+
if let Some(video) = &self.video {
288+
video.data_ui(ctx, ui, ui_layout, query);
289+
}
290+
}
291+
}

0 commit comments

Comments
 (0)