use guiduck_scene::geom::{Point, Size}; use taffy::prelude::{length, percent}; use super::*; use crate::clipboard::{LocalClipboard, Selection}; use crate::event::{Modifiers, PointerButton, PointerInput}; use crate::widget::{Container, WidgetTree}; fn sample_font() -> guiduck_scene::paint::Blob { static FONT: &[u8] = include_bytes!("../../../../guiduck-samples/fonts/DejaVuSans.ttf"); static BLOB: std::sync::OnceLock> = std::sync::OnceLock::new(); BLOB.get_or_init(|| guiduck_scene::paint::Blob::new(std::sync::Arc::new(FONT))) .clone() } /// Root with two text inputs stacked in a column. fn fixture() -> (WidgetTree, crate::widget::WidgetId, crate::widget::WidgetId) { let mut tree = WidgetTree::with_text_context(crate::text::TextContext::hermetic([sample_font()])); let root = tree.insert( Container::new(), taffy::Style { size: taffy::Size { width: percent(1.0_f32), height: percent(1.0_f32), }, flex_direction: taffy::FlexDirection::Column, padding: taffy::Rect::length(10.0_f32), gap: taffy::Size { width: length(10.0_f32), height: length(10.0_f32), }, align_items: Some(taffy::AlignItems::FLEX_START), ..Default::default() }, None, ); let first = tree.insert( TextInput::new(14.0).family("DejaVu Sans"), taffy::Style { size: taffy::Size { width: length(200.0_f32), height: taffy::prelude::auto(), }, ..Default::default() }, Some(root), ); let second = tree.insert( TextInput::new(14.0).family("DejaVu Sans"), taffy::Style { size: taffy::Size { width: length(200.0_f32), height: taffy::prelude::auto(), }, ..Default::default() }, Some(root), ); tree.render_frame(Size::new(400.0, 300.0)); (tree, first, second) } fn button_press( tree: &mut WidgetTree, clipboard: &mut dyn Clipboard, pos: Point, button: PointerButton, ) { tree.dispatch_pointer( PointerInput::Down { time: std::time::Instant::now(), pos, button, }, clipboard, ); tree.dispatch_pointer(PointerInput::Up { pos, button }, clipboard); } fn click(tree: &mut WidgetTree, clipboard: &mut LocalClipboard, pos: Point) { button_press(tree, clipboard, pos, PointerButton::Left); } /// A left click with an explicit press time, for multi-click tests. fn timed_click( tree: &mut WidgetTree, clipboard: &mut LocalClipboard, pos: Point, time: std::time::Instant, ) { tree.dispatch_pointer( PointerInput::Down { pos, button: PointerButton::Left, time, }, clipboard, ); tree.dispatch_pointer( PointerInput::Up { pos, button: PointerButton::Left, }, clipboard, ); } fn type_str(tree: &mut WidgetTree, clipboard: &mut LocalClipboard, text: &str) { for c in text.chars() { press( tree, clipboard, Key::Character(c.to_string()), Modifiers::default(), ); } } fn press(tree: &mut WidgetTree, clipboard: &mut LocalClipboard, key: Key, modifiers: Modifiers) { tree.dispatch_key( &KeyInput { key, modifiers, pressed: true, }, clipboard, ); } const CTRL: Modifiers = Modifiers { ctrl: true, shift: false, alt: false, logo: false, }; const SHIFT: Modifiers = Modifiers { ctrl: false, shift: true, alt: false, logo: false, }; fn content(tree: &WidgetTree, id: crate::widget::WidgetId) -> String { tree.widget::(id).unwrap().content() } #[test] fn click_focuses_and_typing_inserts() { let (mut tree, first, second) = fixture(); let mut clipboard = LocalClipboard::default(); // The focus invariant grants the first field focus on the initial // frame, before any interaction. assert_eq!(tree.focus(), Some(first)); // Click-to-focus moves it. click(&mut tree, &mut clipboard, Point::new(50.0, 60.0)); assert_eq!(tree.focus(), Some(second)); type_str(&mut tree, &mut clipboard, "hello"); assert_eq!(content(&tree, second), "hello"); assert!(tree.needs_frame(), "edits schedule a frame"); } #[test] fn focus_invariant_refocuses_after_removal() { let (mut tree, first, second) = fixture(); assert_eq!(tree.focus(), Some(first)); tree.remove(first); tree.render_frame(Size::new(400.0, 300.0)); assert_eq!( tree.focus(), Some(second), "removing the focused widget refocuses the next frame" ); } #[test] fn background_click_keeps_focus() { let (mut tree, first, _second) = fixture(); let mut clipboard = LocalClipboard::default(); assert_eq!(tree.focus(), Some(first)); // The root container is not focusable; clicking it keeps the focus. click(&mut tree, &mut clipboard, Point::new(300.0, 250.0)); assert_eq!(tree.focus(), Some(first)); } #[test] fn select_all_then_typing_replaces() { let (mut tree, first, _) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); type_str(&mut tree, &mut clipboard, "hello"); press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL); type_str(&mut tree, &mut clipboard, "bye"); assert_eq!(content(&tree, first), "bye"); } #[test] fn editing_keys() { let (mut tree, first, _) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); type_str(&mut tree, &mut clipboard, "abcd"); press( &mut tree, &mut clipboard, Key::Backspace, Modifiers::default(), ); assert_eq!(content(&tree, first), "abc"); press(&mut tree, &mut clipboard, Key::Home, Modifiers::default()); press(&mut tree, &mut clipboard, Key::Delete, Modifiers::default()); assert_eq!(content(&tree, first), "bc"); press(&mut tree, &mut clipboard, Key::Right, Modifiers::default()); type_str(&mut tree, &mut clipboard, "x"); assert_eq!(content(&tree, first), "bxc"); // Shift+End selects to end; typing replaces the selection. press(&mut tree, &mut clipboard, Key::Home, Modifiers::default()); press(&mut tree, &mut clipboard, Key::End, SHIFT); type_str(&mut tree, &mut clipboard, "z"); assert_eq!(content(&tree, first), "z"); } #[test] fn clipboard_round_trip() { let (mut tree, first, second) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); type_str(&mut tree, &mut clipboard, "copy me"); press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL); press(&mut tree, &mut clipboard, Key::Character("c".into()), CTRL); assert_eq!( clipboard.get_text(Selection::Clipboard).as_deref(), Some("copy me") ); // Paste into the second input. press(&mut tree, &mut clipboard, Key::Tab, Modifiers::default()); assert_eq!(tree.focus(), Some(second)); press(&mut tree, &mut clipboard, Key::Character("v".into()), CTRL); assert_eq!(content(&tree, second), "copy me"); assert_eq!(content(&tree, first), "copy me"); // Cut clears the source. press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL); press(&mut tree, &mut clipboard, Key::Character("x".into()), CTRL); assert_eq!(content(&tree, second), ""); assert_eq!( clipboard.get_text(Selection::Clipboard).as_deref(), Some("copy me") ); } #[test] fn newlines_never_enter_a_single_line_field() { let (mut tree, first, second) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); // Typing a newline inserts nothing and does not consume the key (so a // default-button activation could still see it). type_str(&mut tree, &mut clipboard, "ab\ncd"); assert_eq!(content(&tree, first), "abcd", "a typed newline is dropped"); // A multi-line clipboard paste lands as one line — the defect: the paste // path used to insert the newlines the typed path rejects. clipboard.set_text(Selection::Clipboard, "one\ntwo\r\nthree"); press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL); press(&mut tree, &mut clipboard, Key::Character("v".into()), CTRL); assert_eq!( content(&tree, first), "onetwothree", "paste collapses lines" ); // A multi-line primary-selection paste, likewise. clipboard.set_text(Selection::Primary, "left\nright"); button_press( &mut tree, &mut clipboard, Point::new(50.0, 60.0), PointerButton::Middle, ); assert_eq!(content(&tree, second), "leftright", "primary paste too"); // And an IME commit carrying a newline. press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL); tree.dispatch_ime(&ImeInput::Commit("here\nthere".into())); assert_eq!(content(&tree, second), "herethere", "IME commit too"); } #[test] fn multiline_field_accepts_line_breaks() { let mut tree = WidgetTree::with_text_context(crate::text::TextContext::hermetic([sample_font()])); let root = tree.insert(Container::new(), taffy::Style::default(), None); let field = tree.insert( TextInput::new(14.0).family("DejaVu Sans").multiline(true), taffy::Style { size: taffy::Size { width: length(200.0_f32), height: taffy::prelude::auto(), }, ..Default::default() }, Some(root), ); tree.render_frame(Size::new(400.0, 300.0)); let mut clipboard = LocalClipboard::default(); assert_eq!(tree.focus(), Some(field)); // Enter inserts a newline instead of being declined. type_str(&mut tree, &mut clipboard, "a"); press(&mut tree, &mut clipboard, Key::Enter, Modifiers::default()); type_str(&mut tree, &mut clipboard, "b"); assert_eq!(content(&tree, field), "a\nb"); // Pasted newlines survive in a multi-line field. clipboard.set_text(Selection::Clipboard, "c\nd"); press(&mut tree, &mut clipboard, Key::End, Modifiers::default()); press(&mut tree, &mut clipboard, Key::Character("v".into()), CTRL); assert_eq!(content(&tree, field), "a\nbc\nd"); // It names itself a multi-line editor to assistive technology. assert_eq!( tree.widget::(field).unwrap().role(), accesskit::Role::MultilineTextInput ); } #[test] fn single_line_field_declines_enter() { let (mut tree, first, _) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); type_str(&mut tree, &mut clipboard, "text"); // Enter is not consumed by a single-line field, so a default button // could still act on it, and it inserts nothing. let consumed = tree.dispatch_key( &KeyInput { key: Key::Enter, modifiers: Modifiers::default(), pressed: true, }, &mut clipboard, ); assert!(!consumed, "a single-line field declines Enter"); assert_eq!(content(&tree, first), "text", "and inserts no newline"); assert_eq!( tree.widget::(first).unwrap().role(), accesskit::Role::TextInput ); } #[test] fn tab_cycles_focus() { let (mut tree, first, second) = fixture(); let mut clipboard = LocalClipboard::default(); // The focus invariant starts the cycle at the first field. assert_eq!(tree.focus(), Some(first)); press(&mut tree, &mut clipboard, Key::Tab, Modifiers::default()); assert_eq!(tree.focus(), Some(second)); press(&mut tree, &mut clipboard, Key::Tab, Modifiers::default()); assert_eq!(tree.focus(), Some(first), "cycles"); press(&mut tree, &mut clipboard, Key::Tab, SHIFT); assert_eq!(tree.focus(), Some(second), "shift-tab goes backwards"); } #[test] fn ime_preedit_and_commit() { let (mut tree, first, _) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); type_str(&mut tree, &mut clipboard, "ab"); tree.dispatch_ime(&ImeInput::Preedit { text: "しか".into(), cursor: Some((6, 6)), }); // Preedit is displayed but not part of the committed content. assert_eq!( tree.widget::(first).unwrap().display_content(), "abしか" ); assert_eq!(content(&tree, first), "ab"); tree.dispatch_ime(&ImeInput::Commit("鹿".into())); assert_eq!(content(&tree, first), "ab鹿"); // A cancelled composition leaves committed text alone. tree.dispatch_ime(&ImeInput::Preedit { text: "x".into(), cursor: Some((1, 1)), }); tree.dispatch_ime(&ImeInput::Preedit { text: String::new(), cursor: None, }); assert_eq!(content(&tree, first), "ab鹿"); } #[test] fn drag_selects_and_replaces() { let (mut tree, first, _) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); type_str(&mut tree, &mut clipboard, "wide text"); // Press near the start, drag right, release: a selection exists. tree.dispatch_pointer( PointerInput::Down { time: std::time::Instant::now(), pos: Point::new(20.0, 25.0), button: PointerButton::Left, }, &mut clipboard, ); tree.dispatch_pointer(PointerInput::Moved(Point::new(180.0, 25.0)), &mut clipboard); tree.dispatch_pointer( PointerInput::Up { pos: Point::new(180.0, 25.0), button: PointerButton::Left, }, &mut clipboard, ); // Finishing the drag stored the selection as the primary selection, // without touching the regular clipboard. assert_eq!( clipboard.get_text(Selection::Primary).as_deref(), Some("wide text") ); assert_eq!(clipboard.get_text(Selection::Clipboard), None); type_str(&mut tree, &mut clipboard, "replaced"); assert_eq!(content(&tree, first), "replaced"); } #[test] fn keyboard_selection_updates_primary() { let (mut tree, _first, _) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); type_str(&mut tree, &mut clipboard, "primary"); press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL); assert_eq!( clipboard.get_text(Selection::Primary).as_deref(), Some("primary"), "select-all populates the primary selection" ); // Collapsing the selection leaves the primary alone. press(&mut tree, &mut clipboard, Key::Home, Modifiers::default()); press(&mut tree, &mut clipboard, Key::Right, SHIFT); assert_eq!( clipboard.get_text(Selection::Primary).as_deref(), Some("p"), "shift-selection replaces the primary selection" ); } #[test] fn middle_click_pastes_primary() { let (mut tree, first, second) = fixture(); let mut clipboard = LocalClipboard::default(); clipboard.set_text(Selection::Primary, "pasted"); button_press( &mut tree, &mut clipboard, Point::new(50.0, 25.0), PointerButton::Middle, ); assert_eq!(content(&tree, first), "pasted"); assert_eq!( tree.focus(), Some(first), "middle-click focuses like any click" ); // Select the pasted word by dragging, then middle-click the second // field: the drag's selection is what pastes. tree.dispatch_pointer( PointerInput::Down { time: std::time::Instant::now(), pos: Point::new(20.0, 25.0), button: PointerButton::Left, }, &mut clipboard, ); tree.dispatch_pointer(PointerInput::Moved(Point::new(180.0, 25.0)), &mut clipboard); tree.dispatch_pointer( PointerInput::Up { pos: Point::new(180.0, 25.0), button: PointerButton::Left, }, &mut clipboard, ); button_press( &mut tree, &mut clipboard, Point::new(50.0, 60.0), PointerButton::Middle, ); assert_eq!(content(&tree, second), "pasted"); } #[test] fn double_click_selects_word() { let (mut tree, first, _) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); type_str(&mut tree, &mut clipboard, "hello"); let base = std::time::Instant::now(); let pos = Point::new(30.0, 25.0); timed_click(&mut tree, &mut clipboard, pos, base); timed_click( &mut tree, &mut clipboard, pos, base + std::time::Duration::from_millis(100), ); assert_eq!( clipboard.get_text(Selection::Primary).as_deref(), Some("hello"), "double-click selects the word and syncs the primary selection" ); type_str(&mut tree, &mut clipboard, "bye"); assert_eq!(content(&tree, first), "bye", "typing replaces the word"); } #[test] fn triple_click_selects_line() { let (mut tree, first, _) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); type_str(&mut tree, &mut clipboard, "hello world"); let base = std::time::Instant::now(); let pos = Point::new(30.0, 25.0); for i in 0..3 { timed_click( &mut tree, &mut clipboard, pos, base + std::time::Duration::from_millis(100 * i), ); } assert_eq!( clipboard.get_text(Selection::Primary).as_deref(), Some("hello world"), "triple-click selects the line" ); type_str(&mut tree, &mut clipboard, "z"); assert_eq!(content(&tree, first), "z"); } #[test] fn slow_second_click_just_moves_the_caret() { let (mut tree, first, _) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); type_str(&mut tree, &mut clipboard, "hello"); let base = std::time::Instant::now(); let pos = Point::new(30.0, 25.0); timed_click(&mut tree, &mut clipboard, pos, base); timed_click( &mut tree, &mut clipboard, pos, base + std::time::Duration::from_secs(1), ); assert_eq!( clipboard.get_text(Selection::Primary), None, "no selection was made" ); type_str(&mut tree, &mut clipboard, "X"); assert_eq!(content(&tree, first).len(), 6, "insert, not replace"); } #[test] fn user_edits_raise_changed_events() { let (mut tree, first, _) = fixture(); let mut clipboard = LocalClipboard::default(); let root = tree.root().unwrap(); let seen: std::rc::Rc>> = Default::default(); let seen2 = seen.clone(); // Subscribed on the root: Changed bubbles like any other event. tree.on_event(root, crate::event::EventKind::Changed, move |_ctx, ev| { seen2 .borrow_mut() .push(ev.changed().expect("changed payload").to_owned()); }); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); type_str(&mut tree, &mut clipboard, "hi"); assert_eq!(*seen.borrow(), vec!["h".to_owned(), "hi".to_owned()]); // Programmatic set_content is silent… tree.widget_mut::(first) .unwrap() .set_content("reset"); assert_eq!(seen.borrow().len(), 2); // …and the next user edit compares against the programmatic value. press(&mut tree, &mut clipboard, Key::End, Modifiers::default()); press( &mut tree, &mut clipboard, Key::Backspace, Modifiers::default(), ); assert_eq!(seen.borrow().last().map(String::as_str), Some("rese")); // Copy does not edit; paste at the end does. press(&mut tree, &mut clipboard, Key::Character("a".into()), CTRL); press(&mut tree, &mut clipboard, Key::Character("c".into()), CTRL); assert_eq!(seen.borrow().len(), 3); press(&mut tree, &mut clipboard, Key::End, Modifiers::default()); press(&mut tree, &mut clipboard, Key::Character("v".into()), CTRL); assert_eq!(seen.borrow().last().map(String::as_str), Some("reserese")); assert_eq!(seen.borrow().len(), 4); // An IME commit edits; a bare preedit does not. tree.dispatch_ime(&ImeInput::Preedit { text: "x".into(), cursor: Some((1, 1)), }); assert_eq!(seen.borrow().len(), 4); tree.dispatch_ime(&ImeInput::Commit("鹿".into())); assert_eq!(seen.borrow().last().map(String::as_str), Some("reserese鹿")); } #[test] fn right_click_does_not_move_caret() { let (mut tree, first, _) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); type_str(&mut tree, &mut clipboard, "abc"); // Caret sits at the end; a right-click near the start must not move it. button_press( &mut tree, &mut clipboard, Point::new(12.0, 25.0), PointerButton::Right, ); type_str(&mut tree, &mut clipboard, "d"); assert_eq!(content(&tree, first), "abcd"); } #[test] fn focus_reflects_in_accessibility_tree() { let (mut tree, first, _) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 25.0)); type_str(&mut tree, &mut clipboard, "a11y"); let update = tree.accessibility_tree(); let focused = update .nodes .iter() .find(|(id, _)| *id == update.focus) .map(|(_, n)| n) .expect("focused node present"); assert_eq!(focused.role(), accesskit::Role::TextInput); assert_eq!(focused.value(), Some("a11y")); let _ = first; } /// Whether the focused input currently paints its caret (the retained /// fragment holds one more fill than the blink-hidden phase). fn caret_painted(tree: &mut WidgetTree, id: crate::widget::WidgetId) -> bool { tree.render_frame(Size::new(400.0, 300.0)); let mut fragment = guiduck_scene::Fragment::new(); let size = tree.layout(id).size(); tree.widget_mut::(id) .expect("input exists") .paint(&mut fragment, size); let fills = fragment .items .iter() .filter(|i| matches!(i, guiduck_scene::DisplayItem::Fill { .. })) .count(); // Background fill + (optionally) the caret fill; no selection here. fills > 1 } #[test] fn the_caret_blinks_deterministically() { let (mut tree, first, _second) = fixture(); let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(60.0, 25.0)); assert!(caret_painted(&mut tree, first), "fresh focus: caret solid"); // The focused input scheduled a wake; unfocused trees schedule none. let now = std::time::Instant::now(); let deadline = tree.next_wake(now).expect("a blink is scheduled"); assert_eq!(deadline, now + BLINK_INTERVAL); // The deadline arrives: the caret toggles off, and the next toggle is // scheduled. tree.tick(deadline); assert!(tree.needs_frame(), "the toggle repaints"); assert!( !caret_painted(&mut tree, first), "blink phase: caret hidden" ); let second_deadline = tree .next_wake(deadline) .expect("the blink keeps rescheduling"); assert_eq!(second_deadline, deadline + BLINK_INTERVAL); // Typing resets the phase to solid and re-anchors the schedule. type_str(&mut tree, &mut clipboard, "x"); assert!(caret_painted(&mut tree, first), "edits show the caret"); let anchored = tree .next_wake(second_deadline) .expect("typing re-anchored the blink"); // Focus loss stops the blink. Plain `set_focus(None)` would not do it — // the focus invariant refocuses the input on the next frame — so take // the focusables away by disabling the tree: focus clears, the // already-anchored deadline fires once (the unfocused input declines // it), and nothing further is scheduled. let root = tree.root().expect("root"); tree.set_enabled(root, false); tree.render_frame(Size::new(400.0, 300.0)); assert_eq!(tree.focus(), None, "no enabled focusables remain"); tree.tick(anchored); tree.render_frame(Size::new(400.0, 300.0)); assert_eq!( tree.next_wake(anchored + BLINK_INTERVAL), None, "an unfocused input requests no wakes (zero-idle preserved)" ); } /// A clipboard whose reads always answer `Pending`, like a platform that /// must negotiate with the selection's owner. Records what was asked for. #[derive(Default)] struct PendingClipboard { requests: Vec, } impl Clipboard for PendingClipboard { fn request_text(&mut self, selection: Selection) -> crate::clipboard::TextRequest { self.requests.push(selection); crate::clipboard::TextRequest::Pending } fn set_text(&mut self, _selection: Selection, _text: &str) {} } #[test] fn a_pending_paste_inserts_on_delivery_not_on_the_gesture() { let (mut tree, first, _second) = fixture(); let mut clipboard = PendingClipboard::default(); assert_eq!(tree.focus(), Some(first)); // The gesture itself inserts nothing: the platform hasn't answered yet. tree.dispatch_key( &KeyInput { key: Key::Character("v".into()), modifiers: CTRL, pressed: true, }, &mut clipboard, ); assert_eq!(clipboard.requests, vec![Selection::Clipboard]); assert_eq!(content(&tree, first), ""); // The delivery is what pastes. tree.dispatch_paste("arrived"); assert_eq!(content(&tree, first), "arrived"); assert!(tree.needs_frame(), "a delivered paste schedules a frame"); } #[test] fn a_delivered_paste_follows_focus() { let (mut tree, first, second) = fixture(); let mut pending = PendingClipboard::default(); assert_eq!(tree.focus(), Some(first)); // Middle-click asks for the primary selection; the answer is Pending. button_press( &mut tree, &mut pending, Point::new(50.0, 20.0), PointerButton::Middle, ); assert!(pending.requests.contains(&Selection::Primary)); // Focus moves before the platform answers. Delivery goes to the // focused field — pasted text is input, and input follows focus. let mut clipboard = LocalClipboard::default(); click(&mut tree, &mut clipboard, Point::new(50.0, 60.0)); assert_eq!(tree.focus(), Some(second)); tree.dispatch_paste("late"); assert_eq!(content(&tree, first), ""); assert_eq!(content(&tree, second), "late"); } #[test] fn a_delivered_paste_is_sanitized_like_any_other() { let (mut tree, first, _second) = fixture(); assert_eq!(tree.focus(), Some(first)); // A single-line field collapses a delivered multi-line paste, exactly // as it would a synchronous one. tree.dispatch_paste("one\ntwo"); assert_eq!(content(&tree, first), "onetwo"); }