use super::{
events,
identity::NodeIdentity,
render_pass::RenderPass,
render_text, roving,
style::{apply_div_style, apply_semantic_focus_visible_affordance},
};
use crate::{
bridge_view::BridgeView,
ir::{
DataTableCell, DataTableColumn, DataTableColumnWidth, DataTableNode, DataTableRow,
DivStyle, IrNode,
},
};
use gpui::{
AnyElement, AppContext, Context, Empty, FocusHandle, InteractiveElement, IntoElement,
KeyDownEvent, MouseButton, ParentElement, SharedString, StatefulInteractiveElement, Styled,
Window, div, list, px,
};
use std::{cell::Cell, collections::HashMap, rc::Rc, sync::Arc};
const ROW_CLICK_EVENT: i32 = 1;
const CELL_CLICK_EVENT: i32 = 2;
const SORT_EVENT: i32 = 3;
const COLUMN_RESIZE_KEYBOARD_STEP: i32 = 16;
#[derive(Clone, Default)]
struct DataTableFocusHandles {
rows: HashMap<String, FocusHandle>,
cells: HashMap<(String, String), FocusHandle>,
}
#[derive(Clone)]
struct DataTableHeaderDragState {
last_offset_x: Rc<Cell<f32>>,
reorder_emitted: Rc<Cell<bool>>,
}
pub(crate) fn render(
pass: &mut RenderPass<'_>,
path: &str,
table: &DataTableNode,
window: &mut Window,
cx: &mut Context<BridgeView>,
) -> AnyElement {
let view_id = pass.view_id();
let node_id = NodeIdentity::new(view_id, path, table.id.as_deref());
let table_id = node_id.to_string();
let list_id = format!("{table_id}.rows");
let state = pass.retain_list_state(&list_id, table.rows.len());
let columns = order_pinned_columns(table.columns.as_ref());
let rows = prepare_rows(columns.as_ref(), table.rows.as_ref());
let header_focus_handles = prepare_header_focus_handles(
pass,
&table_id,
columns.as_ref(),
table.sort_callback.is_some(),
table.column_reorder.is_some(),
table.column_resize.is_some(),
cx,
);
let body_focus_handles =
prepare_body_focus_handles(pass, &table_id, table, columns.as_ref(), cx);
let focus_visible = pass.focus_visible();
let row_style = table.row_style.clone();
let cell_style = table.cell_style.clone();
let row_click = table.row_click.clone();
let cell_click = table.cell_click.clone();
let row_context_menu = table.row_context_menu.clone();
let cell_context_menu = table.cell_context_menu.clone();
let table_id_for_rows = table_id.clone();
let body_focus_handles_for_rows = body_focus_handles.clone();
let header_focus_handles_for_rows = header_focus_handles.clone();
let columns_for_rows = columns.clone();
let first_row_id = rows.first().map(|row| row.id.clone());
let body = list(state, move |index, window, _cx| {
rows.get(index)
.map(|row| {
let previous_row = index
.checked_sub(1)
.and_then(|previous_index| rows.get(previous_index));
let next_row = rows.get(index + 1);
let row_focus_neighbors = roving::NavigationTargets {
up: previous_row
.and_then(|row| body_focus_handles_for_rows.rows.get(&row.id))
.cloned(),
down: next_row
.and_then(|row| body_focus_handles_for_rows.rows.get(&row.id))
.cloned(),
home: rows
.first()
.and_then(|row| body_focus_handles_for_rows.rows.get(&row.id))
.cloned(),
end: rows
.last()
.and_then(|row| body_focus_handles_for_rows.rows.get(&row.id))
.cloned(),
// Right enters the row's first cell.
right: columns_for_rows.first().and_then(|column| {
body_focus_handles_for_rows
.cells
.get(&(row.id.clone(), column.id.clone()))
.cloned()
}),
..Default::default()
};
render_row(
view_id,
&table_id_for_rows,
row,
previous_row.map(|row| row.id.as_str()),
next_row.map(|row| row.id.as_str()),
&columns_for_rows,
&row_style,
&cell_style,
row_click.as_deref(),
cell_click.as_deref(),
row_context_menu.as_deref(),
cell_context_menu.as_deref(),
&body_focus_handles_for_rows,
&header_focus_handles_for_rows,
row_focus_neighbors,
focus_visible,
window,
)
})
.unwrap_or_else(|| div().into_any_element())
})
.size_full();
let header = render_header(
view_id,
&table_id,
columns.as_ref(),
&table.header_style,
table.sort_callback.as_deref(),
table.column_reorder.as_deref(),
table.column_resize.as_deref(),
&header_focus_handles,
&body_focus_handles,
first_row_id.as_deref(),
focus_visible,
window,
);
apply_div_style(
div()
.id(node_id.to_shared_string())
.flex()
.flex_col()
.size_full()
.child(header)
.child(body),
&table.style,
)
.into_any_element()
}
fn prepare_header_focus_handles(
pass: &mut RenderPass<'_>,
table_id: &str,
columns: &[DataTableColumn],
sort_enabled: bool,
reorder_enabled: bool,
resize_enabled: bool,
cx: &mut Context<BridgeView>,
) -> HashMap<String, FocusHandle> {
if !sort_enabled && !reorder_enabled && !resize_enabled {
return HashMap::new();
}
columns
.iter()
.filter(|column| (sort_enabled && column.sortable) || reorder_enabled || resize_enabled)
.map(|column| {
let header_id = format!("{table_id}.header.{}", column.id);
let focus_handle = pass.ensure_focus_handle(&header_id, cx, Some(true), None);
(column.id.clone(), focus_handle)
})
.collect()
}
fn prepare_body_focus_handles(
pass: &mut RenderPass<'_>,
table_id: &str,
table: &DataTableNode,
columns: &[DataTableColumn],
cx: &mut Context<BridgeView>,
) -> DataTableFocusHandles {
let mut handles = DataTableFocusHandles::default();
if table.row_click.is_some() || table.row_context_menu.is_some() {
for row in table.rows.iter() {
let row_id = data_table_row_id(table_id, &row.id);
handles.rows.insert(
row.id.clone(),
pass.ensure_focus_handle(&row_id, cx, Some(true), None),
);
}
}
if table.cell_click.is_some() || table.cell_context_menu.is_some() {
for row in table.rows.iter() {
for column in columns.iter() {
let cell_id = data_table_cell_id(table_id, &row.id, &column.id);
handles.cells.insert(
(row.id.clone(), column.id.clone()),
pass.ensure_focus_handle(&cell_id, cx, Some(true), None),
);
}
}
}
handles
}
fn header_focus_neighbors(
columns: &[DataTableColumn],
focus_handles: &HashMap<String, FocusHandle>,
column_index: usize,
) -> (Option<FocusHandle>, Option<FocusHandle>) {
let previous = columns
.get(..column_index)
.into_iter()
.flatten()
.rev()
.find(|column| focus_handles.contains_key(&column.id))
.and_then(|column| focus_handles.get(&column.id))
.cloned();
let next = columns
.get(column_index + 1..)
.into_iter()
.flatten()
.find(|column| focus_handles.contains_key(&column.id))
.and_then(|column| focus_handles.get(&column.id))
.cloned();
(previous, next)
}
#[allow(clippy::too_many_arguments)]
fn render_header(
view_id: u64,
table_id: &str,
columns: &[DataTableColumn],
header_style: &DivStyle,
sort_callback: Option<&str>,
column_reorder: Option<&str>,
column_resize: Option<&str>,
focus_handles: &HashMap<String, FocusHandle>,
body_focus_handles: &DataTableFocusHandles,
first_row_id: Option<&str>,
focus_visible: bool,
window: &Window,
) -> AnyElement {
let children = columns.iter().enumerate().map(|(column_index, column)| {
let header_id = format!("{table_id}.header.{}", column.id);
let debug_header_id = header_id.clone();
let mut cell = apply_column_width(
apply_div_style(
div()
.id(SharedString::from(header_id.clone()))
.debug_selector(move || debug_header_id)
.p_2()
.child(column.label.clone()),
&column.style,
),
&column.width,
);
let show_focus_visible = focus_visible
&& focus_handles
.get(&column.id)
.is_some_and(|handle| handle.is_focused(window));
if let Some(handle) = focus_handles.get(&column.id) {
let focus_handle = handle.clone();
cell = cell
.track_focus(&focus_handle)
.focusable()
.on_any_mouse_down(move |_, window, _| {
focus_handle.focus(window);
});
}
if column.sortable
&& let Some(callback_id) = sort_callback
{
let click_callback_id = callback_id.to_owned();
let click_table_id = table_id.to_owned();
let click_column_id = column.id.clone();
let click_header_id = header_id.clone();
cell = cell.on_click(move |_, _, _| {
events::emit_data_table_event(
view_id,
SORT_EVENT,
&click_header_id,
&click_callback_id,
&click_table_id,
None,
Some(&click_column_id),
);
});
}
if (column.sortable && sort_callback.is_some())
|| column_reorder.is_some()
|| column_resize.is_some()
{
let column_sortable = column.sortable;
let sort_callback_id = sort_callback.map(str::to_owned);
let reorder_callback_id = column_reorder.map(str::to_owned);
let resize_callback_id = column_resize.map(str::to_owned);
let key_table_id = table_id.to_owned();
let key_column_id = column.id.clone();
let key_header_id = header_id.clone();
let previous_column_id = column_index
.checked_sub(1)
.and_then(|index| columns.get(index))
.map(|column| column.id.clone());
let next_column_id = columns
.get(column_index + 1)
.map(|column| column.id.clone());
let (left_focus, right_focus) =
header_focus_neighbors(columns, focus_handles, column_index);
let targets = roving::NavigationTargets {
left: left_focus,
right: right_focus,
// Down enters the first body row's cell in this column.
down: first_row_id
.and_then(|row_id| {
body_focus_handles
.cells
.get(&(row_id.to_owned(), column.id.clone()))
})
.cloned(),
home: columns
.iter()
.find(|column| focus_handles.contains_key(&column.id))
.and_then(|column| focus_handles.get(&column.id))
.cloned(),
end: columns
.iter()
.rev()
.find(|column| focus_handles.contains_key(&column.id))
.and_then(|column| focus_handles.get(&column.id))
.cloned(),
..Default::default()
};
cell = cell.on_key_down(move |event: &KeyDownEvent, window, cx| {
if event.keystroke.modifiers.shift
&& let Some(callback_id) = resize_callback_id.as_deref()
{
match event.keystroke.key.as_str() {
"left" => {
events::emit_data_table_column_resize(
view_id,
&key_header_id,
callback_id,
&key_table_id,
&key_column_id,
-COLUMN_RESIZE_KEYBOARD_STEP,
);
cx.stop_propagation();
return;
}
"right" => {
events::emit_data_table_column_resize(
view_id,
&key_header_id,
callback_id,
&key_table_id,
&key_column_id,
COLUMN_RESIZE_KEYBOARD_STEP,
);
cx.stop_propagation();
return;
}
_ => {}
}
}
if event.keystroke.modifiers.alt
&& let Some(callback_id) = reorder_callback_id.as_deref()
{
match event.keystroke.key.as_str() {
"left" => {
if let Some(target_column_id) = previous_column_id.as_deref() {
events::emit_data_table_column_reorder(
view_id,
&key_header_id,
callback_id,
&key_table_id,
&key_column_id,
target_column_id,
"left",
);
cx.stop_propagation();
return;
}
}
"right" => {
if let Some(target_column_id) = next_column_id.as_deref() {
events::emit_data_table_column_reorder(
view_id,
&key_header_id,
callback_id,
&key_table_id,
&key_column_id,
target_column_id,
"right",
);
cx.stop_propagation();
return;
}
}
_ => {}
}
}
if targets.handle(event, window, cx) {
return;
}
if column_sortable
&& let Some(callback_id) = sort_callback_id.as_deref()
&& is_header_activation_key(event)
{
events::emit_data_table_event(
view_id,
SORT_EVENT,
&key_header_id,
callback_id,
&key_table_id,
None,
Some(&key_column_id),
);
cx.stop_propagation();
}
});
}
if column_resize.is_some() || column_reorder.is_some() {
let resize_callback_id = column_resize.map(str::to_owned);
let reorder_callback_id = column_reorder.map(str::to_owned);
let drag_table_id = table_id.to_owned();
let drag_column_id = column.id.clone();
let drag_header_id = header_id.clone();
let previous_column_id = column_index
.checked_sub(1)
.and_then(|index| columns.get(index))
.map(|column| column.id.clone());
let next_column_id = columns
.get(column_index + 1)
.map(|column| column.id.clone());
let drag_state = DataTableHeaderDragState {
last_offset_x: Rc::new(Cell::new(0.0)),
reorder_emitted: Rc::new(Cell::new(false)),
};
cell = cell
.on_drag_move::<DataTableHeaderDragState>(move |event, _, cx| {
let drag = event.drag(cx);
let offset_x = f32::from(event.event.position.x - event.bounds.origin.x);
let delta = (offset_x - drag.last_offset_x.get()).round() as i32;
drag.last_offset_x.set(offset_x);
if event.event.modifiers.alt
&& delta != 0
&& !drag.reorder_emitted.get()
&& let Some(callback_id) = reorder_callback_id.as_deref()
{
let (direction, target_column_id) = if delta < 0 {
("left", previous_column_id.as_deref())
} else {
("right", next_column_id.as_deref())
};
if let Some(target_column_id) = target_column_id {
events::emit_data_table_column_reorder(
view_id,
&drag_header_id,
callback_id,
&drag_table_id,
&drag_column_id,
target_column_id,
direction,
);
drag.reorder_emitted.set(true);
}
return;
}
if delta != 0
&& let Some(callback_id) = resize_callback_id.as_deref()
{
events::emit_data_table_column_resize(
view_id,
&drag_header_id,
callback_id,
&drag_table_id,
&drag_column_id,
delta,
);
}
})
.on_drag(drag_state, |drag, cursor_offset, _, cx| {
drag.last_offset_x.set(f32::from(cursor_offset.x));
drag.reorder_emitted.set(false);
cx.new(|_| Empty)
});
}
apply_semantic_focus_visible_affordance(cell, show_focus_visible).into_any_element()
});
apply_div_style(
div()
.id(SharedString::from(format!("{table_id}.header")))
.flex()
.flex_row()
.children(children),
header_style,
)
.into_any_element()
}
#[allow(clippy::too_many_arguments)]
fn render_row(
view_id: u64,
table_id: &str,
row: &PreparedDataTableRow,
previous_row_id: Option<&str>,
next_row_id: Option<&str>,
columns: &[DataTableColumn],
row_style: &DivStyle,
cell_style: &DivStyle,
row_click: Option<&str>,
cell_click: Option<&str>,
row_context_menu: Option<&str>,
cell_context_menu: Option<&str>,
focus_handles: &DataTableFocusHandles,
header_focus_handles: &HashMap<String, FocusHandle>,
row_focus_neighbors: roving::NavigationTargets,
focus_visible: bool,
window: &Window,
) -> AnyElement {
let row_id = data_table_row_id(table_id, &row.id);
let cells =
columns
.iter()
.enumerate()
.zip(row.cells.iter())
.map(|((column_index, column), cell)| {
let cell_focus_neighbors = cell_focus_neighbors(
focus_handles,
columns,
&row.id,
previous_row_id,
next_row_id,
column_index,
header_focus_handles,
);
render_cell(
view_id,
table_id,
&row.id,
column,
cell.as_ref(),
cell_style,
cell_click,
cell_context_menu,
focus_handles
.cells
.get(&(row.id.clone(), column.id.clone())),
cell_focus_neighbors,
focus_visible,
window,
)
});
let mut element = apply_div_style(
div()
.id(SharedString::from(row_id.clone()))
.flex()
.flex_row()
.children(cells),
row_style,
);
element = apply_div_style(element, &row.style);
let show_focus_visible = focus_visible
&& focus_handles
.rows
.get(&row.id)
.is_some_and(|handle| handle.is_focused(window));
if let Some(handle) = focus_handles.rows.get(&row.id) {
let focus_handle = handle.clone();
element = element
.track_focus(&focus_handle)
.focusable()
.on_any_mouse_down(move |_, window, _| {
focus_handle.focus(window);
});
}
if let Some(callback_id) = row_click {
let callback_id = callback_id.to_owned();
let table_id = table_id.to_owned();
let row_value = row.id.clone();
let click_row_id = row_id.clone();
element = element.on_click(move |_, _, _| {
events::emit_data_table_event(
view_id,
ROW_CLICK_EVENT,
&click_row_id,
&callback_id,
&table_id,
Some(&row_value),
None,
);
});
}
if row_click.is_some() || row_context_menu.is_some() {
let click_callback = row_click.map(str::to_owned);
let context_menu_callback = row_context_menu.map(str::to_owned);
let key_table_id = table_id.to_owned();
let key_row_value = row.id.clone();
let key_row_id = row_id.clone();
let row_focus_neighbors = row_focus_neighbors.clone();
element = element.on_key_down(move |event: &KeyDownEvent, window, cx| {
if row_focus_neighbors.handle(event, window, cx) {
return;
}
if let Some(callback_id) = context_menu_callback.as_deref()
&& events::is_context_menu_key(event)
{
events::emit_data_table_keyboard_context_menu(
view_id,
&key_row_id,
callback_id,
&key_table_id,
&key_row_value,
None,
event,
);
cx.stop_propagation();
return;
}
if let Some(callback_id) = click_callback.as_deref()
&& is_activation_key(event)
{
events::emit_data_table_event(
view_id,
ROW_CLICK_EVENT,
&key_row_id,
callback_id,
&key_table_id,
Some(&key_row_value),
None,
);
cx.stop_propagation();
}
});
}
if let Some(callback_id) = row_context_menu {
let callback_id = callback_id.to_owned();
let table_id = table_id.to_owned();
let row_value = row.id.clone();
element = element.on_mouse_down(MouseButton::Right, move |event, _, _| {
events::emit_data_table_context_menu(
view_id,
&row_id,
&callback_id,
&table_id,
&row_value,
None,
event,
);
});
}
apply_semantic_focus_visible_affordance(element, show_focus_visible).into_any_element()
}
fn order_pinned_columns(columns: &[DataTableColumn]) -> Arc<[DataTableColumn]> {
columns
.iter()
.filter(|column| column.pinned)
.chain(columns.iter().filter(|column| !column.pinned))
.cloned()
.collect::<Vec<_>>()
.into()
}
#[derive(Debug)]
struct PreparedDataTableRow {
id: String,
cells: Vec<Option<DataTableCell>>,
style: DivStyle,
}
fn prepare_rows(columns: &[DataTableColumn], rows: &[DataTableRow]) -> Vec<PreparedDataTableRow> {
let column_indices = columns
.iter()
.enumerate()
.map(|(index, column)| (column.id.as_str(), index))
.collect::<HashMap<_, _>>();
rows.iter()
.map(|row| PreparedDataTableRow {
id: row.id.clone(),
cells: ordered_row_cells(columns.len(), &column_indices, row),
style: row.style.clone(),
})
.collect()
}
fn cell_focus_neighbors(
focus_handles: &DataTableFocusHandles,
columns: &[DataTableColumn],
row_id: &str,
previous_row_id: Option<&str>,
next_row_id: Option<&str>,
column_index: usize,
header_focus_handles: &HashMap<String, FocusHandle>,
) -> roving::NavigationTargets {
let Some(column) = columns.get(column_index) else {
return roving::NavigationTargets::default();
};
roving::NavigationTargets {
left: column_index
.checked_sub(1)
.and_then(|index| columns.get(index))
.and_then(|column| {
focus_handles
.cells
.get(&(row_id.to_owned(), column.id.clone()))
})
.cloned()
.or_else(|| focus_handles.rows.get(row_id).cloned()),
right: columns
.get(column_index + 1)
.and_then(|column| {
focus_handles
.cells
.get(&(row_id.to_owned(), column.id.clone()))
})
.cloned(),
up: previous_row_id
.and_then(|row_id| {
focus_handles
.cells
.get(&(row_id.to_owned(), column.id.clone()))
})
.cloned()
.or_else(|| header_focus_handles.get(&column.id).cloned()),
down: next_row_id
.and_then(|row_id| {
focus_handles
.cells
.get(&(row_id.to_owned(), column.id.clone()))
})
.cloned(),
home: columns.first().and_then(|column| {
focus_handles
.cells
.get(&(row_id.to_owned(), column.id.clone()))
.cloned()
}),
end: columns.last().and_then(|column| {
focus_handles
.cells
.get(&(row_id.to_owned(), column.id.clone()))
.cloned()
}),
}
}
fn ordered_row_cells(
column_count: usize,
column_indices: &HashMap<&str, usize>,
row: &DataTableRow,
) -> Vec<Option<DataTableCell>> {
let mut ordered_cells = vec![None; column_count];
for cell in row.cells.iter() {
if let Some(index) = column_indices.get(cell.column_id.as_str()) {
ordered_cells[*index] = Some(cell.clone());
}
}
ordered_cells
}
#[allow(clippy::too_many_arguments)]
fn render_cell(
view_id: u64,
table_id: &str,
row_id: &str,
column: &DataTableColumn,
cell: Option<&DataTableCell>,
cell_style: &DivStyle,
cell_click: Option<&str>,
cell_context_menu: Option<&str>,
focus_handle: Option<&FocusHandle>,
focus_neighbors: roving::NavigationTargets,
focus_visible: bool,
window: &Window,
) -> AnyElement {
let cell_id = data_table_cell_id(table_id, row_id, &column.id);
let mut cell_div = div().id(SharedString::from(cell_id.clone())).p_2();
if let Some(cell) = cell {
let children = cell.children.iter().enumerate().map(|(index, child)| {
render_static_node(view_id, &format!("{cell_id}.{index}"), child)
});
cell_div = cell_div.children(children);
}
let mut element = apply_column_width(apply_div_style(cell_div, cell_style), &column.width);
if let Some(cell) = cell {
element = apply_div_style(element, &cell.style);
}
let show_focus_visible =
focus_visible && focus_handle.is_some_and(|handle| handle.is_focused(window));
if let Some(handle) = focus_handle {
let focus_handle = handle.clone();
element = element
.track_focus(&focus_handle)
.focusable()
.on_any_mouse_down(move |_, window, _| {
focus_handle.focus(window);
});
}
if let Some(callback_id) = cell_click {
let callback_id = callback_id.to_owned();
let table_id = table_id.to_owned();
let row_id = row_id.to_owned();
let column_id = column.id.clone();
let click_cell_id = cell_id.clone();
element = element.on_click(move |_, _, _| {
events::emit_data_table_event(
view_id,
CELL_CLICK_EVENT,
&click_cell_id,
&callback_id,
&table_id,
Some(&row_id),
Some(&column_id),
);
});
}
if cell_click.is_some() || cell_context_menu.is_some() {
let click_callback = cell_click.map(str::to_owned);
let context_menu_callback = cell_context_menu.map(str::to_owned);
let key_table_id = table_id.to_owned();
let key_row_id = row_id.to_owned();
let key_column_id = column.id.clone();
let key_cell_id = cell_id.clone();
let focus_neighbors = focus_neighbors.clone();
element = element.on_key_down(move |event: &KeyDownEvent, window, cx| {
if focus_neighbors.handle(event, window, cx) {
return;
}
if let Some(callback_id) = context_menu_callback.as_deref()
&& events::is_context_menu_key(event)
{
events::emit_data_table_keyboard_context_menu(
view_id,
&key_cell_id,
callback_id,
&key_table_id,
&key_row_id,
Some(&key_column_id),
event,
);
cx.stop_propagation();
return;
}
if let Some(callback_id) = click_callback.as_deref()
&& is_activation_key(event)
{
events::emit_data_table_event(
view_id,
CELL_CLICK_EVENT,
&key_cell_id,
callback_id,
&key_table_id,
Some(&key_row_id),
Some(&key_column_id),
);
cx.stop_propagation();
}
});
}
if let Some(callback_id) = cell_context_menu {
let callback_id = callback_id.to_owned();
let table_id = table_id.to_owned();
let row_id = row_id.to_owned();
let column_id = column.id.clone();
element = element.on_mouse_down(MouseButton::Right, move |event, _, _| {
events::emit_data_table_context_menu(
view_id,
&cell_id,
&callback_id,
&table_id,
&row_id,
Some(&column_id),
event,
);
});
}
apply_semantic_focus_visible_affordance(element, show_focus_visible).into_any_element()
}
fn data_table_row_id(table_id: &str, row_id: &str) -> String {
format!("{table_id}.row.{row_id}")
}
fn data_table_cell_id(table_id: &str, row_id: &str, column_id: &str) -> String {
format!("{table_id}.cell.{row_id}.{column_id}")
}
fn is_header_activation_key(event: &KeyDownEvent) -> bool {
is_activation_key(event)
}
fn is_activation_key(event: &KeyDownEvent) -> bool {
roving::is_activation_key(event)
}
fn apply_column_width<E>(element: E, width: &DataTableColumnWidth) -> E
where
E: Styled + StatefulInteractiveElement,
{
match width {
DataTableColumnWidth::Auto => element.flex_1(),
DataTableColumnWidth::Px(value) => element.w(px(*value)),
DataTableColumnWidth::Fr(value) => {
let mut element = element.flex_1();
element.style().flex_grow = Some(*value as f32);
element
}
}
}
fn render_static_node(view_id: u64, path: &str, ir: &IrNode) -> AnyElement {
match ir {
IrNode::Text {
id,
content,
runs,
style,
click,
} => render_text::render_with_view_id(
view_id,
path,
id.as_deref(),
content,
runs,
style,
click.as_deref(),
),
IrNode::Spacer { id, style } => {
let node_id = NodeIdentity::new(view_id, path, id.as_deref());
apply_div_style(div().id(node_id.to_shared_string()), style).into_any_element()
}
IrNode::Div(node) => {
let node_id = NodeIdentity::new(view_id, path, node.id.as_deref());
let children = node.children.iter().enumerate().map(|(index, child)| {
render_static_node(view_id, &format!("{path}.{index}"), child)
});
apply_div_style(
div().id(node_id.to_shared_string()).children(children),
&node.style,
)
.into_any_element()
}
_ => div()
.child("Unsupported data-table cell child")
.into_any_element(),
}
}
#[cfg(test)]
mod tests {
use super::{
CELL_CLICK_EVENT, ROW_CLICK_EVENT, SORT_EVENT, apply_column_width, is_activation_key,
order_pinned_columns, prepare_rows,
};
use gpui::{KeyDownEvent, Keystroke};
#[test]
fn held_keys_do_not_activate_table_rows_cells_or_headers() {
assert!(is_activation_key(&KeyDownEvent {
keystroke: Keystroke::parse("space").unwrap(),
is_held: false,
}));
assert!(!is_activation_key(&KeyDownEvent {
keystroke: Keystroke::parse("space").unwrap(),
is_held: true,
}));
assert!(!is_activation_key(&KeyDownEvent {
keystroke: Keystroke::parse("enter").unwrap(),
is_held: true,
}));
}
use crate::{
bridge_view::events,
ir::{DataTableCell, DataTableColumn, DataTableColumnWidth, DataTableRow},
};
use gpui::{InteractiveElement, SharedString, Styled, div, relative};
#[test]
fn fractional_column_widths_apply_weighted_flex_grow() {
let mut one_fr = apply_column_width(
div().id(SharedString::from("one_fr")),
&DataTableColumnWidth::Fr(1),
);
let mut two_fr = apply_column_width(
div().id(SharedString::from("two_fr")),
&DataTableColumnWidth::Fr(2),
);
assert_eq!(one_fr.style().flex_grow, Some(1.0));
assert_eq!(two_fr.style().flex_grow, Some(2.0));
assert_eq!(one_fr.style().flex_shrink, Some(1.0));
assert_eq!(two_fr.style().flex_shrink, Some(1.0));
assert_eq!(one_fr.style().flex_basis, Some(relative(0.).into()));
assert_eq!(two_fr.style().flex_basis, Some(relative(0.).into()));
}
#[test]
fn pinned_columns_render_before_unpinned_columns() {
let columns = vec![
DataTableColumn {
id: "status".into(),
label: "Status".into(),
width: DataTableColumnWidth::Auto,
sortable: false,
pinned: false,
style: Vec::new().into(),
},
DataTableColumn {
id: "task".into(),
label: "Task".into(),
width: DataTableColumnWidth::Auto,
sortable: false,
pinned: true,
style: Vec::new().into(),
},
DataTableColumn {
id: "owner".into(),
label: "Owner".into(),
width: DataTableColumnWidth::Auto,
sortable: false,
pinned: false,
style: Vec::new().into(),
},
];
let ordered = order_pinned_columns(&columns);
assert_eq!(
ordered
.iter()
.map(|column| column.id.as_str())
.collect::<Vec<_>>(),
["task", "status", "owner"]
);
}
#[test]
fn prepared_rows_follow_column_order_and_preserve_missing_cells() {
let columns = vec![
DataTableColumn {
id: "task".into(),
label: "Task".into(),
width: DataTableColumnWidth::Auto,
sortable: false,
pinned: false,
style: Vec::new().into(),
},
DataTableColumn {
id: "owner".into(),
label: "Owner".into(),
width: DataTableColumnWidth::Auto,
sortable: false,
pinned: false,
style: Vec::new().into(),
},
DataTableColumn {
id: "status".into(),
label: "Status".into(),
width: DataTableColumnWidth::Auto,
sortable: false,
pinned: false,
style: Vec::new().into(),
},
];
let rows = vec![DataTableRow {
id: "row_1".into(),
cells: vec![
DataTableCell {
column_id: "status".into(),
children: Vec::new().into(),
style: Vec::new().into(),
},
DataTableCell {
column_id: "task".into(),
children: Vec::new().into(),
style: Vec::new().into(),
},
]
.into(),
style: Vec::new().into(),
}];
let prepared = prepare_rows(&columns, &rows);
assert_eq!(prepared[0].id, "row_1");
assert_eq!(
prepared[0]
.cells
.iter()
.map(|cell| cell.as_ref().map(|cell| cell.column_id.as_str()))
.collect::<Vec<_>>(),
[Some("task"), None, Some("status")]
);
}
#[test]
fn data_table_events_include_semantic_identity() {
events::emit_data_table_event(
7,
ROW_CLICK_EVENT,
"table.row.row_1",
"select_row",
"table",
Some("row_1"),
None,
);
let event = crate::take_semantic_event_snapshot_for_test().unwrap();
assert_eq!(event.event, "data_table_row_click");
assert_eq!(event.view_id, 7);
assert_eq!(event.table_id.as_deref(), Some("table"));
assert_eq!(event.row_id.as_deref(), Some("row_1"));
assert_eq!(event.column_id, None);
events::emit_data_table_event(
7,
CELL_CLICK_EVENT,
"table.cell.row_1.status",
"select_cell",
"table",
Some("row_1"),
Some("status"),
);
let event = crate::take_semantic_event_snapshot_for_test().unwrap();
assert_eq!(event.event, "data_table_cell_click");
assert_eq!(event.row_id.as_deref(), Some("row_1"));
assert_eq!(event.column_id.as_deref(), Some("status"));
events::emit_data_table_event(
7,
SORT_EVENT,
"table.header.status",
"sort_table",
"table",
None,
Some("status"),
);
let event = crate::take_semantic_event_snapshot_for_test().unwrap();
assert_eq!(event.event, "data_table_sort");
assert_eq!(event.row_id, None);
assert_eq!(event.column_id.as_deref(), Some("status"));
use gpui::{Modifiers, MouseButton, MouseDownEvent, point, px};
events::emit_data_table_context_menu(
7,
"table.cell.row_1.status",
"cell_context",
"table",
"row_1",
Some("status"),
&MouseDownEvent {
position: point(px(12.0), px(8.0)),
modifiers: Modifiers::none(),
button: MouseButton::Right,
click_count: 1,
first_mouse: false,
},
);
let event = crate::take_semantic_event_snapshot_for_test().unwrap();
assert_eq!(event.event, "context_menu");
assert_eq!(event.table_id.as_deref(), Some("table"));
assert_eq!(event.row_id.as_deref(), Some("row_1"));
assert_eq!(event.column_id.as_deref(), Some("status"));
}
}