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::list_nav;
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,
pub(crate) type_ahead: Rc<TypeAheadState>,
#[allow(clippy::type_complexity)]
pub(crate) tile_map: Rc<std::cell::RefCell<Vec<(usize, teksilo_core::widget_id::WidgetId)>>>,
#[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 = cfg.type_ahead.clone();
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;
let nav = list_nav::nav_chord(*key, *modifiers, list_nav::ViewKind::TileGrid);
if modifiers.command() && *key == Key::A {
if let Some(ref sel) = cfg.selection
&& sel.mode() == teksilo_data::SelectionMode::Multi
{
if modifiers.shift() {
sel.clear();
} else {
sel.select_all(n);
}
return EventResponse::Handled;
}
return EventResponse::Ignored;
}
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(alias) = list_nav::mac_alias(*key, *modifiers, rtl) {
if alias == list_nav::MacAlias::Activate {
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;
}
return EventResponse::Ignored;
}
if let Some(ref label_fn) = cfg.type_ahead_label
&& !modifiers.ctrl()
&& !modifiers.alt()
&& !modifiers.super_key()
&& let Some(c) = key.to_char()
{
return match ta_state.search(c, current, n, cfg.type_ahead_timeout, |i| label_fn(i)) {
Some(idx) => {
cfg.focused_index.set(Some(idx));
if let Some(ref sel) = cfg.selection {
sel.select(idx);
}
ensure_visible(&cfg, idx, ctx);
EventResponse::Handled
}
None => EventResponse::Ignored,
};
}
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 | Key::End | Key::PageUp | Key::PageDown => {
let Some(chord) = nav else {
return EventResponse::Ignored;
};
Some(match chord.movement {
list_nav::NavMove::First | list_nav::NavMove::RowFirst => 0,
list_nav::NavMove::Last | list_nav::NavMove::RowLast => n - 1,
list_nav::NavMove::Page { down } => {
page_target(&cfg, current, cols, n, down)
}
})
}
Key::Tab if modifiers.ctrl() => return EventResponse::Ignored,
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 => {
let selection = cfg.selection.clone();
let fallback = std::rc::Rc::new(move || {
if let Some(ref sel) = selection {
if sel.mode() == teksilo_data::SelectionMode::Multi {
sel.toggle(current);
} else {
sel.select(current);
}
}
});
match cfg
.tile_map
.borrow()
.iter()
.find(|(i, _)| *i == current)
.map(|(_, id)| *id)
{
Some(tile_id) => ctx.row_space_activate(tile_id, fallback),
None => fallback(),
}
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 op = match nav {
Some(chord) => chord.selection,
None if modifiers.ctrl()
&& !modifiers.shift()
&& !modifiers.alt()
&& (*key == logical_next
|| *key == logical_prev
|| *key == Key::ArrowDown
|| *key == Key::ArrowUp) =>
{
list_nav::SelectionOp::Suppress
}
None if modifiers.shift() => list_nav::SelectionOp::Extend,
None => list_nav::SelectionOp::Replace,
};
if let Some(ref sel) = cfg.selection {
match op {
list_nav::SelectionOp::Replace => sel.select(idx),
list_nav::SelectionOp::Suppress => {}
list_nav::SelectionOp::Extend => sel.extend_to(idx),
list_nav::SelectionOp::ExtendAdditive => sel.extend_to_additive(idx),
}
}
ensure_visible(&cfg, idx, ctx);
EventResponse::Handled
}
}
fn page_target(cfg: &GridKeyConfig, current: usize, cols: usize, n: usize, down: bool) -> usize {
let viewport = cfg.viewport_height.get();
let width = cfg.viewport_width.get();
let origin = cfg.strategy.tile_rect(current, width).y;
let mut candidate = current;
let mut probe = current;
loop {
probe = if down {
match probe.checked_add(cols) {
Some(next) if next < n => next,
_ => break,
}
} else {
match probe.checked_sub(cols) {
Some(prev) => prev,
None => break,
}
};
let y = cfg.strategy.tile_rect(probe, width).y;
if (y - origin).abs() > viewport {
candidate = if candidate == current {
probe
} else {
candidate
};
break;
}
candidate = probe;
}
if candidate == current {
if down {
(current + cols).min(n - 1)
} else {
current.saturating_sub(cols)
}
} else {
candidate
}
}
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);
}