From 4bb8a0845f9ffb8146a253dfbc16efae05d22000 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Tue, 9 Jul 2024 14:19:10 -0700 Subject: [PATCH] Measure maximum width of each cell to render table (#14026) --- crates/repl/src/outputs.rs | 128 +++++++++++++++++++++++++++---------- crates/repl/src/session.rs | 2 + 2 files changed, 97 insertions(+), 33 deletions(-) diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index 60b43f189f5405bb236533381f4f2da15160dfa4..339cd0939a2e65f78e522fe7ef1015ad7c384b23 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -2,11 +2,13 @@ use std::sync::Arc; use crate::stdio::TerminalOutput; use anyhow::Result; -use gpui::{img, AnyElement, FontWeight, ImageData, Render, View}; +use gpui::{img, AnyElement, FontWeight, ImageData, Render, TextRun, View}; use runtimelib::datatable::TableSchema; use runtimelib::media::datatable::TabularDataResource; use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType}; use serde_json::Value; +use settings::Settings; +use theme::ThemeSettings; use ui::{div, prelude::*, v_flex, IntoElement, Styled, ViewContext}; // Given these outputs are destined for the editor with the block decorations API, all of them must report @@ -97,9 +99,67 @@ impl LineHeight for ImageView { /// It uses the https://specs.frictionlessdata.io/tabular-data-resource/ specification for data interchange. pub struct TableView { pub table: TabularDataResource, + pub widths: Vec, +} + +fn cell_content(row: &Value, field: &str) -> String { + match row.get(&field) { + Some(Value::String(s)) => s.clone(), + Some(Value::Number(n)) => n.to_string(), + Some(Value::Bool(b)) => b.to_string(), + Some(Value::Array(arr)) => format!("{:?}", arr), + Some(Value::Object(obj)) => format!("{:?}", obj), + Some(Value::Null) | None => String::new(), + } } impl TableView { + pub fn new(table: TabularDataResource, cx: &mut WindowContext) -> Self { + let mut widths = Vec::with_capacity(table.schema.fields.len()); + + let text_system = cx.text_system(); + let text_style = cx.text_style(); + let text_font = ThemeSettings::get_global(cx).buffer_font.clone(); + let font_size = ThemeSettings::get_global(cx).buffer_font_size; + let mut runs = [TextRun { + len: 0, + font: text_font, + color: text_style.color, + background_color: None, + underline: None, + strikethrough: None, + }]; + + for field in table.schema.fields.iter() { + runs[0].len = field.name.len(); + let mut width = text_system + .layout_line(&field.name, font_size, &runs) + .map(|layout| layout.width) + .unwrap_or(px(0.)); + + let Some(data) = table.data.as_ref() else { + widths.push(width); + continue; + }; + + for row in data { + let content = cell_content(&row, &field.name); + runs[0].len = content.len(); + let cell_width = cx + .text_system() + .layout_line(&content, font_size, &runs) + .map(|layout| layout.width) + .unwrap_or(px(0.)); + + width = width.max(cell_width) + } + + widths.push(width) + } + + Self { table, widths } + } + pub fn render(&self, cx: &ViewContext) -> AnyElement { let data = match &self.table.data { Some(data) => data, @@ -119,6 +179,8 @@ impl TableView { .map(|row| self.render_row(&self.table.schema, false, &row, cx)); v_flex() + .id("table") + .overflow_x_scroll() .w_full() .child(header) .children(body) @@ -137,7 +199,8 @@ impl TableView { let row_cells = schema .fields .iter() - .map(|field| { + .zip(self.widths.iter()) + .map(|(field, width)| { let container = match field.field_type { runtimelib::datatable::FieldType::String => div(), @@ -153,17 +216,11 @@ impl TableView { _ => div(), }; - let value = match row.get(&field.name) { - Some(Value::String(s)) => s.clone(), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => b.to_string(), - Some(Value::Array(arr)) => format!("{:?}", arr), - Some(Value::Object(obj)) => format!("{:?}", obj), - Some(Value::Null) | None => String::new(), - }; + let value = cell_content(row, &field.name); let mut cell = container - .w_full() + .min_w(*width + px(22.)) + .w(*width + px(22.)) .child(value) .px_2() .py_1() @@ -178,7 +235,16 @@ impl TableView { }) .collect::>(); - h_flex().children(row_cells).into_any_element() + let mut total_width = px(0.); + for width in self.widths.iter() { + // Width fudge factor: border + 2 (heading), padding + total_width += *width + px(22.); + } + + h_flex() + .w(total_width) + .children(row_cells) + .into_any_element() } } @@ -266,6 +332,20 @@ impl OutputType { el } + + pub fn new(data: &MimeBundle, cx: &mut WindowContext) -> Self { + match data.richest(rank_mime_type) { + Some(MimeType::Plain(text)) => OutputType::Plain(TerminalOutput::from(text)), + Some(MimeType::Markdown(text)) => OutputType::Plain(TerminalOutput::from(text)), + Some(MimeType::Png(data)) | Some(MimeType::Jpeg(data)) => match ImageView::from(data) { + Ok(view) => OutputType::Image(view), + Err(error) => OutputType::Message(format!("Failed to load image: {}", error)), + }, + Some(MimeType::DataTable(data)) => OutputType::Table(TableView::new(data.clone(), cx)), + // Any other media types are not supported + _ => OutputType::Message("Unsupported media type".to_string()), + } + } } impl LineHeight for OutputType { @@ -283,24 +363,6 @@ impl LineHeight for OutputType { } } -impl From<&MimeBundle> for OutputType { - fn from(data: &MimeBundle) -> Self { - match data.richest(rank_mime_type) { - Some(MimeType::Plain(text)) => OutputType::Plain(TerminalOutput::from(text)), - Some(MimeType::Markdown(text)) => OutputType::Plain(TerminalOutput::from(text)), - Some(MimeType::Png(data)) | Some(MimeType::Jpeg(data)) => match ImageView::from(data) { - Ok(view) => OutputType::Image(view), - Err(error) => OutputType::Message(format!("Failed to load image: {}", error)), - }, - Some(MimeType::DataTable(data)) => OutputType::Table(TableView { - table: data.clone(), - }), - // Any other media types are not supported - _ => OutputType::Message("Unsupported media type".to_string()), - } - } -} - #[derive(Default, Clone)] pub enum ExecutionStatus { #[default] @@ -330,8 +392,8 @@ impl ExecutionView { /// Accept a Jupyter message belonging to this execution pub fn push_message(&mut self, message: &JupyterMessageContent, cx: &mut ViewContext) { let output: OutputType = match message { - JupyterMessageContent::ExecuteResult(result) => (&result.data).into(), - JupyterMessageContent::DisplayData(result) => (&result.data).into(), + JupyterMessageContent::ExecuteResult(result) => OutputType::new(&result.data, cx), + JupyterMessageContent::DisplayData(result) => OutputType::new(&result.data, cx), JupyterMessageContent::StreamContent(result) => { // Previous stream data will combine together, handling colors, carriage returns, etc if let Some(new_terminal) = self.apply_terminal_text(&result.text) { @@ -357,7 +419,7 @@ impl ExecutionView { // Pager data comes in via `?` at the end of a statement in Python, used for showing documentation. // Some UI will show this as a popup. For ease of implementation, it's included as an output here. runtimelib::Payload::Page { data, .. } => { - let output: OutputType = (data).into(); + let output = OutputType::new(data, cx); self.outputs.push(output); } diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index 6ffeea973e47e90c7d8da996a6c2a3d22067885f..bab69f65840a641a899108a913938998f2bb2c03 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -89,6 +89,7 @@ impl EditorBlock { let render = move |cx: &mut BlockContext| { let execution_view = execution_view.clone(); let text_font = ThemeSettings::get_global(cx).buffer_font.family.clone(); + let text_font_size = ThemeSettings::get_global(cx).buffer_font_size; // Note: we'll want to use `cx.anchor_x` when someone runs something with no output -- just show a checkmark and not make the full block below the line let gutter_width = cx.gutter_dimensions.width; @@ -101,6 +102,7 @@ impl EditorBlock { .pl(gutter_width) .child( div() + .text_size(text_font_size) .font_family(text_font) // .ml(gutter_width) .mx_1()