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:
parent
fa0795a50c
commit
08907148ec
19 changed files with 866 additions and 222 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ use winit_core::event::{Ime, WindowEvent};
|
|||
use winit_core::event_loop::AsyncRequestSerial;
|
||||
use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle};
|
||||
use winit_core::window::{
|
||||
CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, Window as CoreWindow,
|
||||
WindowAttributes, WindowButtons, WindowId, WindowLevel,
|
||||
CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme,
|
||||
UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId,
|
||||
WindowLevel,
|
||||
};
|
||||
|
||||
use super::event_loop::sink::EventSink;
|
||||
|
|
@ -508,30 +509,21 @@ impl CoreWindow for Window {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
fn set_ime_cursor_area(&self, position: Position, size: Size) {
|
||||
let window_state = self.window_state.lock().unwrap();
|
||||
if window_state.ime_allowed() {
|
||||
let scale_factor = window_state.scale_factor();
|
||||
let position = position.to_logical(scale_factor);
|
||||
let size = size.to_logical(scale_factor);
|
||||
window_state.set_ime_cursor_area(position, size);
|
||||
}
|
||||
}
|
||||
fn request_ime_update(&self, request: ImeRequest) -> Result<(), ImeRequestError> {
|
||||
let state_changed = self.window_state.lock().unwrap().request_ime_update(request)?;
|
||||
|
||||
#[inline]
|
||||
fn set_ime_allowed(&self, allowed: bool) {
|
||||
let mut window_state = self.window_state.lock().unwrap();
|
||||
|
||||
if window_state.ime_allowed() != allowed && window_state.set_ime_allowed(allowed) {
|
||||
if let Some(allowed) = state_changed {
|
||||
let event = WindowEvent::Ime(if allowed { Ime::Enabled } else { Ime::Disabled });
|
||||
self.window_events_sink.lock().unwrap().push_window_event(event, self.window_id);
|
||||
self.event_loop_awakener.ping();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_ime_purpose(&self, purpose: ImePurpose) {
|
||||
self.window_state.lock().unwrap().set_ime_purpose(purpose);
|
||||
fn ime_capabilities(&self) -> Option<ImeCapabilities> {
|
||||
self.window_state.lock().unwrap().ime_allowed()
|
||||
}
|
||||
|
||||
fn focus_window(&self) {}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,15 @@ use wayland_protocols::xdg::toplevel_icon::v1::client::xdg_toplevel_icon_manager
|
|||
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur;
|
||||
use winit_core::cursor::{CursorIcon, CustomCursor as CoreCustomCursor};
|
||||
use winit_core::error::{NotSupportedError, RequestError};
|
||||
use winit_core::window::{CursorGrabMode, ImePurpose, ResizeDirection, Theme, WindowId};
|
||||
use winit_core::window::{
|
||||
CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, WindowId,
|
||||
};
|
||||
|
||||
use crate::event_loop::OwnedDisplayHandle;
|
||||
use crate::logical_to_physical_rounded;
|
||||
use crate::seat::{
|
||||
PointerConstraintsState, WinitPointerData, WinitPointerDataExt, ZwpTextInputV3Ext,
|
||||
PointerConstraintsState, TextInputClientState, WinitPointerData, WinitPointerDataExt,
|
||||
ZwpTextInputV3Ext,
|
||||
};
|
||||
use crate::state::{WindowCompositorUpdate, WinitState};
|
||||
use crate::types::cursor::{CustomCursor, SelectedCursor, WaylandCustomCursor};
|
||||
|
|
@ -113,11 +116,11 @@ pub struct WindowState {
|
|||
/// The current cursor grabbing mode.
|
||||
cursor_grab_mode: GrabState,
|
||||
|
||||
/// Whether the IME input is allowed for that window.
|
||||
ime_allowed: bool,
|
||||
|
||||
/// The current IME purpose.
|
||||
ime_purpose: ImePurpose,
|
||||
/// The input method properties provided by the application to the IME.
|
||||
///
|
||||
/// This state is cached here so that the window can automatically send the state to the IME as
|
||||
/// soon as it becomes available without application involvement.
|
||||
text_input_state: Option<TextInputClientState>,
|
||||
|
||||
/// The text inputs observed on the window.
|
||||
text_inputs: Vec<ZwpTextInputV3>,
|
||||
|
|
@ -211,8 +214,7 @@ impl WindowState {
|
|||
frame_callback_state: FrameCallbackState::None,
|
||||
seat_focus: Default::default(),
|
||||
has_pending_move: None,
|
||||
ime_allowed: false,
|
||||
ime_purpose: ImePurpose::Normal,
|
||||
text_input_state: None,
|
||||
last_configure: None,
|
||||
max_surface_size: None,
|
||||
min_surface_size: MIN_WINDOW_SIZE,
|
||||
|
|
@ -544,8 +546,12 @@ impl WindowState {
|
|||
|
||||
/// Whether the IME is allowed.
|
||||
#[inline]
|
||||
pub fn ime_allowed(&self) -> bool {
|
||||
self.ime_allowed
|
||||
pub fn ime_allowed(&self) -> Option<ImeCapabilities> {
|
||||
self.text_input_state.as_ref().map(|state| state.capabilities())
|
||||
}
|
||||
|
||||
pub(crate) fn text_input_state(&self) -> Option<&TextInputClientState> {
|
||||
self.text_input_state.as_ref()
|
||||
}
|
||||
|
||||
/// Get the size of the window.
|
||||
|
|
@ -983,53 +989,61 @@ impl WindowState {
|
|||
self.seat_focus.remove(seat);
|
||||
}
|
||||
|
||||
/// Returns `true` if the requested state was applied.
|
||||
pub fn set_ime_allowed(&mut self, allowed: bool) -> bool {
|
||||
self.ime_allowed = allowed;
|
||||
/// Atomically update input method state.
|
||||
///
|
||||
/// Returns `None` if an input method state haven't changed. Alternatively `Some(true)` and
|
||||
/// `Some(false)` is returned respectfully.
|
||||
pub fn request_ime_update(
|
||||
&mut self,
|
||||
request: ImeRequest,
|
||||
) -> Result<Option<bool>, ImeRequestError> {
|
||||
let state_change = match request {
|
||||
ImeRequest::Enable(enable) => {
|
||||
let (capabilities, request_data) = enable.into_raw();
|
||||
|
||||
let mut applied = false;
|
||||
if self.text_input_state.is_some() {
|
||||
return Err(ImeRequestError::AlreadyEnabled);
|
||||
}
|
||||
|
||||
self.text_input_state = Some(TextInputClientState::new(
|
||||
capabilities,
|
||||
request_data,
|
||||
self.scale_factor(),
|
||||
));
|
||||
true
|
||||
},
|
||||
ImeRequest::Update(request_data) => {
|
||||
let scale_factor = self.scale_factor();
|
||||
if let Some(text_input_state) = self.text_input_state.as_mut() {
|
||||
text_input_state.update(request_data, scale_factor);
|
||||
} else {
|
||||
return Err(ImeRequestError::NotEnabled);
|
||||
}
|
||||
false
|
||||
},
|
||||
ImeRequest::Disable => {
|
||||
self.text_input_state = None;
|
||||
true
|
||||
},
|
||||
};
|
||||
|
||||
// Only one input method may be active per (seat, surface),
|
||||
// but there may be multiple seats focused on a surface,
|
||||
// resulting in multiple text input objects.
|
||||
//
|
||||
// WARNING: this doesn't actually handle different seats with independent cursors. There's
|
||||
// no API to set a per-seat input method state, so they all share a single state.
|
||||
for text_input in &self.text_inputs {
|
||||
applied = true;
|
||||
if allowed {
|
||||
text_input.enable();
|
||||
text_input.set_content_type_by_purpose(self.ime_purpose);
|
||||
} else {
|
||||
text_input.disable();
|
||||
}
|
||||
text_input.commit();
|
||||
text_input.set_state(self.text_input_state.as_ref(), state_change);
|
||||
}
|
||||
|
||||
applied
|
||||
}
|
||||
|
||||
/// Set the IME position.
|
||||
pub fn set_ime_cursor_area(&self, position: LogicalPosition<u32>, size: LogicalSize<u32>) {
|
||||
// FIXME: This won't fly unless user will have a way to request IME window per seat, since
|
||||
// the ime windows will be overlapping, but winit doesn't expose API to specify for
|
||||
// which seat we're setting IME position.
|
||||
let (x, y) = (position.x as i32, position.y as i32);
|
||||
let (width, height) = (size.width as i32, size.height as i32);
|
||||
for text_input in self.text_inputs.iter() {
|
||||
text_input.set_cursor_rectangle(x, y, width, height);
|
||||
text_input.commit();
|
||||
if state_change {
|
||||
Ok(Some(self.text_input_state.is_some()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the IME purpose.
|
||||
pub fn set_ime_purpose(&mut self, purpose: ImePurpose) {
|
||||
self.ime_purpose = purpose;
|
||||
|
||||
for text_input in &self.text_inputs {
|
||||
text_input.set_content_type_by_purpose(purpose);
|
||||
text_input.commit();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the IME purpose.
|
||||
pub fn ime_purpose(&self) -> ImePurpose {
|
||||
self.ime_purpose
|
||||
}
|
||||
|
||||
/// Set the scale factor for the given window.
|
||||
#[inline]
|
||||
pub fn set_scale_factor(&mut self, scale_factor: f64) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue