winit-core/window: add Window::request_ime_update

Allow updating IME state atomically to make it easier for platforms
where it's atomic by its nature, like Wayland. The old API is marked
as deprecated and is routed to the new atomic API.

Co-authored-by: dcz <gilapfco.dcz@porcupinefactory.org>
This commit is contained in:
DorotaC
2025-06-28 06:14:20 +02:00
committed by GitHub
parent fa0795a50c
commit 08907148ec
19 changed files with 866 additions and 222 deletions

View File

@@ -26,9 +26,11 @@ use keyboard::{KeyboardData, KeyboardState};
pub use pointer::relative_pointer::RelativePointerState;
pub use pointer::{PointerConstraintsState, WinitPointerData, WinitPointerDataExt};
use text_input::TextInputData;
pub use text_input::{TextInputState, ZwpTextInputV3Ext};
pub use text_input::{ClientState as TextInputClientState, TextInputState};
use touch::TouchPoint;
pub(crate) use crate::seat::text_input::ZwpTextInputV3Ext;
#[derive(Debug, Default)]
pub struct WinitSeatState {
/// The pointer bound on the seat.

View File

@@ -1,5 +1,6 @@
use std::ops::Deref;
use dpi::{LogicalPosition, LogicalSize};
use sctk::globals::GlobalData;
use sctk::reexports::client::globals::{BindError, GlobalList};
use sctk::reexports::client::protocol::wl_surface::WlSurface;
@@ -8,8 +9,9 @@ use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_mana
use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::{
ContentHint, ContentPurpose, Event as TextInputEvent, ZwpTextInputV3,
};
use tracing::warn;
use winit_core::event::{Ime, WindowEvent};
use winit_core::window::ImePurpose;
use winit_core::window::{ImeCapabilities, ImePurpose, ImeRequestData};
use crate::state::WinitState;
@@ -69,10 +71,10 @@ impl Dispatch<ZwpTextInputV3, TextInputData, WinitState> for TextInputState {
None => return,
};
if window.ime_allowed() {
text_input.enable();
text_input.set_content_type_by_purpose(window.ime_purpose());
text_input.commit();
if let Some(text_input_state) = window.text_input_state() {
text_input.set_state(Some(text_input_state), true);
// The input method doesn't have to reply anything, so a synthetic event
// carrying an empty state notifies the application about its presence.
state.events_sink.push_window_event(WindowEvent::Ime(Ime::Enabled), window_id);
}
@@ -156,17 +158,40 @@ impl Dispatch<ZwpTextInputV3, TextInputData, WinitState> for TextInputState {
}
pub trait ZwpTextInputV3Ext {
fn set_content_type_by_purpose(&self, purpose: ImePurpose);
/// Applies the entire state atomically to the input method. It will skip the "enable" request
/// if `already_enabled` is `true`.
fn set_state(&self, state: Option<&ClientState>, send_enable: bool);
}
impl ZwpTextInputV3Ext for ZwpTextInputV3 {
fn set_content_type_by_purpose(&self, purpose: ImePurpose) {
let (hint, purpose) = match purpose {
ImePurpose::Password => (ContentHint::SensitiveData, ContentPurpose::Password),
ImePurpose::Terminal => (ContentHint::None, ContentPurpose::Terminal),
_ => (ContentHint::None, ContentPurpose::Normal),
fn set_state(&self, state: Option<&ClientState>, send_enable: bool) {
let state = match state {
Some(state) => state,
None => {
self.disable();
self.commit();
return;
},
};
self.set_content_type(hint, purpose);
if send_enable {
self.enable();
}
if let Some(content_type) = state.content_type() {
self.set_content_type(content_type.hint, content_type.purpose);
}
if let Some((position, size)) = state.cursor_area() {
let (x, y) = (position.x as i32, position.y as i32);
let (width, height) = (size.width as i32, size.height as i32);
// The same cursor can be applied on different seats.
// It's the compositor's responsibility to make sure that any present popups don't
// overlap.
self.set_cursor_rectangle(x, y, width, height);
}
self.commit();
}
}
@@ -189,11 +214,105 @@ pub struct TextInputDataInner {
}
/// The state of the preedit.
#[derive(Clone)]
struct Preedit {
text: String,
cursor_begin: Option<usize>,
cursor_end: Option<usize>,
}
/// State change requested by the application.
///
/// This is a version that uses text_input abstractions translated from the ones used in
/// winit::core::window::ImeStateChange.
///
/// Fields that are initially set to None are unsupported capabilities
/// and trying to set them raises an error.
#[derive(Debug, PartialEq, Clone)]
pub struct ClientState {
capabilities: ImeCapabilities,
content_type: ContentType,
/// The IME cursor area which should not be covered by the input method popup.
cursor_area: (LogicalPosition<u32>, LogicalSize<u32>),
}
impl ClientState {
pub fn new(
capabilities: ImeCapabilities,
request_data: ImeRequestData,
scale_factor: f64,
) -> Self {
let mut this = Self {
capabilities,
content_type: Default::default(),
cursor_area: Default::default(),
};
this.update(request_data, scale_factor);
this
}
pub fn capabilities(&self) -> ImeCapabilities {
self.capabilities
}
/// Updates the fields of the state which are present in update_fields.
pub fn update(&mut self, request_data: ImeRequestData, scale_factor: f64) {
if let Some(purpose) = request_data.purpose {
if self.capabilities.contains(ImeCapabilities::PURPOSE) {
self.content_type = purpose.into();
} else {
warn!("discarding ImePurpose update without capability enabled.");
}
}
if let Some((position, size)) = request_data.cursor_area {
if self.capabilities.contains(ImeCapabilities::CURSOR_AREA) {
let position: LogicalPosition<u32> = position.to_logical(scale_factor);
let size: LogicalSize<u32> = size.to_logical(scale_factor);
self.cursor_area = (position, size);
} else {
warn!("discarding IME cursor area update without capability enabled.");
}
}
}
pub fn content_type(&self) -> Option<ContentType> {
self.capabilities.contains(ImeCapabilities::PURPOSE).then_some(self.content_type)
}
pub fn cursor_area(&self) -> Option<(LogicalPosition<u32>, LogicalSize<u32>)> {
self.capabilities.contains(ImeCapabilities::CURSOR_AREA).then_some(self.cursor_area)
}
}
/// Arguments to content_type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContentType {
/// Text input purpose
purpose: ContentPurpose,
hint: ContentHint,
}
impl From<ImePurpose> for ContentType {
fn from(purpose: ImePurpose) -> Self {
let (hint, purpose) = match purpose {
ImePurpose::Password => (ContentHint::SensitiveData, ContentPurpose::Password),
ImePurpose::Terminal => (ContentHint::None, ContentPurpose::Terminal),
_ => return Default::default(),
};
Self { hint, purpose }
}
}
impl Default for ContentType {
fn default() -> Self {
ContentType { purpose: ContentPurpose::Normal, hint: ContentHint::None }
}
}
delegate_dispatch!(WinitState: [ZwpTextInputManagerV3: GlobalData] => TextInputState);
delegate_dispatch!(WinitState: [ZwpTextInputV3: TextInputData] => TextInputState);