diff --git a/wezterm-gui/src/scripting/guiwin.rs b/wezterm-gui/src/scripting/guiwin.rs index 937098474e0f..fee6a8f95c1a 100644 --- a/wezterm-gui/src/scripting/guiwin.rs +++ b/wezterm-gui/src/scripting/guiwin.rs @@ -231,7 +231,7 @@ impl UserData for GuiWin { .notify(TermWindowNotif::Apply(Box::new(move |term_window| { tx.try_send(match term_window.composition_status() { DeadKeyStatus::None => None, - DeadKeyStatus::Composing(s) => Some(s.clone()), + DeadKeyStatus::Composing(s, ..) => Some(s.clone()), }) .ok(); }))); diff --git a/wezterm-gui/src/termwindow/render/mod.rs b/wezterm-gui/src/termwindow/render/mod.rs index b4266e9f2e68..7eb97a694211 100644 --- a/wezterm-gui/src/termwindow/render/mod.rs +++ b/wezterm-gui/src/termwindow/render/mod.rs @@ -57,6 +57,7 @@ pub struct LineQuadCacheKey { pub quad_generation: usize, /// Only set if cursor.y == stable_row pub composing: Option, + pub composing_selection: Option>, pub selection: Range, pub shape_hash: [u8; 16], pub top_pixel_y: NotNan, diff --git a/wezterm-gui/src/termwindow/render/screen_line.rs b/wezterm-gui/src/termwindow/render/screen_line.rs index b7d4daa4f2ce..09f463f304b9 100644 --- a/wezterm-gui/src/termwindow/render/screen_line.rs +++ b/wezterm-gui/src/termwindow/render/screen_line.rs @@ -75,16 +75,19 @@ impl crate::TermWindow { }; // Referencing the text being composed, but only if it belongs to this pane - let composing = if cursor_idx.is_some() { - if let DeadKeyStatus::Composing(composing) = &self.dead_key_status { - Some(composing) + let (composing, selected_range) = if cursor_idx.is_some() { + if let DeadKeyStatus::Composing(composing, selected_range) = &self.dead_key_status { + (Some(composing), selected_range.as_ref()) } else { - None + (None, None) } } else { - None + (None, None) }; + let mut selected_start = 0; + let mut selected_width = 0; + let mut composition_width = 0; let (_bidi_enabled, bidi_direction) = params.line.bidi_info(); @@ -93,6 +96,10 @@ impl crate::TermWindow { // Do we need to shape immediately, or can we use the pre-shaped data? if let Some(composing) = composing { composition_width = unicode_column_width(composing, None); + if let Some(selected_range) = selected_range { + selected_start = unicode_column_width(&composing[..selected_range.start], None); + selected_width = unicode_column_width(&composing[selected_range.clone()], None); + } } let cursor_cell = if params.stable_line_idx == Some(params.cursor.y) { @@ -109,6 +116,13 @@ impl crate::TermWindow { 0..0 }; + let selected_cursor_range = if selected_width > 0 { + params.cursor.x + selected_start..params.cursor.x + selected_start + selected_width + } else { + 0..0 + }; + + let cursor_range_pixels = params.left_pixel_x + cursor_range.start as f32 * cell_width ..params.left_pixel_x + cursor_range.end as f32 * cell_width; @@ -412,6 +426,38 @@ impl crate::TermWindow { quad.set_fg_color(cursor_border_color); quad.set_alt_color_and_mix_value(cursor_border_color_alt, cursor_border_mix); + + if !selected_cursor_range.is_empty() + && (cursor_range.start <= selected_cursor_range.start) + && (selected_cursor_range.end <= cursor_range.end) + { + let mut quad = layers.allocate(0)?; + quad.set_position( + pos_x + + (selected_cursor_range.start - cursor_range.start) as f32 + * cell_width, + pos_y, + pos_x + + (selected_cursor_range.end - cursor_range.start) as f32 * cell_width, + pos_y + cell_height, + ); + quad.set_hsv(hsv); + quad.set_has_color(false); + + quad.set_texture( + gl_state + .glyph_cache + .borrow_mut() + .cursor_sprite( + cursor_shape, + ¶ms.render_metrics, + (selected_cursor_range.end - selected_cursor_range.start) as u8, + )? + .texture_coords(), + ); + + quad.set_fg_color(params.selection_bg); + } } } diff --git a/window/src/lib.rs b/window/src/lib.rs index a2512d024ca0..55c07bb49568 100644 --- a/window/src/lib.rs +++ b/window/src/lib.rs @@ -3,6 +3,7 @@ use bitflags::bitflags; use config::{ConfigHandle, Dimension, GeometryOrigin}; use promise::Future; use std::any::Any; +use std::ops::Range; use std::path::PathBuf; use std::rc::Rc; use thiserror::Error; @@ -135,7 +136,7 @@ pub enum DeadKeyStatus { None, /// Holding until composition is done; the string is the uncommitted /// composition text to show as a placeholder - Composing(String), + Composing(String, Option>), } #[derive(Debug)] diff --git a/window/src/os/macos/mod.rs b/window/src/os/macos/mod.rs index a7f00cb46c15..4d35596b949b 100644 --- a/window/src/os/macos/mod.rs +++ b/window/src/os/macos/mod.rs @@ -22,11 +22,16 @@ fn nsstring(s: &str) -> StrongPtr { unsafe { StrongPtr::new(NSString::alloc(nil).init_str(s)) } } -unsafe fn nsstring_to_str<'a>(mut ns: *mut Object) -> &'a str { +unsafe fn unattributed(mut ns: *mut Object) -> *mut Object { let is_astring: bool = msg_send![ns, isKindOfClass: class!(NSAttributedString)]; if is_astring { ns = msg_send![ns, string]; } + ns +} + +unsafe fn nsstring_to_str<'a>(mut ns: *mut Object) -> &'a str { + ns = unattributed(ns); let data = NSString::UTF8String(ns as id) as *const u8; let len = NSString::len(ns as id); let bytes = std::slice::from_raw_parts(data, len); diff --git a/window/src/os/macos/window.rs b/window/src/os/macos/window.rs index a97a118fc12c..11cb6bb44be6 100644 --- a/window/src/os/macos/window.rs +++ b/window/src/os/macos/window.rs @@ -2,7 +2,7 @@ #![allow(clippy::let_unit_value)] use super::keycodes::*; -use super::{nsstring, nsstring_to_str}; +use super::{nsstring, nsstring_to_str, unattributed}; use crate::clipboard::Clipboard as ClipboardContext; use crate::connection::ConnectionOps; use crate::os::macos::menu::{MenuItem, RepresentedItem}; @@ -44,6 +44,7 @@ use raw_window_handle::{ use std::any::Any; use std::cell::RefCell; use std::ffi::c_void; +use std::ops::Range; use std::path::PathBuf; use std::rc::Rc; use std::str::FromStr; @@ -489,6 +490,7 @@ impl Window { ime_last_event: None, live_resizing: false, ime_text: String::new(), + selected_range: None, })); let window: id = msg_send![get_window_class(), alloc]; @@ -1372,6 +1374,7 @@ struct Inner { live_resizing: bool, ime_text: String, + selected_range: Option>, } #[repr(C)] @@ -1835,6 +1838,7 @@ impl WindowView { if let Some(myself) = Self::get_this(this) { let mut inner = myself.inner.borrow_mut(); inner.ime_text = s.to_string(); + inner.selected_range = calc_str_selected_range(astring, selected_range, s.len()); /* let key_is_down = inner.key_is_down.take().unwrap_or(true); @@ -2361,7 +2365,7 @@ impl WindowView { Ok(TranslateStatus::Composing(composing)) => { // Next key press in dead key sequence is pending. inner.events.dispatch(WindowEvent::AdviseDeadKeyStatus( - DeadKeyStatus::Composing(composing), + DeadKeyStatus::Composing(composing, None), )); return; @@ -2470,7 +2474,10 @@ impl WindowView { // If it didn't generate an event, then a composition // is pending. let status = if inner.ime_last_event.is_none() { - DeadKeyStatus::Composing(inner.ime_text.clone()) + DeadKeyStatus::Composing( + inner.ime_text.clone(), + inner.selected_range.clone(), + ) } else { DeadKeyStatus::None }; @@ -2500,7 +2507,10 @@ impl WindowView { let status = if inner.ime_text.is_empty() { DeadKeyStatus::None } else { - DeadKeyStatus::Composing(inner.ime_text.clone()) + DeadKeyStatus::Composing( + inner.ime_text.clone(), + inner.selected_range.clone(), + ) }; inner .events @@ -3189,3 +3199,49 @@ impl WindowView { cls.register() } } + + +fn calc_str_selected_range( + astring: *mut Object, + selected_range: NSRange, + max: usize, +) -> Option> { + unsafe fn sub_tostr_len(ns: *mut Object, range: NSRange, max: usize) -> usize { + let sub = msg_send![ns, substringWithRange: range]; + let len = nsstring_to_str(sub).len(); + std::cmp::min(len, max) + } + + let ns; + let ns_length; + unsafe { + ns = unattributed(astring); + ns_length = msg_send![ns, length]; + } + + let a_start = selected_range.0.location; + let a_end = a_start + selected_range.0.length; + if ns_length < a_end { + return None; + } + + let s_start; + let s_end; + unsafe { + s_start = if 0 < a_start { + sub_tostr_len(ns, NSRange::new(0, a_start), max) + } else { + 0 + }; + s_end = if a_end < ns_length { + s_start + sub_tostr_len(ns, NSRange::new(a_start, a_end - a_start), max) + } else { + max + }; + } + if s_end <= s_start { + return None; + } + + Some(s_start..s_end) +} diff --git a/window/src/os/windows/window.rs b/window/src/os/windows/window.rs index 76075741056b..6ace8ff36f52 100644 --- a/window/src/os/windows/window.rs +++ b/window/src/os/windows/window.rs @@ -23,6 +23,7 @@ use std::collections::HashMap; use std::convert::TryInto; use std::ffi::OsString; use std::io::{self, Error as IoError}; +use std::ops::Range; use std::os::windows::ffi::OsStringExt; use std::path::PathBuf; use std::ptr::{null, null_mut}; @@ -52,6 +53,8 @@ use winreg::RegKey; const GCS_RESULTSTR: DWORD = 0x800; const GCS_COMPSTR: DWORD = 0x8; +const GCS_COMPATTR: DWORD = 0x10; +const ATTR_TARGET_CONVERTED: u8 = 0x1; const ISC_SHOWUICOMPOSITIONWINDOW: DWORD = 0x80000000; #[allow(non_snake_case)] @@ -200,6 +203,43 @@ unsafe impl HasRawDisplayHandle for WindowInner { } } +fn calc_str_selected_range(compstr: &[u16], compattr: &[u8], max: usize) -> Option> { + fn selected(v: &u8) -> bool { + v & ATTR_TARGET_CONVERTED != 0 + } + fn sub_tostr_len(compstr: &[u16], range: Range, max: usize) -> Option { + let sub = &compstr[range]; + let len = OsString::from_wide(sub).into_string().ok()?.len(); + Some(std::cmp::min(len, max)) + } + + if compstr.len() != compattr.len() { + return None; + } + + let a_start = compattr.iter().position(selected)?; + let a_end = compattr.len() - compattr.iter().rev().position(selected)?; + if a_end <= a_start { + return None; + } + + let s_start = if 0 < a_start { + sub_tostr_len(&compstr, 0..a_start, max)? + } else { + 0 + }; + let s_end = if a_end < compattr.len() { + s_start + sub_tostr_len(&compstr, a_start..a_end, max)? + } else { + max + }; + if s_end <= s_start { + return None; + } + + Some(s_start..s_end) +} + unsafe impl HasRawWindowHandle for WindowInner { fn raw_window_handle(&self) -> RawWindowHandle { let mut handle = Win32WindowHandle::empty(); @@ -1976,26 +2016,27 @@ impl ImmContext { } } - pub fn get_str(&self, which: DWORD) -> Result { - // This returns a size in bytes even though it is for a buffer of u16! - let byte_size = - unsafe { ImmGetCompositionStringW(self.imc, which, std::ptr::null_mut(), 0) }; - if byte_size > 0 { - let word_size = byte_size as usize / 2; - let mut wide_buf = vec![0u16; word_size]; - unsafe { - ImmGetCompositionStringW( - self.imc, - which, - wide_buf.as_mut_ptr() as *mut _, - byte_size as u32, - ) - }; - OsString::from_wide(&wide_buf).into_string() + pub fn get_raw(&self, which: DWORD) -> Result, i32> { + let size = unsafe { ImmGetCompositionStringW(self.imc, which, std::ptr::null_mut(), 0) }; + if size < 0 { + Err(size) + } else if size == 0 { + Ok(vec![]) } else { - Ok(String::new()) + let mut buf: Vec; + let len = size as usize / std::mem::size_of::(); + buf = Vec::::with_capacity(len); + unsafe { + buf.set_len(len); + ImmGetCompositionStringW(self.imc, which, buf.as_mut_ptr() as *mut _, size as u32); + } + Ok(buf) } } + + pub fn get_str(&self, which: DWORD) -> Result { + OsString::from_wide(&self.get_raw(which).unwrap_or_default()[..]).into_string() + } } impl Drop for ImmContext { @@ -2073,11 +2114,17 @@ unsafe fn ime_composition( if lparam & GCS_RESULTSTR == 0 { // No finished result; continue with the default // processing - if let Ok(composing) = imc.get_str(GCS_COMPSTR) { + let compstr = imc.get_raw::(GCS_COMPSTR).unwrap_or_default(); + if let Ok(composing) = OsString::from_wide(&compstr[..]).into_string() { + let selected_range = match imc.get_raw::(GCS_COMPATTR) { + Ok(compattr) => calc_str_selected_range(&compstr, &compattr, composing.len()), + Err(_) => None, + }; inner .events .dispatch(WindowEvent::AdviseDeadKeyStatus(DeadKeyStatus::Composing( composing, + selected_range, ))); } // We will show the composing string ourselves. @@ -2698,7 +2745,7 @@ unsafe fn key(hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM) -> Option, @@ -378,6 +382,31 @@ fn compute_default_dpi(xrm: &HashMap, xsettings: &XSettingsMap) } } +fn calc_str_selected_range(text: &str, feedback_array: &[u32], max: usize) -> Option> { + fn selected(v: &u32) -> bool { + v & XIMReverse != 0 + } + + let a_start = feedback_array.iter().position(selected)?; + let a_end = feedback_array.len() - feedback_array.iter().rev().position(selected)?; + if a_end <= a_start { + return None; + } + + let mut char_indices = text.char_indices(); + let s_start = std::cmp::min(char_indices.nth(a_start).unwrap().0, max); + let s_end = if a_end < feedback_array.len() { + std::cmp::min(char_indices.nth(a_end - a_start - 1).unwrap().0, max) + } else { + max + }; + if s_end <= s_start { + return None; + } + + Some(s_start..s_end) +} + impl XConnection { pub(crate) fn update_xrm(&self) { match read_xsettings( @@ -747,7 +776,10 @@ impl XConnection { let mut inner = window.lock().unwrap(); let text = info.text(); - let status = DeadKeyStatus::Composing(text); + let selected_range = + calc_str_selected_range(&text, info.feedback_array(), text.len()); + + let status = DeadKeyStatus::Composing(text, selected_range); inner.dispatch_ime_compose_status(status); } }); diff --git a/window/src/os/x11/keyboard.rs b/window/src/os/x11/keyboard.rs index 11c37b5dd82a..7fc68322960c 100644 --- a/window/src/os/x11/keyboard.rs +++ b/window/src/os/x11/keyboard.rs @@ -598,8 +598,181 @@ impl Keyboard { .feed(xcode, xsym, &self.state) } - pub fn compose_clear(&self) { - self.compose_state.borrow_mut().reset(); + pub fn process_key_release_event( + &self, + xcb_ev: &xcb::x::KeyReleaseEvent, + events: &mut WindowEventSender, + ) { + let xcode = xkb::Keycode::from(xcb_ev.detail()); + self.process_key_event_impl(xcode, false, events, false); + } + + fn process_key_event_impl( + &self, + xcode: xkb::Keycode, + pressed: bool, + events: &mut WindowEventSender, + want_repeat: bool, + ) -> Option { + let phys_code = self.phys_code_map.borrow().get(&xcode).copied(); + let raw_modifiers = self.get_key_modifiers(); + + let xsym = self.state.borrow().key_get_one_sym(xcode); + let handled = Handled::new(); + + let raw_key_event = RawKeyEvent { + key: match phys_code { + Some(phys) => KeyCode::Physical(phys), + None => KeyCode::RawCode(xcode), + }, + phys_code, + raw_code: xcode, + modifiers: raw_modifiers, + repeat_count: 1, + key_is_down: pressed, + handled: handled.clone(), + }; + + let mut kc = None; + let ksym = if pressed { + events.dispatch(WindowEvent::RawKeyEvent(raw_key_event.clone())); + if handled.is_handled() { + self.compose_state.borrow_mut().reset(); + log::trace!("process_key_event: raw key was handled; not processing further"); + + if want_repeat { + return Some(WindowKeyEvent::RawKeyEvent(raw_key_event.clone())); + } + return None; + } + + match self + .compose_state + .borrow_mut() + .feed(xcode, xsym, &self.state) + { + FeedResult::Composing(composition) => { + log::trace!( + "process_key_event: RawKeyEvent FeedResult::Composing: {:?}", + composition + ); + events.dispatch(WindowEvent::AdviseDeadKeyStatus(DeadKeyStatus::Composing( + composition, + None, + ))); + return None; + } + FeedResult::Composed(utf8, sym) => { + if !utf8.is_empty() { + kc.replace(crate::KeyCode::composed(&utf8)); + } + log::trace!( + "process_key_event: RawKeyEvent FeedResult::Composed: \ + {:?}, {:?}. kc -> {:?}", + utf8, + sym, + kc + ); + events.dispatch(WindowEvent::AdviseDeadKeyStatus(DeadKeyStatus::None)); + sym + } + FeedResult::Nothing(utf8, sym) => { + if !utf8.is_empty() { + kc.replace(crate::KeyCode::composed(&utf8)); + } + log::trace!( + "process_key_event: RawKeyEvent FeedResult::Nothing: \ + {:?}, {:?}. kc -> {:?}", + utf8, + sym, + kc + ); + sym + } + FeedResult::Cancelled => { + log::trace!("process_key_event: RawKeyEvent FeedResult::Cancelled"); + events.dispatch(WindowEvent::AdviseDeadKeyStatus(DeadKeyStatus::None)); + return None; + } + } + } else { + xsym + }; + + let kc = match kc { + Some(kc) => kc, + None => match keysym_to_keycode(ksym).or_else(|| keysym_to_keycode(xsym)) { + Some(kc) => kc, + None => { + log::trace!("keysym_to_keycode for {:?} and {:?} -> None", ksym, xsym); + return None; + } + }, + }; + + let event = KeyEvent { + key: kc, + modifiers: raw_modifiers, + repeat_count: 1, + key_is_down: pressed, + raw: Some(raw_key_event), + } + .normalize_shift(); + + if pressed && want_repeat { + events.dispatch(WindowEvent::KeyEvent(event.clone())); + // Returns the event that should be repeated later + Some(WindowKeyEvent::KeyEvent(event)) + } else { + events.dispatch(WindowEvent::KeyEvent(event)); + None + } + } + + fn mod_is_active(&self, modifier: &str) -> bool { + // [TODO] consider state Depressed & consumed mods + self.state + .borrow() + .mod_name_is_active(modifier, xkb::STATE_MODS_EFFECTIVE) + } + + pub fn get_key_modifiers(&self) -> Modifiers { + let mut res = Modifiers::default(); + + if self.mod_is_active(xkb::MOD_NAME_SHIFT) { + res |= Modifiers::SHIFT; + } + if self.mod_is_active(xkb::MOD_NAME_CTRL) { + res |= Modifiers::CTRL; + } + if self.mod_is_active(xkb::MOD_NAME_ALT) { + // Mod1 + res |= Modifiers::ALT; + } + if self.mod_is_active(xkb::MOD_NAME_LOGO) { + // Mod4 + res |= Modifiers::SUPER; + } + res + } + + pub fn process_xkb_event( + &self, + connection: &xcb::Connection, + event: &xcb::Event, + ) -> anyhow::Result<()> { + match event { + xcb::Event::Xkb(xcb::xkb::Event::StateNotify(e)) => { + self.update_state(e); + } + xcb::Event::Xkb( + xcb::xkb::Event::MapNotify(_) | xcb::xkb::Event::NewKeyboardNotify(_), + ) => { + self.update_keymap(connection)?; + } + _ => {} + } + Ok(()) } pub fn update_modifier_state(