use std::cell::{Cell, RefCell};
use std::rc::Rc;
use frust_core::accesskit::{Action, Role};
use frust_core::event::PointerSource;
use frust_core::{
AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, CursorIcon, EventCtx, EventResult,
InputEvent, Key, KeyEvent, LayoutCtx, NamedKey, OverlayBand, OverlayInput, PaintCtx,
PaintScene, PointerButton, PointerEvent, PointerPhase, SemanticsCtx, TOUCH_SLOP, View, Widget,
any,
};
use frust_theme::Theme;
use kurbo::{Point, Size, Vec2};
use peniko::Color;
use super::coordinator::{DragCoordinator, DragPhase, DragSourceId, DragState};
use crate::authoring::presses;
use crate::gesture::{HoldTracker, LONG_PRESS_MS};
use crate::overlay::{OverlayAnchor, OverlaySlot};
use crate::scroll::ambient_scroll_vetoes;
pub const DRAG_THRESHOLD: f64 = 6.0;
pub const GHOST_OPACITY: f32 = 0.85;
const DIM_OPACITY: f32 = 0.38;
const PLACEHOLDER_FILL: Color = Color::from_rgba8(0x00, 0x00, 0x00, 0x14);
const PLACEHOLDER_RADIUS: f64 = 8.0;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DragPolicy {
Immediate,
LongPress,
#[default]
Auto,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SourceFeedback {
#[default]
Dim,
Placeholder,
None,
}
type GhostBuilder<State> = Rc<dyn Fn(&State) -> AnyView<State>>;
pub struct DraggableView<State: 'static, T: 'static> {
child: AnyView<State>,
coordinator: DragCoordinator,
payload: Rc<dyn Fn(&State) -> T>,
policy: DragPolicy,
threshold: f64,
hold_threshold_ms: Option<u64>,
ghost: Option<GhostBuilder<State>>,
ghost_opacity: f32,
feedback: SourceFeedback,
}
pub fn draggable<State, V, T, F>(
child: V,
coordinator: DragCoordinator,
payload: F,
) -> DraggableView<State, T>
where
State: 'static,
V: View<State>,
T: 'static,
F: Fn(&State) -> T + 'static,
{
DraggableView {
child: any(child),
coordinator,
payload: Rc::new(payload),
policy: DragPolicy::Auto,
threshold: DRAG_THRESHOLD,
hold_threshold_ms: None,
ghost: None,
ghost_opacity: GHOST_OPACITY,
feedback: SourceFeedback::Dim,
}
}
impl<State: 'static, T: 'static> DraggableView<State, T> {
pub fn policy(mut self, policy: DragPolicy) -> Self {
self.policy = policy;
self
}
pub fn threshold(mut self, px: f64) -> Self {
self.threshold = px.max(0.0);
self
}
pub fn hold_threshold_ms(mut self, ms: u64) -> Self {
self.hold_threshold_ms = Some(ms);
self
}
pub fn ghost<F, V>(mut self, ghost: F) -> Self
where
F: Fn(&State) -> V + 'static,
V: View<State>,
{
self.ghost = Some(Rc::new(move |state: &State| any(ghost(state))));
self
}
pub fn ghost_opacity(mut self, opacity: f32) -> Self {
self.ghost_opacity = opacity.clamp(0.0, 1.0);
self
}
pub fn while_dragging(mut self, feedback: SourceFeedback) -> Self {
self.feedback = feedback;
self
}
fn resolved_hold_ms(&self) -> f64 {
self.hold_threshold_ms
.map(|ms| ms.max(1) as f64)
.unwrap_or(LONG_PRESS_MS)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Initiation {
Distance,
Hold,
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Gesture {
Idle,
Pressed {
down: Point,
last: Point,
initiation: Initiation,
elapsed: bool,
},
Declined,
Dragging,
Ended,
}
struct Ghost<State: 'static> {
pending: Option<AnyView<State>>,
mounted: Option<AnyView<State>>,
live: bool,
}
pub struct DraggableWidget<State: 'static, T: 'static> {
child: Rc<RefCell<ChildPod>>,
coordinator: DragCoordinator,
payload: Rc<dyn Fn(&State) -> T>,
policy: DragPolicy,
threshold: f64,
ghost_builder: Option<GhostBuilder<State>>,
ghost_opacity: f32,
feedback: SourceFeedback,
gesture: Gesture,
hold: HoldTracker,
source: Option<DragSourceId>,
slot: OverlaySlot<State>,
ghost: Ghost<State>,
window_origin: Point,
scroll_vetoes: Vec<Rc<Cell<bool>>>,
focus_claimed: bool,
keyboard_drag: bool,
}
impl<State: 'static, T: 'static> View<State> for DraggableView<State, T> {
type Element = DraggableWidget<State, T>;
fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
let mut slot = OverlaySlot::new();
slot.set_band(OverlayBand::Tooltip);
slot.set_input(OverlayInput::Transparent);
DraggableWidget {
child: Rc::new(RefCell::new(crate::authoring::build_child(
&self.child,
ctx,
))),
coordinator: self.coordinator.clone(),
payload: Rc::clone(&self.payload),
policy: self.policy,
threshold: self.threshold,
ghost_builder: self.ghost.clone(),
ghost_opacity: self.ghost_opacity,
feedback: self.feedback,
gesture: Gesture::Idle,
hold: HoldTracker::new(self.resolved_hold_ms()),
source: None,
slot,
ghost: Ghost {
pending: None,
mounted: None,
live: false,
},
window_origin: Point::ZERO,
scroll_vetoes: Vec::new(),
focus_claimed: false,
keyboard_drag: false,
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut Self::Element,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
if matches!(element.gesture, Gesture::Idle) {
element.coordinator = self.coordinator.clone();
}
element.payload = Rc::clone(&self.payload);
element.policy = self.policy;
element.threshold = self.threshold;
element.hold.set_threshold_ms(self.resolved_hold_ms());
element.ghost_builder = self.ghost.clone();
if element.ghost_opacity != self.ghost_opacity || element.feedback != self.feedback {
element.ghost_opacity = self.ghost_opacity;
element.feedback = self.feedback;
flags |= ChangeFlags::PAINT;
}
flags |= crate::authoring::rebuild_child(
&prev.child,
&self.child,
&mut element.child.borrow_mut(),
ctx,
);
flags | element.sync_ghost(ctx)
}
fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
if matches!(element.gesture, Gesture::Pressed { .. } | Gesture::Dragging)
&& element.owns_session()
{
element.coordinator.cancel();
}
element.gesture = Gesture::Idle;
element.source = None;
element.focus_claimed = false;
element.keyboard_drag = false;
element.release_scroll_veto();
element.ghost.live = false;
element.ghost.pending = None;
if let Some(mounted) = element.ghost.mounted.take() {
element.slot.rebuild(Some(&mounted), None, ctx);
}
crate::authoring::teardown_child(&self.child, &mut element.child.borrow_mut(), ctx);
}
}
impl<State: 'static, T: 'static> DraggableWidget<State, T> {
fn owns_session(&self) -> bool {
let Some(source) = self.source else {
return false;
};
match self.coordinator.state() {
DragState::Armed { source: armed, .. } => armed == source,
DragState::Dragging(session) => session.source == Some(source),
DragState::Idle | DragState::Dropping { .. } => false,
}
}
fn is_dragging(&self) -> bool {
matches!(self.gesture, Gesture::Dragging)
&& self.coordinator.phase() == DragPhase::Dragging
&& self.owns_session()
}
fn to_window(&self, local: Point) -> Point {
local + self.window_origin.to_vec2()
}
fn set_scroll_veto(&self, raised: bool) {
for veto in &self.scroll_vetoes {
veto.set(raised);
}
}
fn release_scroll_veto(&mut self) {
self.set_scroll_veto(false);
self.scroll_vetoes.clear();
}
fn end_elsewhere(&mut self) {
self.gesture = if self.keyboard_drag {
Gesture::Idle
} else {
Gesture::Ended
};
self.keyboard_drag = false;
self.ghost.live = false;
self.release_scroll_veto();
}
fn sync_ghost(&mut self, ctx: &mut BuildCtx<'_>) -> ChangeFlags {
if matches!(self.gesture, Gesture::Dragging) && !self.is_dragging() {
self.end_elsewhere();
}
let mut flags = ChangeFlags::NONE;
if let Some(next) = self.ghost.pending.take() {
if let Some(prev) = self.ghost.mounted.take() {
flags |= self.slot.rebuild(Some(&prev), None, ctx);
}
if self.ghost.live {
flags |= self.slot.rebuild(None, Some(&next), ctx);
self.ghost.mounted = Some(next);
}
} else if !self.ghost.live
&& let Some(prev) = self.ghost.mounted.take()
{
flags |= self.slot.rebuild(Some(&prev), None, ctx);
}
flags
}
fn press(&mut self, ctx: &mut EventCtx<'_>, event: &InputEvent, at: Point) -> EventResult {
let initiation = match self.policy {
DragPolicy::Immediate => Initiation::Distance,
DragPolicy::LongPress => Initiation::Hold,
DragPolicy::Auto => match ctx.pointer_id().source {
PointerSource::Mouse => Initiation::Distance,
PointerSource::Touch => Initiation::Hold,
},
};
let source = self.coordinator.new_source_id();
if !self.coordinator.arm(source, self.to_window(at)) {
self.gesture = Gesture::Declined;
return crate::authoring::route_event_single(&mut self.child.borrow_mut(), ctx, event);
}
self.source = Some(source);
self.gesture = Gesture::Pressed {
down: at,
last: at,
initiation,
elapsed: false,
};
self.hold.cancel();
self.scroll_vetoes = ambient_scroll_vetoes();
ctx.capture_pointer();
ctx.request_redraw();
let child_took_focus = {
let mut child = self.child.borrow_mut();
child.event_child(ctx, event);
child.holds_live_focus()
};
if !child_took_focus {
ctx.request_focus();
self.focus_claimed = true;
}
EventResult::Handled
}
fn release_focus_claim(&mut self, ctx: &mut EventCtx<'_>) {
if std::mem::take(&mut self.focus_claimed) {
ctx.release_focus();
}
}
fn decline(&mut self) {
if self.owns_session() {
self.coordinator.cancel();
}
self.source = None;
self.release_scroll_veto();
self.gesture = Gesture::Declined;
}
fn begin(&mut self, ctx: &mut EventCtx<'_>, at: Point, down: Point) -> bool {
if !self.owns_session() {
return false;
}
let (payload, custom) = {
let state: &State = ctx.state_mut::<State>();
(
(self.payload)(state),
self.ghost_builder.as_ref().map(|build| build(state)),
)
};
if !self.coordinator.begin(payload) {
return false;
}
let offset = Point::ZERO - down;
let pointer = self.to_window(at);
self.coordinator.set_ghost_offset(offset);
self.coordinator.update_pointer(pointer);
let content = custom.unwrap_or_else(|| {
any(SnapshotView {
source: Rc::clone(&self.child),
})
});
self.ghost.pending = Some(any(GhostFrame {
content,
opacity: self.ghost_opacity,
}));
self.ghost.live = true;
self.slot.set_anchor(OverlayAnchor::Window {
point: pointer,
offset,
});
{
let mut child = self.child.borrow_mut();
child.event_child(ctx, &pointer_event(PointerPhase::Cancel, at));
ctx.release_captured_child(&mut child);
}
self.set_scroll_veto(true);
self.gesture = Gesture::Dragging;
self.keyboard_drag = false;
ctx.request_redraw();
true
}
fn on_session_end(&mut self, ctx: &mut EventCtx<'_>, release_focus: bool) {
self.gesture = Gesture::Idle;
self.source = None;
self.keyboard_drag = false;
self.hold.cancel();
self.release_scroll_veto();
if release_focus {
self.release_focus_claim(ctx);
}
if self.ghost.live || self.ghost.mounted.is_some() {
self.ghost.live = false;
frust_core::mark_pending_result_flush();
}
ctx.request_redraw();
}
fn pointer(
&mut self,
ctx: &mut EventCtx<'_>,
event: &InputEvent,
p: &PointerEvent,
) -> EventResult {
match (p.phase, self.gesture) {
(PointerPhase::Down, _) => {
if presses(p) && self.is_keyboard_dragging() {
self.keyboard_drop(ctx);
self.gesture = Gesture::Ended;
return EventResult::Handled;
}
if !matches!(self.gesture, Gesture::Idle | Gesture::Declined) {
if self.owns_session() {
self.coordinator.cancel();
}
self.on_session_end(ctx, true);
}
self.gesture = Gesture::Idle;
if !presses(p) {
return crate::authoring::route_event_single(
&mut self.child.borrow_mut(),
ctx,
event,
);
}
self.press(ctx, event, p.position)
}
(
PointerPhase::Move,
Gesture::Pressed {
down,
initiation,
elapsed,
..
},
) => {
let travel = (p.position - down).hypot();
let begins = match initiation {
Initiation::Distance => travel > self.threshold,
Initiation::Hold if travel > TOUCH_SLOP => {
self.decline();
return self.child.borrow_mut().event_child(ctx, event);
}
Initiation::Hold => elapsed,
};
if begins && self.begin(ctx, p.position, down) {
return self.drag_move(ctx, p.position);
}
if begins {
self.decline();
} else if let Gesture::Pressed { last, .. } = &mut self.gesture {
*last = p.position;
}
self.child.borrow_mut().event_child(ctx, event);
EventResult::Handled
}
(PointerPhase::Move, Gesture::Dragging) => self.drag_move(ctx, p.position),
(PointerPhase::Up, Gesture::Pressed { elapsed, .. }) => {
if self.owns_session() {
self.coordinator.cancel();
}
let phase = if elapsed {
PointerPhase::Cancel
} else {
PointerPhase::Up
};
{
let mut child = self.child.borrow_mut();
child.event_child(ctx, &pointer_event(phase, p.position));
child.set_active(false);
}
self.on_session_end(ctx, elapsed);
EventResult::Handled
}
(PointerPhase::Cancel, Gesture::Pressed { .. }) => {
if self.owns_session() {
self.coordinator.cancel();
}
{
let mut child = self.child.borrow_mut();
child.event_child(ctx, event);
child.set_active(false);
}
self.on_session_end(ctx, true);
EventResult::Handled
}
(PointerPhase::Up, Gesture::Dragging) => {
if self.owns_session() {
self.coordinator.update_pointer(self.to_window(p.position));
self.coordinator.drop();
}
self.on_session_end(ctx, true);
EventResult::Handled
}
(PointerPhase::Cancel, Gesture::Dragging) => {
if self.owns_session() {
self.coordinator.cancel();
}
self.on_session_end(ctx, true);
EventResult::Handled
}
(PointerPhase::Up | PointerPhase::Cancel, Gesture::Ended) => {
self.on_session_end(ctx, true);
EventResult::Handled
}
(PointerPhase::Move, Gesture::Ended) => EventResult::Handled,
(PointerPhase::Up | PointerPhase::Cancel, Gesture::Declined) => {
self.gesture = Gesture::Idle;
self.release_focus_claim(ctx);
crate::authoring::route_event_single(&mut self.child.borrow_mut(), ctx, event)
}
_ => crate::authoring::route_event_single(&mut self.child.borrow_mut(), ctx, event),
}
}
fn escape(&mut self, ctx: &mut EventCtx<'_>) -> EventResult {
if self.owns_session() {
self.coordinator.cancel();
}
self.on_session_end(ctx, true);
self.gesture = Gesture::Ended;
EventResult::Handled
}
fn is_keyboard_dragging(&self) -> bool {
matches!(self.gesture, Gesture::Dragging)
&& self.owns_session()
&& matches!(self.coordinator.state(), DragState::Dragging(session) if session.keyboard)
}
fn keyboard_cancel(&mut self, ctx: &mut EventCtx<'_>) -> EventResult {
if self.owns_session() {
self.coordinator.cancel();
}
self.on_session_end(ctx, true);
EventResult::Handled
}
fn keyboard_drop(&mut self, ctx: &mut EventCtx<'_>) -> EventResult {
if self.owns_session() {
self.coordinator.drop();
}
self.on_session_end(ctx, true);
EventResult::Handled
}
fn keyboard_lift(&mut self, ctx: &mut EventCtx<'_>) -> EventResult {
let source = self.coordinator.new_source_id();
if !self.coordinator.lift(source) {
return EventResult::Ignored;
}
self.source = Some(source);
let (payload, custom) = {
let state: &State = ctx.state_mut::<State>();
(
(self.payload)(state),
self.ghost_builder.as_ref().map(|build| build(state)),
)
};
if !self.coordinator.begin(payload) {
self.source = None;
return EventResult::Ignored;
}
let content = custom.unwrap_or_else(|| {
any(SnapshotView {
source: Rc::clone(&self.child),
})
});
self.ghost.pending = Some(any(GhostFrame {
content,
opacity: self.ghost_opacity,
}));
self.ghost.live = true;
self.slot.set_anchor(OverlayAnchor::Window {
point: self.window_origin,
offset: Vec2::ZERO,
});
self.gesture = Gesture::Dragging;
self.keyboard_drag = true;
ctx.request_focus();
self.focus_claimed = true;
ctx.request_redraw();
EventResult::Handled
}
fn follow_hovered_target(&mut self, ctx: &mut EventCtx<'_>) {
let DragState::Dragging(session) = self.coordinator.state() else {
return;
};
let Some(target) = session.hovered else {
return;
};
let Some(bounds) = self.coordinator.target_bounds(target) else {
return;
};
self.slot.set_window_anchor(ctx, bounds.origin());
}
fn keyboard_chord(&mut self, ctx: &mut EventCtx<'_>, key: &KeyEvent) -> Option<EventResult> {
if !ctx.has_focus() || self.child.borrow().holds_live_focus() {
return None;
}
let activates = key.key == Key::Named(NamedKey::Enter)
|| matches!(&key.key, Key::Character(s) if s == " ");
if matches!(self.gesture, Gesture::Idle) {
return activates.then(|| self.keyboard_lift(ctx));
}
if !self.is_keyboard_dragging() {
return None;
}
match &key.key {
Key::Named(NamedKey::ArrowRight) | Key::Named(NamedKey::ArrowDown) => {
self.coordinator.move_to_next_target();
self.follow_hovered_target(ctx);
ctx.request_redraw();
Some(EventResult::Handled)
}
Key::Named(NamedKey::ArrowLeft) | Key::Named(NamedKey::ArrowUp) => {
self.coordinator.move_to_previous_target();
self.follow_hovered_target(ctx);
ctx.request_redraw();
Some(EventResult::Handled)
}
_ if activates => Some(self.keyboard_drop(ctx)),
_ => None,
}
}
fn drag_move(&mut self, ctx: &mut EventCtx<'_>, at: Point) -> EventResult {
if !self.is_dragging() {
self.end_elsewhere();
frust_core::mark_pending_result_flush();
ctx.request_redraw();
return EventResult::Handled;
}
let pointer = self.to_window(at);
self.coordinator.update_pointer(pointer);
self.slot.set_window_anchor(ctx, pointer);
ctx.set_cursor(CursorIcon::Grabbing);
EventResult::Handled
}
fn paint_source(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
let feedback = if self.is_dragging() {
self.feedback
} else {
SourceFeedback::None
};
let mut child = self.child.borrow_mut();
match feedback {
SourceFeedback::None => child.paint_child(ctx, scene),
SourceFeedback::Dim => {
scene.push_layer(ctx.origin(), ctx.size(), DIM_OPACITY);
child.paint_child(ctx, scene);
scene.pop_layer();
}
SourceFeedback::Placeholder => {
let (fill, radius) = match Theme::from_paint_ctx(ctx) {
Some(theme) => (theme.scheme().surface_container_highest, theme.shape.small),
None => (PLACEHOLDER_FILL, PLACEHOLDER_RADIUS),
};
scene.fill_rounded_rect(ctx.origin(), ctx.size(), radius, fill);
}
}
}
}
impl<State: 'static, T: 'static> Widget for DraggableWidget<State, T> {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let size = {
let mut child = self.child.borrow_mut();
let size = child.layout_child(ctx, bc);
child.set_origin(Point::ZERO);
size
};
self.slot.layout(ctx);
bc.constrain(size)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.window_origin = ctx.origin();
if let Gesture::Pressed {
initiation: Initiation::Hold,
elapsed,
..
} = &mut self.gesture
&& !*elapsed
{
if self.hold.fired(ctx.frame_time()) {
*elapsed = true;
frust_core::mark_pending_result_flush();
}
ctx.request_frame();
}
self.paint_source(ctx, scene);
if self.ghost.live {
let size = ctx.size();
self.slot.paint(ctx, size);
}
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if event.is_broadcast() {
if let Gesture::Pressed {
initiation: Initiation::Hold,
elapsed: true,
down,
last,
} = self.gesture
&& !self.begin(ctx, last, down)
{
self.decline();
}
self.child.borrow_mut().event_child(ctx, event);
return EventResult::Ignored;
}
if let InputEvent::Pointer(p) = event {
return self.pointer(ctx, event, p);
}
if let InputEvent::Key(key) = event {
if key.key == Key::Named(NamedKey::Escape) && matches!(self.gesture, Gesture::Dragging)
{
return if self.is_keyboard_dragging() {
self.keyboard_cancel(ctx)
} else {
self.escape(ctx)
};
}
if let Some(result) = self.keyboard_chord(ctx, key) {
return result;
}
}
crate::authoring::route_event_single(&mut self.child.borrow_mut(), ctx, event)
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
let keyboard_dragging = self.is_keyboard_dragging();
let dragging = self.is_dragging();
let label = if keyboard_dragging {
"Drop"
} else if dragging {
"Dragging"
} else {
"Draggable"
};
ctx.push_container(
Role::Button,
|node| {
node.add_action(Action::Click);
node.set_label(label);
if !dragging {
node.set_description("Press Enter to lift");
}
},
|ctx| self.child.borrow().semantics_child(ctx),
);
}
fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
visitor(&self.child.borrow());
}
}
fn pointer_event(phase: PointerPhase, at: Point) -> InputEvent {
InputEvent::Pointer(PointerEvent {
phase,
position: at,
button: PointerButton::Primary,
})
}
struct GhostFrame<State: 'static> {
content: AnyView<State>,
opacity: f32,
}
struct GhostFrameWidget {
content: ChildPod,
opacity: f32,
}
impl<State: 'static> View<State> for GhostFrame<State> {
type Element = GhostFrameWidget;
fn build(&self, ctx: &mut BuildCtx<'_>) -> GhostFrameWidget {
GhostFrameWidget {
content: crate::authoring::build_child(&self.content, ctx),
opacity: self.opacity,
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut GhostFrameWidget,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
element.opacity = self.opacity;
crate::authoring::rebuild_child(&prev.content, &self.content, &mut element.content, ctx)
}
fn teardown(&self, element: &mut GhostFrameWidget, ctx: &mut BuildCtx<'_>) {
crate::authoring::teardown_child(&self.content, &mut element.content, ctx);
}
}
impl Widget for GhostFrameWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let size = self.content.layout_child(ctx, bc);
self.content.set_origin(Point::ZERO);
bc.constrain(size)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
if self.opacity >= 1.0 {
self.content.paint_child(ctx, scene);
return;
}
scene.push_layer(ctx.origin(), ctx.size(), self.opacity);
self.content.paint_child(ctx, scene);
scene.pop_layer();
}
crate::authoring::visit_children!(content);
}
struct SnapshotView {
source: Rc<RefCell<ChildPod>>,
}
struct SnapshotWidget {
source: Rc<RefCell<ChildPod>>,
}
impl<State: 'static> View<State> for SnapshotView {
type Element = SnapshotWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> SnapshotWidget {
SnapshotWidget {
source: Rc::clone(&self.source),
}
}
fn rebuild(
&self,
_prev: &Self,
element: &mut SnapshotWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
element.source = Rc::clone(&self.source);
ChangeFlags::NONE
}
}
impl Widget for SnapshotWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(self.source.borrow().size())
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
if let Ok(mut source) = self.source.try_borrow_mut() {
source.paint_child(ctx, scene);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::RecordingScene;
use crate::{EdgeInsets, Padding, StackView, stack};
use frust_core::event::PointerId;
use frust_core::{FrameTime, RenderRoot};
use kurbo::Rect;
use crate::drag::{DragStateChange, DragTargetId};
const WINDOW: Size = Size::new(400.0, 600.0);
const AT: Vec2 = Vec2::new(10.0, 100.0);
const CHILD: Size = Size::new(100.0, 40.0);
type Log = Rc<RefCell<Vec<String>>>;
#[derive(Clone, Copy)]
struct Cfg {
policy: DragPolicy,
feedback: SourceFeedback,
custom_ghost: bool,
in_scroll: bool,
in_nested_scroll: bool,
focusable_child: bool,
}
impl Default for Cfg {
fn default() -> Self {
Cfg {
policy: DragPolicy::Auto,
feedback: SourceFeedback::Dim,
custom_ghost: false,
in_scroll: false,
in_nested_scroll: false,
focusable_child: false,
}
}
}
struct App {
cfg: Cfg,
coordinator: DragCoordinator,
log: Log,
taps: u32,
item: u32,
}
struct Probe {
size: Size,
log: Log,
focusable: bool,
}
struct ProbeWidget {
size: Size,
log: Log,
focusable: bool,
}
impl View<App> for Probe {
type Element = ProbeWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> ProbeWidget {
ProbeWidget {
size: self.size,
log: Rc::clone(&self.log),
focusable: self.focusable,
}
}
fn rebuild(
&self,
_prev: &Self,
element: &mut ProbeWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
element.size = self.size;
element.log = Rc::clone(&self.log);
element.focusable = self.focusable;
ChangeFlags::NONE
}
}
impl Widget for ProbeWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(self.size)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if let InputEvent::Key(key) = event {
self.log.borrow_mut().push(format!("Key {:?}", key.key));
return EventResult::Handled;
}
let InputEvent::Pointer(p) = event else {
return EventResult::Ignored;
};
self.log
.borrow_mut()
.push(format!("{:?}@{},{}", p.phase, p.position.x, p.position.y));
match p.phase {
PointerPhase::Down => {
ctx.capture_pointer();
if self.focusable && presses(p) {
ctx.request_focus();
}
}
PointerPhase::Up => {
let inside =
Rect::from_origin_size(Point::ZERO, ctx.size()).contains(p.position);
if inside {
ctx.state_mut::<App>().taps += 1;
}
}
PointerPhase::Move | PointerPhase::Cancel => {}
}
EventResult::Handled
}
}
fn logic(state: &mut App) -> StackView<App> {
let cfg = state.cfg;
let mut source = draggable(
Probe {
size: CHILD,
log: Rc::clone(&state.log),
focusable: cfg.focusable_child,
},
state.coordinator.clone(),
|state: &App| state.item,
)
.policy(cfg.policy)
.while_dragging(cfg.feedback);
if cfg.custom_ghost {
source = source.ghost(|_: &App| crate::SizedBox(Some(30.0), Some(20.0)));
}
let placed = any(Padding(
EdgeInsets {
left: AT.x,
top: AT.y,
right: 0.0,
bottom: 0.0,
},
source,
));
if cfg.in_scroll {
return stack().child(crate::scroll_view(
stack()
.child(crate::SizedBox(Some(WINDOW.width), Some(2_000.0)))
.child(placed),
));
}
if cfg.in_nested_scroll {
let inner = any(crate::scroll_view(stack().child(placed)));
return stack().child(crate::scroll_view(
stack()
.child(crate::SizedBox(Some(WINDOW.width), Some(2_000.0)))
.child(inner),
));
}
stack().child(placed)
}
struct Harness {
root: RenderRoot<App, StackView<App>>,
state: App,
clock_ms: f64,
changes: Rc<RefCell<Vec<DragStateChange>>>,
_subscription: crate::drag::DragSubscription,
}
impl Harness {
fn new(cfg: Cfg) -> Self {
let coordinator = DragCoordinator::new();
let changes = Rc::new(RefCell::new(Vec::new()));
let sink = Rc::clone(&changes);
let subscription = coordinator.subscribe(move |change| sink.borrow_mut().push(change));
let mut harness = Harness {
root: RenderRoot::new(),
state: App {
cfg,
coordinator,
log: Rc::new(RefCell::new(Vec::new())),
taps: 0,
item: 7,
},
clock_ms: 0.0,
changes,
_subscription: subscription,
};
harness.frame();
harness
}
fn frame(&mut self) -> RecordingScene {
let mut build: fn(&mut App) -> StackView<App> = logic;
self.root.rebuild(&mut build, &mut self.state);
self.root.layout(WINDOW);
self.clock_ms += 16.0;
let mut scene = RecordingScene::default();
self.root.paint(
&mut scene,
FrameTime::from_nanos((self.clock_ms * 1_000_000.0) as u64),
);
scene
}
fn mouse(&mut self, phase: PointerPhase, x: f64, y: f64) {
self.root
.event(&mut self.state, &pointer_event(phase, Point::new(x, y)));
}
fn touch(&mut self, phase: PointerPhase, x: f64, y: f64) {
self.root.event(
&mut self.state,
&InputEvent::PointerContact {
pointer_id: PointerId::touch(0),
event: PointerEvent {
phase,
position: Point::new(x, y),
button: PointerButton::Primary,
},
},
);
}
fn phase(&self) -> DragPhase {
self.state.coordinator.phase()
}
fn key(&mut self, key: Key) {
self.root.event(
&mut self.state,
&InputEvent::Key(frust_core::KeyEvent {
key,
modifiers: frust_core::Modifiers::default(),
repeat: false,
}),
);
}
fn escape(&mut self) {
self.key(Key::Named(NamedKey::Escape));
}
fn target(&mut self, bounds: Rect) -> DragTargetId {
let drag = &self.state.coordinator;
let id = drag.new_target_id();
drag.register_target(id);
drag.set_target_bounds(id, bounds);
id
}
fn log(&self) -> Vec<String> {
self.state.log.borrow().clone()
}
fn mouse_drag(&mut self) {
self.mouse(PointerPhase::Down, 30.0, 120.0);
self.mouse(PointerPhase::Move, 60.0, 150.0);
}
}
fn rects(scene: &RecordingScene) -> Vec<Rect> {
scene
.rects
.iter()
.map(|(origin, size)| Rect::from_origin_size(*origin, *size))
.collect()
}
fn rest() -> Rect {
Rect::from_origin_size(Point::ZERO + AT, CHILD)
}
#[test]
fn a_mouse_drag_begins_past_the_threshold_and_not_before() {
let mut h = Harness::new(Cfg::default());
h.mouse(PointerPhase::Down, 30.0, 120.0);
assert_eq!(h.phase(), DragPhase::Armed, "a primary press arms");
h.mouse(PointerPhase::Move, 34.0, 122.0);
assert_eq!(
h.phase(),
DragPhase::Armed,
"4.5 px is inside the 6 px threshold"
);
h.mouse(PointerPhase::Move, 37.0, 120.0);
assert_eq!(h.phase(), DragPhase::Dragging, "7 px crosses it");
assert_eq!(
h.state.coordinator.with_payload(|item: &u32| *item),
Some(7),
"the payload builder ran against the state at begin"
);
let session = h.state.coordinator.state();
let session = session.session().expect("a session is in flight");
assert_eq!(session.pointer, Point::new(37.0, 120.0));
assert_eq!(session.ghost_offset, Vec2::new(-20.0, -20.0));
}
#[test]
fn a_short_click_passes_through_to_the_child() {
let mut h = Harness::new(Cfg::default());
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Move, 32.0, 121.0);
h.mouse(PointerPhase::Up, 32.0, 121.0);
assert_eq!(h.state.taps, 1, "the child saw the whole click");
assert_eq!(h.log(), vec!["Down@20,20", "Move@22,21", "Up@22,21"]);
assert_eq!(h.phase(), DragPhase::Idle, "the arm was released");
assert!(!h.root.is_pointer_captured());
}
#[test]
fn a_short_tap_on_touch_passes_through_to_the_child() {
let mut h = Harness::new(Cfg::default());
h.touch(PointerPhase::Down, 30.0, 120.0);
h.frame();
h.touch(PointerPhase::Up, 30.0, 120.0);
assert_eq!(h.state.taps, 1);
assert_eq!(h.phase(), DragPhase::Idle);
}
#[test]
fn beginning_a_drag_cancels_the_childs_press() {
let mut h = Harness::new(Cfg::default());
h.mouse_drag();
h.mouse(PointerPhase::Up, 60.0, 150.0);
assert_eq!(h.state.taps, 0, "a drag is never also a tap");
assert_eq!(h.log(), vec!["Down@20,20", "Cancel@50,50"]);
}
#[test]
fn a_touch_long_press_begins_the_drag() {
let mut h = Harness::new(Cfg::default());
h.touch(PointerPhase::Down, 30.0, 120.0);
h.touch(PointerPhase::Move, 40.0, 120.0);
assert_eq!(h.phase(), DragPhase::Armed);
let mut frames = 0;
while h.phase() != DragPhase::Dragging {
h.frame();
frames += 1;
assert!(frames < 60, "the hold never began a drag");
}
assert!(
frames >= 31,
"began after {frames} frames — before the hold elapsed"
);
assert_eq!(
h.state.coordinator.state().session().map(|s| s.pointer),
Some(Point::new(40.0, 120.0)),
"the drag starts where the finger is held"
);
assert_eq!(h.log().last().map(String::as_str), Some("Cancel@30,20"));
}
#[test]
fn an_enclosing_scroll_view_does_not_steal_a_drag_in_flight() {
let mut h = Harness::new(Cfg {
in_scroll: true,
..Cfg::default()
});
h.touch(PointerPhase::Down, 30.0, 120.0);
let mut frames = 0;
while h.phase() != DragPhase::Dragging {
h.frame();
frames += 1;
assert!(frames < 60, "the hold never began a drag");
}
h.touch(PointerPhase::Move, 30.0, 220.0);
h.touch(PointerPhase::Move, 30.0, 320.0);
assert_eq!(
h.phase(),
DragPhase::Dragging,
"the scroll view took nothing over"
);
assert_eq!(
h.state.coordinator.state().session().map(|s| s.pointer),
Some(Point::new(30.0, 320.0))
);
let scene = h.frame();
assert_eq!(
rects(&scene).first(),
Some(&rest()),
"the content did not scroll under the drag"
);
h.touch(PointerPhase::Up, 30.0, 320.0);
assert_eq!(h.phase(), DragPhase::Idle);
}
#[test]
fn a_drag_vetoes_every_enclosing_scroll_surface_not_only_the_nearest() {
let mut h = Harness::new(Cfg {
in_nested_scroll: true,
..Cfg::default()
});
let target = h.target(Rect::new(0.0, 300.0, WINDOW.width, WINDOW.height));
h.mouse(PointerPhase::Down, 30.0, 120.0);
assert_eq!(h.phase(), DragPhase::Armed, "a primary press arms");
h.mouse(PointerPhase::Move, 37.0, 120.0);
assert_eq!(h.phase(), DragPhase::Dragging, "7 px crosses it");
h.mouse(PointerPhase::Move, 30.0, 320.0);
assert_eq!(
h.phase(),
DragPhase::Dragging,
"neither enclosing scroll surface took the gesture over at its own slop"
);
assert_eq!(
h.state
.coordinator
.state()
.session()
.and_then(|s| s.hovered),
Some(target),
"the drag reached a target the inner surface doesn't even cover"
);
let scene = h.frame();
assert_eq!(
rects(&scene).first(),
Some(&rest()),
"neither surface scrolled its content under the drag"
);
h.mouse(PointerPhase::Up, 30.0, 320.0);
assert_eq!(
h.phase(),
DragPhase::Dropping,
"the coordinator reached Dropping on the target outside the inner surface"
);
h.state.coordinator.complete_drop();
assert_eq!(h.phase(), DragPhase::Idle, "the drop completed");
}
#[test]
fn a_touch_press_that_moves_before_the_hold_is_the_childs() {
let mut h = Harness::new(Cfg::default());
h.touch(PointerPhase::Down, 30.0, 120.0);
h.frame();
h.touch(PointerPhase::Move, 30.0, 160.0);
assert_eq!(
h.phase(),
DragPhase::Idle,
"travel past the slop gives it up"
);
for _ in 0..40 {
h.frame();
}
assert_eq!(h.phase(), DragPhase::Idle, "no later hold revives it");
h.touch(PointerPhase::Up, 30.0, 160.0);
assert_eq!(h.log(), vec!["Down@20,20", "Move@20,60", "Up@20,60"]);
}
#[test]
fn an_explicit_policy_overrides_the_device() {
let mut h = Harness::new(Cfg {
policy: DragPolicy::LongPress,
..Cfg::default()
});
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Move, 40.0, 120.0);
assert_eq!(h.phase(), DragPhase::Armed, "a long-press mouse drag waits");
let mut h = Harness::new(Cfg {
policy: DragPolicy::Immediate,
..Cfg::default()
});
h.touch(PointerPhase::Down, 30.0, 120.0);
h.touch(PointerPhase::Move, 40.0, 120.0);
assert_eq!(
h.phase(),
DragPhase::Dragging,
"an immediate touch drag doesn't"
);
}
#[test]
fn the_ghost_opens_and_closes_with_the_session() {
let mut h = Harness::new(Cfg::default());
let idle = h.frame();
assert_eq!(rects(&idle), vec![rest()], "no ghost before a drag");
h.mouse_drag();
let dragging = h.frame();
let ghost = Rect::from_origin_size(Point::new(40.0, 130.0), CHILD);
assert_eq!(rects(&dragging), vec![rest(), ghost]);
assert!(
dragging
.layers
.contains(&(ghost.origin(), CHILD, GHOST_OPACITY)),
"{:?}",
dragging.layers
);
h.mouse(PointerPhase::Move, 200.0, 400.0);
let moved = h.frame();
assert_eq!(
rects(&moved).last().copied(),
Some(Rect::from_origin_size(Point::new(180.0, 380.0), CHILD)),
"the ghost follows the pointer"
);
h.mouse(PointerPhase::Up, 200.0, 400.0);
let ended = h.frame();
assert_eq!(
rects(&ended),
vec![rest()],
"the ghost closed with the session"
);
assert!(ended.layers.is_empty());
}
#[test]
fn the_pointer_stays_captured_for_the_whole_drag() {
let mut h = Harness::new(Cfg::default());
h.mouse_drag();
h.frame();
assert!(h.root.is_pointer_captured());
h.mouse(PointerPhase::Move, 390.0, 590.0);
assert_eq!(
h.state.coordinator.state().session().map(|s| s.pointer),
Some(Point::new(390.0, 590.0))
);
assert!(h.root.is_pointer_captured());
h.mouse(PointerPhase::Up, 390.0, 590.0);
assert!(!h.root.is_pointer_captured(), "released on the physical Up");
}
#[test]
fn releasing_over_a_target_drops_on_it() {
let mut h = Harness::new(Cfg::default());
let target = h.target(Rect::new(70.0, 150.0, 170.0, 250.0));
h.mouse_drag();
assert_eq!(
h.state
.coordinator
.state()
.session()
.and_then(|s| s.hovered),
None,
"(60, 150) is outside the target"
);
h.mouse(PointerPhase::Move, 75.0, 155.0);
assert_eq!(
h.state
.coordinator
.state()
.session()
.and_then(|s| s.hovered),
Some(target),
"the move resolved the target under the pointer"
);
h.mouse(PointerPhase::Up, 80.0, 160.0);
match h.state.coordinator.state() {
DragState::Dropping { session, target: t } => {
assert_eq!(t, target);
assert_eq!(session.pointer, Point::new(80.0, 160.0));
}
other => panic!("expected a drop, got {other:?}"),
}
}
#[test]
fn releasing_outside_every_target_cancels() {
let mut h = Harness::new(Cfg::default());
let target = h.target(Rect::new(70.0, 150.0, 170.0, 250.0));
h.mouse_drag();
h.mouse(PointerPhase::Move, 75.0, 155.0);
h.mouse(PointerPhase::Up, 200.0, 160.0);
assert_eq!(h.phase(), DragPhase::Idle);
let changes = h.changes.borrow();
let tail: Vec<_> = changes.iter().rev().take(4).rev().copied().collect();
assert_eq!(
tail,
vec![
DragStateChange::Leave { target },
DragStateChange::Move {
pointer: Point::new(200.0, 160.0)
},
DragStateChange::Phase {
previous: DragPhase::Dragging,
next: DragPhase::Cancelled,
},
DragStateChange::Phase {
previous: DragPhase::Cancelled,
next: DragPhase::Idle,
},
]
);
}
#[test]
fn escape_cancels_the_drag_and_restores_focus() {
let mut h = Harness::new(Cfg::default());
assert!(
!h.root.is_focus_active(),
"nothing focused before the press"
);
let target = h.target(Rect::new(0.0, 0.0, 400.0, 600.0));
h.mouse_drag();
h.frame();
assert!(
h.root.is_focus_active(),
"the press holds focus for the source"
);
assert_eq!(
h.state
.coordinator
.state()
.session()
.and_then(|s| s.hovered),
Some(target)
);
h.escape();
assert_eq!(h.phase(), DragPhase::Idle, "Escape cancelled the session");
assert!(h.changes.borrow().contains(&DragStateChange::Phase {
previous: DragPhase::Dragging,
next: DragPhase::Cancelled,
}));
assert!(
h.changes
.borrow()
.contains(&DragStateChange::Leave { target }),
"the hovered target heard Leave, not a drop"
);
assert!(
!h.root.is_focus_active(),
"focus is back to what it was before the press"
);
let after = h.frame();
assert_eq!(rects(&after), vec![rest()], "the ghost closed");
h.state.log.borrow_mut().clear();
h.mouse(PointerPhase::Move, 32.0, 122.0);
h.mouse(PointerPhase::Up, 32.0, 122.0);
assert!(h.log().is_empty(), "{:?}", h.log());
assert_eq!(h.state.taps, 0);
assert!(!h.root.is_pointer_captured());
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
assert_eq!(h.state.taps, 1);
}
#[test]
fn escape_cancels_through_a_child_that_holds_focus_and_leaves_it_focused() {
let mut h = Harness::new(Cfg {
focusable_child: true,
..Cfg::default()
});
h.mouse_drag();
assert!(h.root.is_focus_active());
h.escape();
assert_eq!(h.phase(), DragPhase::Idle);
assert!(
!h.log().iter().any(|line| line.starts_with("Key")),
"the source consumed Escape: {:?}",
h.log()
);
h.mouse(PointerPhase::Up, 60.0, 150.0);
assert!(h.root.is_focus_active(), "the child is still focused");
h.key(Key::Character("a".into()));
assert_eq!(
h.log().last().map(String::as_str),
Some("Key Character(\"a\")"),
"keys still reach the focused child"
);
}
#[test]
fn escape_outside_a_drag_reaches_the_child() {
let mut h = Harness::new(Cfg {
focusable_child: true,
..Cfg::default()
});
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
h.escape();
assert_eq!(
h.log().last().map(String::as_str),
Some("Key Named(Escape)")
);
}
#[test]
fn a_drop_releases_the_focus_path_the_press_claimed() {
let mut h = Harness::new(Cfg::default());
h.mouse_drag();
h.mouse(PointerPhase::Up, 60.0, 150.0);
assert_eq!(h.phase(), DragPhase::Idle);
h.escape();
assert!(!h.log().iter().any(|line| line.starts_with("Key")));
h.mouse(PointerPhase::Down, 300.0, 500.0);
h.mouse(PointerPhase::Up, 300.0, 500.0);
assert!(!h.root.is_focus_active());
}
#[test]
fn a_pointer_cancel_abandons_the_session() {
let mut h = Harness::new(Cfg::default());
h.mouse_drag();
h.frame();
h.mouse(PointerPhase::Cancel, 60.0, 150.0);
assert_eq!(h.phase(), DragPhase::Idle);
assert!(h.changes.borrow().contains(&DragStateChange::Phase {
previous: DragPhase::Dragging,
next: DragPhase::Cancelled,
}));
assert!(!h.root.is_pointer_captured());
let after = h.frame();
assert_eq!(rects(&after), vec![rest()], "the ghost closed");
assert_eq!(h.state.taps, 0);
}
#[test]
fn a_session_cancelled_elsewhere_closes_the_ghost_and_swallows_the_rest() {
let mut h = Harness::new(Cfg::default());
h.mouse_drag();
h.frame();
h.state.coordinator.cancel();
let after = h.frame();
assert_eq!(rects(&after), vec![rest()], "closed on the next frame");
h.state.log.borrow_mut().clear();
h.mouse(PointerPhase::Move, 90.0, 160.0);
h.mouse(PointerPhase::Up, 90.0, 160.0);
assert!(
h.log().is_empty(),
"the child hears nothing more: {:?}",
h.log()
);
assert_eq!(h.phase(), DragPhase::Idle);
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
assert_eq!(h.state.taps, 1);
}
#[test]
fn dim_feedback_paints_the_source_under_a_dim_layer() {
let mut h = Harness::new(Cfg::default());
h.mouse_drag();
let scene = h.frame();
assert_eq!(
scene.layers.first(),
Some(&(rest().origin(), CHILD, DIM_OPACITY))
);
assert_eq!(rects(&scene).first(), Some(&rest()));
}
#[test]
fn placeholder_feedback_leaves_the_rest_position_empty() {
let mut h = Harness::new(Cfg {
feedback: SourceFeedback::Placeholder,
..Cfg::default()
});
h.mouse_drag();
let scene = h.frame();
assert_eq!(
rects(&scene),
vec![Rect::from_origin_size(Point::new(40.0, 130.0), CHILD)],
"only the ghost paints the child"
);
}
#[test]
fn no_feedback_leaves_the_source_untouched() {
let mut h = Harness::new(Cfg {
feedback: SourceFeedback::None,
..Cfg::default()
});
h.mouse_drag();
let scene = h.frame();
assert_eq!(
scene.layers.len(),
1,
"only the ghost's layer: {:?}",
scene.layers
);
assert_eq!(rects(&scene).first(), Some(&rest()));
}
#[test]
fn a_ghost_builder_replaces_the_snapshot() {
let mut h = Harness::new(Cfg {
custom_ghost: true,
..Cfg::default()
});
h.mouse_drag();
let scene = h.frame();
assert_eq!(rects(&scene), vec![rest()], "the child is not snapshotted");
assert!(
scene.layers.contains(&(
Point::new(40.0, 130.0),
Size::new(30.0, 20.0),
GHOST_OPACITY
)),
"the built ghost floats at the pointer, at its own size: {:?}",
scene.layers
);
}
#[test]
fn a_secondary_press_never_arms() {
let mut h = Harness::new(Cfg::default());
h.root.event(
&mut h.state,
&InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Down,
position: Point::new(30.0, 120.0),
button: PointerButton::Secondary,
}),
);
assert_eq!(h.phase(), DragPhase::Idle);
assert_eq!(h.log(), vec!["Down@20,20"], "it still reaches the child");
}
#[test]
fn a_plain_tap_leaves_the_source_focused_for_a_later_keyboard_lift() {
let mut h = Harness::new(Cfg::default());
assert!(!h.root.is_focus_active(), "nothing focused yet");
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
assert_eq!(h.state.taps, 1, "the tap still reaches the child");
assert!(
h.root.is_focus_active(),
"a plain tap keeps the focus claim, unlike a drag's end"
);
}
#[test]
fn keyboard_lift_cycle_and_drop_deliver_the_payload_to_the_hovered_target() {
let mut h = Harness::new(Cfg::default());
let first = h.target(Rect::new(0.0, 0.0, 100.0, 100.0));
let second = h.target(Rect::new(100.0, 0.0, 200.0, 100.0));
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
assert!(h.root.is_focus_active());
h.key(Key::Named(NamedKey::Enter));
assert_eq!(h.phase(), DragPhase::Dragging, "Enter lifts");
assert_eq!(
h.state.coordinator.with_payload(|item: &u32| *item),
Some(7),
"the payload builder ran against the state"
);
assert_eq!(
h.state
.coordinator
.state()
.session()
.and_then(|s| s.hovered),
None,
"nothing hovered until a cycle step"
);
h.key(Key::Named(NamedKey::ArrowRight));
assert_eq!(
h.state
.coordinator
.state()
.session()
.and_then(|s| s.hovered),
Some(first),
"the first registered target is hovered after one step"
);
h.key(Key::Named(NamedKey::ArrowRight));
assert_eq!(
h.state
.coordinator
.state()
.session()
.and_then(|s| s.hovered),
Some(second)
);
h.key(Key::Named(NamedKey::Enter));
assert_eq!(
h.phase(),
DragPhase::Dropping,
"Enter drops on the hovered target"
);
match h.state.coordinator.state() {
DragState::Dropping { target, .. } => assert_eq!(target, second),
other => panic!("expected Dropping, got {other:?}"),
}
assert_eq!(
h.state.coordinator.take_payload::<u32>().map(|p| *p),
Some(7),
"the payload the lift carried reaches the dropped-on target"
);
h.state.coordinator.complete_drop();
assert_eq!(h.phase(), DragPhase::Idle);
assert!(
!h.root.is_focus_active(),
"a completed drop releases focus, like a pointer drop"
);
}
#[test]
fn keyboard_drop_with_nothing_hovered_cancels() {
let mut h = Harness::new(Cfg::default());
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
h.key(Key::Named(NamedKey::Enter));
assert_eq!(h.phase(), DragPhase::Dragging);
h.key(Key::Named(NamedKey::Enter));
assert_eq!(h.phase(), DragPhase::Idle, "dropping on nothing cancels");
}
#[test]
fn escape_cancels_a_keyboard_lift_and_a_later_lift_still_works() {
let mut h = Harness::new(Cfg::default());
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
h.key(Key::Named(NamedKey::Enter));
assert_eq!(h.phase(), DragPhase::Dragging);
h.escape();
assert_eq!(h.phase(), DragPhase::Idle);
assert!(
!h.root.is_focus_active(),
"Escape ends the focus session outright, even for a keyboard lift"
);
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
h.key(Key::Named(NamedKey::Enter));
assert_eq!(h.phase(), DragPhase::Dragging, "a later lift still works");
}
#[test]
fn space_also_lifts_and_drops() {
let mut h = Harness::new(Cfg::default());
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
h.key(Key::Character(" ".into()));
assert_eq!(h.phase(), DragPhase::Dragging, "Space lifts");
h.key(Key::Character(" ".into()));
assert_eq!(
h.phase(),
DragPhase::Idle,
"Space drops (on nothing: a cancel)"
);
}
#[test]
fn a_focused_child_keeps_its_own_keys_the_lift_chord_included() {
let mut h = Harness::new(Cfg {
focusable_child: true,
..Cfg::default()
});
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
assert!(h.root.is_focus_active(), "the child holds its own focus");
h.key(Key::Named(NamedKey::Enter));
assert_eq!(
h.phase(),
DragPhase::Idle,
"the child's own focus claim takes the key first — no lift"
);
assert_eq!(h.log().last().map(String::as_str), Some("Key Named(Enter)"));
}
#[test]
fn semantics_reports_a_button_role_with_click_and_a_label_for_each_state() {
let mut h = Harness::new(Cfg::default());
let update = h.root.semantics();
let (_, node) = update
.nodes
.iter()
.find(|(_, n)| n.role() == Role::Button)
.expect("a Role::Button node was pushed");
assert_eq!(
node.label(),
Some("Draggable"),
"idle names no verb Click does not perform"
);
assert_eq!(node.description(), Some("Press Enter to lift"));
assert!(node.supports_action(Action::Click));
h.mouse_drag();
h.frame();
let update = h.root.semantics();
let (_, node) = update
.nodes
.iter()
.find(|(_, n)| n.role() == Role::Button)
.expect("a Role::Button node was pushed");
assert_eq!(
node.label(),
Some("Dragging"),
"a pointer session flips the label, but Click still only cancels it"
);
h.mouse(PointerPhase::Up, 60.0, 150.0);
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
h.key(Key::Named(NamedKey::Enter));
let update = h.root.semantics();
let (_, node) = update
.nodes
.iter()
.find(|(_, n)| n.role() == Role::Button)
.expect("a Role::Button node was pushed");
assert_eq!(
node.label(),
Some("Drop"),
"a live keyboard session labels Click honestly: it drops"
);
}
fn node_center() -> Point {
Point::new(AT.x + CHILD.width / 2.0, AT.y + CHILD.height / 2.0)
}
#[test]
fn an_accessibility_click_on_an_idle_focused_source_is_a_plain_tap() {
let mut h = Harness::new(Cfg::default());
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
assert!(h.root.is_focus_active());
h.state.taps = 0;
h.state.log.borrow_mut().clear();
let center = node_center();
h.mouse(PointerPhase::Down, center.x, center.y);
h.mouse(PointerPhase::Up, center.x, center.y);
assert_eq!(h.state.taps, 1, "Click reaches the child as a plain tap");
assert_eq!(h.phase(), DragPhase::Idle, "Click never lifts a drag");
}
#[test]
fn an_accessibility_click_during_a_live_keyboard_session_with_nothing_hovered_cancels_it() {
let mut h = Harness::new(Cfg::default());
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
h.key(Key::Named(NamedKey::Enter));
assert_eq!(
h.phase(),
DragPhase::Dragging,
"Enter lifts a keyboard session"
);
assert_eq!(
h.state.taps, 1,
"the earlier focusing tap reached the child"
);
h.state.log.borrow_mut().clear();
let center = node_center();
h.mouse(PointerPhase::Down, center.x, center.y);
h.mouse(PointerPhase::Up, center.x, center.y);
assert_eq!(h.phase(), DragPhase::Idle, "the session ended, not dropped");
assert_eq!(h.state.taps, 1, "the matching Up never reached the child");
assert!(h.log().is_empty(), "the Down/Up never reached the child");
assert!(
h.changes.borrow().iter().any(|c| matches!(
c,
DragStateChange::Phase {
next: DragPhase::Cancelled,
..
}
)),
"cancelled, never a drop: {:?}",
h.changes.borrow()
);
assert!(
!h.changes.borrow().iter().any(|c| matches!(
c,
DragStateChange::Phase {
next: DragPhase::Dropping,
..
}
)),
"never a drop: {:?}",
h.changes.borrow()
);
}
#[test]
fn an_accessibility_click_during_a_live_keyboard_session_drops_onto_the_hovered_target() {
let mut h = Harness::new(Cfg::default());
let target = h.target(Rect::new(0.0, 0.0, 400.0, 600.0));
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
h.key(Key::Named(NamedKey::Enter));
h.key(Key::Named(NamedKey::ArrowRight));
assert_eq!(
h.state
.coordinator
.state()
.session()
.and_then(|s| s.hovered),
Some(target),
"cycling landed on the only registered target"
);
h.state.log.borrow_mut().clear();
let center = node_center();
h.mouse(PointerPhase::Down, center.x, center.y);
h.mouse(PointerPhase::Up, center.x, center.y);
assert_eq!(
h.phase(),
DragPhase::Dropping,
"the coordinator reached Dropping on the hovered target"
);
match h.state.coordinator.state() {
DragState::Dropping { target: on, .. } => assert_eq!(on, target),
other => panic!("expected Dropping, got {other:?}"),
}
assert!(h.log().is_empty(), "the Down/Up never reached the child");
h.state.coordinator.complete_drop();
assert_eq!(h.phase(), DragPhase::Idle);
}
#[test]
fn a_keyboard_session_ended_elsewhere_returns_to_idle_ready_for_another_lift() {
let mut h = Harness::new(Cfg::default());
h.mouse(PointerPhase::Down, 30.0, 120.0);
h.mouse(PointerPhase::Up, 30.0, 120.0);
h.key(Key::Named(NamedKey::Enter));
assert_eq!(h.phase(), DragPhase::Dragging, "Enter lifts");
h.state.coordinator.cancel();
let after = h.frame();
assert_eq!(rects(&after), vec![rest()], "the ghost closed");
assert!(
h.root.is_focus_active(),
"the focus claim is left exactly as it was, not released"
);
h.key(Key::Named(NamedKey::Enter));
assert_eq!(
h.phase(),
DragPhase::Dragging,
"the source landed back on Idle, ready for another lift"
);
}
}