use tairitsu_vdom::{VElement, VNode};
#[derive(Clone, PartialEq, Debug, Default)]
pub struct DragConstraints {
pub min_x: Option<f64>,
pub max_x: Option<f64>,
pub min_y: Option<f64>,
pub max_y: Option<f64>,
}
impl DragConstraints {
pub fn new(
min_x: Option<f64>,
max_x: Option<f64>,
min_y: Option<f64>,
max_y: Option<f64>,
) -> Self {
Self {
min_x,
max_x,
min_y,
max_y,
}
}
pub fn horizontal(min: f64, max: f64) -> Self {
Self {
min_x: Some(min),
max_x: Some(max),
min_y: None,
max_y: None,
}
}
pub fn vertical(min: f64, max: f64) -> Self {
Self {
min_x: None,
max_x: None,
min_y: Some(min),
max_y: Some(max),
}
}
pub fn bounded(min_x: f64, max_x: f64, min_y: f64, max_y: f64) -> Self {
Self {
min_x: Some(min_x),
max_x: Some(max_x),
min_y: Some(min_y),
max_y: Some(max_y),
}
}
pub fn constrain_x(&self, x: f64) -> f64 {
let mut constrained = x;
if let Some(min) = self.min_x {
constrained = constrained.max(min);
}
if let Some(max) = self.max_x {
constrained = constrained.min(max);
}
constrained
}
pub fn constrain_y(&self, y: f64) -> f64 {
let mut constrained = y;
if let Some(min) = self.min_y {
constrained = constrained.max(min);
}
if let Some(max) = self.max_y {
constrained = constrained.min(max);
}
constrained
}
pub fn constrain(&self, x: f64, y: f64) -> (f64, f64) {
(self.constrain_x(x), self.constrain_y(y))
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct DragData {
pub x: f64,
pub y: f64,
pub start_x: f64,
pub start_y: f64,
}
impl DragData {
pub fn delta(&self) -> (f64, f64) {
(self.x - self.start_x, self.y - self.start_y)
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct DragLayerState {
pub position: (f64, f64),
pub is_dragging: bool,
pub drag_start: (f64, f64),
pub offset_start: (f64, f64),
pub constraints: DragConstraints,
pub z_index: i32,
pub draggable: bool,
pub class: String,
}
impl DragLayerState {
pub fn new() -> Self {
Self {
position: (0.0, 0.0),
is_dragging: false,
drag_start: (0.0, 0.0),
offset_start: (0.0, 0.0),
constraints: DragConstraints::default(),
z_index: 1000,
draggable: true,
class: String::new(),
}
}
pub fn with_position(mut self, x: f64, y: f64) -> Self {
self.position = (x, y);
self
}
pub fn with_constraints(mut self, constraints: DragConstraints) -> Self {
self.constraints = constraints;
self
}
pub fn with_z_index(mut self, z_index: i32) -> Self {
self.z_index = z_index;
self
}
pub fn with_draggable(mut self, draggable: bool) -> Self {
self.draggable = draggable;
self
}
pub fn with_class(mut self, class: impl Into<String>) -> Self {
self.class = class.into();
self
}
pub fn start_drag(&mut self, x: f64, y: f64) {
if self.draggable {
self.is_dragging = true;
self.drag_start = (x, y);
self.offset_start = self.position;
}
}
pub fn update_drag(&mut self, x: f64, y: f64) -> DragData {
if self.is_dragging {
let delta_x = x - self.drag_start.0;
let delta_y = y - self.drag_start.1;
let new_x = self.constraints.constrain_x(self.offset_start.0 + delta_x);
let new_y = self.constraints.constrain_y(self.offset_start.1 + delta_y);
self.position = (new_x, new_y);
DragData {
x: new_x,
y: new_y,
start_x: self.offset_start.0,
start_y: self.offset_start.1,
}
} else {
DragData {
x: self.position.0,
y: self.position.1,
start_x: self.position.0,
start_y: self.position.1,
}
}
}
pub fn end_drag(&mut self) -> DragData {
self.is_dragging = false;
DragData {
x: self.position.0,
y: self.position.1,
start_x: self.offset_start.0,
start_y: self.offset_start.1,
}
}
pub fn constrain_position(&self, x: f64, y: f64) -> (f64, f64) {
self.constraints.constrain(x, y)
}
pub fn position_style(&self) -> String {
format!(
"position: absolute; left: {}px; top: {}px; z-index: {}; cursor: {};",
self.position.0,
self.position.1,
self.z_index,
if self.draggable { "move" } else { "default" }
)
}
pub fn class_string(&self) -> String {
let base = if self.class.is_empty() {
"hi-drag-layer".to_string()
} else {
format!("hi-drag-layer {}", self.class)
};
if self.is_dragging {
format!("{} hi-dragging", base)
} else {
base
}
}
}
impl Default for DragLayerState {
fn default() -> Self {
Self::new()
}
}
pub fn render_drag_layer(state: &DragLayerState, content: VNode) -> VNode {
let mut el = VElement::new("div")
.class(state.class_string())
.style(state.position_style())
.attr("data-action", "drag-start");
if state.is_dragging {
el = el.attr("data-dragging", "true");
}
VNode::Element(el.child(content))
}
#[derive(Clone, PartialEq, Debug)]
pub enum DragLayerEvent {
DragStart(DragData),
DragMove(DragData),
DragEnd(DragData),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_drag_layer_state_new() {
let state = DragLayerState::new();
assert_eq!(state.position, (0.0, 0.0));
assert!(!state.is_dragging);
assert!(state.draggable);
assert_eq!(state.z_index, 1000);
}
#[test]
fn test_drag_constraints_constrain() {
let constraints = DragConstraints::bounded(0.0, 100.0, 0.0, 100.0);
assert_eq!(constraints.constrain(50.0, 50.0), (50.0, 50.0));
assert_eq!(constraints.constrain(-10.0, 50.0), (0.0, 50.0));
assert_eq!(constraints.constrain(150.0, 50.0), (100.0, 50.0));
}
#[test]
fn test_drag_lifecycle() {
let mut state = DragLayerState::new().with_position(100.0, 100.0);
state.start_drag(100.0, 100.0);
assert!(state.is_dragging);
let data = state.update_drag(120.0, 120.0);
assert_eq!(data.x, 120.0);
assert_eq!(data.y, 120.0);
assert_eq!(state.position, (120.0, 120.0));
let data = state.end_drag();
assert!(!state.is_dragging);
assert_eq!(data.x, 120.0);
}
#[test]
fn test_drag_with_constraints() {
let constraints = DragConstraints::bounded(0.0, 200.0, 0.0, 200.0);
let mut state = DragLayerState::new()
.with_position(100.0, 100.0)
.with_constraints(constraints);
state.start_drag(100.0, 100.0);
state.update_drag(-50.0, -50.0); assert_eq!(state.position, (0.0, 0.0));
state.update_drag(300.0, 300.0); assert_eq!(state.position, (200.0, 200.0)); }
#[test]
fn test_drag_data_delta() {
let data = DragData {
x: 150.0,
y: 150.0,
start_x: 100.0,
start_y: 100.0,
};
assert_eq!(data.delta(), (50.0, 50.0));
}
#[test]
fn test_builder_pattern() {
let state = DragLayerState::new()
.with_position(50.0, 50.0)
.with_z_index(2000)
.with_draggable(false)
.with_class("my-drag-layer");
assert_eq!(state.position, (50.0, 50.0));
assert_eq!(state.z_index, 2000);
assert!(!state.draggable);
assert_eq!(state.class, "my-drag-layer");
}
#[test]
fn test_position_style() {
let state = DragLayerState::new().with_position(123.0, 456.0);
let style = state.position_style();
assert!(style.contains("left: 123px"));
assert!(style.contains("top: 456px"));
}
}