Implement IME-related methods of SctkWinitWindow for apps using single-instance feature
- At least, following apps are using `single-instance` feature: - cosmic-launcher - cosmic-app-library - Once libcosmic text widgets supported the IME, these apps start hitting following methods of SctkWinitWindow which is `todo!()` and will crash at startup: - `set_ime_cursor_area()` - `set_ime_allowed()` - `set_ime_purpose()` - So, this PR implements these method utilizing following wayland protocols. - zwp_text_input_v3 - zwp_text_input_manager_v3
This commit is contained in:
parent
e4da5002ae
commit
d0ef1f9e85
7 changed files with 217 additions and 7 deletions
|
|
@ -7,7 +7,7 @@ use crate::platform_specific::SurfaceIdWrapper;
|
||||||
use crate::{
|
use crate::{
|
||||||
Control,
|
Control,
|
||||||
futures::futures::channel::mpsc,
|
futures::futures::channel::mpsc,
|
||||||
handlers::overlap::OverlapNotifyV1,
|
handlers::{overlap::OverlapNotifyV1, text_input::TextInputManager},
|
||||||
platform_specific::wayland::{
|
platform_specific::wayland::{
|
||||||
handlers::{
|
handlers::{
|
||||||
wp_fractional_scaling::FractionalScalingManager,
|
wp_fractional_scaling::FractionalScalingManager,
|
||||||
|
|
@ -52,8 +52,8 @@ use std::{
|
||||||
use log::error;
|
use log::error;
|
||||||
use wayland_backend::client::Backend;
|
use wayland_backend::client::Backend;
|
||||||
use wayland_client::globals::GlobalError;
|
use wayland_client::globals::GlobalError;
|
||||||
use wayland_protocols::wp::keyboard_shortcuts_inhibit::zv1::client::zwp_keyboard_shortcuts_inhibit_manager_v1;
|
use wayland_protocols::wp::{keyboard_shortcuts_inhibit::zv1::client::zwp_keyboard_shortcuts_inhibit_manager_v1, text_input::zv3::client::zwp_text_input_v3::{ContentHint, ContentPurpose}};
|
||||||
use winit::{dpi::LogicalSize, event_loop::OwnedDisplayHandle};
|
use winit::{dpi::LogicalSize, event_loop::OwnedDisplayHandle, window::ImePurpose};
|
||||||
|
|
||||||
use self::state::SctkState;
|
use self::state::SctkState;
|
||||||
|
|
||||||
|
|
@ -197,6 +197,33 @@ impl SctkEventLoop {
|
||||||
crate::platform_specific::Action::Dropped(id) => {
|
crate::platform_specific::Action::Dropped(id) => {
|
||||||
_ = state.destroyed.remove(&id.inner());
|
_ = state.destroyed.remove(&id.inner());
|
||||||
}
|
}
|
||||||
|
crate::platform_specific::Action::SetImeAllowed(allowed) => {
|
||||||
|
if let Some(text_input) = state.text_input.as_ref() {
|
||||||
|
if allowed {
|
||||||
|
text_input.enable();
|
||||||
|
} else {
|
||||||
|
text_input.disable();
|
||||||
|
}
|
||||||
|
text_input.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::platform_specific::Action::SetImeCursorArea(x, y, width, height) => {
|
||||||
|
if let Some(text_input) = state.text_input.as_ref() {
|
||||||
|
text_input.set_cursor_rectangle(x, y, width, height);
|
||||||
|
text_input.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::platform_specific::Action::SetImePurpose(purpose) => {
|
||||||
|
if let Some(text_input) = state.text_input.as_ref() {
|
||||||
|
let (hint, purpose) = match purpose {
|
||||||
|
ImePurpose::Password => (ContentHint::SensitiveData, ContentPurpose::Password),
|
||||||
|
ImePurpose::Terminal => (ContentHint::None, ContentPurpose::Terminal),
|
||||||
|
_ => (ContentHint::None, ContentPurpose::Normal),
|
||||||
|
};
|
||||||
|
text_input.set_content_type(hint, purpose);
|
||||||
|
text_input.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
crate::platform_specific::Action::SubsurfaceResize(id, size) => {
|
crate::platform_specific::Action::SubsurfaceResize(id, size) => {
|
||||||
// reposition the surface
|
// reposition the surface
|
||||||
if let Some(pos) = state
|
if let Some(pos) = state
|
||||||
|
|
@ -332,6 +359,7 @@ impl SctkEventLoop {
|
||||||
1..=1,
|
1..=1,
|
||||||
(),
|
(),
|
||||||
).ok(),
|
).ok(),
|
||||||
|
text_input_manager: TextInputManager::try_new(®istry_state, &qh),
|
||||||
registry_state,
|
registry_state,
|
||||||
|
|
||||||
queue_handle: qh,
|
queue_handle: qh,
|
||||||
|
|
@ -366,6 +394,9 @@ impl SctkEventLoop {
|
||||||
overlap_notifications: HashMap::new(),
|
overlap_notifications: HashMap::new(),
|
||||||
subsurface_state: None,
|
subsurface_state: None,
|
||||||
pending_corner_radius: HashMap::new(),
|
pending_corner_radius: HashMap::new(),
|
||||||
|
text_input: None,
|
||||||
|
preedit: None,
|
||||||
|
pending_commit: None,
|
||||||
},
|
},
|
||||||
_features: Default::default(),
|
_features: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use crate::{
|
||||||
handlers::{
|
handlers::{
|
||||||
activation::IcedRequestData,
|
activation::IcedRequestData,
|
||||||
overlap::{OverlapNotificationV1, OverlapNotifyV1},
|
overlap::{OverlapNotificationV1, OverlapNotifyV1},
|
||||||
|
text_input::{Preedit, TextInputManager},
|
||||||
},
|
},
|
||||||
platform_specific::{
|
platform_specific::{
|
||||||
Event,
|
Event,
|
||||||
|
|
@ -113,6 +114,7 @@ use wayland_protocols::{
|
||||||
zwp_keyboard_shortcuts_inhibit_manager_v1,
|
zwp_keyboard_shortcuts_inhibit_manager_v1,
|
||||||
zwp_keyboard_shortcuts_inhibitor_v1,
|
zwp_keyboard_shortcuts_inhibitor_v1,
|
||||||
},
|
},
|
||||||
|
text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3,
|
||||||
viewporter::client::wp_viewport::WpViewport,
|
viewporter::client::wp_viewport::WpViewport,
|
||||||
},
|
},
|
||||||
xdg::shell::client::{xdg_surface::XdgSurface, xdg_toplevel::XdgToplevel},
|
xdg::shell::client::{xdg_surface::XdgSurface, xdg_toplevel::XdgToplevel},
|
||||||
|
|
@ -492,7 +494,12 @@ pub struct SctkState {
|
||||||
pub(crate) inhibitor_manager: Option<zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1>,
|
pub(crate) inhibitor_manager: Option<zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1>,
|
||||||
|
|
||||||
pub(crate) corner_radius_manager: Option<CosmicCornerRadiusManagerV1>,
|
pub(crate) corner_radius_manager: Option<CosmicCornerRadiusManagerV1>,
|
||||||
pub(crate) pending_corner_radius: HashMap<core::window::Id, CornerRadius>
|
pub(crate) pending_corner_radius: HashMap<core::window::Id, CornerRadius>,
|
||||||
|
|
||||||
|
pub(crate) text_input_manager: Option<TextInputManager>,
|
||||||
|
pub(crate) text_input: Option<Arc<ZwpTextInputV3>>,
|
||||||
|
pub(crate) preedit: Option<Preedit>,
|
||||||
|
pub(crate) pending_commit: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error that occurred while running an application.
|
/// An error that occurred while running an application.
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ pub mod seat;
|
||||||
pub mod session_lock;
|
pub mod session_lock;
|
||||||
pub mod shell;
|
pub mod shell;
|
||||||
pub mod subcompositor;
|
pub mod subcompositor;
|
||||||
|
pub mod text_input;
|
||||||
pub mod toplevel;
|
pub mod toplevel;
|
||||||
pub mod wp_fractional_scaling;
|
pub mod wp_fractional_scaling;
|
||||||
pub mod wp_viewporter;
|
pub mod wp_viewporter;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use cctk::sctk::{
|
||||||
seat::{pointer::ThemeSpec, SeatHandler},
|
seat::{pointer::ThemeSpec, SeatHandler},
|
||||||
};
|
};
|
||||||
use iced_runtime::keyboard::Modifiers;
|
use iced_runtime::keyboard::Modifiers;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
impl SeatHandler for SctkState {
|
impl SeatHandler for SctkState {
|
||||||
fn seat_state(&mut self) -> &mut cctk::sctk::seat::SeatState {
|
fn seat_state(&mut self) -> &mut cctk::sctk::seat::SeatState {
|
||||||
|
|
@ -141,6 +142,16 @@ impl SeatHandler for SctkState {
|
||||||
}
|
}
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(text_input_manager) = self
|
||||||
|
.text_input
|
||||||
|
.is_none()
|
||||||
|
.then_some(self.text_input_manager.as_ref())
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
self.text_input =
|
||||||
|
Some(Arc::new((text_input_manager).get_text_input(&seat, &qh)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_capability(
|
fn remove_capability(
|
||||||
|
|
@ -193,6 +204,10 @@ impl SeatHandler for SctkState {
|
||||||
}
|
}
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(text_input) = self.text_input.take() {
|
||||||
|
text_input.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_seat(
|
fn remove_seat(
|
||||||
|
|
|
||||||
130
winit/src/platform_specific/wayland/handlers/text_input.rs
Normal file
130
winit/src/platform_specific/wayland/handlers/text_input.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
use cctk::sctk::globals::GlobalData;
|
||||||
|
use cctk::sctk::reexports::client::{Connection, Proxy, QueueHandle};
|
||||||
|
|
||||||
|
use cctk::sctk::reexports::client::delegate_dispatch;
|
||||||
|
use cctk::sctk::reexports::client::Dispatch;
|
||||||
|
use cctk::sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3;
|
||||||
|
use cctk::sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::Event as TextInputEvent;
|
||||||
|
use cctk::sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3;
|
||||||
|
use cctk::sctk::registry::RegistryState;
|
||||||
|
use wayland_client::protocol::wl_seat::WlSeat;
|
||||||
|
use winit::event::{Ime, WindowEvent};
|
||||||
|
use winit::window::WindowId;
|
||||||
|
|
||||||
|
use crate::event_loop::state::SctkState;
|
||||||
|
use crate::sctk_event::SctkEvent;
|
||||||
|
|
||||||
|
pub struct Preedit {
|
||||||
|
text: String,
|
||||||
|
cursor_range: Option<(usize, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TextInputManager {
|
||||||
|
manager: ZwpTextInputManagerV3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextInputManager {
|
||||||
|
pub fn try_new<D>(
|
||||||
|
registry: &RegistryState,
|
||||||
|
qh: &QueueHandle<D>,
|
||||||
|
) -> Option<Self>
|
||||||
|
where
|
||||||
|
D: Dispatch<ZwpTextInputManagerV3, GlobalData> + 'static,
|
||||||
|
{
|
||||||
|
let manager = registry
|
||||||
|
.bind_one::<ZwpTextInputManagerV3, _, _>(qh, 1..=1, GlobalData)
|
||||||
|
.ok()?;
|
||||||
|
Some(Self { manager })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_text_input(
|
||||||
|
&self,
|
||||||
|
seat: &WlSeat,
|
||||||
|
qh: &QueueHandle<SctkState>,
|
||||||
|
) -> ZwpTextInputV3 {
|
||||||
|
self.manager.get_text_input(&seat, &qh, ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<ZwpTextInputManagerV3, GlobalData, SctkState>
|
||||||
|
for TextInputManager
|
||||||
|
{
|
||||||
|
fn event(
|
||||||
|
_state: &mut SctkState,
|
||||||
|
_proxy: &ZwpTextInputManagerV3,
|
||||||
|
_event: <ZwpTextInputManagerV3 as Proxy>::Event,
|
||||||
|
_data: &GlobalData,
|
||||||
|
_conn: &Connection,
|
||||||
|
_qhandle: &QueueHandle<SctkState>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<ZwpTextInputV3, (), SctkState> for TextInputManager {
|
||||||
|
fn event(
|
||||||
|
state: &mut SctkState,
|
||||||
|
_text_input: &ZwpTextInputV3,
|
||||||
|
event: <ZwpTextInputV3 as Proxy>::Event,
|
||||||
|
_data: &(),
|
||||||
|
_conn: &Connection,
|
||||||
|
_qhandle: &QueueHandle<SctkState>,
|
||||||
|
) {
|
||||||
|
let kbd_focus =
|
||||||
|
match state.seats.iter_mut().find_map(|s| s.kbd_focus.clone()) {
|
||||||
|
Some(surface) => surface,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
match event {
|
||||||
|
TextInputEvent::PreeditString {
|
||||||
|
text,
|
||||||
|
cursor_begin,
|
||||||
|
cursor_end,
|
||||||
|
} => {
|
||||||
|
let text = text.unwrap_or_default();
|
||||||
|
let cursor_begin = usize::try_from(cursor_begin)
|
||||||
|
.ok()
|
||||||
|
.and_then(|idx| text.is_char_boundary(idx).then_some(idx));
|
||||||
|
let cursor_end = usize::try_from(cursor_end)
|
||||||
|
.ok()
|
||||||
|
.and_then(|idx| text.is_char_boundary(idx).then_some(idx));
|
||||||
|
let cursor_range =
|
||||||
|
cursor_begin.map(|b| (b, cursor_end.unwrap_or(b)));
|
||||||
|
state.preedit = Some(Preedit { text, cursor_range });
|
||||||
|
}
|
||||||
|
TextInputEvent::CommitString { text } => {
|
||||||
|
state.preedit = None;
|
||||||
|
state.pending_commit = text;
|
||||||
|
}
|
||||||
|
TextInputEvent::Done { .. } => {
|
||||||
|
let id = WindowId::from_raw(kbd_focus.id().as_ptr() as usize);
|
||||||
|
state.sctk_events.push(SctkEvent::Winit(
|
||||||
|
id,
|
||||||
|
WindowEvent::Ime(Ime::Preedit(String::new(), None)),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Commit string
|
||||||
|
if let Some(text) = state.pending_commit.take() {
|
||||||
|
state.sctk_events.push(SctkEvent::Winit(
|
||||||
|
id,
|
||||||
|
WindowEvent::Ime(Ime::Commit(text)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update preedit string
|
||||||
|
if let Some(preedit) = state.preedit.take() {
|
||||||
|
state.sctk_events.push(SctkEvent::Winit(
|
||||||
|
id,
|
||||||
|
WindowEvent::Ime(Ime::Preedit(
|
||||||
|
preedit.text,
|
||||||
|
preedit.cursor_range,
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate_dispatch!(SctkState: [ZwpTextInputManagerV3: GlobalData] => TextInputManager);
|
||||||
|
delegate_dispatch!(SctkState: [ZwpTextInputV3: ()] => TextInputManager);
|
||||||
|
|
@ -27,6 +27,7 @@ use wayland_backend::client::ObjectId;
|
||||||
use wayland_client::{Connection, Proxy};
|
use wayland_client::{Connection, Proxy};
|
||||||
use winit::dpi::Size;
|
use winit::dpi::Size;
|
||||||
use winit::event_loop::OwnedDisplayHandle;
|
use winit::event_loop::OwnedDisplayHandle;
|
||||||
|
use winit::window::ImePurpose;
|
||||||
|
|
||||||
pub(crate) enum Action {
|
pub(crate) enum Action {
|
||||||
Action(iced_runtime::platform_specific::wayland::Action),
|
Action(iced_runtime::platform_specific::wayland::Action),
|
||||||
|
|
@ -37,6 +38,9 @@ pub(crate) enum Action {
|
||||||
RemoveWindow(window::Id),
|
RemoveWindow(window::Id),
|
||||||
Dropped(SurfaceIdWrapper),
|
Dropped(SurfaceIdWrapper),
|
||||||
SubsurfaceResize(window::Id, Size),
|
SubsurfaceResize(window::Id, Size),
|
||||||
|
SetImeAllowed(bool),
|
||||||
|
SetImeCursorArea(i32, i32, i32, i32),
|
||||||
|
SetImePurpose(ImePurpose),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Action {
|
impl std::fmt::Debug for Action {
|
||||||
|
|
@ -64,6 +68,19 @@ impl std::fmt::Debug for Action {
|
||||||
Self::ResizeWindow(arg0) => {
|
Self::ResizeWindow(arg0) => {
|
||||||
f.debug_tuple("ResizeWindow").field(arg0).finish()
|
f.debug_tuple("ResizeWindow").field(arg0).finish()
|
||||||
}
|
}
|
||||||
|
Self::SetImeAllowed(allowed) => {
|
||||||
|
f.debug_tuple("SetImeAllowed").field(allowed).finish()
|
||||||
|
}
|
||||||
|
Self::SetImeCursorArea(x, y, width, height) => f
|
||||||
|
.debug_tuple("SetImeCursorArea")
|
||||||
|
.field(x)
|
||||||
|
.field(y)
|
||||||
|
.field(width)
|
||||||
|
.field(height)
|
||||||
|
.finish(),
|
||||||
|
Self::SetImePurpose(purpose) => {
|
||||||
|
f.debug_tuple("SetImePurpose").field(purpose).finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -266,15 +266,24 @@ impl winit::window::Window for SctkWinitWindow {
|
||||||
position: winit::dpi::Position,
|
position: winit::dpi::Position,
|
||||||
size: winit::dpi::Size,
|
size: winit::dpi::Size,
|
||||||
) {
|
) {
|
||||||
todo!()
|
let guard = self.common.lock().unwrap();
|
||||||
|
let scale_factor = guard.fractional_scale.unwrap_or(1.);
|
||||||
|
let position = position.to_logical(scale_factor);
|
||||||
|
let size = size.to_logical(scale_factor);
|
||||||
|
_ = self.tx.send(Action::SetImeCursorArea(
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
size.width,
|
||||||
|
size.height,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_ime_allowed(&self, allowed: bool) {
|
fn set_ime_allowed(&self, allowed: bool) {
|
||||||
todo!()
|
_ = self.tx.send(Action::SetImeAllowed(allowed));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_ime_purpose(&self, purpose: winit::window::ImePurpose) {
|
fn set_ime_purpose(&self, purpose: winit::window::ImePurpose) {
|
||||||
todo!()
|
_ = self.tx.send(Action::SetImePurpose(purpose));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_blur(&self, blur: bool) {
|
fn set_blur(&self, blur: bool) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue