use super::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ContainerClipboard {
Silent,
WritesBeforeRouting(&'static str),
WritesAfterRouting(&'static str),
AsksForPaste,
}
struct ClipboardLeaf {
text: &'static str,
seen: Rc<RefCell<Vec<EditCommand>>>,
}
impl crate::widget::Widget for ClipboardLeaf {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(100.0, 30.0))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
match event {
InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
ctx.request_focus();
EventResult::Handled
}
InputEvent::EditCommand(cmd) => {
self.seen.borrow_mut().push(cmd.clone());
match cmd {
EditCommand::Copy | EditCommand::Cut => {
ctx.write_clipboard(self.text.to_string());
}
EditCommand::Paste(_) | EditCommand::SelectAll => {}
}
EventResult::Handled
}
InputEvent::Key(k) if k.key == Key::Named(NamedKey::Paste) => {
ctx.request_paste();
EventResult::Handled
}
_ => EventResult::Ignored,
}
}
}
struct ClipboardPair {
top: crate::widget::ChildPod,
bottom: crate::widget::ChildPod,
own: ContainerClipboard,
}
impl crate::widget::Widget for ClipboardPair {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.top.layout_child(ctx, bc);
self.top.set_origin(Point::ZERO);
self.bottom.layout_child(ctx, bc);
self.bottom.set_origin(Point::new(0.0, 30.0));
bc.max()
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
match self.own {
ContainerClipboard::WritesBeforeRouting(text) => ctx.write_clipboard(text.to_string()),
ContainerClipboard::AsksForPaste => ctx.request_paste(),
_ => {}
}
let result = self.route(ctx, event);
if let ContainerClipboard::WritesAfterRouting(text) = self.own {
ctx.write_clipboard(text.to_string());
}
result
}
}
impl ClipboardPair {
fn route(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if event.is_focus_routed() {
for pod in [&mut self.top, &mut self.bottom] {
if pod.is_focused() {
return pod.event_child(ctx, event);
}
}
return EventResult::Ignored;
}
let pos = event.position();
let blurs = matches!(
event,
InputEvent::Pointer(p) if p.phase == PointerPhase::Down
);
let mut result = EventResult::Ignored;
let mut hit = false;
for pod in [&mut self.top, &mut self.bottom] {
if !hit && pod.contains(pos) {
hit = true;
result = pod.event_child(ctx, event);
} else if blurs {
pod.set_focused(false);
}
}
result
}
}
#[derive(Clone)]
struct ClipboardPairView {
own: ContainerClipboard,
top_seen: Rc<RefCell<Vec<EditCommand>>>,
bottom_seen: Rc<RefCell<Vec<EditCommand>>>,
}
impl View<()> for ClipboardPairView {
type Element = ClipboardPair;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> ClipboardPair {
ClipboardPair {
top: crate::widget::ChildPod::new(Box::new(ClipboardLeaf {
text: "top selection",
seen: self.top_seen.clone(),
})),
bottom: crate::widget::ChildPod::new(Box::new(ClipboardLeaf {
text: "bottom selection",
seen: self.bottom_seen.clone(),
})),
own: self.own,
}
}
fn rebuild(&self, _p: &Self, _e: &mut ClipboardPair, _c: &mut BuildCtx<'_>) -> ChangeFlags {
ChangeFlags::NONE
}
}
struct ClipboardHarness {
root: RenderRoot<(), ClipboardPairView>,
state: (),
top_seen: Rc<RefCell<Vec<EditCommand>>>,
bottom_seen: Rc<RefCell<Vec<EditCommand>>>,
}
impl ClipboardHarness {
fn new(own: ContainerClipboard) -> Self {
let mut root: RenderRoot<(), ClipboardPairView> = RenderRoot::new();
let mut state = ();
let view = ClipboardPairView {
own,
top_seen: Rc::new(RefCell::new(Vec::new())),
bottom_seen: Rc::new(RefCell::new(Vec::new())),
};
let (top_seen, bottom_seen) = (view.top_seen.clone(), view.bottom_seen.clone());
root.rebuild(&mut move |_: &mut ()| view.clone(), &mut state);
root.layout(Size::new(100.0, 60.0));
ClipboardHarness {
root,
state,
top_seen,
bottom_seen,
}
}
fn focus(&mut self, leaf: Leaf) {
let y = match leaf {
Leaf::Top => 15.0,
Leaf::Bottom => 45.0,
};
self.root
.event(&mut self.state, &pointer(PointerPhase::Down, 50.0, y));
}
fn dispatch(&mut self, event: &InputEvent) -> EventOutcome {
self.root.event(&mut self.state, event)
}
}
#[test]
fn a_copy_reaches_the_focused_leaf_alone_and_its_text_drains_once() {
let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
h.focus(Leaf::Top);
assert!(
h.root.take_clipboard_write().is_none(),
"a focusing tap writes nothing"
);
h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
assert_eq!(
h.top_seen.borrow().as_slice(),
&[EditCommand::Copy],
"the focused leaf received the command"
);
assert!(
h.bottom_seen.borrow().is_empty(),
"and the unfocused sibling never saw it — focus routing, not hit testing"
);
assert_eq!(
h.root.take_clipboard_write().as_deref(),
Some("top selection")
);
assert_eq!(
h.root.take_clipboard_write(),
None,
"the drain is one-shot: a clipboard write is an edge, not a level"
);
}
#[test]
fn a_copy_follows_the_focus_when_it_moves() {
let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
h.focus(Leaf::Top);
h.focus(Leaf::Bottom);
h.dispatch(&InputEvent::EditCommand(EditCommand::Cut));
assert!(
h.top_seen.borrow().is_empty(),
"the blurred leaf is out of the routed path"
);
assert_eq!(h.bottom_seen.borrow().as_slice(), &[EditCommand::Cut]);
assert_eq!(
h.root.take_clipboard_write().as_deref(),
Some("bottom selection")
);
}
#[test]
fn a_paste_request_drains_once_and_its_answer_is_an_ordinary_dispatch() {
let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
h.focus(Leaf::Top);
assert!(
!h.root.take_paste_request(),
"a focusing tap asks for nothing"
);
h.dispatch(&InputEvent::Key(KeyEvent {
key: Key::Named(NamedKey::Paste),
modifiers: Modifiers::default(),
repeat: false,
}));
assert!(h.root.take_paste_request());
assert!(
!h.root.take_paste_request(),
"the flag is one-shot, so a shell answers a request once"
);
h.dispatch(&InputEvent::EditCommand(EditCommand::Paste(
"from the host".to_string(),
)));
assert_eq!(
h.top_seen.borrow().as_slice(),
&[EditCommand::Paste("from the host".to_string())]
);
assert!(
!h.root.take_paste_request(),
"answering a request does not raise a new one"
);
assert!(
h.root.take_clipboard_write().is_none(),
"and a paste writes nothing back to the host clipboard"
);
}
#[test]
fn a_paste_answered_after_a_blur_reaches_nobody() {
let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
h.focus(Leaf::Top);
h.dispatch(&InputEvent::Key(KeyEvent {
key: Key::Named(NamedKey::Paste),
modifiers: Modifiers::default(),
repeat: false,
}));
assert!(h.root.take_paste_request());
h.dispatch(&pointer(PointerPhase::Down, 50.0, 100.0));
assert!(!h.root.is_focus_active(), "the tap blurred the field");
h.dispatch(&InputEvent::EditCommand(EditCommand::Paste(
"from the host".to_string(),
)));
assert!(h.top_seen.borrow().is_empty());
assert!(h.bottom_seen.borrow().is_empty());
}
#[test]
fn the_last_clipboard_write_of_a_pass_wins_at_the_root() {
let mut h = ClipboardHarness::new(ContainerClipboard::WritesBeforeRouting("container"));
h.focus(Leaf::Top);
h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
assert_eq!(
h.root.take_clipboard_write().as_deref(),
Some("top selection")
);
let mut h = ClipboardHarness::new(ContainerClipboard::WritesAfterRouting("container"));
h.focus(Leaf::Top);
h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
assert_eq!(h.root.take_clipboard_write().as_deref(), Some("container"));
}
#[test]
fn a_write_and_a_paste_request_ride_one_pass_independently() {
let mut h = ClipboardHarness::new(ContainerClipboard::AsksForPaste);
h.focus(Leaf::Top);
assert!(h.root.take_paste_request());
h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
assert_eq!(
h.root.take_clipboard_write().as_deref(),
Some("top selection"),
"the leaf's write landed"
);
assert!(
h.root.take_paste_request(),
"and the container's request landed in the same pass"
);
}
#[test]
fn a_pass_that_asks_for_neither_leaves_both_empty() {
let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
h.focus(Leaf::Top);
h.dispatch(&InputEvent::EditCommand(EditCommand::SelectAll));
h.dispatch(&pointer(PointerPhase::Move, 50.0, 15.0));
assert_eq!(h.top_seen.borrow().as_slice(), &[EditCommand::SelectAll]);
assert_eq!(h.root.take_clipboard_write(), None);
assert!(!h.root.take_paste_request());
}
#[test]
fn an_edit_command_moves_focus_only_when_the_dispatch_asks() {
let mut h = ClipboardHarness::new(ContainerClipboard::Silent);
h.focus(Leaf::Top);
assert!(h.root.is_focus_active());
let gen_before = h.root.focus_ime_generation();
h.dispatch(&InputEvent::EditCommand(EditCommand::Copy));
assert!(
h.root.is_focus_active(),
"a clipboard verb leaves the focus session exactly as it found it"
);
assert_eq!(h.root.focus_ime_generation(), gen_before);
}