use teksilo_canvas::{Point, Rect, Size, Transform2D};
use teksilo_tokens::{InputTokens, PointerKind, TargetDensity};
use crate::environment::LayoutDirection;
use crate::widget_id::WidgetId;
static COMPACT_TOKENS: InputTokens = InputTokens::for_density(TargetDensity::Compact);
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HitSlop {
pub radius: f32,
pub up_to: f32,
}
impl HitSlop {
pub const NONE: HitSlop = HitSlop {
radius: 0.0,
up_to: 0.0,
};
pub fn for_pointer(kind: PointerKind, tokens: &InputTokens) -> Self {
let profile_radius = tokens.profile(kind).hit_slop;
let radius = if kind.is_coarse() {
profile_radius.min(tokens.slop_budget)
} else {
profile_radius
};
Self {
radius: sanitize(radius),
up_to: sanitize(tokens.target_size),
}
}
pub fn is_none(&self) -> bool {
self.radius <= 0.0 || self.up_to <= 0.0
}
pub fn outset_for(&self, size: Size) -> f32 {
let radius = sanitize(self.radius);
if radius <= 0.0 {
return 0.0;
}
let smaller = sanitize(size.width.min(size.height));
((sanitize(self.up_to) - smaller) / 2.0).clamp(0.0, radius)
}
}
fn sanitize(v: f32) -> f32 {
if v.is_finite() && v > 0.0 { v } else { 0.0 }
}
pub fn rect_distance(rect: Rect, point: Point) -> f32 {
let dx = (rect.x - point.x).max(point.x - rect.right()).max(0.0);
let dy = (rect.y - point.y).max(point.y - rect.bottom()).max(0.0);
(dx * dx + dy * dy).sqrt()
}
pub fn circle_distance(center: Point, radius: f32, point: Point) -> f32 {
let dx = point.x - center.x;
let dy = point.y - center.y;
((dx * dx + dy * dy).sqrt() - radius.max(0.0)).max(0.0)
}
pub fn min_singular_value(t: &Transform2D) -> f32 {
let [a, b, c, d, _, _] = t.m;
let e = (a + d) / 2.0;
let f = (a - d) / 2.0;
let g = (b + c) / 2.0;
let h = (b - c) / 2.0;
let q = e.hypot(h);
let r = f.hypot(g);
let sigma_min = (q - r).abs();
if sigma_min.is_finite() {
sigma_min
} else {
0.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HitCandidate {
pub id: WidgetId,
pub distance: f32,
pub outset: f32,
}
pub struct HitContext<'a> {
kind: PointerKind,
tokens: &'a InputTokens,
direction: LayoutDirection,
slop: HitSlop,
read_only: Option<&'a dyn Fn(WidgetId) -> bool>,
}
impl std::fmt::Debug for HitContext<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HitContext")
.field("kind", &self.kind)
.field("density", &self.tokens.density)
.field("direction", &self.direction)
.field("slop", &self.slop)
.field("has_read_only_probe", &self.read_only.is_some())
.finish()
}
}
impl<'a> HitContext<'a> {
pub fn new(kind: PointerKind, tokens: &'a InputTokens) -> Self {
Self {
kind,
tokens,
direction: LayoutDirection::LeftToRight,
slop: HitSlop::for_pointer(kind, tokens),
read_only: None,
}
}
pub fn mouse() -> HitContext<'static> {
HitContext::new(PointerKind::Mouse, &COMPACT_TOKENS)
}
pub fn direction(mut self, direction: LayoutDirection) -> Self {
self.direction = direction;
self
}
pub fn without_slop(mut self) -> Self {
self.slop = HitSlop::NONE;
self
}
pub fn read_only_probe(mut self, probe: &'a dyn Fn(WidgetId) -> bool) -> Self {
self.read_only = Some(probe);
self
}
pub fn kind(&self) -> PointerKind {
self.kind
}
pub fn tokens(&self) -> &InputTokens {
self.tokens
}
pub fn layout_direction(&self) -> LayoutDirection {
self.direction
}
pub fn default_slop(&self) -> HitSlop {
self.slop
}
pub fn slop_enabled(&self) -> bool {
!self.slop.is_none()
}
pub fn is_read_only(&self, id: WidgetId) -> bool {
self.read_only.map(|p| p(id)).unwrap_or(false)
}
}
#[cfg(test)]
mod tests;