use std::cell::Cell;
use std::rc::Rc;
use std::time::Duration;
use teksilo_core::drag_payload::DragPayload;
use teksilo_core::event::{EventResponse, Key, WidgetEvent};
use teksilo_core::signal::Signal;
use teksilo_core::widget::EventContext;
use teksilo_data::{DropPosition, SelectionModel};
use super::layout::{GridLayoutStrategy, ScrollAnchor};
use crate::common::type_ahead::TypeAheadState;
use crate::data_views::ViewId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GridTabTraversal {
#[default]
OutOfGrid,
WithinGrid,
}
pub(crate) struct GridKeyConfig {
pub(crate) len_fn: Rc<dyn Fn() -> usize>,
pub(crate) col_count: Signal<usize>,
pub(crate) focused_index: Signal<Option<usize>>,
pub(crate) selection: Option<SelectionModel>,
pub(crate) scroll_y: Signal<f32>,
pub(crate) max_scroll_y: Signal<f32>,
pub(crate) viewport_height: Rc<Cell<f32>>,
pub(crate) viewport_width: Rc<Cell<f32>>,
pub(crate) viewport_origin: Rc<Cell<Option<teksilo_canvas::Point>>>,
pub(crate) strategy: Rc<dyn GridLayoutStrategy>,
pub(crate) wrap_navigation: bool,
pub(crate) tab_traversal: GridTabTraversal,
#[allow(clippy::type_complexity)]
pub(crate) on_tile_activate: Option<Rc<dyn Fn(usize, &mut EventContext)>>,
pub(crate) reorderable: bool,
#[allow(clippy::type_complexity)]
pub(crate) accept_drop_fn: Rc<dyn Fn(&DragPayload, usize, DropPosition, ViewId) -> bool>,
pub(crate) view_id: ViewId,
pub(crate) make_reorder_payload: Rc<dyn Fn(usize) -> DragPayload>,
pub(crate) type_ahead_timeout: Duration,
#[allow(clippy::type_complexity)]
pub(crate) type_ahead_label: Option<Rc<dyn Fn(usize) -> Option<String>>>,
}
pub(crate) fn build_grid_key_handler(
cfg: GridKeyConfig,
) -> impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static {
let ta_state = TypeAheadState::new();
move |event, ctx| {
let WidgetEvent::KeyDown { key, modifiers, .. } = event else {
return EventResponse::Ignored;
};
let n = (cfg.len_fn)();
if n == 0 {
return EventResponse::Ignored;
}
let cols = cfg.col_count.get().max(1);
let rtl = ctx.is_rtl();
let cursor = cfg
.focused_index
.get()
.or_else(|| {
cfg.selection
.as_ref()
.and_then(|s| s.selected_indices().first().copied())
})
.map(|i| i.min(n - 1));
let current = cursor.unwrap_or(0);
let col = current % cols;
if modifiers.command() && *key == Key::A {
if let Some(ref sel) = cfg.selection {
sel.select_all(n);
}
return EventResponse::Handled;
}
let logical_prev = if rtl { Key::ArrowRight } else { Key::ArrowLeft };
let logical_next = if rtl { Key::ArrowLeft } else { Key::ArrowRight };
if modifiers.alt() && cfg.reorderable {
let target = if *key == logical_next && current + 1 < n {
Some(current + 1)
} else if *key == logical_prev && current > 0 {
Some(current - 1)
} else if *key == Key::ArrowDown && current + cols < n {
Some(current + cols)
} else if *key == Key::ArrowUp && current >= cols {
Some(current - cols)
} else {
None
};
if let Some(t) = target {
let position = if t > current {
DropPosition::After
} else {
DropPosition::Before
};
let payload = (cfg.make_reorder_payload)(current);
if (cfg.accept_drop_fn)(&payload, t, position, cfg.view_id) {
cfg.focused_index.set(Some(t));
if let Some(ref sel) = cfg.selection {
sel.select(t);
}
ensure_visible(&cfg, t, ctx);
}
return EventResponse::Handled;
}
}
if let Some(ref label_fn) = cfg.type_ahead_label
&& !modifiers.ctrl()
&& !modifiers.alt()
&& !modifiers.super_key()
&& let Some(c) = key.to_char()
&& let Some(idx) =
ta_state.search(c, current, n, cfg.type_ahead_timeout, |i| label_fn(i))
{
cfg.focused_index.set(Some(idx));
if let Some(ref sel) = cfg.selection {
sel.select(idx);
}
ensure_visible(&cfg, idx, ctx);
return EventResponse::Handled;
}
let new_idx: Option<usize> = if *key == logical_next {
if cursor.is_none() {
Some(0)
} else if !cfg.wrap_navigation && col == cols - 1 {
None
} else {
Some((current + 1).min(n - 1))
}
} else if *key == logical_prev {
if cursor.is_none() {
Some(n - 1)
} else if !cfg.wrap_navigation && col == 0 {
None
} else {
Some(current.saturating_sub(1))
}
} else {
match key {
Key::ArrowDown => {
if cursor.is_none() {
Some(0)
} else if current + cols < n {
Some(current + cols)
} else {
None
}
}
Key::ArrowUp => {
if cursor.is_none() {
Some(n - 1)
} else if current >= cols {
Some(current - cols)
} else {
None
}
}
Key::Home if modifiers.command() => Some(0),
Key::End if modifiers.command() => Some(n - 1),
Key::Home => Some(current - col), Key::End => Some((current - col + cols - 1).min(n - 1)),
Key::PageDown => {
let rows = rows_per_page(&cfg);
page_scroll(&cfg, rows as f32);
Some((current + rows * cols).min(n - 1))
}
Key::PageUp => {
let rows = rows_per_page(&cfg);
page_scroll(&cfg, -(rows as f32));
Some(current.saturating_sub(rows * cols))
}
Key::Tab if cfg.tab_traversal == GridTabTraversal::WithinGrid => {
if modifiers.shift() {
if current == 0 {
None
} else {
Some(current - 1)
}
} else if current + 1 < n {
Some(current + 1)
} else {
None
}
}
Key::Enter => {
cfg.focused_index.set(Some(current));
if let Some(ref cb) = cfg.on_tile_activate {
cb(current, ctx);
} else if let Some(ref sel) = cfg.selection {
sel.select(current);
}
return EventResponse::Handled;
}
Key::Space if modifiers.ctrl() => {
if let Some(ref sel) = cfg.selection {
sel.toggle(current);
}
cfg.focused_index.set(Some(current));
return EventResponse::Handled;
}
Key::Space => {
if let Some(ref sel) = cfg.selection {
sel.select(current);
}
cfg.focused_index.set(Some(current));
return EventResponse::Handled;
}
Key::Escape => {
cfg.focused_index.set(None);
return EventResponse::Handled;
}
_ => return EventResponse::Ignored,
}
};
let Some(idx) = new_idx else {
return EventResponse::Ignored;
};
cfg.focused_index.set(Some(idx));
let cursor_only = modifiers.ctrl()
&& !modifiers.shift()
&& !modifiers.alt()
&& (*key == logical_next
|| *key == logical_prev
|| *key == Key::ArrowDown
|| *key == Key::ArrowUp);
if !cursor_only && let Some(ref sel) = cfg.selection {
if modifiers.shift() {
sel.extend_to(idx);
} else {
sel.select(idx);
}
}
ensure_visible(&cfg, idx, ctx);
EventResponse::Handled
}
}
fn rows_per_page(cfg: &GridKeyConfig) -> usize {
let vp = cfg.viewport_height.get();
let step = cfg.strategy.estimated_row_height().max(1.0);
((vp / step).floor() as usize).max(1)
}
fn page_scroll(cfg: &GridKeyConfig, rows: f32) {
let step = cfg.strategy.estimated_row_height().max(1.0);
let max = cfg.max_scroll_y.get();
let new_y = (cfg.scroll_y.get() + rows * step).clamp(0.0, max);
cfg.scroll_y.set(new_y);
}
fn ensure_visible(cfg: &GridKeyConfig, idx: usize, ctx: &mut EventContext) {
let delta = cfg.strategy.scroll_delta_to_reveal(
idx,
cfg.scroll_y.get(),
cfg.viewport_height.get(),
cfg.viewport_width.get(),
ScrollAnchor::Auto,
);
if delta.abs() > 0.01 {
let max = cfg.max_scroll_y.get();
let new_y = (cfg.scroll_y.get() + delta).clamp(0.0, max);
cfg.scroll_y.set(new_y);
}
let Some(origin) = cfg.viewport_origin.get() else {
return;
};
let vp_w = cfg.viewport_width.get();
let r = cfg.strategy.tile_rect(idx, vp_w);
let scroll_y = cfg.scroll_y.get();
let rect =
teksilo_canvas::Rect::new(origin.x + r.x, origin.y + r.y - scroll_y, r.width, r.height);
ctx.ensure_visible(rect);
}