use std::rc::Rc;
use std::time::Duration;
use teksilo_core::event::{EventResponse, Key, WidgetEvent};
use teksilo_core::signal::Signal;
use teksilo_core::widget::EventContext;
use teksilo_data::SelectionMode;
use super::PaneBoundaries;
use super::body::SharedColumnWidths;
use super::column::{EditTriggers, TabTraversal};
use super::row_navigator::RowNavigator;
use super::selection::{CellSelectionModel, TableSelectionMode};
use crate::common::row_metrics::SharedRowMetrics;
use crate::common::type_ahead::TypeAheadState;
use crate::data_views::RowSelection;
#[derive(Clone)]
pub(crate) struct KeyHandlerConfig {
pub navigator: Rc<dyn RowNavigator>,
pub col_count: usize,
pub tree_column_display_pos: usize,
pub focused_cell: Signal<Option<(usize, usize)>>,
pub selection_mode: TableSelectionMode,
pub selection: Option<RowSelection>,
pub cell_selection: Option<CellSelectionModel>,
pub scroll_y: Signal<f32>,
pub max_scroll_y: Signal<f32>,
pub viewport_height: Rc<std::cell::Cell<f32>>,
pub body_bounds: Rc<std::cell::Cell<teksilo_canvas::Rect>>,
pub row_metrics: SharedRowMetrics,
pub tab_traversal: TabTraversal,
pub editing_cell: Signal<Option<(usize, usize)>>,
pub display_col_to_id: Rc<dyn Fn(usize) -> Option<String>>,
pub display_col_triggers: Rc<dyn Fn(usize) -> EditTriggers>,
#[allow(clippy::type_complexity)]
pub on_cell_edit_request:
Option<Rc<dyn Fn(usize, &str, &mut teksilo_core::widget::EventContext)>>,
#[allow(clippy::type_complexity)]
pub on_row_activate: Option<Rc<dyn Fn(usize, &mut teksilo_core::widget::EventContext)>>,
pub type_ahead: Rc<TypeAheadState>,
#[allow(clippy::type_complexity)]
pub type_ahead_label: Option<Rc<dyn Fn(usize) -> Option<String>>>,
pub type_ahead_timeout: Duration,
pub column_widths: SharedColumnWidths,
pub pane_boundaries: PaneBoundaries,
pub scroll_x: Signal<f32>,
pub max_scroll_x: Signal<f32>,
pub middle_viewport_width: Rc<std::cell::Cell<f32>>,
}
pub(crate) fn build_key_handler(
cfg: KeyHandlerConfig,
) -> impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static {
move |event, ctx: &mut EventContext| {
let WidgetEvent::KeyDown { key, modifiers, .. } = event else {
return EventResponse::Ignored;
};
let row_count = cfg.navigator.row_count();
if row_count == 0 || cfg.col_count == 0 {
return EventResponse::Ignored;
}
let raw = cfg.focused_cell.get().or_else(|| {
cfg.selection
.as_ref()
.and_then(|s| s.selected_indices().first().copied())
.map(|r| (r, 0))
});
let cursor = raw.map(|(r, c)| (r.min(row_count - 1), c.min(cfg.col_count - 1)));
let (row, col) = cursor.unwrap_or((0, 0));
if raw.is_some() && raw != Some((row, col)) {
cfg.focused_cell.set(Some((row, col)));
}
let rtl = ctx.is_rtl();
let on_tree_column = col == cfg.tree_column_display_pos;
let is_collapse_key = if rtl {
matches!(key, Key::ArrowRight)
} else {
matches!(key, Key::ArrowLeft)
};
let is_expand_key = if rtl {
matches!(key, Key::ArrowLeft)
} else {
matches!(key, Key::ArrowRight)
};
if is_collapse_key && on_tree_column && cfg.navigator.is_expanded(row) {
cfg.navigator.toggle_expanded(row);
return EventResponse::Handled;
}
if is_expand_key
&& on_tree_column
&& cfg.navigator.has_children(row)
&& !cfg.navigator.is_expanded(row)
{
cfg.navigator.toggle_expanded(row);
return EventResponse::Handled;
}
let viewport_h = cfg.viewport_height.get();
let new_pos: Option<(usize, usize)> = match key {
Key::ArrowUp => match cursor {
None => cfg.navigator.last_row().map(|r| (r, col)),
Some(_) => cfg.navigator.prev_row(row).map(|r| (r, col)),
},
Key::ArrowDown => match cursor {
None => cfg.navigator.first_row().map(|r| (r, col)),
Some(_) => cfg.navigator.next_row(row).map(|r| (r, col)),
},
Key::ArrowLeft => {
if rtl {
match cursor {
None => Some((row, 0)),
Some(_) => (col + 1 < cfg.col_count).then_some((row, col + 1)),
}
} else {
match cursor {
None => Some((row, cfg.col_count - 1)),
Some(_) => (col > 0).then(|| (row, col - 1)),
}
}
}
Key::ArrowRight => {
if rtl {
match cursor {
None => Some((row, cfg.col_count - 1)),
Some(_) => (col > 0).then(|| (row, col - 1)),
}
} else {
match cursor {
None => Some((row, 0)),
Some(_) => (col + 1 < cfg.col_count).then_some((row, col + 1)),
}
}
}
Key::Home if !modifiers.command() => Some((row, 0)),
Key::End if !modifiers.command() => Some((row, cfg.col_count - 1)),
Key::Home if modifiers.command() => cfg.navigator.first_row().map(|r| (r, 0)),
Key::End if modifiers.command() => {
cfg.navigator.last_row().map(|r| (r, cfg.col_count - 1))
}
Key::PageUp => {
let new_y = (cfg.scroll_y.get() - viewport_h).max(0.0);
cfg.scroll_y.set(new_y);
let r = {
let mut m = cfg.row_metrics.borrow_mut();
m.resize(row_count);
let target_y = (m.row_top(row) - viewport_h).max(0.0);
m.row_at(target_y)
};
let r = if r == row { row.saturating_sub(1) } else { r };
Some((r, col))
}
Key::PageDown => {
let new_y = (cfg.scroll_y.get() + viewport_h).min(cfg.max_scroll_y.get());
cfg.scroll_y.set(new_y);
let r = {
let mut m = cfg.row_metrics.borrow_mut();
m.resize(row_count);
let target_y = m.row_top(row) + viewport_h;
m.row_at(target_y)
};
let r = if r == row {
(row + 1).min(row_count - 1)
} else {
r.min(row_count - 1)
};
Some((r, col))
}
Key::Tab if modifiers.ctrl() => return EventResponse::Ignored,
Key::Tab => {
if modifiers.shift() {
if col > 0 {
Some((row, col - 1))
} else if let Some(prev) = cfg.navigator.prev_row(row) {
Some((prev, cfg.col_count - 1))
} else if cfg.tab_traversal == TabTraversal::OutOfTable {
return EventResponse::Ignored;
} else {
Some((row, col))
}
} else {
if col + 1 < cfg.col_count {
Some((row, col + 1))
} else if let Some(next) = cfg.navigator.next_row(row) {
Some((next, 0))
} else if cfg.tab_traversal == TabTraversal::OutOfTable {
return EventResponse::Ignored;
} else {
Some((row, col))
}
}
}
Key::Space => {
toggle_selection(&cfg, row, col);
cfg.focused_cell.set(Some((row, col)));
return EventResponse::Handled;
}
Key::Enter => {
if let Some(ref f) = cfg.on_row_activate {
f(row, ctx);
} else {
toggle_selection(&cfg, row, col);
}
return EventResponse::Handled;
}
Key::F2 if (cfg.display_col_triggers)(col).contains(EditTriggers::F2) => {
if let Some(col_id) = (cfg.display_col_to_id)(col) {
cfg.editing_cell.set(Some((row, col)));
if let Some(ref f) = cfg.on_cell_edit_request {
f(row, &col_id, ctx);
}
return EventResponse::Handled;
}
return EventResponse::Ignored;
}
k if (cfg.display_col_triggers)(col).contains(EditTriggers::ANY_KEY)
&& !modifiers.ctrl()
&& !modifiers.alt()
&& !modifiers.super_key()
&& k.to_char().is_some() =>
{
if let Some(col_id) = (cfg.display_col_to_id)(col) {
cfg.editing_cell.set(Some((row, col)));
if let Some(ref f) = cfg.on_cell_edit_request {
f(row, &col_id, ctx);
}
return EventResponse::Ignored;
}
return EventResponse::Ignored;
}
k if cfg.type_ahead_label.is_some()
&& !modifiers.ctrl()
&& !modifiers.alt()
&& !modifiers.super_key()
&& k.to_char().is_some() =>
{
let c = k.to_char().unwrap();
let label = cfg.type_ahead_label.as_ref().unwrap();
if let Some(nr) =
cfg.type_ahead
.search(c, row, row_count, cfg.type_ahead_timeout, |i| label(i))
{
cfg.focused_cell.set(Some((nr, col)));
apply_selection_extension(&cfg, nr, col, false);
ensure_row_visible(&cfg, nr, row_count, ctx);
ensure_col_visible(&cfg, col);
return EventResponse::Handled;
}
return EventResponse::Ignored;
}
Key::A if modifiers.command() => {
select_all(&cfg, row_count);
return EventResponse::Handled;
}
Key::Escape => {
if cfg.editing_cell.get().is_some() {
cfg.editing_cell.set(None);
} else {
cfg.focused_cell.set(None);
}
return EventResponse::Handled;
}
_ => None,
};
if let Some((nr, nc)) = new_pos {
cfg.focused_cell.set(Some((nr, nc)));
let is_arrow = matches!(
key,
Key::ArrowUp | Key::ArrowDown | Key::ArrowLeft | Key::ArrowRight
);
let move_cursor_only = is_arrow && modifiers.ctrl() && !modifiers.shift();
if !move_cursor_only {
apply_selection_extension(&cfg, nr, nc, modifiers.shift());
}
ensure_row_visible(&cfg, nr, row_count, ctx);
ensure_col_visible(&cfg, nc);
return EventResponse::Handled;
}
EventResponse::Ignored
}
}
fn ensure_row_visible(
cfg: &KeyHandlerConfig,
row: usize,
row_count: usize,
ctx: &mut EventContext,
) {
let scroll = cfg.scroll_y.get();
let new_scroll = {
let mut m = cfg.row_metrics.borrow_mut();
m.resize(row_count);
m.scroll_for_ensure_visible(
row,
scroll,
cfg.viewport_height.get(),
cfg.max_scroll_y.get(),
)
};
if (new_scroll - scroll).abs() > f32::EPSILON {
cfg.scroll_y.set(new_scroll);
}
crate::common::row_metrics::chase_row_into_outer_view(
ctx,
&cfg.row_metrics,
cfg.body_bounds.get(),
row,
new_scroll,
);
}
fn ensure_col_visible(cfg: &KeyHandlerConfig, display_col: usize) {
let b = cfg.pane_boundaries;
if display_col < b.leading_count || display_col >= b.middle_end {
return;
}
let widths = cfg.column_widths.borrow();
let Some(w) = widths.get(display_col).copied() else {
return;
};
let x: f32 = widths[b.leading_count..display_col].iter().sum();
drop(widths);
let viewport_w = cfg.middle_viewport_width.get();
let scroll = cfg.scroll_x.get();
let max = cfg.max_scroll_x.get();
let new_scroll = if x < scroll {
x
} else if x + w > scroll + viewport_w {
(x + w - viewport_w).max(0.0)
} else {
scroll
}
.clamp(0.0, max.max(0.0));
if (new_scroll - scroll).abs() > f32::EPSILON {
cfg.scroll_x.set(new_scroll);
}
}
fn toggle_selection(cfg: &KeyHandlerConfig, row: usize, col: usize) {
match cfg.selection_mode {
TableSelectionMode::SingleRow | TableSelectionMode::MultiRow => {
if let Some(ref s) = cfg.selection {
if s.is_selected(row) {
if cfg.selection_mode == TableSelectionMode::MultiRow {
s.toggle(row);
} else {
s.clear();
}
} else {
s.select(row);
}
}
}
TableSelectionMode::SingleCell | TableSelectionMode::MultiCell => {
if let Some(ref cs) = cfg.cell_selection {
if cs.is_selected(row, col) && cfg.selection_mode == TableSelectionMode::MultiCell {
cs.toggle(row, col);
} else {
cs.select(row, col);
}
}
}
TableSelectionMode::None => {}
}
}
fn apply_selection_extension(cfg: &KeyHandlerConfig, row: usize, col: usize, shift: bool) {
match cfg.selection_mode {
TableSelectionMode::MultiRow => {
if let Some(ref s) = cfg.selection {
if shift && s.mode() == SelectionMode::Multi {
s.extend_to(row);
} else {
s.select(row);
}
}
}
TableSelectionMode::SingleRow => {
if let Some(ref s) = cfg.selection {
s.select(row);
}
}
TableSelectionMode::MultiCell => {
if let Some(ref cs) = cfg.cell_selection {
if shift {
cs.extend_to(row, col);
} else {
cs.select(row, col);
}
}
}
TableSelectionMode::SingleCell => {
if let Some(ref cs) = cfg.cell_selection {
cs.select(row, col);
}
}
TableSelectionMode::None => {}
}
}
fn select_all(cfg: &KeyHandlerConfig, row_count: usize) {
match cfg.selection_mode {
TableSelectionMode::MultiRow => {
if let Some(ref s) = cfg.selection {
s.select_all(row_count);
}
}
TableSelectionMode::MultiCell => {
if let Some(ref cs) = cfg.cell_selection {
cs.select_all(row_count, cfg.col_count);
}
}
_ => {}
}
}