Files
Ely-GPUI-Components/src/tables/grid.rs
T

469 lines
17 KiB
Rust

use std::rc::Rc;
use gpui::{
AnyElement, App, Bounds, Div, DragMoveEvent, ElementId, Entity, EntityId, FocusHandle,
InteractiveElement, IntoElement, MouseButton, MouseMoveEvent, ParentElement, Pixels, Point,
SharedString, StatefulInteractiveElement, Styled, Window, canvas, div, point, prelude::*,
};
use super::gridkeys::{Metrics, across, begin, fill, fit, keys, land};
use crate::{
forms::{Editing, Input},
primitives::{FocusNext, FocusPrev},
theme::{ActiveTheme, ControlSize, TextSize},
typography::Ellipsis,
};
pub(crate) type OnCells = Rc<dyn Fn(&[(usize, usize, SharedString)], &mut Window, &mut App)>;
pub(crate) type Text = Rc<dyn Fn(usize, usize) -> SharedString>;
/// A rectangle of cells shown as one, from its first corner to its last.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Merge {
pub from: (usize, usize),
pub to: (usize, usize),
}
/// A grid's own state: the cursor and the corner its range reaches from, whether it edits, how far it has scrolled, its bounds, and where a fill would reach.
#[derive(Default)]
pub(crate) struct Sheet {
pub cursor: (usize, usize),
pub anchor: (usize, usize),
pub editing: bool,
pub offset: Point<Pixels>,
pub bounds: Bounds<Pixels>,
pub fill: Option<(usize, usize)>,
pub rows: usize,
}
/// What a grid face supplies: its size, its labels, the text a cell holds and shows, and who hears edits.
pub(crate) struct Face {
pub id: ElementId,
pub rows: usize,
pub cols: usize,
pub titles: Rc<dyn Fn(usize) -> SharedString>,
pub numbered: bool,
pub raw: Text,
pub shown: Text,
pub merges: Vec<Merge>,
pub frozen: (usize, usize),
pub fills: bool,
pub on_change: Option<OnCells>,
}
/// A drag of a grid's fill handle, named by its owner.
pub(crate) struct FillDrag {
owner: EntityId,
}
/// The range the cursor and its anchor span, as top-left and bottom-right.
pub(crate) fn range(sheet: &Sheet) -> ((usize, usize), (usize, usize)) {
let ((r0, c0), (r1, c1)) = (sheet.anchor, sheet.cursor);
((r0.min(r1), c0.min(c1)), (r0.max(r1), c0.max(c1)))
}
/// Draws a grid face: headers, labels, visible cells, the range, the cursor and its fill handle.
pub(crate) fn grid(face: Face, base: Div, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = face.id.clone();
let focus: FocusHandle =
crate::primitives::tab_stop((id.clone(), "focus").into(), true, window, cx);
let sheet: Entity<Sheet> =
window.use_keyed_state((id.clone(), "sheet"), cx, |_, _| Sheet::default());
let editor = window.use_keyed_state((id.clone(), "editor"), cx, |_, _| Editing::default());
let shrunk = sheet.update(cx, |sheet, _| {
sheet.rows = face.rows;
fit(sheet, (face.rows, face.cols))
});
if shrunk {
editor.update(cx, |editing, cx| editing.finish(true, window, cx));
}
if sheet.read(cx).editing && editor.read(cx).field().is_none() {
sheet.update(cx, |sheet, _| sheet.editing = false);
window.focus(&focus);
}
let theme = cx.theme();
let colors = theme.colors.clone();
let rem = window.rem_size();
let metrics = Metrics::new(&face, window, cx);
let (rows, cols) = (face.rows, face.cols);
let (offset, bounds, editing, fill_to) = {
let sheet = sheet.read(cx);
(sheet.offset, sheet.bounds, sheet.editing, sheet.fill)
};
let ((top, left), (bottom, right)) = range(sheet.read(cx));
let cursor = sheet.read(cx).cursor;
let focused = focus.contains_focused(window, cx);
let span = |from: Pixels, cell: Pixels, count: usize, size: Pixels| {
let first = (from / cell).floor() as usize;
let last = ((from + size) / cell).ceil() as usize;
first.min(count)..last.min(count)
};
let view_rows = span(
offset.y + metrics.frozen.y,
metrics.tall,
rows,
bounds.size.height,
);
let view_cols = span(
offset.x + metrics.frozen.x,
metrics.wide,
cols,
bounds.size.width,
);
let x_of = |col: usize| {
let x = metrics.wide * col as f32;
metrics.label + if col < face.frozen.1 { x } else { x - offset.x }
};
let y_of = |row: usize| {
let y = metrics.tall * row as f32;
metrics.tall + if row < face.frozen.0 { y } else { y - offset.y }
};
let merged = |row: usize, col: usize| {
face.merges.iter().find(|merge| {
(merge.from.0..=merge.to.0).contains(&row) && (merge.from.1..=merge.to.1).contains(&col)
})
};
let field = editing.then(|| editor.read(cx).field()).flatten();
let cell = |row: usize, col: usize| -> Option<AnyElement> {
let merge = merged(row, col);
if merge.is_some_and(|merge| merge.from != (row, col)) {
return None;
}
let (wide, tall) = match merge {
Some(merge) => (
metrics.wide * (merge.to.1 - col + 1) as f32,
metrics.tall * (merge.to.0 - row + 1) as f32,
),
None => (metrics.wide, metrics.tall),
};
let text = (face.shown)(row, col);
let number = text.trim().replace(',', "").parse::<f64>().is_ok();
let content: AnyElement = match (&field, (row, col) == cursor) {
(Some(field), true) => {
let editor = editor.clone();
div()
.size_full()
.on_key_down(move |event, window, cx| {
if event.keystroke.key == "escape" {
cx.stop_propagation();
editor.update(cx, |editing, cx| editing.finish(true, window, cx));
}
})
.child(Input::new(field).size(ControlSize::Sm))
.into_any_element()
}
_ => Ellipsis::new(text).into_any_element(),
};
Some(
div()
.absolute()
.left(x_of(col))
.top(y_of(row))
.w(wide)
.h(tall)
.flex()
.items_center()
.when(number, |cell| cell.justify_end())
.px_2()
.border_r_1()
.border_b_1()
.border_color(colors.border)
.bg(colors.bg)
.child(content)
.into_any_element(),
)
};
let frozen_rows = 0..face.frozen.0.min(rows);
let frozen_cols = 0..face.frozen.1.min(cols);
let layer = |rows: std::ops::Range<usize>, cols: std::ops::Range<usize>| {
rows.flat_map(move |row| cols.clone().map(move |col| (row, col)))
.filter_map(|(row, col)| cell(row, col))
.collect::<Vec<_>>()
};
let main = layer(view_rows.clone(), view_cols.clone());
let top_band = layer(frozen_rows.clone(), view_cols.clone());
let left_band = layer(view_rows.clone(), frozen_cols.clone());
let corner_band = layer(frozen_rows, frozen_cols);
let headers = view_cols
.clone()
.chain(0..face.frozen.1.min(cols))
.map(|col| {
div()
.absolute()
.left(x_of(col))
.top_0()
.w(metrics.wide)
.h(metrics.tall)
.flex()
.items_center()
.justify_center()
.border_r_1()
.border_b_1()
.border_color(colors.border)
.bg(if (left..=right).contains(&col) && focused {
colors.active
} else {
colors.sunken
})
.text_color(colors.fg_muted)
.child((face.titles)(col))
})
.collect::<Vec<_>>();
let labels = face.numbered.then(|| {
view_rows
.clone()
.chain(0..face.frozen.0.min(rows))
.map(|row| {
div()
.absolute()
.left_0()
.top(y_of(row))
.w(metrics.label)
.h(metrics.tall)
.flex()
.items_center()
.justify_center()
.border_r_1()
.border_b_1()
.border_color(colors.border)
.bg(if (top..=bottom).contains(&row) && focused {
colors.active
} else {
colors.sunken
})
.text_color(colors.fg_muted)
.child((row + 1).to_string())
})
.collect::<Vec<_>>()
});
let area = Bounds::new(
point(x_of(left), y_of(top)),
gpui::size(
metrics.wide * (right - left + 1) as f32,
metrics.tall * (bottom - top + 1) as f32,
),
);
let owner = sheet.entity_id();
let marks = [
div()
.absolute()
.left(area.origin.x)
.top(area.origin.y)
.w(area.size.width)
.h(area.size.height)
.bg(colors.selection)
.border_1()
.border_color(colors.accent.opacity(0.6))
.into_any_element(),
div()
.absolute()
.left(x_of(cursor.1))
.top(y_of(cursor.0))
.w(metrics.wide)
.h(metrics.tall)
.border_2()
.border_color(colors.accent)
.into_any_element(),
];
let preview = fill_to.map(|(row, col)| {
let down = row > bottom;
let (to_row, to_col) = if down {
(row, right)
} else {
(bottom, col.max(right))
};
div()
.absolute()
.left(area.origin.x)
.top(area.origin.y)
.w(metrics.wide * (to_col - left + 1) as f32)
.h(metrics.tall * (to_row - top + 1) as f32)
.border_1()
.border_color(colors.fg_muted)
});
let handle = (face.fills && !editing).then(|| {
let reach = theme.status_dot();
div()
.id((id.clone(), "fill"))
.absolute()
.left(area.origin.x + area.size.width - reach.to_pixels(rem) * 0.5)
.top(area.origin.y + area.size.height - reach.to_pixels(rem) * 0.5)
.size(reach)
.bg(colors.accent)
.border_1()
.border_color(colors.bg)
.cursor_crosshair()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_drag(FillDrag { owner }, |_, _, _, cx| {
cx.new(|_| gpui::EmptyView)
})
});
let (face_rows, face_cols) = (rows, cols);
let (pressed, dragged, moved, measured, scrolled, dropped) = (
sheet.clone(),
sheet.clone(),
sheet.clone(),
sheet.clone(),
sheet.clone(),
sheet.clone(),
);
let (raw, on_change, begin_with) = (face.raw.clone(), face.on_change.clone(), editor.clone());
let (fill_raw, fill_hears) = (face.raw.clone(), face.on_change.clone());
let numbered = face.numbered;
let (merges, frozen) = (Rc::new(face.merges.clone()), face.frozen);
let hovered_merges = merges.clone();
let ahead = (sheet.clone(), editor.clone(), merges.clone());
let behind = (sheet.clone(), editor.clone(), merges.clone());
let keyed = keys(face, metrics, sheet.clone(), editor.clone());
let focus_on_press = focus.clone();
let text_size = theme.text_size(TextSize::Sm);
base.id(id.clone())
.track_focus(&focus)
.relative()
.overflow_hidden()
.text_size(text_size)
.text_color(colors.fg)
.bg(colors.bg)
.border_1()
.border_color(colors.border)
.on_key_down(keyed)
.capture_action(move |_: &FocusNext, window, cx| {
let (sheet, editor, merges) = &ahead;
let size = (face_rows, face_cols);
across(
(sheet, editor),
(merges, size, false),
(metrics, frozen),
window,
cx,
)
})
.capture_action(move |_: &FocusPrev, window, cx| {
let (sheet, editor, merges) = &behind;
let size = (face_rows, face_cols);
across(
(sheet, editor),
(merges, size, true),
(metrics, frozen),
window,
cx,
)
})
.on_mouse_down(MouseButton::Left, move |event, window, cx| {
let at = event.position - pressed.read(cx).bounds.origin;
let Some(cell) = metrics.cell_at(at, pressed.read(cx).offset, (face_rows, face_cols))
else {
return;
};
let cell = land(&merges, cell);
window.focus(&focus_on_press);
pressed.update(cx, |sheet, cx| {
sheet.cursor = cell;
if !event.modifiers.shift {
sheet.anchor = cell;
}
cx.notify();
});
if event.click_count == 2 {
let text = raw(cell.0, cell.1).to_string();
begin(
&pressed,
&begin_with,
on_change.clone(),
text,
false,
window,
cx,
);
}
})
.on_mouse_move(move |event: &MouseMoveEvent, _, cx| {
if event.pressed_button != Some(MouseButton::Left) || cx.has_active_drag() {
return;
}
let at = event.position - moved.read(cx).bounds.origin;
if let Some(cell) = metrics.cell_at(at, moved.read(cx).offset, (face_rows, face_cols)) {
let cell = land(&hovered_merges, cell);
moved.update(cx, |sheet, cx| {
if sheet.cursor != cell && !sheet.editing {
sheet.cursor = cell;
cx.notify();
}
});
}
})
.on_scroll_wheel(move |event, _, cx| {
let delta = event.delta.pixel_delta(metrics.tall);
scrolled.update(cx, |sheet, cx| {
let most = point(
(metrics.wide * face_cols as f32 + metrics.label - sheet.bounds.size.width)
.max(Pixels::ZERO),
(metrics.tall * (face_rows + 1) as f32 - sheet.bounds.size.height)
.max(Pixels::ZERO),
);
sheet.offset = point(
(sheet.offset.x - delta.x).clamp(Pixels::ZERO, most.x),
(sheet.offset.y - delta.y).clamp(Pixels::ZERO, most.y),
);
cx.notify();
});
cx.stop_propagation();
})
.on_drag_move(move |event: &DragMoveEvent<FillDrag>, _, cx| {
if event.drag(cx).owner != owner {
return;
}
let at = event.event.position - dragged.read(cx).bounds.origin;
let cell = metrics.cell_at(at, dragged.read(cx).offset, (face_rows, face_cols));
dragged.update(cx, |sheet, cx| {
if sheet.fill != cell {
sheet.fill = cell;
cx.notify();
}
});
})
.on_drop(move |drag: &FillDrag, window, cx| {
if drag.owner == owner {
fill(&dropped, &fill_raw, &fill_hears, window, cx);
}
})
.children(main)
.children(top_band)
.children(left_band)
.children(corner_band)
.children(marks)
.children(preview)
.children(handle)
.children(headers)
.children(labels.into_iter().flatten())
.when(numbered, |grid| {
grid.child(
div()
.absolute()
.left_0()
.top_0()
.w(metrics.label)
.h(metrics.tall)
.bg(colors.sunken)
.border_r_1()
.border_b_1()
.border_color(colors.border),
)
})
.child(
canvas(
move |bounds, _, cx| {
if measured.read(cx).bounds != bounds {
measured.update(cx, |sheet, cx| {
sheet.bounds = bounds;
cx.notify();
});
}
},
|_, _, _, _| {},
)
.absolute()
.inset_0(),
)
}