diff --git a/wezterm-gui/src/scripting/guiwin.rs b/wezterm-gui/src/scripting/guiwin.rs index 06f50aab0fc9..ca4d2da9fdd3 100644 --- a/wezterm-gui/src/scripting/guiwin.rs +++ b/wezterm-gui/src/scripting/guiwin.rs @@ -232,7 +232,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/pane.rs b/wezterm-gui/src/termwindow/render/pane.rs index a2160fa7a41d..ba00124afa50 100644 --- a/wezterm-gui/src/termwindow/render/pane.rs +++ b/wezterm-gui/src/termwindow/render/pane.rs @@ -397,7 +397,7 @@ impl crate::TermWindow { cursor_is_default_color: self.cursor_is_default_color, }), match (self.pos.is_active, &self.term_window.dead_key_status) { - (true, DeadKeyStatus::Composing(composing)) => { + (true, DeadKeyStatus::Composing(composing, _)) => { Some(composing.to_string()) } _ => None, @@ -474,7 +474,7 @@ impl crate::TermWindow { shape_hash, shape_generation: quad_key.shape_generation, composing: if self.cursor.y == stable_row && self.pos.is_active { - if let DeadKeyStatus::Composing(composing) = + if let DeadKeyStatus::Composing(composing, _) = &self.term_window.dead_key_status { Some((self.cursor.x, composing.to_string())) diff --git a/wezterm-gui/src/termwindow/render/screen_line.rs b/wezterm-gui/src/termwindow/render/screen_line.rs index 2f2675a02f6d..1b7e691d7d25 100644 --- a/wezterm-gui/src/termwindow/render/screen_line.rs +++ b/wezterm-gui/src/termwindow/render/screen_line.rs @@ -76,16 +76,18 @@ 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(); @@ -94,6 +96,11 @@ 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) { @@ -110,6 +117,12 @@ 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; @@ -419,6 +432,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 ac97cc1dcabc..bdd9938d4fa2 100644 --- a/window/src/lib.rs +++ b/window/src/lib.rs @@ -4,6 +4,7 @@ use config::window::WindowLevel; 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; @@ -150,7 +151,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 898a203470e6..2482aeb3ea5c 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}; @@ -45,6 +45,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; @@ -501,6 +502,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]; @@ -1441,6 +1443,7 @@ struct Inner { live_resizing: bool, ime_text: String, + selected_range: Option>, } #[repr(C)] @@ -1906,6 +1909,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); @@ -2432,7 +2436,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; @@ -2541,7 +2545,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 }; @@ -2571,7 +2578,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 @@ -3300,3 +3310,48 @@ 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: u64; + 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/wayland/inputhandler.rs b/window/src/os/wayland/inputhandler.rs index 357d941f1747..316420afb050 100644 --- a/window/src/os/wayland/inputhandler.rs +++ b/window/src/os/wayland/inputhandler.rs @@ -190,7 +190,7 @@ impl Dispatch for TextInputState { })); } let status = if let Some(text) = pending_state.pre_edit.take() { - DeadKeyStatus::Composing(text) + DeadKeyStatus::Composing(text, None) } else { DeadKeyStatus::None }; diff --git a/window/src/os/windows/window.rs b/window/src/os/windows/window.rs index b38479c37909..1c3b74760a2c 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}; @@ -53,6 +54,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 @@ fn callback_behavior() -> glium::debug::DebugCallbackBehavior { } } +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 HasRawDisplayHandle for WindowInner { fn raw_display_handle(&self) -> RawDisplayHandle { RawDisplayHandle::Windows(WindowsDisplayHandle::empty()) @@ -2022,26 +2062,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 { @@ -2119,11 +2160,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. @@ -2744,7 +2791,7 @@ unsafe fn key(hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM) -> Option, @@ -390,6 +394,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( @@ -790,7 +819,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 8fd3edeee6e2..956bd38f67ac 100644 --- a/window/src/os/x11/keyboard.rs +++ b/window/src/os/x11/keyboard.rs @@ -325,6 +325,7 @@ impl KeyboardWithFallback { ); events.dispatch(WindowEvent::AdviseDeadKeyStatus(DeadKeyStatus::Composing( composition, + None, ))); return None; }