use std::rc::Rc;
use teksilo_canvas::{Point, Rect, Size};
use teksilo_tokens::{BorderRole, SurfaceRole};
use crate::build_context::BuildContext;
use crate::signal::Signal;
use crate::widget_id::WidgetId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DropTargetDragState {
Idle,
HoverAccept,
HoverReject,
}
impl DropTargetDragState {
pub fn surface_role(self) -> SurfaceRole {
match self {
Self::Idle => SurfaceRole::Transparent,
Self::HoverAccept => SurfaceRole::AccentSubtle,
Self::HoverReject => SurfaceRole::StatusError,
}
}
pub fn border_role(self) -> BorderRole {
match self {
Self::Idle => BorderRole::Transparent,
Self::HoverAccept => BorderRole::Accent,
Self::HoverReject => BorderRole::Error,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DropTargetVariant {
#[default]
Default,
Prominent,
Subtle,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DropRegion {
Center,
Top,
Bottom,
Leading,
Trailing,
}
impl DropRegion {
pub const ALL: [DropRegion; 5] = [
DropRegion::Leading,
DropRegion::Trailing,
DropRegion::Top,
DropRegion::Bottom,
DropRegion::Center,
];
pub fn is_side(self) -> bool {
!matches!(self, DropRegion::Center)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DropRegionSet {
pub center: bool,
pub top: bool,
pub bottom: bool,
pub leading: bool,
pub trailing: bool,
}
impl Default for DropRegionSet {
fn default() -> Self {
Self {
center: true,
top: false,
bottom: false,
leading: false,
trailing: false,
}
}
}
impl DropRegionSet {
pub fn none() -> Self {
Self {
center: false,
top: false,
bottom: false,
leading: false,
trailing: false,
}
}
pub fn contains(self, region: DropRegion) -> bool {
match region {
DropRegion::Center => self.center,
DropRegion::Top => self.top,
DropRegion::Bottom => self.bottom,
DropRegion::Leading => self.leading,
DropRegion::Trailing => self.trailing,
}
}
pub fn with(mut self, region: DropRegion) -> Self {
match region {
DropRegion::Center => self.center = true,
DropRegion::Top => self.top = true,
DropRegion::Bottom => self.bottom = true,
DropRegion::Leading => self.leading = true,
DropRegion::Trailing => self.trailing = true,
}
self
}
pub fn iter(self) -> impl Iterator<Item = DropRegion> {
DropRegion::ALL
.into_iter()
.filter(move |r| self.contains(*r))
}
}
pub fn clamp_size_factor(factor: f32) -> f32 {
factor.clamp(0.1, 1.0)
}
pub fn region_at(
local: Point,
size: Size,
set: DropRegionSet,
size_factor: f32,
) -> Option<DropRegion> {
let f = clamp_size_factor(size_factor);
let ex = size.width * f;
let ey = size.height * f;
if set.leading && local.x < ex {
Some(DropRegion::Leading)
} else if set.trailing && local.x > size.width - ex {
Some(DropRegion::Trailing)
} else if set.top && local.y < ey {
Some(DropRegion::Top)
} else if set.bottom && local.y > size.height - ey {
Some(DropRegion::Bottom)
} else if set.center {
Some(DropRegion::Center)
} else {
None
}
}
pub fn region_rect(region: DropRegion, bounds: Rect, size_factor: f32) -> Rect {
let f = clamp_size_factor(size_factor);
let ex = bounds.width * f;
let ey = bounds.height * f;
match region {
DropRegion::Center => bounds,
DropRegion::Leading => Rect::new(bounds.x, bounds.y, ex, bounds.height),
DropRegion::Trailing => {
Rect::new(bounds.x + bounds.width - ex, bounds.y, ex, bounds.height)
}
DropRegion::Top => Rect::new(bounds.x, bounds.y, bounds.width, ey),
DropRegion::Bottom => Rect::new(bounds.x, bounds.y + bounds.height - ey, bounds.width, ey),
}
}
#[derive(Clone)]
pub struct DropTargetStyleConfig {
pub content_id: WidgetId,
pub drag_state: Signal<DropTargetDragState>,
pub active_region: Signal<Option<DropRegion>>,
pub regions: DropRegionSet,
pub region_hints: Vec<(DropRegion, WidgetId)>,
pub size_factor: f32,
pub variant: DropTargetVariant,
}
pub trait DropTargetStyle: 'static {
fn make_body(&self, cfg: &DropTargetStyleConfig, ctx: &mut BuildContext) -> WidgetId;
}
pub type SharedDropTargetStyle = Rc<dyn DropTargetStyle>;
#[cfg(test)]
mod tests {
use super::*;
const FULL: DropRegionSet = DropRegionSet {
center: true,
top: true,
bottom: true,
leading: true,
trailing: true,
};
#[test]
fn center_only_classifies_everything_as_center() {
let set = DropRegionSet::default();
let size = Size::new(400.0, 300.0);
for &(x, y) in &[(0.0, 0.0), (200.0, 150.0), (399.0, 299.0)] {
assert_eq!(
region_at(Point::new(x, y), size, set, 0.2),
Some(DropRegion::Center)
);
}
}
#[test]
fn full_five_zone_edges_and_center() {
let size = Size::new(400.0, 300.0);
assert_eq!(
region_at(Point::new(5.0, 150.0), size, FULL, 0.2),
Some(DropRegion::Leading)
);
assert_eq!(
region_at(Point::new(395.0, 150.0), size, FULL, 0.2),
Some(DropRegion::Trailing)
);
assert_eq!(
region_at(Point::new(200.0, 5.0), size, FULL, 0.2),
Some(DropRegion::Top)
);
assert_eq!(
region_at(Point::new(200.0, 295.0), size, FULL, 0.2),
Some(DropRegion::Bottom)
);
assert_eq!(
region_at(Point::new(200.0, 150.0), size, FULL, 0.2),
Some(DropRegion::Center)
);
}
#[test]
fn only_enabled_edges_are_tested() {
let set = DropRegionSet::none()
.with(DropRegion::Leading)
.with(DropRegion::Trailing);
let size = Size::new(400.0, 300.0);
assert_eq!(
region_at(Point::new(5.0, 150.0), size, set, 0.2),
Some(DropRegion::Leading)
);
assert_eq!(
region_at(Point::new(200.0, 150.0), size, set, 0.2),
None,
"middle with no Center enabled must reject"
);
assert_eq!(region_at(Point::new(200.0, 5.0), size, set, 0.2), None);
}
#[test]
fn factor_half_bisects_left_right() {
let set = DropRegionSet::none()
.with(DropRegion::Leading)
.with(DropRegion::Trailing);
let size = Size::new(400.0, 300.0);
assert_eq!(
region_at(Point::new(199.0, 150.0), size, set, 0.5),
Some(DropRegion::Leading)
);
assert_eq!(
region_at(Point::new(201.0, 150.0), size, set, 0.5),
Some(DropRegion::Trailing)
);
}
#[test]
fn leading_wins_overlap_with_large_factor() {
let size = Size::new(400.0, 300.0);
assert_eq!(
region_at(Point::new(200.0, 150.0), size, FULL, 0.9),
Some(DropRegion::Leading)
);
}
#[test]
fn size_factor_is_clamped() {
assert_eq!(clamp_size_factor(0.05), 0.1);
assert_eq!(clamp_size_factor(2.0), 1.0);
assert_eq!(clamp_size_factor(0.3), 0.3);
}
#[test]
fn region_rect_strips() {
let b = Rect::new(10.0, 20.0, 400.0, 300.0);
assert_eq!(region_rect(DropRegion::Center, b, 0.25), b);
let lead = region_rect(DropRegion::Leading, b, 0.25);
assert_eq!(
(lead.x, lead.y, lead.width, lead.height),
(10.0, 20.0, 100.0, 300.0)
);
let trail = region_rect(DropRegion::Trailing, b, 0.25);
assert_eq!(
(trail.x, trail.y, trail.width, trail.height),
(310.0, 20.0, 100.0, 300.0)
);
let top = region_rect(DropRegion::Top, b, 0.25);
assert_eq!(
(top.x, top.y, top.width, top.height),
(10.0, 20.0, 400.0, 75.0)
);
let bottom = region_rect(DropRegion::Bottom, b, 0.25);
assert_eq!(
(bottom.x, bottom.y, bottom.width, bottom.height),
(10.0, 245.0, 400.0, 75.0)
);
}
}