use crate::backend::InputEvent;
use crate::components::scroll::{ScrollAxis, ScrollConfig, ScrollOffset};
use crate::ecs::{Entity, World};
use crate::event::hit_test::hit_test;
use crate::types::Fixed;
pub struct ScrollDragState {
pub active: bool,
pub resolved: bool,
pub target: Entity,
pub hit_entity: Entity,
pub start_x: Fixed,
pub start_y: Fixed,
pub last_x: Fixed,
pub last_y: Fixed,
pub vel_x: Fixed,
pub vel_y: Fixed,
}
impl Default for ScrollDragState {
fn default() -> Self {
let null = Entity {
id: 0,
generation: 0,
};
Self {
active: false,
resolved: false,
target: null,
hit_entity: null,
start_x: Fixed::ZERO,
start_y: Fixed::ZERO,
last_x: Fixed::ZERO,
last_y: Fixed::ZERO,
vel_x: Fixed::ZERO,
vel_y: Fixed::ZERO,
}
}
}
const DIRECTION_THRESHOLD: Fixed = Fixed::from_int(5);
pub fn scroll_system(
world: &mut World,
root: Entity,
event: &InputEvent,
screen_w: u16,
screen_h: u16,
) {
match event {
InputEvent::Touch { x, y } => {
let hit = hit_test(world, root, *x, *y, screen_w, screen_h);
if let Some(state) = world.resource_mut::<ScrollDragState>() {
state.active = true;
state.resolved = false;
state.hit_entity = hit.unwrap_or(Entity {
id: 0,
generation: 0,
});
state.start_x = *x;
state.start_y = *y;
state.last_x = *x;
state.last_y = *y;
state.vel_x = Fixed::ZERO;
state.vel_y = Fixed::ZERO;
}
}
InputEvent::TouchMove { x, y } => {
let (active, resolved, _target, hit_entity, last_x, last_y, start_x, start_y) = {
let Some(state) = world.resource::<ScrollDragState>() else {
return;
};
(
state.active,
state.resolved,
state.target,
state.hit_entity,
state.last_x,
state.last_y,
state.start_x,
state.start_y,
)
};
if !active {
return;
}
if !resolved {
let total_dx = (*x - start_x).abs();
let total_dy = (*y - start_y).abs();
if total_dx < DIRECTION_THRESHOLD && total_dy < DIRECTION_THRESHOLD {
if let Some(state) = world.resource_mut::<ScrollDragState>() {
state.last_x = *x;
state.last_y = *y;
}
return;
}
let gesture_dir = if total_dy > total_dx {
ScrollAxis::Vertical
} else {
ScrollAxis::Horizontal
};
let scroll_dx = -(*x - start_x);
let scroll_dy = -(*y - start_y);
let found = find_scroll_target_for_direction(
world,
hit_entity,
gesture_dir,
scroll_dx,
scroll_dy,
);
if let Some(state) = world.resource_mut::<ScrollDragState>() {
state.resolved = true;
if let Some(t) = found {
state.target = t;
} else {
state.active = false;
return;
}
}
}
let (target,) = {
let Some(state) = world.resource::<ScrollDragState>() else {
return;
};
(state.target,)
};
let dx = *x - last_x;
let dy = *y - last_y;
let config = world.get::<ScrollConfig>(target);
let dir = config.map(|c| c.direction).unwrap_or(ScrollAxis::Vertical);
let computed = world.get::<crate::widget::ComputedRect>(target);
let container_h = computed.map(|c| c.0.h).unwrap_or(Fixed::ZERO);
let container_w = computed.map(|c| c.0.w).unwrap_or(Fixed::ZERO);
let content_h: Fixed = config.map(|c| c.content_height).unwrap_or(container_h);
let content_w: Fixed = config.map(|c| c.content_width).unwrap_or(container_w);
let max_y = (content_h - container_h).max(Fixed::ZERO);
let max_x = (content_w - container_w).max(Fixed::ZERO);
if let Some(scroll) = world.get_mut::<ScrollOffset>(target) {
match dir {
ScrollAxis::Vertical => {
let eff_dy = elastic_resist(scroll.y, -dy, max_y);
scroll.y += eff_dy;
}
ScrollAxis::Horizontal => {
let eff_dx = elastic_resist(scroll.x, -dx, max_x);
scroll.x += eff_dx;
}
ScrollAxis::Both => {
let eff_dx = elastic_resist(scroll.x, -dx, max_x);
let eff_dy = elastic_resist(scroll.y, -dy, max_y);
scroll.x += eff_dx;
scroll.y += eff_dy;
}
}
}
world.insert(target, crate::widget::dirty::Dirty);
let dir = world
.get::<ScrollConfig>(target)
.map(|c| c.direction)
.unwrap_or(ScrollAxis::Vertical);
if let Some(state) = world.resource_mut::<ScrollDragState>() {
match dir {
ScrollAxis::Vertical => {
state.vel_x = Fixed::ZERO;
state.vel_y = -dy;
}
ScrollAxis::Horizontal => {
state.vel_x = -dx;
state.vel_y = Fixed::ZERO;
}
ScrollAxis::Both => {
state.vel_x = -dx;
state.vel_y = -dy;
}
}
state.last_x = *x;
state.last_y = *y;
}
}
InputEvent::Release { .. } => {
if let Some(state) = world.resource_mut::<ScrollDragState>() {
state.active = false;
}
}
_ => {}
}
}
pub fn scroll_inertia_system(world: &mut World) {
let (active, resolved, target, vel_x, vel_y) = {
let Some(state) = world.resource::<ScrollDragState>() else {
return;
};
(
state.active,
state.resolved,
state.target,
state.vel_x,
state.vel_y,
)
};
if active || !resolved {
return;
}
let (offset_x, offset_y, max_x, max_y, elastic, dir) = {
let Some(scroll) = world.get::<ScrollOffset>(target) else {
return;
};
let config = world.get::<crate::components::scroll::ScrollConfig>(target);
let computed = world.get::<crate::widget::ComputedRect>(target);
let container_h = computed.map(|c| c.0.h).unwrap_or(Fixed::ZERO);
let container_w = computed.map(|c| c.0.w).unwrap_or(Fixed::ZERO);
let content_h: Fixed = config.map(|c| c.content_height).unwrap_or(container_h);
let content_w: Fixed = config.map(|c| c.content_width).unwrap_or(container_w);
let max_y = (content_h - container_h).max(Fixed::ZERO);
let max_x = (content_w - container_w).max(Fixed::ZERO);
let elastic = config.map(|c| c.elastic).unwrap_or(true);
let dir = config.map(|c| c.direction).unwrap_or(ScrollAxis::Vertical);
(scroll.x, scroll.y, max_x, max_y, elastic, dir)
};
let mut new_vel_x = vel_x;
let mut new_vel_y = vel_y;
let mut bouncing = false;
if elastic {
match dir {
ScrollAxis::Vertical | ScrollAxis::Both => {
if offset_y < Fixed::ZERO {
let diff = -offset_y;
new_vel_y = if diff.abs() <= Fixed::from_int(3) {
diff
} else {
diff / 3
};
bouncing = true;
} else if offset_y > max_y {
let diff = max_y - offset_y;
new_vel_y = if diff.abs() <= Fixed::from_int(3) {
diff
} else {
diff / 3
};
bouncing = true;
}
}
_ => {}
}
match dir {
ScrollAxis::Horizontal | ScrollAxis::Both => {
if offset_x < Fixed::ZERO {
let diff = -offset_x;
new_vel_x = if diff.abs() <= Fixed::from_int(3) {
diff
} else {
diff / 3
};
bouncing = true;
} else if offset_x > max_x {
let diff = max_x - offset_x;
new_vel_x = if diff.abs() <= Fixed::from_int(3) {
diff
} else {
diff / 3
};
bouncing = true;
}
}
_ => {}
}
}
if new_vel_x == Fixed::ZERO && new_vel_y == Fixed::ZERO {
return;
}
if let Some(scroll) = world.get_mut::<ScrollOffset>(target) {
scroll.x += new_vel_x;
scroll.y += new_vel_y;
}
world.insert(target, crate::widget::dirty::Dirty);
if let Some(state) = world.resource_mut::<ScrollDragState>() {
if bouncing {
state.vel_x = Fixed::ZERO;
state.vel_y = Fixed::ZERO;
} else {
state.vel_x = new_vel_x * 9 / 10;
state.vel_y = new_vel_y * 9 / 10;
if state.vel_x.abs() < Fixed::ONE {
state.vel_x = Fixed::ZERO;
}
if state.vel_y.abs() < Fixed::ONE {
state.vel_y = Fixed::ZERO;
}
}
}
}
fn find_scroll_target_for_direction(
world: &World,
start: Entity,
gesture_dir: ScrollAxis,
delta_x: Fixed,
delta_y: Fixed,
) -> Option<Entity> {
let mut current = start;
let mut last_matching: Option<Entity> = None;
loop {
if world.get::<ScrollOffset>(current).is_some() {
let config = world.get::<ScrollConfig>(current);
let scroll_dir = config.map(|c| c.direction).unwrap_or(ScrollAxis::Vertical);
let dir_matches = matches!(
(scroll_dir, gesture_dir),
(ScrollAxis::Both, _)
| (ScrollAxis::Vertical, ScrollAxis::Vertical)
| (ScrollAxis::Horizontal, ScrollAxis::Horizontal)
);
if dir_matches {
last_matching = Some(current);
let at_boundary = is_at_boundary(world, current, delta_x, delta_y);
if !at_boundary {
return Some(current);
}
}
}
if let Some(parent) = world.get::<crate::widget::Parent>(current) {
current = parent.0;
} else {
break;
}
}
last_matching
}
fn elastic_resist(offset: Fixed, delta: Fixed, max: Fixed) -> Fixed {
const DAMPING: i32 = 200;
let new_offset = offset + delta;
if new_offset >= Fixed::ZERO && new_offset <= max {
return delta;
}
let overscroll = if new_offset < Fixed::ZERO {
-new_offset
} else {
new_offset - max
};
let over_int = overscroll.to_int();
let resistance_denom = DAMPING + over_int;
if resistance_denom == 0 {
return Fixed::ZERO;
}
delta * DAMPING / resistance_denom
}
fn is_at_boundary(world: &World, entity: Entity, delta_x: Fixed, delta_y: Fixed) -> bool {
let Some(scroll) = world.get::<ScrollOffset>(entity) else {
return false;
};
let config = world.get::<ScrollConfig>(entity);
let computed = world.get::<crate::widget::ComputedRect>(entity);
let container_h = computed.map(|c| c.0.h).unwrap_or(Fixed::ZERO);
let container_w = computed.map(|c| c.0.w).unwrap_or(Fixed::ZERO);
let content_h = config.map(|c| c.content_height).unwrap_or(container_h);
let content_w = config.map(|c| c.content_width).unwrap_or(container_w);
let max_y = (content_h - container_h).max(Fixed::ZERO);
let max_x = (content_w - container_w).max(Fixed::ZERO);
let at_y = (scroll.y <= Fixed::ZERO && delta_y < Fixed::ZERO)
|| (scroll.y >= max_y && delta_y > Fixed::ZERO);
let at_x = (scroll.x <= Fixed::ZERO && delta_x < Fixed::ZERO)
|| (scroll.x >= max_x && delta_x > Fixed::ZERO);
let dir = config.map(|c| c.direction).unwrap_or(ScrollAxis::Vertical);
match dir {
ScrollAxis::Vertical => at_y,
ScrollAxis::Horizontal => at_x,
ScrollAxis::Both => at_y || at_x,
}
}