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::list_nav;
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)>>,
#[allow(clippy::type_complexity)]
pub cell_map: Rc<std::cell::RefCell<Vec<((usize, usize), teksilo_core::widget_id::WidgetId)>>>,
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)
};
let plain_arrow =
!modifiers.shift() && !modifiers.ctrl() && !modifiers.alt() && !modifiers.super_key();
if plain_arrow && is_collapse_key && on_tree_column {
if cfg.navigator.is_expanded(row) {
cfg.navigator.toggle_expanded(row);
return EventResponse::Handled;
}
if let Some(parent) = cfg.navigator.parent_row(row) {
cfg.focused_cell.set(Some((parent, col)));
apply_selection_extension(&cfg, parent, col, false);
ensure_row_visible(&cfg, parent, row_count, ctx);
return EventResponse::Handled;
}
}
if plain_arrow
&& is_expand_key
&& on_tree_column
&& cfg.navigator.has_children(row)
&& !cfg.navigator.is_expanded(row)
{
cfg.navigator.toggle_expanded(row);
return EventResponse::Handled;
}
if is_hierarchical(&cfg)
&& let Some(chord) = list_nav::tree_chord(*key, *modifiers)
{
let handled = with_navigator_subtree(&cfg, |ops| match chord {
list_nav::TreeChord::ExpandSubtree => {
crate::common::tree_expand::expand_subtree(ops, row)
}
list_nav::TreeChord::CollapseSubtree => {
crate::common::tree_expand::collapse_subtree(ops, row)
}
list_nav::TreeChord::ExpandOne => {
if cfg.navigator.has_children(row) && !cfg.navigator.is_expanded(row) {
cfg.navigator.toggle_expanded(row);
true
} else {
false
}
}
list_nav::TreeChord::CollapseOne => {
if cfg.navigator.is_expanded(row) {
cfg.navigator.toggle_expanded(row);
true
} else {
false
}
}
});
return if handled {
EventResponse::Handled
} else {
EventResponse::Ignored
};
}
if let Some(alias) = list_nav::mac_alias(*key, *modifiers, rtl) {
let handled = match alias {
list_nav::MacAlias::Activate => {
if let Some(ref f) = cfg.on_row_activate {
f(row, ctx);
true
} else {
false
}
}
list_nav::MacAlias::CollapseOrParent => {
if cfg.navigator.is_expanded(row) {
cfg.navigator.toggle_expanded(row);
true
} else if let Some(parent) = cfg.navigator.parent_row(row) {
cfg.focused_cell.set(Some((parent, col)));
apply_selection_extension(&cfg, parent, col, false);
ensure_row_visible(&cfg, parent, row_count, ctx);
true
} else {
false
}
}
list_nav::MacAlias::ExpandSubtree => with_navigator_subtree(&cfg, |ops| {
crate::common::tree_expand::expand_subtree(ops, row)
}),
list_nav::MacAlias::CollapseSubtree => with_navigator_subtree(&cfg, |ops| {
crate::common::tree_expand::collapse_subtree(ops, row)
}),
};
return if handled {
EventResponse::Handled
} else {
EventResponse::Ignored
};
}
if cfg.editing_cell.get().is_some()
&& !matches!(key, Key::Escape | Key::Enter | Key::Tab | Key::F2)
{
return EventResponse::Ignored;
}
let viewport_h = cfg.viewport_height.get();
let view_kind = if cfg.selection_mode.is_cell_mode() {
list_nav::ViewKind::CellGrid
} else {
list_nav::ViewKind::Linear
};
let nav = list_nav::nav_chord(*key, *modifiers, view_kind);
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 | Key::End | Key::PageUp | Key::PageDown => {
let Some(chord) = nav else {
return EventResponse::Ignored;
};
match chord.movement {
list_nav::NavMove::RowFirst => Some((row, 0)),
list_nav::NavMove::RowLast => Some((row, cfg.col_count - 1)),
list_nav::NavMove::First => cfg
.navigator
.first_row()
.map(|r| (r, corner_column(&cfg, view_kind, col, false))),
list_nav::NavMove::Last => cfg
.navigator
.last_row()
.map(|r| (r, corner_column(&cfg, view_kind, col, true))),
list_nav::NavMove::Page { down } => {
let new_y = if down {
(cfg.scroll_y.get() + viewport_h).min(cfg.max_scroll_y.get())
} else {
(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 = if down {
m.row_top(row) + viewport_h
} else {
(m.row_top(row) - viewport_h).max(0.0)
};
m.row_at(target_y)
};
let r = if r == row && down {
(row + 1).min(row_count - 1)
} else if r == row {
row.saturating_sub(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
if cfg.selection_mode == TableSelectionMode::MultiCell && modifiers.ctrl() =>
{
select_column(&cfg, col, row_count);
cfg.focused_cell.set(Some((row, col)));
return EventResponse::Handled;
}
Key::Space
if cfg.selection_mode == TableSelectionMode::MultiCell && modifiers.shift() =>
{
select_row_cells(&cfg, row);
cfg.focused_cell.set(Some((row, col)));
return EventResponse::Handled;
}
Key::Space => {
if let Some(cell_id) = cfg
.cell_map
.borrow()
.iter()
.find(|(pos, _)| *pos == (row, col))
.map(|(_, id)| *id)
{
let fallback_cfg = cfg.clone();
ctx.row_space_activate(
cell_id,
std::rc::Rc::new(move || {
toggle_selection(&fallback_cfg, row, col);
}),
);
cfg.focused_cell.set(Some((row, col)));
return EventResponse::Handled;
}
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() && modifiers.shift() => {
if !cfg.selection_mode.is_multi() {
return EventResponse::Ignored;
}
clear_selection(&cfg);
return EventResponse::Handled;
}
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();
match nav.map(|c| c.selection) {
Some(list_nav::SelectionOp::Suppress) => {}
Some(list_nav::SelectionOp::Extend) => {
apply_selection_extension(&cfg, nr, nc, true)
}
Some(list_nav::SelectionOp::ExtendAdditive) => {
apply_additive_extension(&cfg, nr, nc)
}
Some(list_nav::SelectionOp::Replace) => {
apply_selection_extension(&cfg, nr, nc, false)
}
None if !move_cursor_only => {
apply_selection_extension(&cfg, nr, nc, modifiers.shift())
}
None => {}
}
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 corner_column(
cfg: &KeyHandlerConfig,
view_kind: list_nav::ViewKind,
col: usize,
last: bool,
) -> usize {
if view_kind != list_nav::ViewKind::CellGrid {
return col;
}
if is_hierarchical(cfg) {
col
} else if last {
cfg.col_count - 1
} else {
0
}
}
fn is_hierarchical(cfg: &KeyHandlerConfig) -> bool {
cfg.navigator
.first_row()
.and_then(|r| cfg.navigator.depth(r))
.is_some()
}
fn with_navigator_subtree<R>(
cfg: &KeyHandlerConfig,
f: impl FnOnce(&crate::common::tree_expand::SubtreeOps) -> R,
) -> R {
let nav = &cfg.navigator;
let count = || nav.row_count();
let row = |i: usize| {
nav.depth(i)
.map(|d| (d, nav.has_children(i), nav.is_expanded(i)))
};
let set = |i: usize, on: bool| {
if nav.is_expanded(i) != on {
nav.toggle_expanded(i);
}
};
f(&crate::common::tree_expand::SubtreeOps {
visible_count: &count,
row: &row,
set_expanded: &set,
})
}
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 apply_additive_extension(cfg: &KeyHandlerConfig, row: usize, col: usize) {
match cfg.selection_mode {
TableSelectionMode::MultiRow => {
if let Some(ref s) = cfg.selection {
s.extend_to_additive(row);
}
}
TableSelectionMode::MultiCell => {
if let Some(ref cs) = cfg.cell_selection {
cs.extend_to(row, col);
}
}
_ => apply_selection_extension(cfg, row, col, true),
}
}
fn select_column(cfg: &KeyHandlerConfig, col: usize, row_count: usize) {
if let Some(ref cs) = cfg.cell_selection {
cs.select_cells((0..row_count).map(|r| (r, col)));
}
}
fn select_row_cells(cfg: &KeyHandlerConfig, row: usize) {
if let Some(ref cs) = cfg.cell_selection {
cs.select_cells((0..cfg.col_count).map(|c| (row, c)));
}
}
fn clear_selection(cfg: &KeyHandlerConfig) {
match cfg.selection_mode {
TableSelectionMode::MultiRow => {
if let Some(ref s) = cfg.selection {
s.clear();
}
}
TableSelectionMode::MultiCell => {
if let Some(ref cs) = cfg.cell_selection {
cs.clear();
}
}
_ => {}
}
}
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);
}
}
_ => {}
}
}