gpui: Text input configuration and web implementation (#63264)

Allows setting various attributes for the underlying mirror textarea on
the web. Allows GPUI app authors to specify things like "this element
should be auto-corrected"

---

Release Notes:

- N/A or Added/Fixed/Improved ...
This commit is contained in:
Cameron Mcloughlin authored and GitHub committed 2026-08-26 17:57:23 +00:00
1 parent f7fcd09512
commit a3c4abf07c
7 files changed
+502 -29

No files matched your search

+206 -1
View File
@@ -1,5 +1,6 @@
use crate::{
App, Bounds, ClipboardItem, Context, Entity, InputHandler, Pixels, UTF16Selection, Window,
App, Bounds, ClipboardItem, Context, Entity, InputHandler, Pixels, TextInputConfiguration,
UTF16Selection, Window,
};
use std::ops::Range;
@@ -102,6 +103,15 @@ pub trait EntityInputHandler: 'static + Sized {
fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
true
}
/// See [`InputHandler::text_input_configuration`] for details
fn text_input_configuration(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> TextInputConfiguration {
TextInputConfiguration::default()
}
}
/// The canonical implementation of [`crate::PlatformInputHandler`]. Call [`Window::handle_input`]
@@ -244,4 +254,199 @@ impl<V: EntityInputHandler> InputHandler for ElementInputHandler<V> {
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))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
AnyWindowHandle, AppContext as _, FocusHandle, InteractiveElement as _, IntoElement,
ParentElement as _, Render, Styled as _, TestAppContext, TextInputAction, canvas, div,
};
#[gpui::test]
fn text_input_configuration_forwarded_only_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,
}
});
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();
};
// Nothing is focused, so the platform learns the default configuration.
draw(cx);
assert_eq!(
test_window.text_input_configurations(),
vec![TextInputConfiguration::default()]
);
// Focusing the view routes its configuration to the platform.
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()]
);
// Redrawing without a change forwards nothing.
draw(cx);
assert_eq!(test_window.text_input_configurations().len(), 2);
// Changing the configuration forwards the new value.
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);
// Losing focus reverts the platform to the default configuration.
cx.update_window(window, |_, window, _| window.blur())
.unwrap();
draw(cx);
assert_eq!(
test_window.text_input_configurations().last(),
Some(&TextInputConfiguration::default())
);
assert_eq!(test_window.text_input_configurations().len(), 4);
}
struct ConfigurationTestView {
focus_handle: FocusHandle,
configuration: TextInputConfiguration,
}
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 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()
}
}
}
+92
View File
@@ -827,6 +827,11 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn capslock(&self) -> Capslock;
fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
/// Apply the focused text region's [`TextInputConfiguration`] to the
/// platform's text input session (e.g. attributes of the hidden editable
/// element on web). Called only when the configuration changes, because
/// reconfiguring a live input session can restart the IME connection.
fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {}
fn prompt(
&self,
level: PromptLevel,
@@ -1654,6 +1659,15 @@ impl PlatformInputHandler {
})
.unwrap_or(false)
}
/// See [`InputHandler::text_input_configuration`].
pub fn text_input_configuration(
&mut self,
window: &mut Window,
cx: &mut App,
) -> TextInputConfiguration {
self.handler.text_input_configuration(window, cx)
}
}
/// A struct representing a selection in a text buffer, in UTF16 characters.
@@ -1823,6 +1837,84 @@ pub trait InputHandler: 'static {
fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
false
}
/// Get this handler's preferences for platform text assistance.
///
/// GPUI re-queries this every frame and forwards it to the platform window
/// only when it changes, so implementations must be cheap and may vary the
/// result with application state (e.g. with the cursor's position).
fn text_input_configuration(
&mut self,
_window: &mut Window,
_cx: &mut App,
) -> TextInputConfiguration {
TextInputConfiguration::default()
}
}
/// Platform text-assistance preferences for the focused text region.
///
/// Returned by [`InputHandler::text_input_configuration`] and forwarded to the
/// platform whenever it changes; the platform maps the fields onto its native
/// input-session attributes (on web, DOM attributes of the hidden editable
/// element such as `autocorrect` and `enterkeyhint`).
///
/// The default disables all text assistance and requests no particular action
/// key presentation.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TextInputConfiguration {
/// Whether the platform may automatically correct entered text.
pub autocorrect: bool,
/// How software keyboards automatically capitalize entered text.
pub autocapitalize: Autocapitalize,
/// Whether software keyboards may offer word suggestions and spellcheck.
pub suggestions: bool,
/// The action advertised on a software keyboard's confirm ("enter") key.
pub input_action: TextInputAction,
}
/// Automatic capitalization applied by software keyboards.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Autocapitalize {
/// No automatic capitalization.
#[default]
None,
/// Capitalize the first letter of each word.
Words,
/// Capitalize the first letter of each sentence.
Sentences,
/// Capitalize every letter.
Characters,
}
/// The action a software keyboard advertises on its confirm ("enter") key.
///
/// This affects only how the key is presented (icon or label); pressing it is
/// still delivered as ordinary input.
///
/// The variants are the HTML `enterkeyhint` attribute's value set
/// (<https://html.spec.whatwg.org/multipage/interaction.html#input-modalities:-the-enterkeyhint-attribute>),
/// which also maps onto Android's `IME_ACTION_*` constants and iOS's
/// `UIReturnKeyType`; [`TextInputAction::Unspecified`] means "emit no hint".
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextInputAction {
/// Let the platform choose its default presentation.
#[default]
Unspecified,
/// Inserting a line break.
Enter,
/// Committing the field's value.
Done,
/// Navigating to the typed target.
Go,
/// Moving to the next field.
Next,
/// Moving to the previous field.
Previous,
/// Executing a search.
Search,
/// Sending a message.
Send,
}
/// The variables that can be configured when creating a new window
+13 -2
View File
@@ -2,8 +2,8 @@ use crate::{
AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels,
DispatchEventResult, GpuSpecs, Pixels, PlatformAtlas, PlatformDisplay,
PlatformHeadlessRenderer, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PromptButton, RequestFrameOptions, Scene, Size, TestPlatform, TileId, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams,
PromptButton, RequestFrameOptions, Scene, Size, TestPlatform, TextInputConfiguration, TileId,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams,
};
use collections::HashMap;
use gpui_util::ResultExt as _;
@@ -42,6 +42,7 @@ pub(crate) struct TestWindowState {
frame_scheduled: bool,
frame_callback_pending: bool,
input_handler: Option<PlatformInputHandler>,
text_input_configurations: Vec<TextInputConfiguration>,
is_fullscreen: bool,
appearance: WindowAppearance,
external_drag_files: Vec<(PathBuf, bool)>,
@@ -104,6 +105,7 @@ impl TestWindow {
frame_scheduled: false,
frame_callback_pending: false,
input_handler: None,
text_input_configurations: Vec::new(),
is_fullscreen: false,
appearance: WindowAppearance::Light,
external_drag_files: Vec::new(),
@@ -133,6 +135,11 @@ impl TestWindow {
self.0.lock().frame_scheduled
}
/// Every [`TextInputConfiguration`] forwarded to this window, in order.
pub fn text_input_configurations(&self) -> Vec<TextInputConfiguration> {
self.0.lock().text_input_configurations.clone()
}
pub fn simulate_resize(&mut self, size: Size<Pixels>) {
let scale_factor = self.scale_factor();
let mut lock = self.0.lock();
@@ -258,6 +265,10 @@ impl PlatformWindow for TestWindow {
self.0.lock().input_handler.take()
}
fn set_text_input_configuration(&mut self, configuration: TextInputConfiguration) {
self.0.lock().text_input_configurations.push(configuration);
}
fn prompt(
&self,
_level: crate::PromptLevel,
+31 -5
View File
@@ -18,11 +18,11 @@ use crate::{
RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR,
SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ScaledPixels, Scene, Shadow, SharedString, Size,
StrikethroughStyle, Style, SubpixelSprite, SubscriberSet, Subscription, SystemWindowTab,
SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task, TextRenderingMode, TextStyle,
TextStyleRefinement, ThermalState, TransformationMatrix, Underline, UnderlineStyle,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations,
WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size,
transparent_black,
SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task, TextInputConfiguration,
TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState, TransformationMatrix,
Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, point,
prelude::*, px, rems, size, transparent_black,
};
use anyhow::{Context as _, Result, anyhow};
@@ -1156,6 +1156,10 @@ pub struct Window {
pub(crate) element_opacity: f32,
pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
pub(crate) requested_autoscroll: Option<Bounds<Pixels>>,
/// The [`TextInputConfiguration`] most recently forwarded to the platform
/// window, so that only actual changes are forwarded (reconfiguring a live
/// input session can restart the IME connection).
last_text_input_configuration: Option<TextInputConfiguration>,
pub(crate) image_cache_stack: Vec<AnyImageCache>,
pub(crate) rendered_frame: Frame,
pub(crate) next_frame: Frame,
@@ -1848,6 +1852,7 @@ impl Window {
content_mask_stack: Vec::new(),
element_opacity: 1.0,
requested_autoscroll: None,
last_text_input_configuration: None,
rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
next_frame_callbacks,
@@ -2916,6 +2921,7 @@ impl Window {
{
self.platform_window.set_input_handler(input_handler);
}
self.apply_text_input_configuration(cx);
self.layout_engine.as_mut().unwrap().clear();
self.text_system().finish_frame();
@@ -4840,6 +4846,26 @@ impl Window {
}
}
/// Forwards the focused input handler's [`TextInputConfiguration`] to the
/// platform window when it differs from the last forwarded value. With no
/// input handler the default configuration applies, so a field's
/// preferences don't outlive its focus.
fn apply_text_input_configuration(&mut self, cx: &mut App) {
let configuration = match self.platform_window.take_input_handler() {
Some(mut input_handler) => {
let configuration = input_handler.text_input_configuration(self, cx);
self.platform_window.set_input_handler(input_handler);
configuration
}
None => TextInputConfiguration::default(),
};
if self.last_text_input_configuration.as_ref() != Some(&configuration) {
self.platform_window
.set_text_input_configuration(configuration.clone());
self.last_text_input_configuration = Some(configuration);
}
}
/// Register a mouse event listener on the window for the next frame. The type of event
/// is determined by the first parameter of the given listener. When the next frame is rendered
/// the listener will be cleared.
+85 -11
View File
@@ -196,12 +196,8 @@ impl WebWindowInner {
let pointer_type = event.pointer_type();
let position = pointer_position_in_element(&event);
if pointer_type != "touch" || this.pointer_targets_text_input(position) {
if pointer_type == "touch" {
this.ime_mirror.set_read_only(false);
}
this.ime_mirror.focus();
}
this.gesture_start_visual_viewport_height
.set(this.visual_viewport_height());
// Capture the pointer so drags that leave the canvas keep
// delivering pointermove/pointerup here; otherwise a release
@@ -229,6 +225,17 @@ impl WebWindowInner {
click_count,
first_mouse: false,
}));
// Decide focus after dispatching the MouseDown so text-input
// acceptance reflects the selection produced by this tap rather
// than the previous one. This still runs within the user
// gesture, so focusing here is allowed to show the keyboard.
if pointer_type != "touch" || this.pointer_targets_text_input(position) {
if pointer_type == "touch" {
this.ime_mirror.set_read_only(false);
}
this.ime_mirror.focus();
}
})
}
@@ -268,13 +275,68 @@ impl WebWindowInner {
click_count,
}));
if event.pointer_type() == "touch" {
// A keyboard opening or closing mid-gesture reflows the layout,
// so the release position no longer refers to the content the
// user aimed at (a tap that summoned the keyboard often ends up
// below the shrunken layout, which would immediately dismiss it
// again). Let the pointerdown decision stand instead.
let viewport_stable =
this.gesture_start_visual_viewport_height.get() == this.visual_viewport_height();
if event.pointer_type() == "touch" && viewport_stable {
this.sync_virtual_keyboard(this.pointer_targets_text_input(position));
}
this.schedule_ime_mirror_sync();
})
}
/// The visual viewport's current height in layout pixels, or zero when
/// the API is unavailable.
fn visual_viewport_height(&self) -> f64 {
self.browser_window
.visual_viewport()
.map_or(0.0, |viewport| viewport.height() * viewport.scale())
}
/// Whether the software keyboard is likely hidden — a heuristic, since
/// no cross-browser keyboard-visibility signal exists. It infers from
/// the visual viewport: a shown keyboard shrinks its height well below
/// the greatest height seen at the current width (the width only changes
/// on rotation, which restarts the calibration). `window.innerHeight`
/// can't serve as the reference because Android shrinks it along with
/// the keyboard. Unknown states err toward "visible" so ordinary
/// editable taps don't gratuitously restart the IME session.
///
/// Restricted to coarse-pointer environments: elsewhere (desktop
/// browsers, including touchscreen laptops) viewport height tracks
/// user window resizes rather than a software keyboard, so the
/// calibration would misfire. Split-screen resizes on mobile can still
/// fool it; tracking `visualViewport` resize events around focus
/// transitions would be sturdier.
fn keyboard_likely_dismissed(&self) -> bool {
let coarse_pointer = self
.browser_window
.match_media("(pointer: coarse)")
.ok()
.flatten()
.is_some_and(|media_query_list| media_query_list.matches());
if !coarse_pointer {
return false;
}
let Some(viewport) = self.browser_window.visual_viewport() else {
return false;
};
let width = viewport.width() * viewport.scale();
let height = viewport.height() * viewport.scale();
let (probe_width, probe_height) = self.visual_viewport_probe.get();
let max_height = if width == probe_width {
probe_height.max(height)
} else {
height
};
self.visual_viewport_probe.set((width, max_height));
height >= max_height * 0.85
}
/// Cancels touch default handling separately because iOS does not consistently
/// transfer pointer-event cancellation to the corresponding touch event.
fn register_touch_end(self: &Rc<Self>) -> EventListenerHandle {
@@ -306,12 +368,24 @@ impl WebWindowInner {
fn sync_virtual_keyboard(self: &Rc<Self>, editable: bool) {
let was_editable = !self.ime_mirror.read_only();
self.ime_mirror.set_read_only(!editable);
// Cycle only on an actual editability transition. Cycling on every
// tap would restart the IME connection right as the keyboard reads
// the tapped caret's context, racing its word segmentation.
if editable != was_editable {
// Trigger a focus event only when the keyboard actually needs
// summoning. Cycling focus on every tap would restart the IME
// connection right as the keyboard reads the tapped caret's context,
// racing its word segmentation. But `focus()` on an already-focused
// element is a no-op, so a dismissed keyboard would otherwise never
// return for taps that stay within editable content: detect that
// through the visual viewport and force a fresh focus event.
let editable_needs_focus_event = editable
&& (!was_editable || !self.ime_mirror.is_focused() || self.keyboard_likely_dismissed());
if editable_needs_focus_event || (!editable && was_editable) {
self.suppress_focus_status_events.set(true);
if editable {
// A same-task blur/focus cycle may be coalesced by iOS, but
// this branch only runs when the keyboard is already gone,
// so a coalesced cycle loses nothing.
if self.ime_mirror.is_focused() {
self.ime_mirror.blur();
}
self.ime_mirror.focus();
} else {
self.ime_mirror.blur();
+57 -8
View File
@@ -17,6 +17,7 @@
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use gpui::{Autocapitalize, TextInputAction, TextInputConfiguration};
use wasm_bindgen::JsCast;
use crate::window::WebWindowInner;
@@ -100,23 +101,63 @@ impl ImeMirror {
// whose font is smaller than 16px; with page zoom disabled the user
// can never zoom back out, so keep the hidden IME input at 16px.
style.set_property("font-size", "16px").ok();
// The element is an IME conduit, not a form field: browser-side text
// assistance would mutate it behind the app's back.
element.set_spellcheck(false);
element.set_attribute("autocomplete", "off").ok();
element.set_attribute("autocapitalize", "off").ok();
element.set_attribute("autocorrect", "off").ok();
body.append_child(&element)
.map_err(|e| anyhow::anyhow!("Failed to append input to body: {e:?}"))?;
element.focus().ok();
Ok(Self {
let this = Self {
element,
text: RefCell::new(String::new()),
selection: Cell::new((0, 0)),
window_hint: Cell::new(0),
sync_scheduled: Cell::new(false),
})
};
// Until an input handler asks otherwise, the element is an IME
// conduit, not a form field: browser-side text assistance would
// mutate it behind the app's back.
this.apply_configuration(&TextInputConfiguration::default());
Ok(this)
}
/// Maps a [`TextInputConfiguration`] onto the element's text-assistance
/// attributes. Callers must only invoke this on actual configuration
/// changes (GPUI diffs before forwarding): mutating the focused element
/// can restart the IME's input connection.
pub(crate) fn apply_configuration(&self, configuration: &TextInputConfiguration) {
let element: &web_sys::Element = self.element.as_ref();
self.element.set_spellcheck(configuration.suggestions);
let on_off = |enabled: bool| if enabled { "on" } else { "off" };
element
.set_attribute("autocomplete", on_off(configuration.suggestions))
.ok();
element
.set_attribute("autocorrect", on_off(configuration.autocorrect))
.ok();
element
.set_attribute(
"autocapitalize",
match configuration.autocapitalize {
Autocapitalize::None => "off",
Autocapitalize::Words => "words",
Autocapitalize::Sentences => "sentences",
Autocapitalize::Characters => "characters",
},
)
.ok();
let enter_key_hint = match configuration.input_action {
TextInputAction::Unspecified => None,
TextInputAction::Enter => Some("enter"),
TextInputAction::Done => Some("done"),
TextInputAction::Go => Some("go"),
TextInputAction::Next => Some("next"),
TextInputAction::Previous => Some("previous"),
TextInputAction::Search => Some("search"),
TextInputAction::Send => Some("send"),
};
match enter_key_hint {
Some(hint) => element.set_attribute("enterkeyhint", hint).ok(),
None => element.remove_attribute("enterkeyhint").ok(),
};
}
pub(crate) fn event_target(&self) -> &web_sys::EventTarget {
@@ -127,6 +168,14 @@ impl ImeMirror {
self.element.focus().ok();
}
pub(crate) fn is_focused(&self) -> bool {
let element: &web_sys::Element = self.element.as_ref();
web_sys::window()
.and_then(|window| window.document())
.and_then(|document| document.active_element())
.is_some_and(|active| &active == element)
}
pub(crate) fn blur(&self) {
self.element.blur().ok();
}
+18 -2
View File
@@ -9,8 +9,8 @@ use gpui::{
AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, DispatchEventResult, GpuSpecs,
Modifiers, MouseButton, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
ResizeEdge, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControlArea, WindowControls, WindowDecorations, WindowParams, px,
ResizeEdge, Scene, Size, TextInputConfiguration, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, px,
};
use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig, wgpu};
use wasm_bindgen::prelude::*;
@@ -63,6 +63,16 @@ pub(crate) struct WebWindowInner {
/// synchronously from inside an input dispatch, and a `RefCell`
/// double-borrow panic on wasm never unwinds, wedging the app.
pub(crate) suppress_focus_status_events: Cell<bool>,
/// The visual viewport's width and greatest height seen at that width,
/// in layout pixels. The keyboard-visibility probe compares the current
/// height against this maximum; the width detects rotation, which must
/// restart the calibration.
pub(crate) visual_viewport_probe: Cell<(f64, f64)>,
/// The visual viewport height when the current pointer gesture began.
/// A mid-gesture change means the software keyboard opened or closed and
/// reflowed the layout, so the release position no longer refers to what
/// the user aimed at.
pub(crate) gesture_start_visual_viewport_height: Cell<f64>,
mql_handle: RefCell<Option<MqlHandle>>,
pending_physical_size: Cell<Option<(u32, u32)>>,
raf_id: Cell<Option<i32>>,
@@ -185,6 +195,8 @@ impl WebWindow {
notify_scale: Cell::new(false),
is_composing: Cell::new(false),
suppress_focus_status_events: Cell::new(false),
visual_viewport_probe: Cell::new((0.0, 0.0)),
gesture_start_visual_viewport_height: Cell::new(0.0),
mql_handle: RefCell::new(None),
pending_physical_size: Cell::new(None),
raf_id: Cell::new(None),
@@ -650,6 +662,10 @@ impl PlatformWindow for WebWindow {
self.inner.state.borrow_mut().input_handler.take()
}
fn set_text_input_configuration(&mut self, configuration: TextInputConfiguration) {
self.inner.ime_mirror.apply_configuration(&configuration);
}
fn prompt(
&self,
_level: PromptLevel,