use teksilo_canvas::{Rect, SizeProposal};
use teksilo_core::build_context::BuildContext;
use teksilo_core::widget::{LayoutContext, LayoutResponse, Widget, WidgetPlacement};
use teksilo_core::widget_builder::HandlerSet;
use teksilo_core::widget_id::WidgetId;
pub struct DeadZone {
child: Option<WidgetId>,
pending: Option<Box<dyn Widget>>,
}
impl DeadZone {
pub fn new() -> Self {
Self {
child: None,
pending: None,
}
}
pub fn child(mut self, widget: impl Widget + 'static) -> Self {
self.pending = Some(Box::new(widget));
self
}
pub fn child_id(mut self, id: WidgetId) -> Self {
self.child = Some(id);
self
}
}
impl Default for DeadZone {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for DeadZone {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeadZone").finish()
}
}
impl Widget for DeadZone {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
if let Some(pending) = self.pending.take() {
self.child = Some(ctx.add_boxed(pending));
}
ctx.apply_self_handlers(
HandlerSet::new()
.gesture_dead_zone(true)
.on_tap(|_e, _ctx| {})
.on_drag(|_phase, _ctx| {}),
);
self.child.into_iter().collect()
}
fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
self.child
.and_then(|id| ctx.child_layout_response(id, proposal))
.unwrap_or_else(|| proposal.resolve(0.0, 0.0).into())
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = bounds.origin();
child.size = bounds.size();
}
}
fn children(&self) -> Vec<WidgetId> {
self.child.into_iter().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::icon_button::IconButton;
use crate::primitives::IconWidget;
use std::cell::Cell;
use std::rc::Rc;
use teksilo_canvas::Point;
use teksilo_core::event::{Modifiers, PointerButton, WidgetEvent};
use teksilo_core::widget_builder::WidgetBuilder;
use teksilo_core::widget_tree::WidgetTree;
#[test]
fn dead_zone_blocks_ancestor_drag_but_lets_the_button_click() {
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let dragged = Rc::new(Cell::new(false));
let clicked = Rc::new(Cell::new(false));
let d = dragged.clone();
let c = clicked.clone();
let button = tree
.add(IconButton::new(IconWidget::checkmark(16.0)).on_activate_fn(move |_| c.set(true)));
let dead = tree.add(DeadZone::new().child_id(button));
let ancestor = tree.add(crate::primitives::HStack::new().add_child(dead).on_drag(
move |phase, _ctx| {
if let teksilo_core::gesture::DragPhase::Started { .. } = phase {
d.set(true);
}
},
));
tree.layout(SizeProposal::exact(120.0, 60.0));
let b = tree.bounds(button);
let (cx, cy) = (b.x + b.width / 2.0, b.y + b.height / 2.0);
tree.dispatch_event(WidgetEvent::PointerDown {
position: Point::new(cx, cy),
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
tree.dispatch_event(WidgetEvent::PointerUp {
position: Point::new(cx, cy),
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
assert!(
clicked.get(),
"the button inside the dead zone still activates"
);
tree.pointer_down_button(Point::new(cx, cy), PointerButton::Primary);
for i in 1..=10 {
tree.pointer_move(Point::new(cx + (i as f32) * 3.0, cy + 1.0));
}
tree.pointer_up_button(Point::new(cx + 30.0, cy + 1.0), PointerButton::Primary);
assert!(
!dragged.get(),
"a jittery press on the dead-zone button must not start the ancestor drag"
);
let _ = ancestor;
}
#[test]
fn dead_zone_forwards_shrink_so_a_wrapped_child_still_compresses() {
use crate::primitives::{FixedSize, HStack, Shrinkable};
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let dead = tree.add(
DeadZone::new().child(
Shrinkable::new()
.min_width(20.0)
.child(FixedSize::new().width(100.0).height(20.0)),
),
);
let rigid = tree.add(FixedSize::new().width(100.0).height(20.0));
tree.add(HStack::new().add_child(rigid).add_child(dead));
tree.layout(SizeProposal::exact(120.0, 20.0));
let w = tree.bounds(dead).width;
assert!(
w < 100.0,
"the DeadZone must forward the child's shrink weight (width was {w}, expected < 100)"
);
}
}