WIP

Antonio Scandurra created

Change summary

crates/gpui/src/platform.rs            |  19 +
crates/gpui/src/platform/mac/window.rs | 313 +++++++++++++++++++++++++--
crates/gpui/src/platform/test.rs       |   2 
3 files changed, 303 insertions(+), 31 deletions(-)

Detailed changes

crates/gpui/src/platform.rs 🔗

@@ -26,6 +26,7 @@ use serde::Deserialize;
 use std::{
     any::Any,
     fmt::{self, Display},
+    ops::Range,
     path::{Path, PathBuf},
     rc::Rc,
     str::FromStr,
@@ -88,6 +89,23 @@ pub trait Dispatcher: Send + Sync {
     fn run_on_main_thread(&self, task: Runnable);
 }
 
+pub trait InputHandler {
+    fn select(&mut self, range: Range<usize>);
+    fn selected_range(&self) -> Option<Range<usize>>;
+    fn set_composition(
+        &mut self,
+        marked_text: &str,
+        new_selected_range: Option<Range<usize>>,
+        replacement_range: Option<Range<usize>>,
+    );
+    fn commit(&mut self, text: &str, replacement_range: Option<Range<usize>>);
+    fn cancel_composition(&mut self);
+    fn finish_composition(&mut self);
+    fn unmark(&mut self);
+    fn marked_range(&self) -> Option<Range<usize>>;
+    fn text_for_range(&self, range: Range<usize>) -> Option<String>;
+}
+
 pub trait Window: WindowContext {
     fn as_any_mut(&mut self) -> &mut dyn Any;
     fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>);
@@ -95,6 +113,7 @@ pub trait Window: WindowContext {
     fn on_resize(&mut self, callback: Box<dyn FnMut()>);
     fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>);
     fn on_close(&mut self, callback: Box<dyn FnOnce()>);
+    fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>);
     fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
     fn activate(&self);
     fn set_title(&mut self, title: &str);

crates/gpui/src/platform/mac/window.rs 🔗

@@ -7,7 +7,8 @@ use crate::{
     },
     keymap::Keystroke,
     platform::{self, Event, WindowBounds, WindowContext},
-    KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent, Scene,
+    InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent,
+    Scene,
 };
 use block::ConcreteBlock;
 use cocoa::{
@@ -16,7 +17,9 @@ use cocoa::{
         NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask,
     },
     base::{id, nil},
-    foundation::{NSAutoreleasePool, NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger},
+    foundation::{
+        NSAutoreleasePool, NSInteger, NSNotFound, NSPoint, NSRect, NSSize, NSString, NSUInteger,
+    },
     quartzcore::AutoresizingMask,
 };
 use core_graphics::display::CGRect;
@@ -34,9 +37,13 @@ use smol::Timer;
 use std::{
     any::Any,
     cell::{Cell, RefCell},
+    cmp,
     convert::TryInto,
-    ffi::c_void,
-    mem, ptr,
+    ffi::{c_void, CStr},
+    mem,
+    ops::Range,
+    os::raw::c_char,
+    ptr,
     rc::{Rc, Weak},
     sync::Arc,
     time::Duration,
@@ -48,12 +55,44 @@ static mut WINDOW_CLASS: *const Class = ptr::null();
 static mut VIEW_CLASS: *const Class = ptr::null();
 
 #[repr(C)]
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Debug)]
 struct NSRange {
     pub location: NSUInteger,
     pub length: NSUInteger,
 }
 
+impl NSRange {
+    fn invalid() -> Self {
+        Self {
+            location: NSNotFound as NSUInteger,
+            length: 0,
+        }
+    }
+
+    fn is_valid(&self) -> bool {
+        self.location != NSNotFound as NSUInteger
+    }
+
+    fn to_range(&self) -> Option<Range<usize>> {
+        if self.is_valid() {
+            let start = self.location as usize;
+            let end = start + self.length as usize;
+            Some(start..end)
+        } else {
+            None
+        }
+    }
+}
+
+impl From<Range<usize>> for NSRange {
+    fn from(range: Range<usize>) -> Self {
+        NSRange {
+            location: range.start as NSUInteger,
+            length: range.len() as NSUInteger,
+        }
+    }
+}
+
 unsafe impl objc::Encode for NSRange {
     fn encode() -> objc::Encoding {
         let encoding = format!(
@@ -189,6 +228,10 @@ unsafe fn build_classes() {
             sel!(hasMarkedText),
             has_marked_text as extern "C" fn(&Object, Sel) -> BOOL,
         );
+        decl.add_method(
+            sel!(markedRange),
+            marked_range as extern "C" fn(&Object, Sel) -> NSRange,
+        );
         decl.add_method(
             sel!(selectedRange),
             selected_range as extern "C" fn(&Object, Sel) -> NSRange,
@@ -205,10 +248,11 @@ unsafe fn build_classes() {
             sel!(setMarkedText:selectedRange:replacementRange:),
             set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange),
         );
+        decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel));
         decl.add_method(
             sel!(attributedSubstringForProposedRange:actualRange:),
             attributed_substring_for_proposed_range
-                as extern "C" fn(&Object, Sel, NSRange, id) -> id,
+                as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id,
         );
 
         decl.register()
@@ -225,6 +269,8 @@ struct WindowState {
     resize_callback: Option<Box<dyn FnMut()>>,
     should_close_callback: Option<Box<dyn FnMut() -> bool>>,
     close_callback: Option<Box<dyn FnOnce()>>,
+    input_handler: Option<Box<dyn InputHandler>>,
+    pending_key_event: Option<PendingKeyEvent>,
     synthetic_drag_counter: usize,
     executor: Rc<executor::Foreground>,
     scene_to_render: Option<Scene>,
@@ -236,6 +282,13 @@ struct WindowState {
     previous_modifiers_changed_event: Option<Event>,
 }
 
+#[derive(Default, Debug)]
+struct PendingKeyEvent {
+    set_marked_text: Option<(String, Option<Range<usize>>, Option<Range<usize>>)>,
+    unmark_text: bool,
+    insert_text: Option<String>,
+}
+
 impl Window {
     pub fn open(
         id: usize,
@@ -311,6 +364,8 @@ impl Window {
                 should_close_callback: None,
                 close_callback: None,
                 activate_callback: None,
+                input_handler: None,
+                pending_key_event: None,
                 synthetic_drag_counter: 0,
                 executor,
                 scene_to_render: Default::default(),
@@ -419,6 +474,10 @@ impl platform::Window for Window {
         self.0.as_ref().borrow_mut().activate_callback = Some(callback);
     }
 
+    fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>) {
+        self.0.as_ref().borrow_mut().input_handler = Some(input_handler);
+    }
+
     fn prompt(
         &self,
         level: platform::PromptLevel,
@@ -636,38 +695,103 @@ extern "C" fn dealloc_view(this: &Object, _: Sel) {
 }
 
 extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL {
+    let had_marked_text = with_input_handler(this, |input_handler| input_handler.marked_range())
+        .flatten()
+        .is_some();
     let window_state = unsafe { get_window_state(this) };
+
     let mut window_state_borrow = window_state.as_ref().borrow_mut();
 
     let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
     if let Some(event) = event {
-        match &event {
-            Event::KeyDown(KeyDownEvent {
-                keystroke,
-                input,
-                is_held,
-            }) => {
-                let keydown = (keystroke.clone(), input.clone());
+        let mut event = match event {
+            Event::KeyDown(event) => {
+                let keydown = (event.keystroke.clone(), event.input.clone());
                 // Ignore events from held-down keys after some of the initially-pressed keys
                 // were released.
-                if *is_held {
+                if event.is_held {
                     if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) {
                         return YES;
                     }
                 } else {
                     window_state_borrow.last_fresh_keydown = Some(keydown);
                 }
+
+                event
             }
             _ => return NO,
+        };
+
+        // TODO: handle "live conversion"
+        window_state_borrow.pending_key_event = Some(Default::default());
+        drop(window_state_borrow);
+
+        // TODO:
+        // Since Mac Eisu Kana keys cannot be handled by interpretKeyEvents to enable/
+        // disable an IME, we need to pass the event to processInputKeyBindings.
+        // processInputKeyBindings is available at least on 10.11-11.0.
+        // if (keyCode == kVK_JIS_Eisu || keyCode == kVK_JIS_Kana) {
+        //     if ([NSTextInputContext
+        //         respondsToSelector:@selector(processInputKeyBindings:)]) {
+        //             [NSTextInputContext performSelector:@selector(processInputKeyBindings:)
+        //                 withObject:theEvent];
+        //         }
+        // } else {
+        unsafe {
+            let input_context: id = msg_send![this, inputContext];
+            let _: BOOL = msg_send![input_context, handleEvent: native_event];
+        }
+        // }
+
+        let pending_event = window_state.borrow_mut().pending_key_event.take().unwrap();
+        let mut inserted_text = false;
+        let has_marked_text = pending_event.set_marked_text.is_some();
+        if let Some(text) = pending_event.insert_text.as_ref() {
+            if !text.is_empty() && (had_marked_text || has_marked_text || text.len() > 1) {
+                with_input_handler(this, |input_handler| input_handler.commit(&text, None));
+                inserted_text = true;
+            }
         }
 
-        if let Some(mut callback) = window_state_borrow.event_callback.take() {
-            drop(window_state_borrow);
-            let handled = callback(event);
-            window_state.borrow_mut().event_callback = Some(callback);
-            handled as BOOL
+        with_input_handler(this, |input_handler| {
+            if let Some((text, new_selected_range, replacement_range)) =
+                pending_event.set_marked_text
+            {
+                input_handler.set_composition(&text, new_selected_range, replacement_range)
+            } else if had_marked_text && !inserted_text {
+                if pending_event.unmark_text {
+                    input_handler.finish_composition();
+                } else {
+                    input_handler.cancel_composition();
+                }
+            }
+        });
+
+        if has_marked_text {
+            YES
         } else {
-            NO
+            let mut handled = false;
+            let mut window_state_borrow = window_state.borrow_mut();
+            if let Some(mut callback) = window_state_borrow.event_callback.take() {
+                drop(window_state_borrow);
+
+                if inserted_text {
+                    handled = true;
+                } else if let Some(text) = pending_event.insert_text {
+                    if text.len() == 1 {
+                        event.keystroke.key = text;
+                        handled = callback(Event::KeyDown(event));
+                    } else if event.keystroke.cmd || event.keystroke.ctrl {
+                        handled = callback(Event::KeyDown(event));
+                    }
+                } else {
+                    handled = callback(Event::KeyDown(event));
+                }
+
+                window_state.borrow_mut().event_callback = Some(callback);
+            }
+
+            handled as BOOL
         }
     } else {
         NO
@@ -926,27 +1050,138 @@ extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
     unsafe { msg_send![class!(NSArray), array] }
 }
 
-extern "C" fn has_marked_text(_: &Object, _: Sel) -> BOOL {
-    false as BOOL
+extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL {
+    with_input_handler(this, |input_handler| input_handler.marked_range())
+        .flatten()
+        .is_some() as BOOL
 }
 
-extern "C" fn selected_range(_: &Object, _: Sel) -> NSRange {
-    NSRange {
-        location: 0,
-        length: 0,
-    }
+extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange {
+    with_input_handler(this, |input_handler| input_handler.marked_range())
+        .flatten()
+        .map_or(NSRange::invalid(), |range| range.into())
+}
+
+extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange {
+    with_input_handler(this, |input_handler| input_handler.selected_range())
+        .flatten()
+        .map_or(NSRange::invalid(), |range| range.into())
 }
 
 extern "C" fn first_rect_for_character_range(_: &Object, _: Sel, _: NSRange, _: id) -> NSRect {
     NSRect::new(NSPoint::new(0., 0.), NSSize::new(20., 20.))
 }
 
-extern "C" fn insert_text(_: &Object, _: Sel, _: id, _: NSRange) {}
+extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) {
+    unsafe {
+        let is_attributed_string: BOOL =
+            msg_send![text, isKindOfClass: [class!(NSAttributedString)]];
+        let text: id = if is_attributed_string == YES {
+            msg_send![text, string]
+        } else {
+            text
+        };
+        let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
+            .to_str()
+            .unwrap();
 
-extern "C" fn set_marked_text(_: &Object, _: Sel, _: id, _: NSRange, _: NSRange) {}
+        let window_state = get_window_state(this);
+        let mut window_state = window_state.borrow_mut();
+        if window_state.pending_key_event.is_some() && !replacement_range.is_valid() {
+            window_state.pending_key_event.as_mut().unwrap().insert_text = Some(text.to_string());
+            drop(window_state);
+        } else {
+            drop(window_state);
+            with_input_handler(this, |input_handler| {
+                input_handler.commit(text, replacement_range.to_range());
+            });
+        }
 
-extern "C" fn attributed_substring_for_proposed_range(_: &Object, _: Sel, _: NSRange, _: id) -> id {
-    unsafe { msg_send![class!(NSAttributedString), alloc] }
+        with_input_handler(this, |input_handler| input_handler.unmark());
+    }
+}
+
+extern "C" fn set_marked_text(
+    this: &Object,
+    _: Sel,
+    text: id,
+    selected_range: NSRange,
+    replacement_range: NSRange,
+) {
+    println!("set_marked_text");
+    unsafe {
+        let is_attributed_string: BOOL =
+            msg_send![text, isKindOfClass: [class!(NSAttributedString)]];
+        let text: id = if is_attributed_string == YES {
+            msg_send![text, string]
+        } else {
+            text
+        };
+        let selected_range = selected_range.to_range();
+        let replacement_range = replacement_range.to_range();
+        let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
+            .to_str()
+            .unwrap();
+
+        let window_state = get_window_state(this);
+        let mut window_state = window_state.borrow_mut();
+        if let Some(pending) = window_state.pending_key_event.as_mut() {
+            pending.set_marked_text = Some((text.to_string(), selected_range, replacement_range));
+        } else {
+            drop(window_state);
+            with_input_handler(this, |input_handler| {
+                input_handler.set_composition(text, selected_range, replacement_range);
+            });
+        }
+    }
+}
+
+extern "C" fn unmark_text(this: &Object, _: Sel) {
+    println!("unmark_text");
+    let window_state = unsafe { get_window_state(this) };
+    let mut window_state = window_state.borrow_mut();
+    if let Some(pending) = window_state.pending_key_event.as_mut() {
+        pending.unmark_text = true;
+        pending.set_marked_text.take();
+    } else {
+        drop(window_state);
+        with_input_handler(this, |input_handler| input_handler.finish_composition());
+    }
+}
+
+extern "C" fn attributed_substring_for_proposed_range(
+    this: &Object,
+    _: Sel,
+    range: NSRange,
+    actual_range: *mut c_void,
+) -> id {
+    with_input_handler(this, |input_handler| {
+        let actual_range = actual_range as *mut NSRange;
+        if !actual_range.is_null() {
+            unsafe { *actual_range = NSRange::invalid() };
+        }
+
+        let requested_range = range.to_range()?;
+        if requested_range.is_empty() {
+            return None;
+        }
+
+        let selected_range = input_handler.selected_range()?;
+        let intersection = cmp::max(requested_range.start, selected_range.start)
+            ..cmp::min(requested_range.end, selected_range.end);
+        if intersection.start >= intersection.end {
+            return None;
+        }
+
+        unsafe {
+            let selected_text = ns_string(&input_handler.text_for_range(intersection)?);
+            let string: id = msg_send![class!(NSAttributedString), alloc];
+            let string: id = msg_send![string, initWithString: selected_text];
+            Some(string)
+        }
+    })
+    .flatten()
+    .unwrap_or(nil)
 }
 
 async fn synthetic_drag(
@@ -974,3 +1209,19 @@ async fn synthetic_drag(
 unsafe fn ns_string(string: &str) -> id {
     NSString::alloc(nil).init_str(string).autorelease()
 }
+
+fn with_input_handler<F, R>(window: &Object, f: F) -> Option<R>
+where
+    F: FnOnce(&mut dyn InputHandler) -> R,
+{
+    let window_state = unsafe { get_window_state(window) };
+    let mut window_state_borrow = window_state.as_ref().borrow_mut();
+    if let Some(mut input_handler) = window_state_borrow.input_handler.take() {
+        drop(window_state_borrow);
+        let result = f(input_handler.as_mut());
+        window_state.borrow_mut().input_handler = Some(input_handler);
+        Some(result)
+    } else {
+        None
+    }
+}

crates/gpui/src/platform/test.rs 🔗

@@ -255,6 +255,8 @@ impl super::Window for Window {
         self.close_handlers.push(callback);
     }
 
+    fn set_input_handler(&mut self, _: Box<dyn crate::InputHandler>) {}
+
     fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str]) -> oneshot::Receiver<usize> {
         let (done_tx, done_rx) = oneshot::channel();
         self.pending_prompts.borrow_mut().push_back(done_tx);