1#![allow(non_snake_case)]
7
8use std::{
9 cell::{Cell, RefCell},
10 ffi::c_void,
11 ptr::NonNull,
12};
13
14use objc2::{
15 AnyThread, DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send,
16 rc::Retained,
17 runtime::{AnyObject, NSObject, ProtocolObject},
18 sel,
19};
20use objc2_app_kit::{
21 NSApplication, NSControlStateValueOff, NSControlStateValueOn, NSDragOperation,
22 NSDraggingContext, NSDraggingItem, NSDraggingSession, NSDraggingSource, NSEvent,
23 NSEventModifierFlags, NSEventType, NSImage, NSMenu, NSMenuItem, NSMenuItemBadge,
24 NSPasteboardWriting, NSResponder, NSTextInputClient, NSView,
25};
26use objc2_core_foundation::{CGPoint, CGRect, CGSize};
27use objc2_foundation::{
28 NSArray, NSAttributedString, NSAttributedStringKey, NSData, NSNotFound, NSObjectProtocol,
29 NSPoint, NSRange, NSRangePointer, NSRect, NSString, NSUInteger,
30};
31use objc2_quartz_core::CATransaction;
32
33use cbf::data::{
34 context_menu::{ContextMenu, ContextMenuIcon, ContextMenuItem, ContextMenuItemType},
35 drag::DragStartRequest,
36 edit::EditAction,
37 ime::{ImeBoundsUpdate, ImeCompositionBounds, ImeRect, ImeTextRange, TextSelectionBounds},
38 key::{KeyEvent, KeyEventType},
39 mouse::{MouseEvent, MouseWheelEvent, PointerType},
40};
41
42use super::bindings::{CALayerHost, ContextId};
43use super::choice_menu::ChromeChoiceMenuPresenter;
44#[cfg(feature = "macos-choice-menu-default")]
45use super::choice_menu::MacChoiceMenuPresenter;
46use crate::data::choice_menu::{ChromeChoiceMenu, ChromeChoiceMenuSelectionMode};
47use crate::ffi::{
48 convert_nsevent_to_key_event, convert_nsevent_to_mouse_event,
49 convert_nsevent_to_mouse_wheel_event,
50};
51
52pub trait BrowserViewMacDelegate {
54 fn on_key_event(&self, view: &BrowserViewMac, event: KeyEvent, commands: Vec<String>);
56 fn on_edit_action(&self, view: &BrowserViewMac, action: EditAction);
58 fn on_ime_event(&self, view: &BrowserViewMac, event: BrowserViewMacImeEvent);
60 fn on_char_event(&self, view: &BrowserViewMac, event: KeyEvent);
62 fn on_mouse_event(&self, view: &BrowserViewMac, event: MouseEvent);
64 fn on_mouse_wheel_event(&self, view: &BrowserViewMac, event: MouseWheelEvent);
66 fn on_context_menu_command(&self, view: &BrowserViewMac, menu_id: u64, command_id: i32);
68 fn on_context_menu_dismissed(&self, view: &BrowserViewMac, menu_id: u64);
70 fn on_choice_menu_selected(&self, view: &BrowserViewMac, request_id: u64, indices: Vec<i32>);
72 fn on_choice_menu_dismissed(&self, view: &BrowserViewMac, request_id: u64);
74 fn on_focus_changed(&self, view: &BrowserViewMac, focused: bool);
76 fn on_native_drag_update(
78 &self,
79 _view: &BrowserViewMac,
80 _event: BrowserViewMacNativeDragUpdate,
81 ) {
82 }
83 fn on_native_drag_drop(&self, _view: &BrowserViewMac, _event: BrowserViewMacNativeDragDrop) {}
85 fn on_native_drag_cancel(&self, _view: &BrowserViewMac, _session_id: u64) {}
87}
88
89pub struct BrowserViewMacConfig {
91 pub frame: CGRect,
92 pub delegate: Box<dyn BrowserViewMacDelegate>,
93}
94
95const NO_MENU_ID: u64 = 0;
96const NO_COMMAND_ID: i32 = i32::MIN;
97const NO_CHOICE_MENU_REQUEST_ID: u64 = 0;
98const NO_CHOICE_MENU_ACTION: i32 = i32::MIN;
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum BrowserViewMacImeEvent {
103 SetComposition {
105 text: String,
106 selection: Option<ImeTextRange>,
107 replacement: Option<ImeTextRange>,
108 },
109 CommitText {
111 text: String,
112 replacement: Option<ImeTextRange>,
113 relative_caret_position: i32,
114 },
115 FinishComposingText { keep_selection: bool },
117}
118
119#[derive(Debug, Clone, PartialEq)]
120pub struct BrowserViewMacNativeDragUpdate {
121 pub session_id: u64,
122 pub modifiers: u32,
123 pub position_in_widget_x: f32,
124 pub position_in_widget_y: f32,
125 pub position_in_screen_x: f32,
126 pub position_in_screen_y: f32,
127}
128
129#[derive(Debug, Clone, PartialEq)]
130pub struct BrowserViewMacNativeDragDrop {
131 pub session_id: u64,
132 pub modifiers: u32,
133 pub position_in_widget_x: f32,
134 pub position_in_widget_y: f32,
135 pub position_in_screen_x: f32,
136 pub position_in_screen_y: f32,
137}
138
139pub struct BrowserViewMacIvars {
141 delegate: Box<dyn BrowserViewMacDelegate>,
142 has_marked_text: Cell<bool>,
143 marked_range: Cell<NSRange>,
144 selected_range: Cell<NSRange>,
145 ime_handled: Cell<bool>,
146 suppress_key_up: Cell<bool>,
147 ime_insert_expected: Cell<bool>,
148 saw_insert_command: Cell<bool>,
149 sent_char_event: Cell<bool>,
150 ime_bounds: RefCell<Option<ImeBoundsUpdate>>,
151 browser_layer: Retained<CALayerHost>,
152 browser_layer_frame: Cell<CGRect>,
153 edit_commands: RefCell<Vec<String>>,
154 pending_char_event: RefCell<Option<KeyEvent>>,
155 context_menu_id: Cell<u64>,
156 context_menu_selected_command_id: Cell<i32>,
157 choice_menu_request_id: Cell<u64>,
158 choice_menu_selected_action: Cell<i32>,
159 active_drag_source: RefCell<Option<Retained<AnyObject>>>,
160}
161
162define_class!(
163 #[unsafe(super(NSView, NSResponder, NSObject))]
165 #[thread_kind = objc2::MainThreadOnly]
166 #[name = "BrowserViewMac"]
167 #[ivars = BrowserViewMacIvars]
168 pub struct BrowserViewMac;
169
170 impl BrowserViewMac {
171 #[unsafe(method(keyDown:))]
172 fn key_down(&self, event: &NSEvent) {
173 let had_marked_text = self.ivars().has_marked_text.get();
174 self.ivars().ime_handled.set(false);
175 self.ivars().suppress_key_up.set(false);
176 self.ivars().saw_insert_command.set(false);
177 self.ivars().sent_char_event.set(false);
178 self.ivars().edit_commands.borrow_mut().clear();
179 self.ivars().pending_char_event.borrow_mut().take();
180
181 let nsevent_ptr = NonNull::from(event).cast::<c_void>();
182 self.ivars()
183 .pending_char_event
184 .replace(Some(convert_nsevent_to_key_event(0, nsevent_ptr)));
185
186 let events = NSArray::arrayWithObject(event);
187 self.interpretKeyEvents(&events);
188
189 if self.ivars().ime_handled.get() {
190 self.ivars().pending_char_event.borrow_mut().take();
191 self.ivars().suppress_key_up.set(true);
192 return;
193 }
194
195 if had_marked_text && should_ignore_accelerator_with_marked_text(event) {
196 self.ivars().pending_char_event.borrow_mut().take();
204 self.ivars().suppress_key_up.set(true);
205 return;
206 }
207
208 self.forward_key_event(event);
209 if self.ivars().saw_insert_command.get() && !self.ivars().sent_char_event.get() {
210 let pending_char_event = self.ivars().pending_char_event.borrow().clone();
211 if let Some(text) = synthesized_char_text(pending_char_event.as_ref())
212 && let Some(event) = build_char_event(pending_char_event, text)
213 {
214 self.send_mac_char_event(event);
215 }
216 }
217 self.ivars().pending_char_event.borrow_mut().take();
218 }
219
220 #[unsafe(method(keyUp:))]
221 fn key_up(&self, event: &NSEvent) {
222 if self.ivars().suppress_key_up.replace(false) {
223 return;
224 }
225 self.forward_key_event(event);
228 }
229
230 #[unsafe(method(flagsChanged:))]
231 fn flags_changed(&self, event: &NSEvent) {
232 self.forward_key_event(event);
233 }
234
235 #[unsafe(method(isFlipped))]
236 fn is_flipped(&self) -> bool {
237 false
238 }
239
240 #[unsafe(method(acceptsFirstResponder))]
241 fn accepts_first_responder(&self) -> bool {
242 true
243 }
244
245 #[unsafe(method(becomeFirstResponder))]
246 fn become_first_responder(&self) -> bool {
247 self.ivars().delegate.on_focus_changed(self, true);
248 true
249 }
250
251 #[unsafe(method(resignFirstResponder))]
252 fn resign_first_responder(&self) -> bool {
253 self.ivars().delegate.on_focus_changed(self, false);
254 true
255 }
256
257 #[unsafe(method(undo:))]
258 fn undo(&self, _sender: &AnyObject) {
259 self.send_edit_action(EditAction::Undo);
260 }
261
262 #[unsafe(method(redo:))]
263 fn redo(&self, _sender: &AnyObject) {
264 self.send_edit_action(EditAction::Redo);
265 }
266
267 #[unsafe(method(cut:))]
268 fn cut(&self, _sender: &AnyObject) {
269 self.send_edit_action(EditAction::Cut);
270 }
271
272 #[unsafe(method(copy:))]
273 fn copy(&self, _sender: &AnyObject) {
274 self.send_edit_action(EditAction::Copy);
275 }
276
277 #[unsafe(method(paste:))]
278 fn paste(&self, _sender: &AnyObject) {
279 self.send_edit_action(EditAction::Paste);
280 }
281
282 #[unsafe(method(selectAll:))]
283 fn select_all(&self, _sender: &AnyObject) {
284 self.send_edit_action(EditAction::SelectAll);
285 }
286
287 #[unsafe(method(mouseDown:))]
288 fn mouse_down(&self, event: &NSEvent) {
289 self.forward_mouse_event(event);
290 }
291
292 #[unsafe(method(rightMouseDown:))]
293 fn right_mouse_down(&self, event: &NSEvent) {
294 self.forward_mouse_event(event);
295 }
296
297 #[unsafe(method(otherMouseDown:))]
298 fn other_mouse_down(&self, event: &NSEvent) {
299 self.forward_mouse_event(event);
300 }
301
302 #[unsafe(method(mouseUp:))]
303 fn mouse_up(&self, event: &NSEvent) {
304 self.forward_mouse_event(event);
305 }
306
307 #[unsafe(method(rightMouseUp:))]
308 fn right_mouse_up(&self, event: &NSEvent) {
309 self.forward_mouse_event(event);
310 }
311
312 #[unsafe(method(otherMouseUp:))]
313 fn other_mouse_up(&self, event: &NSEvent) {
314 self.forward_mouse_event(event);
315 }
316
317 #[unsafe(method(mouseMoved:))]
318 fn mouse_moved(&self, event: &NSEvent) {
319 self.forward_mouse_event(event);
320 }
321
322 #[unsafe(method(mouseDragged:))]
323 fn mouse_dragged(&self, event: &NSEvent) {
324 self.forward_mouse_event(event);
325 }
326
327 #[unsafe(method(rightMouseDragged:))]
328 fn right_mouse_dragged(&self, event: &NSEvent) {
329 self.forward_mouse_event(event);
330 }
331
332 #[unsafe(method(otherMouseDragged:))]
333 fn other_mouse_dragged(&self, event: &NSEvent) {
334 self.forward_mouse_event(event);
335 }
336
337 #[unsafe(method(mouseEntered:))]
338 fn mouse_entered(&self, event: &NSEvent) {
339 self.forward_mouse_event(event);
340 }
341
342 #[unsafe(method(mouseExited:))]
343 fn mouse_exited(&self, event: &NSEvent) {
344 self.forward_mouse_event(event);
345 }
346
347 #[unsafe(method(scrollWheel:))]
348 fn scroll_wheel(&self, event: &NSEvent) {
349 self.forward_mouse_wheel_event(event);
350 }
351
352 #[unsafe(method(contextMenuItemSelected:))]
353 fn context_menu_item_selected(&self, sender: &AnyObject) {
354 let tag: isize = unsafe { msg_send![sender, tag] };
355 self.ivars()
356 .context_menu_selected_command_id
357 .set(tag as i32);
358 }
359
360 #[unsafe(method(choiceMenuItemSelected:))]
361 fn choice_menu_item_selected(&self, sender: &AnyObject) {
362 let tag: isize = unsafe { msg_send![sender, tag] };
363 self.ivars().choice_menu_selected_action.set(tag as i32);
364 }
365 }
366
367 unsafe impl NSTextInputClient for BrowserViewMac {
368 #[unsafe(method(insertText:replacementRange:))]
369 unsafe fn insertText_replacementRange(&self, string: &AnyObject, replacement_range: NSRange) {
370 let Some(text) = extract_insert_text(string) else { return };
371
372 if self.ivars().ime_insert_expected.get() {
373 self.mark_ime_handled();
374 self.ivars().ime_insert_expected.set(false);
375 self.update_marked_state(false, ns_not_found_range(), ns_not_found_range());
376 self.send_mac_ime_event(BrowserViewMacImeEvent::CommitText {
377 text,
378 replacement: nsrange_to_text_range(replacement_range),
379 relative_caret_position: 0,
380 });
381 } else {
382 let pending_char_event = self.ivars().pending_char_event.borrow().clone();
386 if let Some(event) = build_char_event(pending_char_event, text) {
387 self.ivars().sent_char_event.set(true);
388 self.send_mac_char_event(event);
389 }
390 }
391 }
392
393 #[unsafe(method(doCommandBySelector:))]
394 unsafe fn doCommandBySelector(&self, selector: objc2::runtime::Sel) {
395 let mut command = selector.name().to_str().unwrap().to_string();
396 if let Some(stripped) = command.strip_suffix(':') {
397 command = stripped.to_string();
398 }
399 if command.to_ascii_lowercase().starts_with("insert") {
400 self.ivars().saw_insert_command.set(true);
403 return;
404 }
405 self.ivars().edit_commands.borrow_mut().push(command);
406 }
407
408 #[unsafe(method(setMarkedText:selectedRange:replacementRange:))]
409 unsafe fn setMarkedText_selectedRange_replacementRange(
410 &self,
411 string: &AnyObject,
412 selected_range: NSRange,
413 replacement_range: NSRange,
414 ) {
415 if let Some(text) = extract_insert_text(string) {
416 self.mark_ime_handled();
417 self.ivars().ime_insert_expected.set(true);
418 let marked_range = composition_range_for_text(&text);
419 self.update_marked_state(true, marked_range, selected_range);
420 self.send_mac_ime_event(BrowserViewMacImeEvent::SetComposition {
421 text,
422 selection: nsrange_to_text_range(selected_range),
423 replacement: nsrange_to_text_range(replacement_range),
424 });
425 }
426 }
427
428 #[unsafe(method(unmarkText))]
429 fn unmarkText(&self) {
430 self.mark_ime_handled();
431 let had_marked = self.ivars().has_marked_text.get();
432 self.ivars().ime_insert_expected.set(had_marked);
433 self.update_marked_state(false, ns_not_found_range(), ns_not_found_range());
434 self.send_mac_ime_event(BrowserViewMacImeEvent::FinishComposingText {
435 keep_selection: false,
436 });
437 }
438
439 #[unsafe(method(selectedRange))]
440 fn selectedRange(&self) -> NSRange {
441 self.ivars().selected_range.get()
442 }
443
444 #[unsafe(method(markedRange))]
445 fn markedRange(&self) -> NSRange {
446 if self.ivars().has_marked_text.get() {
447 self.ivars().marked_range.get()
448 } else {
449 ns_not_found_range()
450 }
451 }
452
453 #[unsafe(method(hasMarkedText))]
454 fn hasMarkedText(&self) -> bool {
455 self.ivars().has_marked_text.get()
456 }
457
458 #[unsafe(method(attributedSubstringForProposedRange:actualRange:))]
462 unsafe fn attributedSubstringForProposedRange_actualRange(
463 &self,
464 range: NSRange,
465 actual_range: NSRangePointer,
466 ) -> *const AnyObject {
467 if !actual_range.is_null() {
468 unsafe { actual_range.write(range) };
469 }
470
471 std::ptr::null()
472 }
473
474 #[unsafe(method(validAttributesForMarkedText))]
478 fn validAttributesForMarkedText(&self) -> *const AnyObject {
479 let array: Retained<NSArray<NSAttributedStringKey>> = NSArray::new();
480 Retained::autorelease_return(array) as _
481 }
482
483 #[unsafe(method(firstRectForCharacterRange:actualRange:))]
484 unsafe fn firstRectForCharacterRange_actualRange(
485 &self,
486 range: NSRange,
487 actual_range: NSRangePointer,
488 ) -> NSRect {
489 if !actual_range.is_null() {
490 unsafe { actual_range.write(range) };
491 }
492
493 self.ime_candidate_rect(range)
494 }
495
496 #[unsafe(method(characterIndexForPoint:))]
497 fn characterIndexForPoint(&self, _point: NSPoint) -> NSUInteger {
498 NSNotFound as NSUInteger
499 }
500 }
501);
502
503struct HostDragSourceIvars {
504 view: Retained<BrowserViewMac>,
505 session_id: u64,
506 operation_mask: NSDragOperation,
507}
508
509define_class!(
510 #[unsafe(super(NSObject))]
511 #[thread_kind = objc2::MainThreadOnly]
512 #[ivars = HostDragSourceIvars]
513 struct HostDragSource;
514
515 unsafe impl NSObjectProtocol for HostDragSource {}
516
517 unsafe impl NSDraggingSource for HostDragSource {
518 #[unsafe(method(draggingSession:sourceOperationMaskForDraggingContext:))]
519 fn draggingSession_sourceOperationMaskForDraggingContext(
520 &self,
521 _session: &NSDraggingSession,
522 _context: NSDraggingContext,
523 ) -> NSDragOperation {
524 self.ivars().operation_mask
525 }
526
527 #[unsafe(method(draggingSession:movedToPoint:))]
528 fn draggingSession_movedToPoint(
529 &self,
530 _session: &NSDraggingSession,
531 screen_point: NSPoint,
532 ) {
533 self.ivars()
534 .view
535 .emit_native_drag_update(self.ivars().session_id, screen_point);
536 }
537
538 #[unsafe(method(draggingSession:endedAtPoint:operation:))]
539 fn draggingSession_endedAtPoint_operation(
540 &self,
541 _session: &NSDraggingSession,
542 screen_point: NSPoint,
543 operation: NSDragOperation,
544 ) {
545 let should_treat_as_drop =
546 !operation.is_empty() || self.ivars().view.contains_screen_point(screen_point);
547 if should_treat_as_drop {
548 self.ivars()
549 .view
550 .emit_native_drag_drop(self.ivars().session_id, screen_point);
551 } else {
552 self.ivars()
553 .view
554 .emit_native_drag_cancel(self.ivars().session_id);
555 }
556 self.ivars().view.ivars().active_drag_source.replace(None);
557 }
558 }
559);
560
561impl HostDragSource {
562 fn new(
563 mtm: MainThreadMarker,
564 view: Retained<BrowserViewMac>,
565 session_id: u64,
566 operation_mask: NSDragOperation,
567 ) -> Retained<Self> {
568 let this = Self::alloc(mtm).set_ivars(HostDragSourceIvars {
569 view,
570 session_id,
571 operation_mask,
572 });
573 unsafe { msg_send![super(this), init] }
574 }
575}
576
577impl BrowserViewMac {
578 pub fn new(mtm: MainThreadMarker, config: BrowserViewMacConfig) -> Retained<Self> {
580 let browser_layer = CALayerHost::init(CALayerHost::alloc());
581 browser_layer.setFrame(config.frame);
582 browser_layer.setGeometryFlipped(true);
583
584 let this = Self::alloc(mtm).set_ivars(BrowserViewMacIvars {
585 delegate: config.delegate,
586 has_marked_text: Cell::new(false),
587 marked_range: Cell::new(ns_not_found_range()),
588 selected_range: Cell::new(ns_not_found_range()),
589 ime_handled: Cell::new(false),
590 suppress_key_up: Cell::new(false),
591 ime_insert_expected: Cell::new(false),
592 saw_insert_command: Cell::new(false),
593 sent_char_event: Cell::new(false),
594 ime_bounds: RefCell::new(None),
595 browser_layer,
596 browser_layer_frame: Cell::new(config.frame),
597 edit_commands: RefCell::new(Vec::new()),
598 pending_char_event: RefCell::new(None),
599 context_menu_id: Cell::new(NO_MENU_ID),
600 context_menu_selected_command_id: Cell::new(NO_COMMAND_ID),
601 choice_menu_request_id: Cell::new(NO_CHOICE_MENU_REQUEST_ID),
602 choice_menu_selected_action: Cell::new(NO_CHOICE_MENU_ACTION),
603 active_drag_source: RefCell::new(None),
604 });
605 let this: Retained<Self> = unsafe { msg_send![super(this), init] };
606
607 this.setFrame(config.frame);
608 this.setWantsLayer(true);
609 this.layer()
610 .expect("BrowserViewMac must have a layer")
611 .addSublayer(&this.ivars().browser_layer);
612
613 this
614 }
615
616 pub fn set_context_id(&self, context_id: ContextId) {
618 unsafe {
619 self.ivars().browser_layer.setContextId(context_id);
620 }
621 }
622
623 pub fn set_layer_frame(&self, frame: CGRect) {
625 CATransaction::begin();
627 CATransaction::setDisableActions(true);
628 self.ivars().browser_layer.setFrame(frame);
629 CATransaction::commit();
630
631 self.ivars().browser_layer_frame.set(frame);
632 }
633
634 pub fn set_ime_bounds(&self, update: ImeBoundsUpdate) {
636 self.ivars().ime_bounds.replace(Some(update));
637 }
638
639 pub fn browser_layer(&self) -> &CALayerHost {
641 &self.ivars().browser_layer
642 }
643
644 pub fn show_context_menu(&self, menu: ContextMenu) {
646 if self.window().is_none() {
647 return;
648 }
649
650 self.ivars().context_menu_id.set(menu.menu_id);
651 self.ivars()
652 .context_menu_selected_command_id
653 .set(NO_COMMAND_ID);
654
655 let mtm = MainThreadMarker::new().expect("BrowserViewMac must be on main thread");
656 let ns_menu = build_ns_menu(mtm, &menu.items, self);
657
658 let bounds = self.bounds();
659 let x = menu.x as f64;
660 let y = if self.isFlipped() {
661 menu.y as f64
662 } else {
663 (bounds.size.height - menu.y as f64).max(0.0)
664 };
665 let location = NSPoint::new(x, y);
666
667 _ = ns_menu.popUpMenuPositioningItem_atLocation_inView(None, location, Some(self));
668
669 let menu_id = self.ivars().context_menu_id.replace(NO_MENU_ID);
670 let command_id = self
671 .ivars()
672 .context_menu_selected_command_id
673 .replace(NO_COMMAND_ID);
674
675 if menu_id == NO_MENU_ID {
676 return;
677 }
678
679 if command_id == NO_COMMAND_ID {
680 self.ivars()
681 .delegate
682 .on_context_menu_dismissed(self, menu_id);
683 } else {
684 self.ivars()
685 .delegate
686 .on_context_menu_command(self, menu_id, command_id);
687 }
688 }
689
690 pub fn show_choice_menu(&self, menu: ChromeChoiceMenu) {
692 #[cfg(feature = "macos-choice-menu-default")]
693 return self.show_choice_menu_with_presenter(menu, &MacChoiceMenuPresenter);
694 #[cfg(not(feature = "macos-choice-menu-default"))]
695 {
696 self.ivars()
697 .delegate
698 .on_choice_menu_dismissed(self, menu.request_id);
699 }
700 }
701
702 pub fn show_choice_menu_with_presenter<P: ChromeChoiceMenuPresenter>(
704 &self,
705 menu: ChromeChoiceMenu,
706 presenter: &P,
707 ) {
708 if self.window().is_none() {
709 self.ivars()
710 .delegate
711 .on_choice_menu_dismissed(self, menu.request_id);
712 return;
713 }
714
715 if matches!(menu.selection_mode, ChromeChoiceMenuSelectionMode::Multiple) {
716 self.ivars()
717 .delegate
718 .on_choice_menu_dismissed(self, menu.request_id);
719 return;
720 }
721
722 self.ivars().choice_menu_request_id.set(menu.request_id);
723 self.ivars()
724 .choice_menu_selected_action
725 .set(NO_CHOICE_MENU_ACTION);
726
727 let result = presenter.show_choice_menu(self, &menu);
728
729 let request_id = self
730 .ivars()
731 .choice_menu_request_id
732 .replace(NO_CHOICE_MENU_REQUEST_ID);
733 self.ivars()
734 .choice_menu_selected_action
735 .set(NO_CHOICE_MENU_ACTION);
736
737 if request_id == NO_CHOICE_MENU_REQUEST_ID {
738 return;
739 }
740
741 match result {
742 Some(indices) if !indices.is_empty() => {
743 self.ivars()
744 .delegate
745 .on_choice_menu_selected(self, request_id, indices);
746 }
747 _ => {
748 self.ivars()
749 .delegate
750 .on_choice_menu_dismissed(self, request_id);
751 }
752 }
753 }
754
755 pub fn start_native_drag_session(&self, request: &DragStartRequest) -> bool {
757 let Some(window) = self.window() else {
758 return false;
759 };
760 let Some(image_data) = request.image.as_ref() else {
761 return false;
762 };
763 if image_data.png_bytes.is_empty() {
764 return false;
765 }
766
767 let data = unsafe {
768 NSData::initWithBytes_length(
769 NSData::alloc(),
770 image_data.png_bytes.as_ptr().cast(),
771 image_data.png_bytes.len() as NSUInteger,
772 )
773 };
774 let Some(image) = NSImage::initWithData(NSImage::alloc(), &data) else {
775 return false;
776 };
777 let image_scale = if image_data.scale > 0.0 {
778 image_data.scale as f64
779 } else {
780 1.0
781 };
782 image.setSize(CGSize::new(
783 image_data.pixel_width as f64 / image_scale,
784 image_data.pixel_height as f64 / image_scale,
785 ));
786
787 let writer_string = if !request.data.text.is_empty() {
788 request.data.text.as_str()
789 } else if let Some(url_info) = request.data.url_infos.first() {
790 url_info.url.as_str()
791 } else if !request.source_origin.is_empty() {
792 request.source_origin.as_str()
793 } else {
794 "Atelier"
795 };
796 let writer = NSString::from_str(writer_string);
797 let writer_ref: &ProtocolObject<dyn NSPasteboardWriting> =
798 ProtocolObject::from_ref(&*writer);
799 let dragging_item =
800 NSDraggingItem::initWithPasteboardWriter(NSDraggingItem::alloc(), writer_ref);
801
802 let mut mouse_location = window.mouseLocationOutsideOfEventStream();
803 mouse_location = self.convertPoint_fromView(mouse_location, None);
804 let image_size = image.size();
805 let drag_frame = NSRect::new(
806 NSPoint::new(
807 mouse_location.x - image_data.cursor_offset_x as f64,
808 mouse_location.y - image_size.height + image_data.cursor_offset_y as f64,
809 ),
810 image_size,
811 );
812 unsafe {
813 dragging_item.setDraggingFrame_contents(drag_frame, Some(&*image));
814 }
815
816 let mtm = MainThreadMarker::new().expect("BrowserViewMac must be on main thread");
817 let drag_event = NSApplication::sharedApplication(mtm)
818 .currentEvent()
819 .or_else(|| {
820 let screen_mouse = window.mouseLocationOutsideOfEventStream();
821 NSEvent::mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure(
822 NSEventType::LeftMouseDragged,
823 screen_mouse,
824 NSEventModifierFlags::empty(),
825 0.0,
826 window.windowNumber(),
827 None,
828 0,
829 1,
830 1.0,
831 )
832 });
833 let Some(drag_event) = drag_event else {
834 return false;
835 };
836
837 let operation_mask =
838 NSDragOperation::from_bits_truncate(request.allowed_operations.bits() as _);
839 let source = HostDragSource::new(
840 mtm,
841 Retained::from(self),
842 request.session_id,
843 operation_mask,
844 );
845 let source_ref: &ProtocolObject<dyn NSDraggingSource> = ProtocolObject::from_ref(&*source);
846 let items = NSArray::arrayWithObject(&*dragging_item);
847
848 self.beginDraggingSessionWithItems_event_source(&items, &drag_event, source_ref);
849
850 self.ivars()
851 .active_drag_source
852 .replace(Some(source.into_super().into()));
853
854 true
855 }
856
857 fn forward_key_event(&self, event: &NSEvent) {
858 let nsevent_ptr = NonNull::from(event).cast::<c_void>();
859 let key_event = convert_nsevent_to_key_event(0, nsevent_ptr);
861
862 let commands = std::mem::take(&mut *self.ivars().edit_commands.borrow_mut());
864
865 self.ivars()
866 .delegate
867 .on_key_event(self, key_event, commands);
868 }
869
870 fn send_edit_action(&self, action: EditAction) {
871 self.ivars().delegate.on_edit_action(self, action);
872 }
873
874 fn forward_mouse_event(&self, event: &NSEvent) {
875 let nsevent_ptr = NonNull::from(event).cast::<c_void>();
876 let nsview_ptr = NonNull::from(self).cast::<c_void>();
877 let mouse_event =
878 convert_nsevent_to_mouse_event(0, nsevent_ptr, nsview_ptr, PointerType::Mouse, false);
879 self.ivars().delegate.on_mouse_event(self, mouse_event);
880 }
881
882 fn forward_mouse_wheel_event(&self, event: &NSEvent) {
883 let nsevent_ptr = NonNull::from(event).cast::<c_void>();
884 let nsview_ptr = NonNull::from(self).cast::<c_void>();
885 let wheel_event = convert_nsevent_to_mouse_wheel_event(0, nsevent_ptr, nsview_ptr);
886 self.ivars()
887 .delegate
888 .on_mouse_wheel_event(self, wheel_event);
889 }
890
891 fn send_mac_ime_event(&self, event: BrowserViewMacImeEvent) {
892 self.ivars().delegate.on_ime_event(self, event);
893 }
894
895 pub(crate) fn take_choice_menu_result(&self) -> Option<i32> {
896 let action = self
897 .ivars()
898 .choice_menu_selected_action
899 .replace(NO_CHOICE_MENU_ACTION);
900 (action != NO_CHOICE_MENU_ACTION).then_some(action)
901 }
902
903 fn send_mac_char_event(&self, event: KeyEvent) {
904 self.ivars().delegate.on_char_event(self, event);
905 }
906
907 fn update_marked_state(
908 &self,
909 has_marked_text: bool,
910 marked_range: NSRange,
911 selected_range: NSRange,
912 ) {
913 let ivars = self.ivars();
914 ivars.has_marked_text.set(has_marked_text);
915 ivars.marked_range.set(marked_range);
916 ivars.selected_range.set(selected_range);
917 }
918
919 fn mark_ime_handled(&self) {
920 self.ivars().ime_handled.set(true);
921 }
922
923 fn ime_candidate_rect(&self, range: NSRange) -> CGRect {
924 let fallback = if let Some(window) = self.window() {
925 window.frame()
926 } else {
927 CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(0.0, 0.0))
928 };
929
930 let bounds = self.ivars().ime_bounds.borrow();
931 let Some(bounds) = bounds.as_ref() else {
932 return fallback;
933 };
934
935 let layer_frame = self.ivars().browser_layer_frame.get();
936
937 if let Some(composition) = bounds.composition.as_ref()
938 && let Some(rect) = rect_for_composition_range(range, composition)
939 {
940 let rect = flip_rect_in_layer(rect, layer_frame.size.height);
941 return self.to_screen_rect(offset_rect(
942 rect,
943 layer_frame.origin.x,
944 layer_frame.origin.y,
945 ));
946 }
947
948 if let Some(selection) = bounds.selection.as_ref() {
949 let rect = flip_rect_in_layer(rect_from_selection(selection), layer_frame.size.height);
950 return self.to_screen_rect(offset_rect(
951 rect,
952 layer_frame.origin.x,
953 layer_frame.origin.y,
954 ));
955 }
956
957 fallback
958 }
959
960 fn to_screen_rect(&self, rect: CGRect) -> CGRect {
961 let window_rect = self.convertRect_toView(rect, None);
962
963 if let Some(window) = self.window() {
964 window.convertRectToScreen(window_rect)
965 } else {
966 window_rect
967 }
968 }
969
970 fn emit_native_drag_update(&self, session_id: u64, screen_point: NSPoint) {
971 let (widget_x, widget_y, screen_x, screen_y) = self.drag_points(screen_point);
972 let modifiers = self
973 .window()
974 .and_then(|_| MainThreadMarker::new())
975 .and_then(|mtm| NSApplication::sharedApplication(mtm).currentEvent())
976 .map(|event| event.modifierFlags().bits() as u32)
977 .unwrap_or(0);
978 self.ivars().delegate.on_native_drag_update(
979 self,
980 BrowserViewMacNativeDragUpdate {
981 session_id,
982 modifiers,
983 position_in_widget_x: widget_x,
984 position_in_widget_y: widget_y,
985 position_in_screen_x: screen_x,
986 position_in_screen_y: screen_y,
987 },
988 );
989 }
990
991 fn emit_native_drag_drop(&self, session_id: u64, screen_point: NSPoint) {
992 let (widget_x, widget_y, screen_x, screen_y) = self.drag_points(screen_point);
993 let modifiers = self
994 .window()
995 .and_then(|_| MainThreadMarker::new())
996 .and_then(|mtm| NSApplication::sharedApplication(mtm).currentEvent())
997 .map(|event| event.modifierFlags().bits() as u32)
998 .unwrap_or(0);
999 self.ivars().delegate.on_native_drag_drop(
1000 self,
1001 BrowserViewMacNativeDragDrop {
1002 session_id,
1003 modifiers,
1004 position_in_widget_x: widget_x,
1005 position_in_widget_y: widget_y,
1006 position_in_screen_x: screen_x,
1007 position_in_screen_y: screen_y,
1008 },
1009 );
1010 }
1011
1012 fn emit_native_drag_cancel(&self, session_id: u64) {
1013 self.ivars()
1014 .delegate
1015 .on_native_drag_cancel(self, session_id);
1016 }
1017
1018 fn drag_points(&self, screen_point: NSPoint) -> (f32, f32, f32, f32) {
1019 let mut local_point = NSPoint::new(0.0, 0.0);
1020 if let Some(window) = self.window() {
1021 let base_point = window.convertPointFromScreen(screen_point);
1022 local_point = self.convertPoint_fromView(base_point, None);
1023 }
1024 let bounds = self.bounds();
1025 let widget_x = local_point.x as f32;
1026 let widget_y = (bounds.size.height - local_point.y).max(0.0) as f32;
1027 (
1028 widget_x,
1029 widget_y,
1030 screen_point.x as f32,
1031 screen_point.y as f32,
1032 )
1033 }
1034
1035 fn contains_screen_point(&self, screen_point: NSPoint) -> bool {
1036 let Some(window) = self.window() else {
1037 return false;
1038 };
1039 let base_point = window.convertPointFromScreen(screen_point);
1040 let local_point = self.convertPoint_fromView(base_point, None);
1041 let bounds = self.bounds();
1042 local_point.x >= bounds.origin.x
1043 && local_point.x <= bounds.origin.x + bounds.size.width
1044 && local_point.y >= bounds.origin.y
1045 && local_point.y <= bounds.origin.y + bounds.size.height
1046 }
1047}
1048
1049#[inline]
1050fn ns_not_found_range() -> NSRange {
1051 NSRange::new(NSNotFound as usize, 0)
1052}
1053
1054fn build_ns_menu(
1055 mtm: MainThreadMarker,
1056 items: &[ContextMenuItem],
1057 target: &BrowserViewMac,
1058) -> Retained<NSMenu> {
1059 let title = NSString::from_str("");
1060 let menu = NSMenu::initWithTitle(NSMenu::alloc(mtm), &title);
1061
1062 for item in items {
1063 if !item.visible {
1064 continue;
1065 }
1066
1067 if let Some(menu_item) = build_ns_menu_item(mtm, item, target) {
1068 menu.addItem(&menu_item);
1069 }
1070 }
1071
1072 menu
1073}
1074
1075fn build_ns_menu_item(
1076 mtm: MainThreadMarker,
1077 item: &ContextMenuItem,
1078 target: &BrowserViewMac,
1079) -> Option<Retained<NSMenuItem>> {
1080 let title_text = menu_item_title(item);
1081
1082 let menu_item = match item.r#type {
1083 ContextMenuItemType::Separator => {
1084 return Some(NSMenuItem::separatorItem(mtm));
1085 }
1086 ContextMenuItemType::Title => {
1087 let title = NSString::from_str(&title_text);
1088 return Some(NSMenuItem::sectionHeaderWithTitle(&title, mtm));
1089 }
1090 _ => {
1091 let title = NSString::from_str(&title_text);
1092 let key_equivalent = item
1093 .accelerator
1094 .as_ref()
1095 .map(|accel| accel.key_equivalent.as_str())
1096 .unwrap_or("");
1097 let key_equivalent = NSString::from_str(key_equivalent);
1098
1099 let action = if matches!(
1100 item.r#type,
1101 ContextMenuItemType::Submenu | ContextMenuItemType::ActionableSubmenu
1102 ) {
1103 None
1104 } else {
1105 Some(sel!(contextMenuItemSelected:))
1106 };
1107
1108 unsafe {
1109 NSMenuItem::initWithTitle_action_keyEquivalent(
1110 NSMenuItem::alloc(mtm),
1111 &title,
1112 action,
1113 &key_equivalent,
1114 )
1115 }
1116 }
1117 };
1118
1119 if matches!(
1120 item.r#type,
1121 ContextMenuItemType::Submenu | ContextMenuItemType::ActionableSubmenu
1122 ) {
1123 let submenu = build_ns_menu(mtm, &item.submenu, target);
1124 menu_item.setSubmenu(Some(&submenu));
1125 } else {
1126 unsafe {
1127 menu_item.setTarget(Some(target));
1128 }
1129 }
1130
1131 menu_item.setEnabled(item.enabled);
1132 menu_item.setHidden(!item.visible);
1133 menu_item.setTag(item.command_id as isize);
1134
1135 if let Some(subtitle) = menu_item_subtitle(item) {
1136 let subtitle = NSString::from_str(&subtitle);
1137 menu_item.setSubtitle(Some(&subtitle));
1138 }
1139
1140 if !item.minor_text.is_empty() {
1141 let tooltip = NSString::from_str(&item.minor_text);
1142 menu_item.setToolTip(Some(&tooltip));
1143 }
1144
1145 if let Some(icon) = build_ns_image(item.icon.as_ref().or(item.minor_icon.as_ref())) {
1146 menu_item.setImage(Some(&icon));
1147 }
1148
1149 if item.is_alerted {
1150 let badge = NSMenuItemBadge::alertsWithCount(1);
1151 menu_item.setBadge(Some(&badge));
1152 } else if item.is_new_feature {
1153 let badge = NSMenuItemBadge::newItemsWithCount(1);
1154 menu_item.setBadge(Some(&badge));
1155 }
1156
1157 if let Some(accel) = item.accelerator.as_ref() {
1158 let modifier_mask = NSEventModifierFlags::from_bits_truncate(accel.modifier_mask as _);
1159 menu_item.setKeyEquivalentModifierMask(modifier_mask);
1160 }
1161
1162 if matches!(
1163 item.r#type,
1164 ContextMenuItemType::Check | ContextMenuItemType::Radio
1165 ) {
1166 if item.checked {
1167 menu_item.setState(NSControlStateValueOn);
1168 } else {
1169 menu_item.setState(NSControlStateValueOff);
1170 }
1171 }
1172
1173 Some(menu_item)
1174}
1175
1176fn menu_item_title(item: &ContextMenuItem) -> String {
1177 let mut title = if item.label.is_empty() {
1178 item.accessible_name.clone()
1179 } else {
1180 item.label.clone()
1181 };
1182
1183 if item.may_have_mnemonics {
1184 title = strip_mnemonic(&title);
1185 }
1186
1187 title
1188}
1189
1190fn menu_item_subtitle(item: &ContextMenuItem) -> Option<String> {
1191 if !item.secondary_label.is_empty() {
1192 Some(item.secondary_label.clone())
1193 } else if !item.minor_text.is_empty() {
1194 Some(item.minor_text.clone())
1195 } else {
1196 None
1197 }
1198}
1199
1200fn strip_mnemonic(input: &str) -> String {
1201 let mut output = String::with_capacity(input.len());
1202 let mut chars = input.chars().peekable();
1203
1204 while let Some(ch) = chars.next() {
1205 if ch == '&' {
1206 if matches!(chars.peek(), Some('&')) {
1207 output.push('&');
1208 chars.next();
1209 }
1210 continue;
1211 }
1212 output.push(ch);
1213 }
1214
1215 output
1216}
1217
1218fn build_ns_image(icon: Option<&ContextMenuIcon>) -> Option<Retained<NSImage>> {
1219 let icon = icon?;
1220 if icon.png_bytes.is_empty() {
1221 return None;
1222 }
1223
1224 let data = unsafe {
1225 NSData::initWithBytes_length(
1226 NSData::alloc(),
1227 icon.png_bytes.as_ptr().cast(),
1228 icon.png_bytes.len() as NSUInteger,
1229 )
1230 };
1231 let image = NSImage::initWithData(NSImage::alloc(), &data)?;
1232 image.setSize(CGSize::new(icon.width as f64, icon.height as f64));
1233 Some(image)
1234}
1235
1236fn nsrange_to_text_range(range: NSRange) -> Option<ImeTextRange> {
1237 if range.location == NSNotFound as usize {
1238 return None;
1239 }
1240
1241 let start = range.location.min(i32::MAX as usize) as i32;
1242 let end = range.end().min(i32::MAX as usize).max(start as usize) as i32;
1243
1244 Some(ImeTextRange { start, end })
1245}
1246
1247fn composition_range_for_text(text: &str) -> NSRange {
1248 let length = text.encode_utf16().count();
1249 NSRange::new(0, length)
1250}
1251
1252fn build_char_event(template: Option<KeyEvent>, text: String) -> Option<KeyEvent> {
1253 if text.is_empty() {
1254 return None;
1255 }
1256
1257 let mut event =
1258 template.unwrap_or_else(|| KeyEvent::char_input(0, 0, 0, text.clone(), text.clone()));
1259 event.type_ = KeyEventType::Char;
1260 event.text = Some(text.clone());
1261
1262 if event
1263 .unmodified_text
1264 .as_deref()
1265 .map(str::is_empty)
1266 .unwrap_or(true)
1267 {
1268 event.unmodified_text = Some(text);
1269 }
1270
1271 Some(event)
1272}
1273
1274fn synthesized_char_text(template: Option<&KeyEvent>) -> Option<String> {
1275 let event = template?;
1276 event
1277 .text
1278 .as_ref()
1279 .filter(|text| !text.is_empty())
1280 .cloned()
1281 .or_else(|| {
1282 event
1283 .unmodified_text
1284 .as_ref()
1285 .filter(|text| !text.is_empty())
1286 .cloned()
1287 })
1288}
1289
1290fn extract_insert_text(value: &AnyObject) -> Option<String> {
1291 let mut text = if let Some(attributed) = value.downcast_ref::<NSAttributedString>() {
1292 Some(attributed.string().to_string())
1293 } else {
1294 value
1295 .downcast_ref::<NSString>()
1296 .map(|ns_string| ns_string.to_string())
1297 }?;
1298
1299 text = text
1302 .chars()
1303 .filter(|c| matches!(c, '\r' | '\n') || !c.is_control())
1304 .collect::<String>();
1305
1306 if text.is_empty() {
1307 return None;
1308 }
1309
1310 Some(text)
1311}
1312
1313const VKEY_TAB: i32 = 0x09;
1321const VKEY_RETURN: i32 = 0x0D;
1322const VKEY_ESCAPE: i32 = 0x1B;
1323const VKEY_PRIOR: i32 = 0x21;
1324const VKEY_NEXT: i32 = 0x22;
1325const VKEY_LEFT: i32 = 0x25;
1326const VKEY_UP: i32 = 0x26;
1327const VKEY_RIGHT: i32 = 0x27;
1328const VKEY_DOWN: i32 = 0x28;
1329
1330fn should_ignore_accelerator_with_marked_text(event: &NSEvent) -> bool {
1331 let nsevent_ptr = NonNull::from(event).cast::<c_void>();
1332 let key_event = convert_nsevent_to_key_event(0, nsevent_ptr);
1333 should_ignore_accelerator_with_marked_text_key_event(&key_event)
1334}
1335
1336fn should_ignore_accelerator_with_marked_text_key_event(event: &KeyEvent) -> bool {
1343 matches!(
1344 event.key_code,
1345 VKEY_RETURN
1346 | VKEY_TAB
1347 | VKEY_ESCAPE
1348 | VKEY_LEFT
1349 | VKEY_UP
1350 | VKEY_RIGHT
1351 | VKEY_DOWN
1352 | VKEY_PRIOR
1353 | VKEY_NEXT
1354 )
1355}
1356
1357fn rect_for_composition_range(
1358 range: NSRange,
1359 composition: &ImeCompositionBounds,
1360) -> Option<CGRect> {
1361 if composition.range_start < 0 || composition.range_end < composition.range_start {
1362 return None;
1363 }
1364 if composition.character_bounds.is_empty() {
1365 return None;
1366 }
1367 if range.location == NSNotFound as usize {
1368 return None;
1369 }
1370
1371 let start = range.location.min(i32::MAX as usize) as i32;
1372 let end = range.end().min(i32::MAX as usize).max(range.location) as i32;
1373
1374 if start < composition.range_start || end > composition.range_end {
1375 return None;
1376 }
1377
1378 let local_start = (start - composition.range_start) as usize;
1379 if local_start >= composition.character_bounds.len() {
1380 return None;
1381 }
1382
1383 if range.length == 0 {
1384 return Some(rect_from_ime(&composition.character_bounds[local_start]));
1385 }
1386
1387 let local_end = (end - composition.range_start) as usize;
1388 let clamped_end = local_end.min(composition.character_bounds.len());
1389 if clamped_end <= local_start {
1390 return Some(rect_from_ime(&composition.character_bounds[local_start]));
1391 }
1392
1393 let mut rect = rect_from_ime(&composition.character_bounds[local_start]);
1394 for bounds in &composition.character_bounds[local_start + 1..clamped_end] {
1395 rect = union_rect(rect, rect_from_ime(bounds));
1396 }
1397
1398 Some(rect)
1399}
1400
1401fn rect_from_selection(selection: &TextSelectionBounds) -> CGRect {
1402 rect_from_ime(&selection.caret_rect)
1403}
1404
1405fn rect_from_ime(rect: &ImeRect) -> CGRect {
1406 CGRect::new(
1407 CGPoint::new(rect.x as f64, rect.y as f64),
1408 CGSize::new(rect.width as f64, rect.height as f64),
1409 )
1410}
1411
1412fn union_rect(a: CGRect, b: CGRect) -> CGRect {
1413 let min_x = a.origin.x.min(b.origin.x);
1414 let min_y = a.origin.y.min(b.origin.y);
1415 let max_x = (a.origin.x + a.size.width).max(b.origin.x + b.size.width);
1416 let max_y = (a.origin.y + a.size.height).max(b.origin.y + b.size.height);
1417
1418 CGRect::new(
1419 CGPoint::new(min_x, min_y),
1420 CGSize::new((max_x - min_x).max(0.0), (max_y - min_y).max(0.0)),
1421 )
1422}
1423
1424fn offset_rect(rect: CGRect, offset_x: f64, offset_y: f64) -> CGRect {
1425 CGRect::new(
1426 CGPoint::new(rect.origin.x + offset_x, rect.origin.y + offset_y),
1427 rect.size,
1428 )
1429}
1430
1431fn flip_rect_in_layer(rect: CGRect, layer_height: f64) -> CGRect {
1432 let flipped_y = (layer_height - (rect.origin.y + rect.size.height)).max(0.0);
1433 CGRect::new(CGPoint::new(rect.origin.x, flipped_y), rect.size)
1434}
1435
1436#[cfg(test)]
1437mod tests {
1438 use cbf::data::key::{KeyEvent, KeyEventType};
1439 use objc2::rc::Retained;
1440 use objc2_foundation::NSString;
1441
1442 use super::{
1443 VKEY_DOWN, VKEY_ESCAPE, VKEY_LEFT, VKEY_NEXT, VKEY_PRIOR, VKEY_RETURN, VKEY_RIGHT,
1444 VKEY_TAB, VKEY_UP, build_char_event, extract_insert_text,
1445 should_ignore_accelerator_with_marked_text_key_event, synthesized_char_text,
1446 };
1447
1448 #[test]
1449 fn marked_text_guard_matches_chromium_ime_navigation_keys() {
1450 for key_code in [
1451 VKEY_RETURN,
1452 VKEY_TAB,
1453 VKEY_ESCAPE,
1454 VKEY_PRIOR,
1455 VKEY_NEXT,
1456 VKEY_LEFT,
1457 VKEY_UP,
1458 VKEY_RIGHT,
1459 VKEY_DOWN,
1460 ] {
1461 let event = KeyEvent::raw_key_down(0, key_code, 0);
1462 assert!(should_ignore_accelerator_with_marked_text_key_event(&event));
1463 }
1464 }
1465
1466 #[test]
1467 fn marked_text_guard_leaves_regular_typing_keys_alone() {
1468 let event = KeyEvent::raw_key_down(0, 0x41, 0);
1469 assert!(!should_ignore_accelerator_with_marked_text_key_event(
1470 &event
1471 ));
1472 }
1473
1474 #[test]
1475 fn char_event_preserves_key_metadata_from_pending_keydown() {
1476 let mut template = KeyEvent::raw_key_down(42, 191, 4);
1477 template.dom_code = Some("Slash".to_string());
1478 template.dom_key = Some("/".to_string());
1479 template.unmodified_text = Some("/".to_string());
1480
1481 let event = build_char_event(Some(template), "/".to_string()).unwrap();
1482
1483 assert_eq!(event.type_, KeyEventType::Char);
1484 assert_eq!(event.key_code, 191);
1485 assert_eq!(event.platform_key_code, 42);
1486 assert_eq!(event.modifiers, 4);
1487 assert_eq!(event.dom_code.as_deref(), Some("Slash"));
1488 assert_eq!(event.dom_key.as_deref(), Some("/"));
1489 assert_eq!(event.text.as_deref(), Some("/"));
1490 assert_eq!(event.unmodified_text.as_deref(), Some("/"));
1491 }
1492
1493 #[test]
1494 fn extract_insert_text_keeps_enter_but_drops_other_controls() {
1495 let text: Retained<NSString> = NSString::from_str("a\r\n\t\u{0}b");
1496 let extracted = extract_insert_text(text.as_ref()).unwrap();
1497 assert_eq!(extracted, "a\r\nb");
1498 }
1499
1500 #[test]
1501 fn synthesized_char_text_uses_enter_from_key_event() {
1502 let mut event = KeyEvent::raw_key_down(0, VKEY_RETURN, 0);
1503 event.text = Some("\r".to_string());
1504 event.unmodified_text = Some("\r".to_string());
1505
1506 assert_eq!(synthesized_char_text(Some(&event)).as_deref(), Some("\r"));
1507 }
1508}