1use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc};
15
16use gpui_kit::component::Size as ComponentSize;
17use gpui_kit::{
18 App, Context, InteractiveElement, MouseButton, MouseDownEvent, ParentElement as _, Pixels,
19 Role, SharedString, Size, StatefulInteractiveElement as _, Styled, WeakEntity, Window, div,
20 prelude::FluentBuilder as _, px, size,
21};
22
23use super::{
24 CELL_PADDING, CellChange, ColumnAlign, LiveGridDelegate, LiveGridState,
25 a11y::column_index,
26 columns::CellContent,
27 edit::EditState,
28 editors::{CellCommit, CellEditHandle, GridCommit},
29 frame::{HostCell, RowPlanner},
30 selection::FocusedCell,
31};
32use crate::{
33 actions::ActivateCell,
34 element::{CellHost, HostLayer},
35 model::{CellValue, GridColumns as _, GridRow, display_text_in},
36 store::PinnedEdge,
37};
38
39pub(crate) type CellActivation = Rc<dyn Fn(&mut Window, &mut App)>;
42
43pub(super) struct ActiveCell<K, C> {
47 key: K,
48 column: C,
49 handler: CellActivation,
50}
51
52pub(super) struct CellPress<K, C> {
55 key: K,
56 column: C,
57 pinned: Option<PinnedEdge>,
59 button: MouseButton,
60 accepted: bool,
62}
63
64pub(super) struct WidgetSlot<K, C> {
67 press: Option<CellPress<K, C>>,
68 activation: RefCell<Option<ActiveCell<K, C>>>,
71 commit: RefCell<Option<Rc<dyn CellCommit>>>,
73}
74
75impl<K, C> Default for WidgetSlot<K, C> {
76 fn default() -> Self {
77 Self {
78 press: None,
79 activation: RefCell::new(None),
80 commit: RefCell::new(None),
81 }
82 }
83}
84
85impl<K, C> WidgetSlot<K, C> {
86 pub(super) fn begin_frame(&self) {
89 self.activation.borrow_mut().take();
90 }
91
92 fn commit<D: LiveGridDelegate>(
94 &self,
95 grid: &WeakEntity<LiveGridState<D>>,
96 ) -> Rc<dyn CellCommit> {
97 let mut commit = self.commit.borrow_mut();
98 Rc::clone(commit.get_or_insert_with(|| {
99 Rc::new(GridCommit { grid: grid.clone() }) as Rc<dyn CellCommit>
100 }))
101 }
102}
103
104pub struct CellElementContext<'a, R: GridRow> {
107 row: &'a Arc<R>,
108 column: R::Column,
109 value: CellValue<'a>,
110 text: &'a str,
111 change: Option<CellChange>,
112 size: ComponentSize,
113 cell_size: Size<Pixels>,
114 selected: bool,
115 focused: bool,
116 edit_state: Option<&'a EditState>,
118 activation: Option<&'a RefCell<Option<ActiveCell<R::Key, R::Column>>>>,
121 commit: Option<Rc<dyn CellCommit>>,
124}
125
126impl<'a, R: GridRow> CellElementContext<'a, R> {
127 pub fn row(&self) -> &'a R {
129 self.row
130 }
131
132 pub fn shared_row(&self) -> &'a Arc<R> {
135 self.row
136 }
137
138 pub fn key(&self) -> &'a R::Key {
140 self.row.key()
141 }
142
143 pub fn column(&self) -> R::Column {
145 self.column
146 }
147
148 pub fn value(&self) -> CellValue<'a> {
151 self.value
152 }
153
154 pub fn text(&self) -> &'a str {
156 self.text
157 }
158
159 pub fn change(&self) -> Option<CellChange> {
165 self.change
166 }
167
168 pub fn size(&self) -> ComponentSize {
171 self.size
172 }
173
174 pub fn cell_size(&self) -> Size<Pixels> {
176 self.cell_size
177 }
178
179 pub fn is_selected(&self) -> bool {
181 self.selected
182 }
183
184 pub fn is_focused(&self) -> bool {
186 self.focused
187 }
188
189 pub fn edit_state(&self) -> Option<&'a EditState> {
193 self.edit_state
194 }
195
196 pub fn edit(&self) -> Option<CellEditHandle> {
203 let commit = self.commit.as_ref()?;
204 let row: Arc<dyn Any> = Arc::clone(self.row) as Arc<dyn Any>;
205 Some(CellEditHandle::new(
206 Rc::clone(commit),
207 row,
208 self.column.ix(),
209 ))
210 }
211
212 pub fn on_activate(&self, handler: impl Fn(&mut Window, &mut App) + 'static) {
219 if let Some(activation) = self.activation {
220 *activation.borrow_mut() = Some(ActiveCell {
221 key: self.row.key().clone(),
222 column: self.column,
223 handler: Rc::new(handler),
224 });
225 }
226 }
227}
228
229fn component_size(row_height: Pixels) -> ComponentSize {
232 [
233 ComponentSize::XSmall,
234 ComponentSize::Small,
235 ComponentSize::Medium,
236 ComponentSize::Large,
237 ]
238 .into_iter()
239 .find(|size| size.table_row_height() == row_height)
240 .unwrap_or(ComponentSize::Size(row_height))
241}
242
243struct HostA11y {
246 label: Option<SharedString>,
249 description: SharedString,
251 column_index: usize,
252 selected: Option<bool>,
254}
255
256impl<D: LiveGridDelegate> RowPlanner<'_, D> {
257 pub(super) fn plan_cell_host(
267 &mut self,
268 cell: &HostCell<'_, D::Row>,
269 window: &mut Window,
270 ) -> bool {
271 let row = cell.row;
272 let live = row.is_live();
273 let bounds = cell.host_bounds();
274 let control = cell.content == CellContent::Control;
275 let widgets = self.widgets;
276 let activation = (live && control && cell.focused && row.pinned.is_none())
279 .then_some(&widgets.activation);
280 let delegate = self.delegate;
281 self.host_text.clear();
282 let text = display_text_in(cell.value, cell.number_format, &mut self.host_text);
283 let context = CellElementContext {
284 row: row.row,
285 column: cell.column,
286 value: cell.value,
287 text,
288 change: cell.change,
289 size: component_size(self.row_height),
290 cell_size: size(
291 (bounds.size.width - CELL_PADDING * 2.).max(px(0.)),
292 bounds.size.height,
293 ),
294 selected: cell.selected,
295 focused: cell.focused,
296 edit_state: cell.edit_state,
297 activation,
298 commit: (live && self.edit.has_editor(cell.column.ix()) && row.pinned.is_none())
299 .then(|| widgets.commit(&self.grid)),
300 };
301 let Some(element) = delegate.render_cell(&context, window, self.app) else {
302 return false;
303 };
304 let a11y = self.a11y.as_ref().map(|input| HostA11y {
305 label: (!control).then(|| SharedString::from(text.to_owned())),
306 description: cell.title.clone(),
307 column_index: column_index(cell.view),
308 selected: input.cell_selected(cell.selected),
309 });
310 let column = cell.column;
311 if !live {
312 let grid = self.grid.clone();
313 self.hosts.push(CellHost {
314 layer: HostLayer::Widget,
315 column: column.ix(),
316 zone: cell.zone,
317 bounds,
318 element: CellHost::host_div(HostLayer::Widget, column.id())
319 .relative()
320 .flex()
321 .items_center()
322 .px(CELL_PADDING)
323 .map(|this| align_host(this, cell.align))
324 .child(element)
325 .child(
326 div()
327 .absolute()
328 .inset_0()
329 .block_mouse_except_scroll()
330 .on_any_mouse_down(move |event, window, cx| {
331 let _ = grid.update(cx, |state, cx| {
332 state.on_body_mouse_down(event, window, cx);
333 });
334 }),
335 ),
336 });
337 return true;
338 }
339 let press = HostPress {
340 grid: self.grid.clone(),
341 row: Arc::clone(row.row),
342 column,
343 pinned: row.pinned,
344 };
345 let element = CellHost::host_div(HostLayer::Widget, column.id())
346 .flex()
347 .items_center()
348 .px(CELL_PADDING)
349 .map(|this| align_host(this, cell.align))
350 .block_mouse_except_scroll()
351 .capture_any_mouse_down({
352 let press = press.clone();
353 move |event, window, cx| press.record(event, window, cx)
354 })
355 .on_any_mouse_down(move |event, window, cx| press.accept(event, window, cx))
356 .when_some(a11y, |this, a11y| {
357 this.role(Role::GridCell)
358 .aria_column_index(a11y.column_index)
359 .aria_description(a11y.description)
360 .when_some(a11y.label, |this, label| this.aria_label(label))
361 .when_some(a11y.selected, |this, selected| this.aria_selected(selected))
362 })
363 .child(element);
364 self.hosts.push(CellHost {
365 layer: HostLayer::Widget,
366 column: column.ix(),
367 zone: cell.zone,
368 bounds,
369 element,
370 });
371 true
372 }
373}
374
375fn align_host<E: Styled>(host: E, align: ColumnAlign) -> E {
377 match align {
378 ColumnAlign::Start => host.justify_start(),
379 ColumnAlign::End => host.justify_end(),
380 ColumnAlign::Auto => host.justify_center(),
382 }
383}
384
385struct HostPress<D: LiveGridDelegate> {
388 grid: WeakEntity<LiveGridState<D>>,
389 row: Arc<D::Row>,
390 column: <D::Row as GridRow>::Column,
391 pinned: Option<PinnedEdge>,
392}
393
394impl<D: LiveGridDelegate> Clone for HostPress<D> {
395 fn clone(&self) -> Self {
396 Self {
397 grid: self.grid.clone(),
398 row: Arc::clone(&self.row),
399 column: self.column,
400 pinned: self.pinned,
401 }
402 }
403}
404
405impl<D: LiveGridDelegate> HostPress<D> {
406 fn record(&self, event: &MouseDownEvent, window: &mut Window, cx: &mut App) {
409 let press = CellPress {
410 key: self.row.key().clone(),
411 column: self.column,
412 pinned: self.pinned,
413 button: event.button,
414 accepted: false,
415 };
416 if self
417 .grid
418 .update(cx, |state, _| state.widgets.press = Some(press))
419 .is_err()
420 {
421 return;
422 }
423 let grid = self.grid.clone();
424 window.defer(cx, move |window, cx| {
425 let _ = grid.update(cx, |state, cx| state.settle_cell_press(window, cx));
426 });
427 }
428
429 fn accept(&self, event: &MouseDownEvent, window: &mut Window, cx: &mut App) {
432 let _ = self.grid.update(cx, |state, cx| {
433 if let Some(press) = &mut state.widgets.press {
434 press.accepted = true;
435 }
436 state.on_body_mouse_down(event, window, cx);
437 });
438 }
439}
440
441impl<D: LiveGridDelegate> LiveGridState<D> {
442 pub(super) fn bind_cell_actions<E: InteractiveElement>(root: E, cx: &Context<Self>) -> E {
444 root.on_action(cx.listener(Self::on_activate_cell))
445 }
446
447 fn on_activate_cell(&mut self, _: &ActivateCell, window: &mut Window, cx: &mut Context<Self>) {
451 let handler = self.selection.focus().and_then(|focus| {
452 let key = focus.row()?;
453 let column = focus.column();
454 if self.columns.column(column.ix()).content != CellContent::Control {
455 return None;
456 }
457 let activation = self.widgets.activation.borrow();
458 activation
459 .as_ref()
460 .filter(|active| active.key == *key && active.column == column)
461 .map(|active| Rc::clone(&active.handler))
462 });
463 match handler {
464 Some(handler) => window.defer(cx, move |window, cx| handler(window, cx)),
465 None => cx.propagate(),
466 }
467 }
468
469 fn settle_cell_press(&mut self, window: &mut Window, cx: &mut Context<Self>) {
475 let Some(press) = self.widgets.press.take() else {
476 return;
477 };
478 if !press.accepted && press.pinned.is_none() {
479 self.focus_press(FocusedCell::new(press.key, press.column), cx);
480 if press.button != MouseButton::Left {
481 self.on_body_mouse_up(cx);
482 }
483 }
484 if !self.is_body_focused(window, cx) {
485 #[cfg(debug_assertions)]
486 self.warn_tab_stop_in_cell(window, cx);
487 window.focus(&self.focus_handle, cx);
488 }
489 }
490
491 #[cfg(debug_assertions)]
494 fn warn_tab_stop_in_cell(&self, window: &Window, cx: &App) {
495 use std::sync::atomic::{AtomicBool, Ordering};
496
497 static WARNED: AtomicBool = AtomicBool::new(false);
498 let in_cell = window.focused(cx).is_some_and(|focused| {
499 focused.tab_stop && self.focus_handle.contains(&focused, window)
500 });
501 if in_cell && !WARNED.swap(true, Ordering::Relaxed) {
502 log::warn!(
503 "an element in a grid cell took the keyboard focus as a Tab stop; \
504 give it `tab_stop(false)`: the grid is one Tab stop"
505 );
506 }
507 }
508}