Measure maximum width of each cell to render table (#14026)

This commit is contained in:
Kyle Kelley 2024-07-09 14:19:10 -07:00 committed by GitHub
parent c4bca874b6
commit 4bb8a0845f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 97 additions and 33 deletions

View File

@ -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<Pixels>,
}
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<ExecutionView>) -> 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::<Vec<_>>();
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<Self>) {
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);
}

View File

@ -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()