use std::cell::Cell;
use std::rc::Rc;
use teksilo_canvas::{Point, Rect};
use teksilo_core::event::{EventResponse, PointerButton, ScrollDelta, WidgetEvent};
use teksilo_core::widget::EventContext;
use teksilo_text::text_document::{MoveMode, SelectionType};
use super::state::{DragState, SharedState};
use super::sync_cursor_signals;
use crate::common::scroll::{OverscrollBehavior, scroll_clamp_axis, scroll_response};
use crate::rich_text::hit_test;
fn to_engine_local(state: &SharedState, position: &Point) -> Point {
let st = state.borrow();
Point::new(
position.x + st.node_origin.x - st.viewport_origin.x,
position.y + st.node_origin.y - st.viewport_origin.y,
)
}
pub(super) fn handle_pointer_event(
state: &SharedState,
v_scrollbar_bounds: &Rc<Cell<Rect>>,
h_scrollbar_bounds: &Rc<Cell<Rect>>,
event: &WidgetEvent,
ctx: &mut EventContext,
) -> EventResponse {
match event {
WidgetEvent::PointerDown {
position,
button,
modifiers,
} => {
if *button != PointerButton::Primary {
return EventResponse::Ignored;
}
if v_scrollbar_bounds.get().contains(*position)
|| h_scrollbar_bounds.get().contains(*position)
{
return EventResponse::Ignored;
}
let local = to_engine_local(state, position);
let hit = {
let st = state.borrow();
hit_test::hit_test_at(&st.engine, local, 0.0, 0.0)
};
let Some(hit) = hit else {
return EventResponse::Ignored;
};
let shift = modifiers.shift();
let alt = modifiers.alt();
{
let mut st = state.borrow_mut();
if alt {
add_caret_at(&mut st, hit.position);
} else {
let mode = if shift {
MoveMode::KeepAnchor
} else {
MoveMode::MoveAnchor
};
st.clear_extra_carets();
st.cursor.set_position(hit.position, mode);
}
st.cursor_affinity = hit.affinity;
st.drag_state = DragState::Selecting {
auto_scroll_v_per_s: 0.0,
};
st.preferred_x = None;
}
sync_cursor_signals(state);
super::keyboard::ensure_caret_visible(state);
ctx.request_frame();
EventResponse::Ignored
}
WidgetEvent::PointerMove { position } => {
let (dragging, viewport_height) = {
let st = state.borrow();
(
matches!(st.drag_state, DragState::Selecting { .. }),
st.viewport_height,
)
};
if !dragging {
return EventResponse::Ignored;
}
let local = to_engine_local(state, position);
let clamped = Point::new(
local.x,
local.y.clamp(2.0, (viewport_height - 2.0).max(2.0)),
);
let hit = {
let st = state.borrow();
hit_test::hit_test_at(&st.engine, clamped, 0.0, 0.0)
};
if let Some(hit) = hit {
{
let mut st = state.borrow_mut();
st.cursor.set_position(hit.position, MoveMode::KeepAnchor);
st.cursor_affinity = hit.affinity;
}
sync_cursor_signals(state);
}
{
let mut st = state.borrow_mut();
let v = auto_scroll_velocity(local.y, viewport_height);
st.drag_state = DragState::Selecting {
auto_scroll_v_per_s: v,
};
if v != 0.0
&& let Some(handle) = &st.frame_request
{
handle.set(true);
}
}
ctx.request_frame();
EventResponse::Handled
}
WidgetEvent::PointerUp { .. } => {
state.borrow_mut().drag_state = DragState::Idle;
EventResponse::Ignored
}
_ => EventResponse::Ignored,
}
}
fn auto_scroll_velocity(y: f32, viewport_height: f32) -> f32 {
const MARGIN: f32 = 20.0;
const MAX_PER_SEC: f32 = 60.0 * 60.0;
if y < MARGIN {
let intensity = ((MARGIN - y) / MARGIN).clamp(0.0, 1.0);
-MAX_PER_SEC * intensity
} else if y > viewport_height - MARGIN {
let intensity = ((y - (viewport_height - MARGIN)) / MARGIN).clamp(0.0, 1.0);
MAX_PER_SEC * intensity
} else {
0.0
}
}
pub(super) fn add_caret_at(st: &mut super::state::CodeEditorState, pos: usize) {
if st.cursor.position() == pos {
return;
}
if let Some(i) = st.extra_carets.iter().position(|c| c.position() == pos) {
st.extra_carets.remove(i);
return;
}
let c = st.document.cursor();
c.set_position(pos, MoveMode::MoveAnchor);
st.extra_carets.push(c);
}
pub(super) fn handle_scroll(
state: &SharedState,
overscroll: OverscrollBehavior,
event: &WidgetEvent,
ctx: &mut EventContext,
) -> EventResponse {
let WidgetEvent::Scroll { delta, .. } = event else {
return EventResponse::Ignored;
};
let (dx, dy) = match delta {
ScrollDelta::Lines { x, y } => (*x * 16.0, *y * 16.0),
ScrollDelta::Pixels { x, y } => (*x, *y),
};
let st = state.borrow();
let (new_x, moved_x) = scroll_clamp_axis(st.scroll_x.get(), dx, st.max_scroll_x.get());
let (new_y, moved_y) = scroll_clamp_axis(st.scroll_y.get(), dy, st.max_scroll_y.get());
if moved_x {
st.scroll_x.set(new_x);
}
if moved_y {
st.scroll_y.set(new_y);
}
drop(st);
if moved_x || moved_y {
ctx.request_frame();
}
scroll_response(
moved_x || moved_y,
overscroll == OverscrollBehavior::Contain,
)
}
pub(super) fn handle_double_tap(state: &SharedState, pos: Point, ctx: &mut EventContext) {
tap_select(state, pos, SelectionType::WordUnderCursor);
ctx.request_frame();
}
pub(super) fn handle_triple_tap(state: &SharedState, pos: Point, ctx: &mut EventContext) {
tap_select(state, pos, SelectionType::LineUnderCursor);
ctx.request_frame();
}
fn tap_select(state: &SharedState, pos: Point, kind: SelectionType) {
let local = to_engine_local(state, &pos);
let hit = {
let st = state.borrow();
hit_test::hit_test_at(&st.engine, local, 0.0, 0.0)
};
let Some(hit) = hit else {
return;
};
{
let mut st = state.borrow_mut();
st.clear_extra_carets();
st.cursor.set_position(hit.position, MoveMode::MoveAnchor);
st.cursor.select(kind);
}
sync_cursor_signals(state);
}