use crate::{
App, Bounds, ClipboardItem, Context, Entity, InputHandler, Pixels, TextInputConfiguration,
UTF16Selection, Window,
};
use std::ops::Range;
pub fn utf8_to_utf16_offset(text: &str, utf8_offset: usize) -> usize {
text.char_indices()
.take_while(|(offset, _)| *offset < utf8_offset)
.map(|(_, character)| character.len_utf16())
.sum()
}
pub fn utf16_to_utf8_offset(text: &str, utf16_offset: usize) -> usize {
let mut consumed_utf16 = 0;
for (utf8_offset, character) in text.char_indices() {
if consumed_utf16 >= utf16_offset {
return utf8_offset;
}
consumed_utf16 += character.len_utf16();
}
text.len()
}
pub trait EntityInputHandler: 'static + Sized {
fn text_for_range(
&mut self,
range: Range<usize>,
adjusted_range: &mut Option<Range<usize>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<String>;
fn selected_text_range(
&mut self,
ignore_disabled_input: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<UTF16Selection>;
fn marked_text_range(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Range<usize>>;
fn unmark_text(&mut self, window: &mut Window, cx: &mut Context<Self>);
fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut Context<Self>) {
if let Some(text) = item.text() {
self.replace_text_in_range(None, &text, window, cx);
}
}
fn replace_text_in_range(
&mut self,
range: Option<Range<usize>>,
text: &str,
window: &mut Window,
cx: &mut Context<Self>,
);
fn replace_and_mark_text_in_range(
&mut self,
range: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
window: &mut Window,
cx: &mut Context<Self>,
);
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
element_bounds: Bounds<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>>;
fn selection_bounds_for_range(
&mut self,
_range_utf16: Range<usize>,
_element_bounds: Bounds<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Vec<Bounds<Pixels>> {
Vec::new()
}
fn character_index_for_point(
&mut self,
point: crate::Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<usize>;
fn set_selected_text_range(
&mut self,
_range_utf16: Range<usize>,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
}
fn text_length_utf16(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
None
}
fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
true
}
fn text_input_configuration(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> TextInputConfiguration {
TextInputConfiguration::default()
}
fn text_input_editable_range(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Range<usize>> {
None
}
}
pub struct ElementInputHandler<V> {
view: Entity<V>,
element_bounds: Bounds<Pixels>,
}
impl<V: 'static> ElementInputHandler<V> {
pub fn new(element_bounds: Bounds<Pixels>, view: Entity<V>) -> Self {
ElementInputHandler {
view,
element_bounds,
}
}
}
impl<V: EntityInputHandler> InputHandler for ElementInputHandler<V> {
fn selected_text_range(
&mut self,
ignore_disabled_input: bool,
window: &mut Window,
cx: &mut App,
) -> Option<UTF16Selection> {
self.view.update(cx, |view, cx| {
view.selected_text_range(ignore_disabled_input, window, cx)
})
}
fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>> {
self.view
.update(cx, |view, cx| view.marked_text_range(window, cx))
}
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
adjusted_range: &mut Option<Range<usize>>,
window: &mut Window,
cx: &mut App,
) -> Option<String> {
self.view.update(cx, |view, cx| {
view.text_for_range(range_utf16, adjusted_range, window, cx)
})
}
fn replace_text_in_range(
&mut self,
replacement_range: Option<Range<usize>>,
text: &str,
window: &mut Window,
cx: &mut App,
) {
self.view.update(cx, |view, cx| {
view.replace_text_in_range(replacement_range, text, window, cx)
});
}
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
window: &mut Window,
cx: &mut App,
) {
self.view.update(cx, |view, cx| {
view.replace_and_mark_text_in_range(
range_utf16,
new_text,
new_selected_range,
window,
cx,
)
});
}
fn unmark_text(&mut self, window: &mut Window, cx: &mut App) {
self.view
.update(cx, |view, cx| view.unmark_text(window, cx));
}
fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
self.view
.update(cx, |view, cx| view.paste(item, window, cx));
}
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
window: &mut Window,
cx: &mut App,
) -> Option<Bounds<Pixels>> {
self.view.update(cx, |view, cx| {
view.bounds_for_range(range_utf16, self.element_bounds, window, cx)
})
}
fn selection_bounds_for_range(
&mut self,
range_utf16: Range<usize>,
window: &mut Window,
cx: &mut App,
) -> Vec<Bounds<Pixels>> {
self.view.update(cx, |view, cx| {
view.selection_bounds_for_range(range_utf16, self.element_bounds, window, cx)
})
}
fn character_index_for_point(
&mut self,
point: crate::Point<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Option<usize> {
self.view.update(cx, |view, cx| {
view.character_index_for_point(point, window, cx)
})
}
fn set_selected_text_range(
&mut self,
range_utf16: Range<usize>,
window: &mut Window,
cx: &mut App,
) {
self.view.update(cx, |view, cx| {
view.set_selected_text_range(range_utf16, window, cx)
})
}
fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
Some(self.element_bounds)
}
fn text_length_utf16(&mut self, window: &mut Window, cx: &mut App) -> Option<usize> {
self.view
.update(cx, |view, cx| view.text_length_utf16(window, cx))
}
fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
self.view
.update(cx, |view, cx| view.accepts_text_input(window, cx))
}
fn prefers_ime_for_printable_keys(&mut self, window: &mut Window, cx: &mut App) -> bool {
self.view
.update(cx, |view, cx| view.accepts_text_input(window, cx))
}
fn text_input_configuration(
&mut self,
window: &mut Window,
cx: &mut App,
) -> TextInputConfiguration {
self.view
.update(cx, |view, cx| view.text_input_configuration(window, cx))
}
fn text_input_editable_range(
&mut self,
window: &mut Window,
cx: &mut App,
) -> Option<Range<usize>> {
self.view
.update(cx, |view, cx| view.text_input_editable_range(window, cx))
}
}
#[cfg(test)]
mod configuration_tests {
use super::*;
use crate::{
AnyWindowHandle, AppContext as _, FocusHandle, InteractiveElement as _, IntoElement,
ParentElement as _, Render, Styled as _, TestAppContext, TextInputAction,
TextInputStateChange, canvas, div,
};
#[gpui::test]
fn text_input_configuration_and_focus_state_are_forwarded_on_change(cx: &mut TestAppContext) {
let custom = TextInputConfiguration {
autocorrect: true,
input_action: TextInputAction::Send,
..Default::default()
};
let window = cx.add_window({
let custom = custom.clone();
move |_, cx| ConfigurationTestView {
focus_handle: cx.focus_handle(),
configuration: custom,
accepts_text_input: true,
}
});
let view = window.root(cx).unwrap();
let test_window = cx.test_window(window.into());
let window = AnyWindowHandle::from(window);
let draw = |cx: &mut TestAppContext| {
cx.update_window(window, |_, window, cx| window.draw(cx).clear(cx))
.unwrap();
};
draw(cx);
assert_eq!(
test_window.text_input_configurations(),
vec![TextInputConfiguration::default()]
);
assert!(test_window.text_input_state_changes().is_empty());
cx.update_window(window, |_, window, cx| {
let focus_handle = view.read(cx).focus_handle.clone();
window.focus(&focus_handle, cx);
})
.unwrap();
draw(cx);
assert_eq!(
test_window.text_input_configurations(),
vec![TextInputConfiguration::default(), custom.clone()]
);
assert_eq!(
test_window.text_input_state_changes(),
vec![TextInputStateChange::FocusGained]
);
draw(cx);
assert_eq!(test_window.text_input_configurations().len(), 2);
assert_eq!(test_window.text_input_state_changes().len(), 1);
let updated = TextInputConfiguration {
suggestions: true,
..custom
};
view.update(cx, {
let updated = updated.clone();
|view, cx| {
view.configuration = updated;
cx.notify();
}
});
draw(cx);
assert_eq!(
test_window.text_input_configurations().last(),
Some(&updated)
);
assert_eq!(test_window.text_input_configurations().len(), 3);
assert_eq!(test_window.text_input_state_changes().len(), 1);
cx.update_window(window, |_, window, cx| window.blur(cx))
.unwrap();
draw(cx);
assert_eq!(
test_window.text_input_configurations().last(),
Some(&TextInputConfiguration::default())
);
assert_eq!(test_window.text_input_configurations().len(), 4);
assert_eq!(
test_window.text_input_state_changes(),
vec![
TextInputStateChange::FocusGained,
TextInputStateChange::FocusLost
]
);
}
#[gpui::test]
fn text_notifications_require_a_focused_input_and_track_field_changes(cx: &mut TestAppContext) {
let window = cx.add_window(|_, cx| ConfigurationTestView {
focus_handle: cx.focus_handle(),
configuration: TextInputConfiguration::default(),
accepts_text_input: true,
});
let view = window.root(cx).unwrap();
let mut test_window = cx.test_window(window.into());
let window = AnyWindowHandle::from(window);
let first_focus = view.read_with(cx, |view, _| view.focus_handle.clone());
cx.update_window(window, |_, window, cx| {
window.focus(&first_focus, cx);
window.draw(cx).clear(cx);
window.notify_text_input_changed(&first_focus);
window.notify_text_selection_changed(&first_focus);
})
.unwrap();
use crate::PlatformWindow as _;
let mut installed_handler = test_window.take_input_handler().unwrap();
assert!(installed_handler.query_accepts_focused_text_input());
assert_eq!(installed_handler.focus_id(), Some(first_focus.id));
let second_focus = cx
.update_window(window, |_, window, cx| {
let second_focus = view.update(cx, |view, cx| {
view.focus_handle = cx.focus_handle();
cx.notify();
view.focus_handle.clone()
});
window.focus(&second_focus, cx);
second_focus
})
.unwrap();
assert!(!installed_handler.query_accepts_focused_text_input());
test_window.set_input_handler(installed_handler);
cx.update_window(window, |_, window, cx| {
window.draw(cx).clear(cx);
window.notify_text_input_changed(&first_focus);
})
.unwrap();
assert_eq!(
test_window.text_input_state_changes(),
[
TextInputStateChange::FocusGained,
TextInputStateChange::ContentChanged,
TextInputStateChange::SelectionChanged,
]
);
let installed_handler = test_window.take_input_handler().unwrap();
assert_eq!(installed_handler.focus_id(), Some(second_focus.id));
test_window.set_input_handler(installed_handler);
cx.update_window(window, |_, window, cx| {
window.draw(cx).clear(cx);
window.notify_text_selection_changed(&second_focus);
})
.unwrap();
view.update(cx, |view, cx| {
view.accepts_text_input = false;
cx.notify();
});
cx.update_window(window, |_, window, cx| {
window.draw(cx).clear(cx);
window.notify_text_input_changed(&second_focus);
window.notify_text_selection_changed(&second_focus);
let ordinary_focus = cx.focus_handle();
window.focus(&ordinary_focus, cx);
window.draw(cx).clear(cx);
window.notify_text_input_changed(&ordinary_focus);
})
.unwrap();
assert_eq!(
test_window.text_input_state_changes(),
[
TextInputStateChange::FocusGained,
TextInputStateChange::ContentChanged,
TextInputStateChange::SelectionChanged,
TextInputStateChange::SelectionChanged,
TextInputStateChange::FocusLost,
]
);
}
struct ConfigurationTestView {
focus_handle: FocusHandle,
configuration: TextInputConfiguration,
accepts_text_input: bool,
}
impl Render for ConfigurationTestView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let focus_handle = self.focus_handle.clone();
div().size_full().track_focus(&self.focus_handle).child(
canvas(
|_, _, _| {},
move |bounds, _, window, cx| {
window.handle_input(
&focus_handle,
ElementInputHandler::new(bounds, view),
cx,
);
},
)
.size_full(),
)
}
}
impl EntityInputHandler for ConfigurationTestView {
fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
self.accepts_text_input
}
fn text_for_range(
&mut self,
_range: std::ops::Range<usize>,
_adjusted_range: &mut Option<std::ops::Range<usize>>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<String> {
None
}
fn selected_text_range(
&mut self,
_ignore_disabled_input: bool,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<UTF16Selection> {
None
}
fn marked_text_range(
&self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<std::ops::Range<usize>> {
None
}
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
fn replace_text_in_range(
&mut self,
_range: Option<std::ops::Range<usize>>,
_text: &str,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
}
fn replace_and_mark_text_in_range(
&mut self,
_range: Option<std::ops::Range<usize>>,
_new_text: &str,
_new_selected_range: Option<std::ops::Range<usize>>,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
}
fn bounds_for_range(
&mut self,
_range_utf16: std::ops::Range<usize>,
_element_bounds: Bounds<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
None
}
fn character_index_for_point(
&mut self,
_point: crate::Point<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
None
}
fn text_input_configuration(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> TextInputConfiguration {
self.configuration.clone()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_offsets_convert_across_unicode_encodings() {
let cases = [
("", vec![(0, 0)]),
("plain", vec![(0, 0), (1, 1), (5, 5)]),
(
"A😀日本e\u{301}",
vec![(0, 0), (1, 1), (5, 3), (8, 4), (11, 5), (12, 6), (14, 7)],
),
];
for (text, boundaries) in cases {
for (utf8, utf16) in boundaries {
assert_eq!(utf8_to_utf16_offset(text, utf8), utf16, "{text:?}");
assert_eq!(utf16_to_utf8_offset(text, utf16), utf8, "{text:?}");
}
assert_eq!(
utf8_to_utf16_offset(text, usize::MAX),
text.encode_utf16().count()
);
assert_eq!(utf16_to_utf8_offset(text, usize::MAX), text.len());
}
let text = "A😀B";
assert_eq!(utf16_to_utf8_offset(text, 2), 5);
assert_eq!(utf8_to_utf16_offset(text, 2), 3);
}
}