1use std::sync::Arc;
2use std::time::Duration;
3
4use crate::stdio::TerminalOutput;
5use anyhow::Result;
6use base64::prelude::*;
7use gpui::{
8 img, percentage, Animation, AnimationExt, AnyElement, ClipboardItem, FontWeight, Image,
9 ImageFormat, Render, RenderImage, Task, TextRun, Transformation, View,
10};
11use runtimelib::datatable::TableSchema;
12use runtimelib::media::datatable::TabularDataResource;
13use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType};
14use serde_json::Value;
15use settings::Settings;
16use theme::ThemeSettings;
17use ui::{div, prelude::*, v_flex, IntoElement, Styled, Tooltip, ViewContext};
18
19use markdown_preview::{
20 markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown,
21 markdown_renderer::render_markdown_block,
22};
23
24/// When deciding what to render from a collection of mediatypes, we need to rank them in order of importance
25fn rank_mime_type(mimetype: &MimeType) -> usize {
26 match mimetype {
27 MimeType::DataTable(_) => 6,
28 MimeType::Png(_) => 4,
29 MimeType::Jpeg(_) => 3,
30 MimeType::Markdown(_) => 2,
31 MimeType::Plain(_) => 1,
32 // All other media types are not supported in Zed at this time
33 _ => 0,
34 }
35}
36
37pub(crate) trait SupportsClipboard {
38 fn clipboard_content(&self, cx: &WindowContext) -> Option<ClipboardItem>;
39 fn has_clipboard_content(&self, cx: &WindowContext) -> bool;
40}
41
42/// ImageView renders an image inline in an editor, adapting to the line height to fit the image.
43pub struct ImageView {
44 clipboard_image: Arc<Image>,
45 height: u32,
46 width: u32,
47 image: Arc<RenderImage>,
48}
49
50impl ImageView {
51 fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
52 let line_height = cx.line_height();
53
54 let (height, width) = if self.height as f32 / line_height.0 == u8::MAX as f32 {
55 let height = u8::MAX as f32 * line_height.0;
56 let width = self.width as f32 * height / self.height as f32;
57 (height, width)
58 } else {
59 (self.height as f32, self.width as f32)
60 };
61
62 let image = self.image.clone();
63
64 div()
65 .h(Pixels(height))
66 .w(Pixels(width))
67 .child(img(image))
68 .into_any_element()
69 }
70
71 fn from(base64_encoded_data: &str) -> Result<Self> {
72 let bytes = BASE64_STANDARD.decode(base64_encoded_data)?;
73
74 let format = image::guess_format(&bytes)?;
75 let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
76
77 // Convert from RGBA to BGRA.
78 for pixel in data.chunks_exact_mut(4) {
79 pixel.swap(0, 2);
80 }
81
82 let height = data.height();
83 let width = data.width();
84
85 let gpui_image_data = RenderImage::new(vec![image::Frame::new(data)]);
86
87 let format = match format {
88 image::ImageFormat::Png => ImageFormat::Png,
89 image::ImageFormat::Jpeg => ImageFormat::Jpeg,
90 image::ImageFormat::Gif => ImageFormat::Gif,
91 image::ImageFormat::WebP => ImageFormat::Webp,
92 image::ImageFormat::Tiff => ImageFormat::Tiff,
93 image::ImageFormat::Bmp => ImageFormat::Bmp,
94 _ => {
95 return Err(anyhow::anyhow!("unsupported image format"));
96 }
97 };
98
99 // Convert back to a GPUI image for use with the clipboard
100 let clipboard_image = Arc::new(Image {
101 format,
102 bytes,
103 id: gpui_image_data.id.0 as u64,
104 });
105
106 return Ok(ImageView {
107 clipboard_image,
108 height,
109 width,
110 image: Arc::new(gpui_image_data),
111 });
112 }
113}
114
115impl SupportsClipboard for ImageView {
116 fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
117 Some(ClipboardItem::new_image(self.clipboard_image.as_ref()))
118 }
119
120 fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
121 true
122 }
123}
124
125/// TableView renders a static table inline in a buffer.
126/// It uses the https://specs.frictionlessdata.io/tabular-data-resource/ specification for data interchange.
127pub struct TableView {
128 pub table: TabularDataResource,
129 pub widths: Vec<Pixels>,
130 cached_clipboard_content: ClipboardItem,
131}
132
133fn cell_content(row: &Value, field: &str) -> String {
134 match row.get(&field) {
135 Some(Value::String(s)) => s.clone(),
136 Some(Value::Number(n)) => n.to_string(),
137 Some(Value::Bool(b)) => b.to_string(),
138 Some(Value::Array(arr)) => format!("{:?}", arr),
139 Some(Value::Object(obj)) => format!("{:?}", obj),
140 Some(Value::Null) | None => String::new(),
141 }
142}
143
144// Declare constant for the padding multiple on the line height
145const TABLE_Y_PADDING_MULTIPLE: f32 = 0.5;
146
147impl TableView {
148 pub fn new(table: TabularDataResource, cx: &mut WindowContext) -> Self {
149 let mut widths = Vec::with_capacity(table.schema.fields.len());
150
151 let text_system = cx.text_system();
152 let text_style = cx.text_style();
153 let text_font = ThemeSettings::get_global(cx).buffer_font.clone();
154 let font_size = ThemeSettings::get_global(cx).buffer_font_size;
155 let mut runs = [TextRun {
156 len: 0,
157 font: text_font,
158 color: text_style.color,
159 background_color: None,
160 underline: None,
161 strikethrough: None,
162 }];
163
164 for field in table.schema.fields.iter() {
165 runs[0].len = field.name.len();
166 let mut width = text_system
167 .layout_line(&field.name, font_size, &runs)
168 .map(|layout| layout.width)
169 .unwrap_or(px(0.));
170
171 let Some(data) = table.data.as_ref() else {
172 widths.push(width);
173 continue;
174 };
175
176 for row in data {
177 let content = cell_content(&row, &field.name);
178 runs[0].len = content.len();
179 let cell_width = cx
180 .text_system()
181 .layout_line(&content, font_size, &runs)
182 .map(|layout| layout.width)
183 .unwrap_or(px(0.));
184
185 width = width.max(cell_width)
186 }
187
188 widths.push(width)
189 }
190
191 let cached_clipboard_content = Self::create_clipboard_content(&table);
192
193 Self {
194 table,
195 widths,
196 cached_clipboard_content: ClipboardItem::new_string(cached_clipboard_content),
197 }
198 }
199
200 fn escape_markdown(s: &str) -> String {
201 s.replace('|', "\\|")
202 .replace('*', "\\*")
203 .replace('_', "\\_")
204 .replace('`', "\\`")
205 .replace('[', "\\[")
206 .replace(']', "\\]")
207 .replace('<', "<")
208 .replace('>', ">")
209 }
210
211 fn create_clipboard_content(table: &TabularDataResource) -> String {
212 let data = match table.data.as_ref() {
213 Some(data) => data,
214 None => &Vec::new(),
215 };
216 let schema = table.schema.clone();
217
218 let mut markdown = format!(
219 "| {} |\n",
220 table
221 .schema
222 .fields
223 .iter()
224 .map(|field| field.name.clone())
225 .collect::<Vec<_>>()
226 .join(" | ")
227 );
228
229 markdown.push_str("|---");
230 for _ in 1..table.schema.fields.len() {
231 markdown.push_str("|---");
232 }
233 markdown.push_str("|\n");
234
235 let body = data
236 .iter()
237 .map(|record: &Value| {
238 let row_content = schema
239 .fields
240 .iter()
241 .map(|field| Self::escape_markdown(&cell_content(record, &field.name)))
242 .collect::<Vec<_>>();
243
244 row_content.join(" | ")
245 })
246 .collect::<Vec<String>>();
247
248 for row in body {
249 markdown.push_str(&format!("| {} |\n", row));
250 }
251
252 markdown
253 }
254
255 pub fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
256 let data = match &self.table.data {
257 Some(data) => data,
258 None => return div().into_any_element(),
259 };
260
261 let mut headings = serde_json::Map::new();
262 for field in &self.table.schema.fields {
263 headings.insert(field.name.clone(), Value::String(field.name.clone()));
264 }
265 let header = self.render_row(&self.table.schema, true, &Value::Object(headings), cx);
266
267 let body = data
268 .iter()
269 .map(|row| self.render_row(&self.table.schema, false, &row, cx));
270
271 v_flex()
272 .id("table")
273 .overflow_x_scroll()
274 .w_full()
275 .child(header)
276 .children(body)
277 .into_any_element()
278 }
279
280 pub fn render_row(
281 &self,
282 schema: &TableSchema,
283 is_header: bool,
284 row: &Value,
285 cx: &ViewContext<ExecutionView>,
286 ) -> AnyElement {
287 let theme = cx.theme();
288
289 let line_height = cx.line_height();
290
291 let row_cells = schema
292 .fields
293 .iter()
294 .zip(self.widths.iter())
295 .map(|(field, width)| {
296 let container = match field.field_type {
297 runtimelib::datatable::FieldType::String => div(),
298
299 runtimelib::datatable::FieldType::Number
300 | runtimelib::datatable::FieldType::Integer
301 | runtimelib::datatable::FieldType::Date
302 | runtimelib::datatable::FieldType::Time
303 | runtimelib::datatable::FieldType::Datetime
304 | runtimelib::datatable::FieldType::Year
305 | runtimelib::datatable::FieldType::Duration
306 | runtimelib::datatable::FieldType::Yearmonth => v_flex().items_end(),
307
308 _ => div(),
309 };
310
311 let value = cell_content(row, &field.name);
312
313 let mut cell = container
314 .min_w(*width + px(22.))
315 .w(*width + px(22.))
316 .child(value)
317 .px_2()
318 .py((TABLE_Y_PADDING_MULTIPLE / 2.0) * line_height)
319 .border_color(theme.colors().border);
320
321 if is_header {
322 cell = cell.border_1().bg(theme.colors().border_focused)
323 } else {
324 cell = cell.border_1()
325 }
326 cell
327 })
328 .collect::<Vec<_>>();
329
330 let mut total_width = px(0.);
331 for width in self.widths.iter() {
332 // Width fudge factor: border + 2 (heading), padding
333 total_width += *width + px(22.);
334 }
335
336 h_flex()
337 .w(total_width)
338 .children(row_cells)
339 .into_any_element()
340 }
341}
342
343impl SupportsClipboard for TableView {
344 fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
345 Some(self.cached_clipboard_content.clone())
346 }
347
348 fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
349 true
350 }
351}
352
353/// Userspace error from the kernel
354pub struct ErrorView {
355 pub ename: String,
356 pub evalue: String,
357 pub traceback: TerminalOutput,
358}
359
360impl ErrorView {
361 fn render(&self, cx: &mut ViewContext<ExecutionView>) -> Option<AnyElement> {
362 let theme = cx.theme();
363
364 let padding = cx.line_height() / 2.;
365
366 Some(
367 v_flex()
368 .gap_3()
369 .child(
370 h_flex()
371 .font_buffer(cx)
372 .child(
373 Label::new(format!("{}: ", self.ename.clone()))
374 // .size(LabelSize::Large)
375 .color(Color::Error)
376 .weight(FontWeight::BOLD),
377 )
378 .child(
379 Label::new(self.evalue.clone())
380 // .size(LabelSize::Large)
381 .weight(FontWeight::BOLD),
382 ),
383 )
384 .child(
385 div()
386 .w_full()
387 .px(padding)
388 .py(padding)
389 .border_l_1()
390 .border_color(theme.status().error_border)
391 .child(self.traceback.render(cx)),
392 )
393 .into_any_element(),
394 )
395 }
396}
397
398pub struct MarkdownView {
399 raw_text: String,
400 contents: Option<ParsedMarkdown>,
401 parsing_markdown_task: Option<Task<Result<()>>>,
402}
403
404impl MarkdownView {
405 pub fn from(text: String, cx: &mut ViewContext<Self>) -> Self {
406 let task = cx.spawn(|markdown_view, mut cx| {
407 let text = text.clone();
408 let parsed = cx
409 .background_executor()
410 .spawn(async move { parse_markdown(&text, None, None).await });
411
412 async move {
413 let content = parsed.await;
414
415 markdown_view.update(&mut cx, |markdown, cx| {
416 markdown.parsing_markdown_task.take();
417 markdown.contents = Some(content);
418 cx.notify();
419 })
420 }
421 });
422
423 Self {
424 raw_text: text.clone(),
425 contents: None,
426 parsing_markdown_task: Some(task),
427 }
428 }
429}
430
431impl SupportsClipboard for MarkdownView {
432 fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
433 Some(ClipboardItem::new_string(self.raw_text.clone()))
434 }
435
436 fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
437 true
438 }
439}
440
441impl Render for MarkdownView {
442 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
443 let Some(parsed) = self.contents.as_ref() else {
444 return div().into_any_element();
445 };
446
447 let mut markdown_render_context =
448 markdown_preview::markdown_renderer::RenderContext::new(None, cx);
449
450 v_flex()
451 .gap_3()
452 .py_4()
453 .children(parsed.children.iter().map(|child| {
454 div().relative().child(
455 div()
456 .relative()
457 .child(render_markdown_block(child, &mut markdown_render_context)),
458 )
459 }))
460 .into_any_element()
461 }
462}
463
464pub struct Output {
465 content: OutputContent,
466 display_id: Option<String>,
467}
468
469impl Output {
470 pub fn new(data: &MimeBundle, display_id: Option<String>, cx: &mut WindowContext) -> Self {
471 Self {
472 content: OutputContent::new(data, cx),
473 display_id,
474 }
475 }
476
477 pub fn from(content: OutputContent) -> Self {
478 Self {
479 content,
480 display_id: None,
481 }
482 }
483}
484
485impl SupportsClipboard for Output {
486 fn clipboard_content(&self, cx: &WindowContext) -> Option<ClipboardItem> {
487 match &self.content {
488 OutputContent::Plain(terminal) => terminal.clipboard_content(cx),
489 OutputContent::Stream(terminal) => terminal.clipboard_content(cx),
490 OutputContent::Image(image) => image.clipboard_content(cx),
491 OutputContent::ErrorOutput(error) => error.traceback.clipboard_content(cx),
492 OutputContent::Message(_) => None,
493 OutputContent::Table(table) => table.clipboard_content(cx),
494 OutputContent::Markdown(markdown) => markdown.read(cx).clipboard_content(cx),
495 OutputContent::ClearOutputWaitMarker => None,
496 }
497 }
498
499 fn has_clipboard_content(&self, cx: &WindowContext) -> bool {
500 match &self.content {
501 OutputContent::Plain(terminal) => terminal.has_clipboard_content(cx),
502 OutputContent::Stream(terminal) => terminal.has_clipboard_content(cx),
503 OutputContent::Image(image) => image.has_clipboard_content(cx),
504 OutputContent::ErrorOutput(error) => error.traceback.has_clipboard_content(cx),
505 OutputContent::Message(_) => false,
506 OutputContent::Table(table) => table.has_clipboard_content(cx),
507 OutputContent::Markdown(markdown) => markdown.read(cx).has_clipboard_content(cx),
508 OutputContent::ClearOutputWaitMarker => false,
509 }
510 }
511}
512
513pub enum OutputContent {
514 Plain(TerminalOutput),
515 Stream(TerminalOutput),
516 Image(ImageView),
517 ErrorOutput(ErrorView),
518 Message(String),
519 Table(TableView),
520 Markdown(View<MarkdownView>),
521 ClearOutputWaitMarker,
522}
523
524impl OutputContent {
525 fn render(&self, cx: &mut ViewContext<ExecutionView>) -> Option<AnyElement> {
526 let el = match self {
527 // Note: in typical frontends we would show the execute_result.execution_count
528 // Here we can just handle either
529 Self::Plain(stdio) => Some(stdio.render(cx)),
530 Self::Markdown(markdown) => Some(markdown.clone().into_any_element()),
531 Self::Stream(stdio) => Some(stdio.render(cx)),
532 Self::Image(image) => Some(image.render(cx)),
533 Self::Message(message) => Some(div().child(message.clone()).into_any_element()),
534 Self::Table(table) => Some(table.render(cx)),
535 Self::ErrorOutput(error_view) => error_view.render(cx),
536 Self::ClearOutputWaitMarker => None,
537 };
538
539 el
540 }
541
542 pub fn new(data: &MimeBundle, cx: &mut WindowContext) -> Self {
543 match data.richest(rank_mime_type) {
544 Some(MimeType::Plain(text)) => OutputContent::Plain(TerminalOutput::from(text, cx)),
545 Some(MimeType::Markdown(text)) => {
546 let view = cx.new_view(|cx| MarkdownView::from(text.clone(), cx));
547 OutputContent::Markdown(view)
548 }
549 Some(MimeType::Png(data)) | Some(MimeType::Jpeg(data)) => match ImageView::from(data) {
550 Ok(view) => OutputContent::Image(view),
551 Err(error) => OutputContent::Message(format!("Failed to load image: {}", error)),
552 },
553 Some(MimeType::DataTable(data)) => {
554 OutputContent::Table(TableView::new(data.clone(), cx))
555 }
556 // Any other media types are not supported
557 _ => OutputContent::Message("Unsupported media type".to_string()),
558 }
559 }
560}
561
562#[derive(Default, Clone, Debug)]
563pub enum ExecutionStatus {
564 #[default]
565 Unknown,
566 ConnectingToKernel,
567 Queued,
568 Executing,
569 Finished,
570 ShuttingDown,
571 Shutdown,
572 KernelErrored(String),
573 Restarting,
574}
575
576pub struct ExecutionView {
577 pub outputs: Vec<Output>,
578 pub status: ExecutionStatus,
579}
580
581impl ExecutionView {
582 pub fn new(status: ExecutionStatus, _cx: &mut ViewContext<Self>) -> Self {
583 Self {
584 outputs: Default::default(),
585 status,
586 }
587 }
588
589 /// Accept a Jupyter message belonging to this execution
590 pub fn push_message(&mut self, message: &JupyterMessageContent, cx: &mut ViewContext<Self>) {
591 let output: Output = match message {
592 JupyterMessageContent::ExecuteResult(result) => Output::new(
593 &result.data,
594 result.transient.as_ref().and_then(|t| t.display_id.clone()),
595 cx,
596 ),
597 JupyterMessageContent::DisplayData(result) => {
598 Output::new(&result.data, result.transient.display_id.clone(), cx)
599 }
600 JupyterMessageContent::StreamContent(result) => {
601 // Previous stream data will combine together, handling colors, carriage returns, etc
602 if let Some(new_terminal) = self.apply_terminal_text(&result.text, cx) {
603 Output::from(new_terminal)
604 } else {
605 return;
606 }
607 }
608 JupyterMessageContent::ErrorOutput(result) => {
609 let mut terminal = TerminalOutput::new(cx);
610 terminal.append_text(&result.traceback.join("\n"));
611
612 Output::from(OutputContent::ErrorOutput(ErrorView {
613 ename: result.ename.clone(),
614 evalue: result.evalue.clone(),
615 traceback: terminal,
616 }))
617 }
618 JupyterMessageContent::ExecuteReply(reply) => {
619 for payload in reply.payload.iter() {
620 match payload {
621 // Pager data comes in via `?` at the end of a statement in Python, used for showing documentation.
622 // Some UI will show this as a popup. For ease of implementation, it's included as an output here.
623 runtimelib::Payload::Page { data, .. } => {
624 let output = Output::new(data, None, cx);
625 self.outputs.push(output);
626 }
627
628 // There are other payloads that could be handled here, such as updating the input.
629 // Below are the other payloads that _could_ be handled, but are not required for Zed.
630
631 // Set next input adds text to the next cell. Not required to support.
632 // However, this could be implemented by adding text to the buffer.
633 // Trigger in python using `get_ipython().set_next_input("text")`
634 //
635 // runtimelib::Payload::SetNextInput { text, replace } => {},
636
637 // Not likely to be used in the context of Zed, where someone could just open the buffer themselves
638 // Python users can trigger this with the `%edit` magic command
639 // runtimelib::Payload::EditMagic { filename, line_number } => {},
640
641 // Ask the user if they want to exit the kernel. Not required to support.
642 // runtimelib::Payload::AskExit { keepkernel } => {},
643 _ => {}
644 }
645 }
646 cx.notify();
647 return;
648 }
649 JupyterMessageContent::ClearOutput(options) => {
650 if !options.wait {
651 self.outputs.clear();
652 cx.notify();
653 return;
654 }
655
656 // Create a marker to clear the output after we get in a new output
657 Output::from(OutputContent::ClearOutputWaitMarker)
658 }
659 JupyterMessageContent::Status(status) => {
660 match status.execution_state {
661 ExecutionState::Busy => {
662 self.status = ExecutionStatus::Executing;
663 }
664 ExecutionState::Idle => self.status = ExecutionStatus::Finished,
665 }
666 cx.notify();
667 return;
668 }
669 _msg => {
670 return;
671 }
672 };
673
674 // Check for a clear output marker as the previous output, so we can clear it out
675 if let Some(output) = self.outputs.last() {
676 if let OutputContent::ClearOutputWaitMarker = output.content {
677 self.outputs.clear();
678 }
679 }
680
681 self.outputs.push(output);
682
683 cx.notify();
684 }
685
686 pub fn update_display_data(
687 &mut self,
688 data: &MimeBundle,
689 display_id: &str,
690 cx: &mut ViewContext<Self>,
691 ) {
692 let mut any = false;
693
694 self.outputs.iter_mut().for_each(|output| {
695 if let Some(other_display_id) = output.display_id.as_ref() {
696 if other_display_id == display_id {
697 output.content = OutputContent::new(data, cx);
698 any = true;
699 }
700 }
701 });
702
703 if any {
704 cx.notify();
705 }
706 }
707
708 fn apply_terminal_text(
709 &mut self,
710 text: &str,
711 cx: &mut ViewContext<Self>,
712 ) -> Option<OutputContent> {
713 if let Some(last_output) = self.outputs.last_mut() {
714 match &mut last_output.content {
715 OutputContent::Stream(last_stream) => {
716 last_stream.append_text(text);
717 // Don't need to add a new output, we already have a terminal output
718 cx.notify();
719 return None;
720 }
721 // Edge case note: a clear output marker
722 OutputContent::ClearOutputWaitMarker => {
723 // Edge case note: a clear output marker is handled by the caller
724 // since we will return a new output at the end here as a new terminal output
725 }
726 // A different output type is "in the way", so we need to create a new output,
727 // which is the same as having no prior output
728 _ => {}
729 }
730 }
731
732 let mut new_terminal = TerminalOutput::new(cx);
733 new_terminal.append_text(text);
734 Some(OutputContent::Stream(new_terminal))
735 }
736}
737
738impl Render for ExecutionView {
739 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
740 let status = match &self.status {
741 ExecutionStatus::ConnectingToKernel => Label::new("Connecting to kernel...")
742 .color(Color::Muted)
743 .into_any_element(),
744 ExecutionStatus::Executing => h_flex()
745 .gap_2()
746 .child(
747 Icon::new(IconName::ArrowCircle)
748 .size(IconSize::Small)
749 .color(Color::Muted)
750 .with_animation(
751 "arrow-circle",
752 Animation::new(Duration::from_secs(3)).repeat(),
753 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
754 ),
755 )
756 .child(Label::new("Executing...").color(Color::Muted))
757 .into_any_element(),
758 ExecutionStatus::Finished => Icon::new(IconName::Check)
759 .size(IconSize::Small)
760 .into_any_element(),
761 ExecutionStatus::Unknown => Label::new("Unknown status")
762 .color(Color::Muted)
763 .into_any_element(),
764 ExecutionStatus::ShuttingDown => Label::new("Kernel shutting down...")
765 .color(Color::Muted)
766 .into_any_element(),
767 ExecutionStatus::Restarting => Label::new("Kernel restarting...")
768 .color(Color::Muted)
769 .into_any_element(),
770 ExecutionStatus::Shutdown => Label::new("Kernel shutdown")
771 .color(Color::Muted)
772 .into_any_element(),
773 ExecutionStatus::Queued => Label::new("Queued...")
774 .color(Color::Muted)
775 .into_any_element(),
776 ExecutionStatus::KernelErrored(error) => Label::new(format!("Kernel error: {}", error))
777 .color(Color::Error)
778 .into_any_element(),
779 };
780
781 if self.outputs.len() == 0 {
782 return v_flex()
783 .min_h(cx.line_height())
784 .justify_center()
785 .child(status)
786 .into_any_element();
787 }
788
789 div()
790 .w_full()
791 .children(self.outputs.iter().enumerate().map(|(index, output)| {
792 h_flex()
793 .w_full()
794 .items_start()
795 .child(
796 div().flex_1().child(
797 output
798 .content
799 .render(cx)
800 .unwrap_or_else(|| div().into_any_element()),
801 ),
802 )
803 .when(output.has_clipboard_content(cx), |el| {
804 let clipboard_content = output.clipboard_content(cx);
805
806 el.child(
807 div().pl_1().child(
808 IconButton::new(
809 ElementId::Name(format!("copy-output-{}", index).into()),
810 IconName::Copy,
811 )
812 .style(ButtonStyle::Transparent)
813 .tooltip(move |cx| Tooltip::text("Copy Output", cx))
814 .on_click(cx.listener(
815 move |_, _, cx| {
816 if let Some(clipboard_content) = clipboard_content.as_ref()
817 {
818 cx.write_to_clipboard(clipboard_content.clone());
819 // todo!(): let the user know that the content was copied
820 }
821 },
822 )),
823 ),
824 )
825 })
826 }))
827 .children(match self.status {
828 ExecutionStatus::Executing => vec![status],
829 ExecutionStatus::Queued => vec![status],
830 _ => vec![],
831 })
832 .into_any_element()
833 }
834}