use std::cell::Cell;
use std::rc::Rc;
use teksilo_canvas::{Point, Rect};
use teksilo_core::gesture::DragPhase;
use teksilo_core::signal::Signal;
use teksilo_core::widget::EventContext;
use teksilo_data::SelectionModel;
use super::layout::GridLayoutStrategy;
#[derive(Debug, Clone, Copy)]
pub(crate) struct MarqueeState {
pub(crate) origin: Point,
pub(crate) current: Point,
pub(crate) origin_scroll: f32,
pub(crate) additive: bool,
}
impl MarqueeState {
pub(crate) fn local_rect(&self, scroll_y: f32) -> Rect {
let origin_y = self.origin.y + (self.origin_scroll - scroll_y);
let x = self.origin.x.min(self.current.x);
let y = origin_y.min(self.current.y);
let w = (self.origin.x - self.current.x).abs();
let h = (origin_y - self.current.y).abs();
Rect::new(x, y, w, h)
}
}
const MARQUEE_EDGE_ZONE: f32 = 32.0;
const MARQUEE_MAX_VELOCITY: f32 = 12.0;
pub(crate) fn marquee_auto_scroll_step(pointer_y: f32, viewport_height: f32) -> f32 {
let above = (MARQUEE_EDGE_ZONE - pointer_y).max(0.0);
let below = (pointer_y - (viewport_height - MARQUEE_EDGE_ZONE)).max(0.0);
if above > 0.0 {
-(above / MARQUEE_EDGE_ZONE).min(1.0) * MARQUEE_MAX_VELOCITY
} else if below > 0.0 {
(below / MARQUEE_EDGE_ZONE).min(1.0) * MARQUEE_MAX_VELOCITY
} else {
0.0
}
}
pub(crate) struct MarqueeConfig {
pub(crate) marquee: Signal<Option<MarqueeState>>,
pub(crate) selection: SelectionModel,
pub(crate) strategy: Rc<dyn GridLayoutStrategy>,
pub(crate) scroll_y: Signal<f32>,
pub(crate) viewport_width: Rc<Cell<f32>>,
pub(crate) len_fn: Rc<dyn Fn() -> usize>,
pub(crate) additive_mods: Rc<Cell<bool>>,
}
pub(crate) fn build_marquee_handler(
cfg: MarqueeConfig,
) -> impl FnMut(DragPhase, &mut EventContext) + 'static {
move |phase, ctx| {
let vp_w = cfg.viewport_width.get();
let scroll = cfg.scroll_y.get();
match phase {
DragPhase::Started { position, .. } => {
let cp = Point::new(position.x, position.y + scroll);
if cfg
.strategy
.index_at_point(cp, (cfg.len_fn)(), vp_w)
.is_some()
{
return;
}
cfg.marquee.set(Some(MarqueeState {
origin: position,
current: position,
origin_scroll: scroll,
additive: cfg.additive_mods.get(),
}));
ctx.request_frame();
}
DragPhase::Moved { position, .. } => {
if let Some(mut st) = cfg.marquee.get() {
st.current = position;
cfg.marquee.set(Some(st));
ctx.request_frame();
}
}
DragPhase::Ended { position } => {
if let Some(mut st) = cfg.marquee.get() {
st.current = position;
let local = st.local_rect(scroll);
let content = Rect::new(local.x, local.y + scroll, local.width, local.height);
let hits = cfg
.strategy
.hit_indices_in_rect(content, (cfg.len_fn)(), vp_w);
cfg.selection.select_indices(hits, st.additive);
cfg.marquee.set(None);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn state(origin: Point, current: Point, origin_scroll: f32) -> MarqueeState {
MarqueeState {
origin,
current,
origin_scroll,
additive: false,
}
}
#[test]
fn local_rect_matches_naive_diff_when_scroll_is_unchanged() {
let st = state(Point::new(10.0, 20.0), Point::new(60.0, 90.0), 0.0);
let r = st.local_rect(0.0);
assert!((r.x - 10.0).abs() < 0.001);
assert!((r.y - 20.0).abs() < 0.001);
assert!((r.width - 50.0).abs() < 0.001);
assert!((r.height - 70.0).abs() < 0.001);
}
#[test]
fn local_rect_tracks_anchor_as_scroll_changes() {
let st = state(Point::new(5.0, 380.0), Point::new(5.0, 380.0), 0.0);
let r = st.local_rect(100.0);
assert!(
(r.y - 280.0).abs() < 0.001,
"origin should have visually moved up by the scrolled amount, got y={}",
r.y
);
assert!((r.height - 100.0).abs() < 0.001, "height = {}", r.height);
}
#[test]
fn local_rect_content_span_grows_with_auto_scroll() {
let st = state(Point::new(5.0, 380.0), Point::new(5.0, 380.0), 0.0);
let before = st.local_rect(0.0);
let content_before = before.y + 0.0;
let after = st.local_rect(100.0);
let content_after = after.y + 100.0;
assert!(
(content_before - content_after).abs() < 0.001,
"content-space anchor drifted: {content_before} vs {content_after}"
);
assert!((after.height - before.height - 100.0).abs() < 0.001);
}
#[test]
fn auto_scroll_step_is_zero_away_from_edges() {
assert_eq!(marquee_auto_scroll_step(200.0, 400.0), 0.0);
}
#[test]
fn auto_scroll_step_ramps_near_bottom_edge() {
let near = marquee_auto_scroll_step(400.0 - 16.0, 400.0);
assert!(near > 0.0, "should scroll down, got {near}");
assert!(near < MARQUEE_MAX_VELOCITY);
let at_edge = marquee_auto_scroll_step(400.0, 400.0);
assert!((at_edge - MARQUEE_MAX_VELOCITY).abs() < 0.001);
let beyond = marquee_auto_scroll_step(500.0, 400.0);
assert!((beyond - MARQUEE_MAX_VELOCITY).abs() < 0.001, "{beyond}");
}
#[test]
fn auto_scroll_step_ramps_near_top_edge() {
let near = marquee_auto_scroll_step(16.0, 400.0);
assert!(near < 0.0, "should scroll up, got {near}");
assert!(near > -MARQUEE_MAX_VELOCITY);
let at_edge = marquee_auto_scroll_step(0.0, 400.0);
assert!((at_edge + MARQUEE_MAX_VELOCITY).abs() < 0.001);
let beyond = marquee_auto_scroll_step(-50.0, 400.0);
assert!((beyond + MARQUEE_MAX_VELOCITY).abs() < 0.001, "{beyond}");
}
}