Implement an InputHandler trait for gpui2

Conrad Irwin , Marshall , Max , and Julia created

Co-Authored-By: Marshall <marshall@zed.dev>
Co-Authored-By: Max <max@zed.dev>
Co-Authored-By: Julia <julia@zed.dev>

Change summary

crates/editor2/src/editor.rs             | 399 +++++++++++++------------
crates/editor2/src/element.rs            |   4 
crates/gpui2/src/app.rs                  |   2 
crates/gpui2/src/gpui2.rs                |   2 
crates/gpui2/src/window.rs               |  28 +
crates/gpui2/src/window_input_handler.rs |  89 +++++
6 files changed, 322 insertions(+), 202 deletions(-)

Detailed changes

crates/editor2/src/editor.rs 🔗

@@ -38,8 +38,8 @@ use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     actions, div, px, relative, AnyElement, AppContext, BackgroundExecutor, Context,
     DispatchContext, Div, Element, Entity, EventEmitter, FocusHandle, FontStyle, FontWeight, Hsla,
-    Model, Pixels, Render, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext,
-    WeakView, WindowContext,
+    Model, Pixels, PlatformInputHandler, Render, Styled, Subscription, Task, TextStyle, View,
+    ViewContext, VisualContext, WeakView, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -82,7 +82,7 @@ use std::{
 };
 pub use sum_tree::Bias;
 use sum_tree::TreeMap;
-use text::Rope;
+use text::{OffsetUtf16, Rope};
 use theme::{
     ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
 };
@@ -9485,214 +9485,225 @@ impl Render for Editor {
 
 //         false
 //     }
-//
-//     fn text_for_range(&self, range_utf16: Range<usize>, cx: &AppContext) -> Option<String> {
-//         Some(
-//             self.buffer
-//                 .read(cx)
-//                 .read(cx)
-//                 .text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end))
-//                 .collect(),
-//         )
-//     }
-
-//     fn selected_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
-//         // Prevent the IME menu from appearing when holding down an alphabetic key
-//         // while input is disabled.
-//         if !self.input_enabled {
-//             return None;
-//         }
-
-//         let range = self.selections.newest::<OffsetUtf16>(cx).range();
-//         Some(range.start.0..range.end.0)
-//     }
 
-//     fn marked_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
-//         let snapshot = self.buffer.read(cx).read(cx);
-//         let range = self.text_highlights::<InputComposition>(cx)?.1.get(0)?;
-//         Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0)
-//     }
-
-//     fn unmark_text(&mut self, cx: &mut ViewContext<Self>) {
-//         self.clear_highlights::<InputComposition>(cx);
-//         self.ime_transaction.take();
-//     }
-
-//     fn replace_text_in_range(
-//         &mut self,
-//         range_utf16: Option<Range<usize>>,
-//         text: &str,
-//         cx: &mut ViewContext<Self>,
-//     ) {
-//         if !self.input_enabled {
-//             cx.emit(Event::InputIgnored { text: text.into() });
-//             return;
-//         }
-
-//         self.transact(cx, |this, cx| {
-//             let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
-//                 let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
-//                 Some(this.selection_replacement_ranges(range_utf16, cx))
-//             } else {
-//                 this.marked_text_ranges(cx)
-//             };
+impl PlatformInputHandler for Editor {
+    fn text_for_range(&self, range_utf16: Range<usize>) -> Option<String> {
+        // Some(
+        //     self.buffer
+        //         .read(cx)
+        //         .read(cx)
+        //         .text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end))
+        //         .collect(),
+        // )
+        todo!()
+    }
 
-//             let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| {
-//                 let newest_selection_id = this.selections.newest_anchor().id;
-//                 this.selections
-//                     .all::<OffsetUtf16>(cx)
-//                     .iter()
-//                     .zip(ranges_to_replace.iter())
-//                     .find_map(|(selection, range)| {
-//                         if selection.id == newest_selection_id {
-//                             Some(
-//                                 (range.start.0 as isize - selection.head().0 as isize)
-//                                     ..(range.end.0 as isize - selection.head().0 as isize),
-//                             )
-//                         } else {
-//                             None
-//                         }
-//                     })
-//             });
+    fn selected_text_range(&self) -> Option<Range<usize>> {
+        // Prevent the IME menu from appearing when holding down an alphabetic key
+        // while input is disabled.
+        // if !self.input_enabled {
+        //     return None;
+        // }
 
-//             cx.emit(Event::InputHandled {
-//                 utf16_range_to_replace: range_to_replace,
-//                 text: text.into(),
-//             });
+        // let range = self.selections.newest::<OffsetUtf16>(cx).range();
+        // Some(range.start.0..range.end.0)
+        todo!()
+    }
 
-//             if let Some(new_selected_ranges) = new_selected_ranges {
-//                 this.change_selections(None, cx, |selections| {
-//                     selections.select_ranges(new_selected_ranges)
-//                 });
-//             }
+    fn marked_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
+        // let snapshot = self.buffer.read(cx).read(cx);
+        // let range = self.text_highlights::<InputComposition>(cx)?.1.get(0)?;
+        // Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0)
+        todo!()
+    }
 
-//             this.handle_input(text, cx);
-//         });
+    fn unmark_text(&mut self, cx: &mut ViewContext<Self>) {
+        // self.clear_highlights::<InputComposition>(cx);
+        // self.ime_transaction.take();
+        todo!()
+    }
 
-//         if let Some(transaction) = self.ime_transaction {
-//             self.buffer.update(cx, |buffer, cx| {
-//                 buffer.group_until_transaction(transaction, cx);
-//             });
-//         }
+    fn replace_text_in_range(
+        &mut self,
+        //range_utf16: Option<Range<usize>>,
+        // text: &str,
+        cx: &mut ViewContext<Self>,
+    ) {
+        // if !self.input_enabled {
+        //     cx.emit(Event::InputIgnored { text: text.into() });
+        //     return;
+        // }
 
-//         self.unmark_text(cx);
-//     }
+        // self.transact(cx, |this, cx| {
+        //     let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
+        //         let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
+        //         Some(this.selection_replacement_ranges(range_utf16, cx))
+        //     } else {
+        //         this.marked_text_ranges(cx)
+        //     };
+
+        //     let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| {
+        //         let newest_selection_id = this.selections.newest_anchor().id;
+        //         this.selections
+        //             .all::<OffsetUtf16>(cx)
+        //             .iter()
+        //             .zip(ranges_to_replace.iter())
+        //             .find_map(|(selection, range)| {
+        //                 if selection.id == newest_selection_id {
+        //                     Some(
+        //                         (range.start.0 as isize - selection.head().0 as isize)
+        //                             ..(range.end.0 as isize - selection.head().0 as isize),
+        //                     )
+        //                 } else {
+        //                     None
+        //                 }
+        //             })
+        //     });
+
+        //     cx.emit(Event::InputHandled {
+        //         utf16_range_to_replace: range_to_replace,
+        //         text: text.into(),
+        //     });
+
+        //     if let Some(new_selected_ranges) = new_selected_ranges {
+        //         this.change_selections(None, cx, |selections| {
+        //             selections.select_ranges(new_selected_ranges)
+        //         });
+        //     }
 
-//     fn replace_and_mark_text_in_range(
-//         &mut self,
-//         range_utf16: Option<Range<usize>>,
-//         text: &str,
-//         new_selected_range_utf16: Option<Range<usize>>,
-//         cx: &mut ViewContext<Self>,
-//     ) {
-//         if !self.input_enabled {
-//             cx.emit(Event::InputIgnored { text: text.into() });
-//             return;
-//         }
+        //     this.handle_input(text, cx);
+        // });
 
-//         let transaction = self.transact(cx, |this, cx| {
-//             let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) {
-//                 let snapshot = this.buffer.read(cx).read(cx);
-//                 if let Some(relative_range_utf16) = range_utf16.as_ref() {
-//                     for marked_range in &mut marked_ranges {
-//                         marked_range.end.0 = marked_range.start.0 + relative_range_utf16.end;
-//                         marked_range.start.0 += relative_range_utf16.start;
-//                         marked_range.start =
-//                             snapshot.clip_offset_utf16(marked_range.start, Bias::Left);
-//                         marked_range.end =
-//                             snapshot.clip_offset_utf16(marked_range.end, Bias::Right);
-//                     }
-//                 }
-//                 Some(marked_ranges)
-//             } else if let Some(range_utf16) = range_utf16 {
-//                 let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
-//                 Some(this.selection_replacement_ranges(range_utf16, cx))
-//             } else {
-//                 None
-//             };
+        // if let Some(transaction) = self.ime_transaction {
+        //     self.buffer.update(cx, |buffer, cx| {
+        //         buffer.group_until_transaction(transaction, cx);
+        //     });
+        // }
 
-//             let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| {
-//                 let newest_selection_id = this.selections.newest_anchor().id;
-//                 this.selections
-//                     .all::<OffsetUtf16>(cx)
-//                     .iter()
-//                     .zip(ranges_to_replace.iter())
-//                     .find_map(|(selection, range)| {
-//                         if selection.id == newest_selection_id {
-//                             Some(
-//                                 (range.start.0 as isize - selection.head().0 as isize)
-//                                     ..(range.end.0 as isize - selection.head().0 as isize),
-//                             )
-//                         } else {
-//                             None
-//                         }
-//                     })
-//             });
+        // self.unmark_text(cx);
+        todo!()
+    }
 
-//             cx.emit(Event::InputHandled {
-//                 utf16_range_to_replace: range_to_replace,
-//                 text: text.into(),
-//             });
+    fn replace_and_mark_text_in_range(
+        &mut self,
+        range_utf16: Option<Range<usize>>,
+        text: &str,
+        new_selected_range_utf16: Option<Range<usize>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        // if !self.input_enabled {
+        //     cx.emit(Event::InputIgnored { text: text.into() });
+        //     return;
+        // }
 
-//             if let Some(ranges) = ranges_to_replace {
-//                 this.change_selections(None, cx, |s| s.select_ranges(ranges));
-//             }
+        // let transaction = self.transact(cx, |this, cx| {
+        //     let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) {
+        //         let snapshot = this.buffer.read(cx).read(cx);
+        //         if let Some(relative_range_utf16) = range_utf16.as_ref() {
+        //             for marked_range in &mut marked_ranges {
+        //                 marked_range.end.0 = marked_range.start.0 + relative_range_utf16.end;
+        //                 marked_range.start.0 += relative_range_utf16.start;
+        //                 marked_range.start =
+        //                     snapshot.clip_offset_utf16(marked_range.start, Bias::Left);
+        //                 marked_range.end =
+        //                     snapshot.clip_offset_utf16(marked_range.end, Bias::Right);
+        //             }
+        //         }
+        //         Some(marked_ranges)
+        //     } else if let Some(range_utf16) = range_utf16 {
+        //         let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
+        //         Some(this.selection_replacement_ranges(range_utf16, cx))
+        //     } else {
+        //         None
+        //     };
+
+        //     let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| {
+        //         let newest_selection_id = this.selections.newest_anchor().id;
+        //         this.selections
+        //             .all::<OffsetUtf16>(cx)
+        //             .iter()
+        //             .zip(ranges_to_replace.iter())
+        //             .find_map(|(selection, range)| {
+        //                 if selection.id == newest_selection_id {
+        //                     Some(
+        //                         (range.start.0 as isize - selection.head().0 as isize)
+        //                             ..(range.end.0 as isize - selection.head().0 as isize),
+        //                     )
+        //                 } else {
+        //                     None
+        //                 }
+        //             })
+        //     });
+
+        //     cx.emit(Event::InputHandled {
+        //         utf16_range_to_replace: range_to_replace,
+        //         text: text.into(),
+        //     });
+
+        //     if let Some(ranges) = ranges_to_replace {
+        //         this.change_selections(None, cx, |s| s.select_ranges(ranges));
+        //     }
 
-//             let marked_ranges = {
-//                 let snapshot = this.buffer.read(cx).read(cx);
-//                 this.selections
-//                     .disjoint_anchors()
-//                     .iter()
-//                     .map(|selection| {
-//                         selection.start.bias_left(&*snapshot)..selection.end.bias_right(&*snapshot)
-//                     })
-//                     .collect::<Vec<_>>()
-//             };
+        //     let marked_ranges = {
+        //         let snapshot = this.buffer.read(cx).read(cx);
+        //         this.selections
+        //             .disjoint_anchors()
+        //             .iter()
+        //             .map(|selection| {
+        //                 selection.start.bias_left(&*snapshot)..selection.end.bias_right(&*snapshot)
+        //             })
+        //             .collect::<Vec<_>>()
+        //     };
+
+        //     if text.is_empty() {
+        //         this.unmark_text(cx);
+        //     } else {
+        //         this.highlight_text::<InputComposition>(
+        //             marked_ranges.clone(),
+        //             this.style(cx).composition_mark,
+        //             cx,
+        //         );
+        //     }
 
-//             if text.is_empty() {
-//                 this.unmark_text(cx);
-//             } else {
-//                 this.highlight_text::<InputComposition>(
-//                     marked_ranges.clone(),
-//                     this.style(cx).composition_mark,
-//                     cx,
-//                 );
-//             }
+        //     this.handle_input(text, cx);
+
+        //     if let Some(new_selected_range) = new_selected_range_utf16 {
+        //         let snapshot = this.buffer.read(cx).read(cx);
+        //         let new_selected_ranges = marked_ranges
+        //             .into_iter()
+        //             .map(|marked_range| {
+        //                 let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0;
+        //                 let new_start = OffsetUtf16(new_selected_range.start + insertion_start);
+        //                 let new_end = OffsetUtf16(new_selected_range.end + insertion_start);
+        //                 snapshot.clip_offset_utf16(new_start, Bias::Left)
+        //                     ..snapshot.clip_offset_utf16(new_end, Bias::Right)
+        //             })
+        //             .collect::<Vec<_>>();
+
+        //         drop(snapshot);
+        //         this.change_selections(None, cx, |selections| {
+        //             selections.select_ranges(new_selected_ranges)
+        //         });
+        //     }
+        // });
 
-//             this.handle_input(text, cx);
-
-//             if let Some(new_selected_range) = new_selected_range_utf16 {
-//                 let snapshot = this.buffer.read(cx).read(cx);
-//                 let new_selected_ranges = marked_ranges
-//                     .into_iter()
-//                     .map(|marked_range| {
-//                         let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0;
-//                         let new_start = OffsetUtf16(new_selected_range.start + insertion_start);
-//                         let new_end = OffsetUtf16(new_selected_range.end + insertion_start);
-//                         snapshot.clip_offset_utf16(new_start, Bias::Left)
-//                             ..snapshot.clip_offset_utf16(new_end, Bias::Right)
-//                     })
-//                     .collect::<Vec<_>>();
-
-//                 drop(snapshot);
-//                 this.change_selections(None, cx, |selections| {
-//                     selections.select_ranges(new_selected_ranges)
-//                 });
-//             }
-//         });
+        // self.ime_transaction = self.ime_transaction.or(transaction);
+        // if let Some(transaction) = self.ime_transaction {
+        //     self.buffer.update(cx, |buffer, cx| {
+        //         buffer.group_until_transaction(transaction, cx);
+        //     });
+        // }
 
-//         self.ime_transaction = self.ime_transaction.or(transaction);
-//         if let Some(transaction) = self.ime_transaction {
-//             self.buffer.update(cx, |buffer, cx| {
-//                 buffer.group_until_transaction(transaction, cx);
-//             });
-//         }
+        // if self.text_highlights::<InputComposition>(cx).is_none() {
+        //     self.ime_transaction.take();
+        // }
+        todo!()
+    }
 
-//         if self.text_highlights::<InputComposition>(cx).is_none() {
-//             self.ime_transaction.take();
-//         }
-//     }
-// }
+    fn bounds_for_range(&self, range_utf16: Range<usize>) -> Option<gpui::Bounds<f32>> {
+        todo!()
+    }
+}
 
 // fn build_style(
 //     settings: &ThemeSettings,

crates/editor2/src/element.rs 🔗

@@ -2623,6 +2623,10 @@ impl Element<Editor> for EditorElement {
             }
         });
 
+        if editor.focus_handle.is_focused(cx) {
+            cx.set_input_handler(editor.handle);
+        }
+
         cx.with_content_mask(ContentMask { bounds }, |cx| {
             let gutter_bounds = Bounds {
                 origin: bounds.origin,

crates/gpui2/src/app.rs 🔗

@@ -154,7 +154,7 @@ type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut AppContext) + 'static>;
 // }
 
 pub struct AppContext {
-    this: Weak<AppCell>,
+    pub(crate) this: Weak<AppCell>,
     pub(crate) platform: Rc<dyn Platform>,
     app_metadata: AppMetadata,
     text_system: Arc<TextSystem>,

crates/gpui2/src/gpui2.rs 🔗

@@ -24,6 +24,7 @@ mod text_system;
 mod util;
 mod view;
 mod window;
+mod window_input_handler;
 
 mod private {
     /// A mechanism for restricting implementations of a trait to only those in GPUI.
@@ -64,6 +65,7 @@ pub use text_system::*;
 pub use util::arc_cow::ArcCow;
 pub use view::*;
 pub use window::*;
+pub use window_input_handler::*;
 
 use derive_more::{Deref, DerefMut};
 use std::{

crates/gpui2/src/window.rs 🔗

@@ -2,13 +2,14 @@ use crate::{
     px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace,
     Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchContext, DisplayId,
     Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId,
-    GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch,
-    KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton,
-    MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay,
-    PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
-    RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
-    Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
-    VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
+    GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, InputHandler, IsZero, KeyListener,
+    KeyMatch, KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite,
+    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas,
+    PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel,
+    Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
+    SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription,
+    TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView,
+    WindowBounds, WindowInputHandler, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::{anyhow, Result};
 use collections::HashMap;
@@ -191,6 +192,7 @@ pub struct Window {
     default_prevented: bool,
     mouse_position: Point<Pixels>,
     requested_cursor_style: Option<CursorStyle>,
+    requested_input_handler: Option<Box<dyn PlatformInputHandler>>,
     scale_factor: f32,
     bounds: WindowBounds,
     bounds_observers: SubscriberSet<(), AnyObserver>,
@@ -285,6 +287,7 @@ impl Window {
             default_prevented: true,
             mouse_position,
             requested_cursor_style: None,
+            requested_input_handler: None,
             scale_factor,
             bounds,
             bounds_observers: SubscriberSet::new(),
@@ -676,6 +679,17 @@ impl<'a> WindowContext<'a> {
         self.window.requested_cursor_style = Some(style)
     }
 
+    pub fn set_input_handler<V>(&mut self, handler: WeakView<V>, cx: ViewContext<V>)
+    where
+        V: InputHandler + 'static,
+    {
+        self.window.requested_input_handler = Some(Box::new(WindowInputHandler {
+            cx: cx.app.this.clone(),
+            window: cx.window_handle(),
+            handler,
+        }))
+    }
+
     /// Called during painting to invoke the given closure in a new stacking context. The given
     /// z-index is interpreted relative to the previous call to `stack`.
     pub fn stack<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R {

crates/gpui2/src/window_input_handler.rs 🔗

@@ -0,0 +1,89 @@
+use crate::{AnyWindowHandle, AppCell, Context, PlatformInputHandler, ViewContext, WeakView};
+use std::{ops::Range, rc::Weak};
+
+pub struct WindowInputHandler<V>
+where
+    V: InputHandler,
+{
+    pub cx: Weak<AppCell>,
+    pub window: AnyWindowHandle,
+    pub handler: WeakView<V>,
+}
+
+impl<V: InputHandler + 'static> PlatformInputHandler for WindowInputHandler<V> {
+    fn selected_text_range(&self) -> Option<std::ops::Range<usize>> {
+        self.update(|view, cx| view.selected_text_range(cx))
+            .flatten()
+    }
+
+    fn marked_text_range(&self) -> Option<std::ops::Range<usize>> {
+        self.update(|view, cx| view.marked_text_range(cx)).flatten()
+    }
+
+    fn text_for_range(&self, range_utf16: std::ops::Range<usize>) -> Option<String> {
+        self.update(|view, cx| view.text_for_range(range_utf16, cx))
+            .flatten()
+    }
+
+    fn replace_text_in_range(
+        &mut self,
+        replacement_range: Option<std::ops::Range<usize>>,
+        text: &str,
+    ) {
+        self.update(|view, cx| view.replace_text_in_range(replacement_range, text, cx));
+    }
+
+    fn replace_and_mark_text_in_range(
+        &mut self,
+        range_utf16: Option<std::ops::Range<usize>>,
+        new_text: &str,
+        new_selected_range: Option<std::ops::Range<usize>>,
+    ) {
+        self.update(|view, cx| {
+            view.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx)
+        });
+    }
+
+    fn unmark_text(&mut self) {
+        self.update(|view, cx| view.unmark_text(cx));
+    }
+
+    fn bounds_for_range(&self, range_utf16: std::ops::Range<usize>) -> Option<crate::Bounds<f32>> {
+        self.update(|view, cx| view.bounds_for_range(range_utf16, cx))
+            .flatten()
+    }
+}
+
+impl<V: InputHandler + 'static> WindowInputHandler<V> {
+    fn update<T>(&self, f: impl FnOnce(&mut V, &mut ViewContext<V>) -> T) -> Option<T> {
+        let cx = self.cx.upgrade()?;
+        let mut cx = cx.borrow_mut();
+        cx.update_window(self.window, |_, cx| self.handler.update(cx, f).ok())
+            .ok()?
+    }
+}
+
+pub trait InputHandler: Sized {
+    fn text_for_range(&self, range: Range<usize>, cx: &mut ViewContext<Self>) -> Option<String>;
+    fn selected_text_range(&self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
+    fn marked_text_range(&self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
+    fn unmark_text(&mut self, cx: &mut ViewContext<Self>);
+    fn replace_text_in_range(
+        &mut self,
+        range: Option<Range<usize>>,
+        text: &str,
+        cx: &mut ViewContext<Self>,
+    );
+    fn replace_and_mark_text_in_range(
+        &mut self,
+        range: Option<Range<usize>>,
+        new_text: &str,
+        new_selected_range: Option<Range<usize>>,
+        cx: &mut ViewContext<Self>,
+    );
+    fn bounds_for_range(
+        &self,
+        range_utf16: std::ops::Range<usize>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<crate::Bounds<f32>>;
+}