diff --git a/wezterm-gui/src/scripting/guiwin.rs b/wezterm-gui/src/scripting/guiwin.rs index 06f50aab0fc9..6a48ad0f4beb 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(composing) => Some(composing.text.clone()), }) .ok(); }))); diff --git a/wezterm-gui/src/termwindow/render/mod.rs b/wezterm-gui/src/termwindow/render/mod.rs index f08137b4c4c0..21cce1a995e3 100644 --- a/wezterm-gui/src/termwindow/render/mod.rs +++ b/wezterm-gui/src/termwindow/render/mod.rs @@ -9,6 +9,7 @@ use crate::shapecache::*; use crate::termwindow::render::paint::AllowImage; use crate::termwindow::{BorrowedShapeCacheKey, RenderState, ShapedInfo, TermWindowNotif}; use crate::utilsprites::RenderMetrics; +use crate::Composing; use ::window::bitmaps::{TextureCoord, TextureRect, TextureSize}; use ::window::{DeadKeyStatus, PointF, RectF, SizeF, WindowOps}; use anyhow::{anyhow, Context}; @@ -56,7 +57,7 @@ pub struct LineQuadCacheKey { pub shape_generation: usize, pub quad_generation: usize, /// Only set if cursor.y == stable_row - pub composing: Option, + pub composing: Option, pub selection: Range, pub shape_hash: [u8; 16], pub top_pixel_y: NotNan, diff --git a/wezterm-gui/src/termwindow/render/pane.rs b/wezterm-gui/src/termwindow/render/pane.rs index a2160fa7a41d..9eb989d39cf0 100644 --- a/wezterm-gui/src/termwindow/render/pane.rs +++ b/wezterm-gui/src/termwindow/render/pane.rs @@ -398,7 +398,7 @@ impl crate::TermWindow { }), match (self.pos.is_active, &self.term_window.dead_key_status) { (true, DeadKeyStatus::Composing(composing)) => { - Some(composing.to_string()) + Some(composing.clone()) } _ => None, }, @@ -430,7 +430,7 @@ impl crate::TermWindow { config_generation: self.term_window.config.generation(), shape_generation: self.term_window.shape_generation, quad_generation: self.term_window.quad_generation, - composing: composing.clone(), + composing, selection: selrange.clone(), cursor, shape_hash, @@ -477,7 +477,7 @@ impl crate::TermWindow { if let DeadKeyStatus::Composing(composing) = &self.term_window.dead_key_status { - Some((self.cursor.x, composing.to_string())) + Some((self.cursor.x, composing.text.clone())) } else { None } diff --git a/wezterm-gui/src/termwindow/render/screen_line.rs b/wezterm-gui/src/termwindow/render/screen_line.rs index 2f2675a02f6d..65ad8b3d2bd3 100644 --- a/wezterm-gui/src/termwindow/render/screen_line.rs +++ b/wezterm-gui/src/termwindow/render/screen_line.rs @@ -5,6 +5,7 @@ use crate::termwindow::render::{ RenderScreenLineParams, RenderScreenLineResult, }; use crate::termwindow::LineToElementShapeItem; +use crate::Composing; use ::window::DeadKeyStatus; use anyhow::Context; use config::{HsbTransform, TextStyle}; @@ -17,6 +18,7 @@ use termwiz::surface::CursorShape; use wezterm_bidi::Direction; use wezterm_term::color::ColorAttribute; use wezterm_term::CellAttributes; +use window::ComposingAttribute; impl crate::TermWindow { /// "Render" a line of the terminal screen into the vertex buffer. @@ -64,6 +66,9 @@ impl crate::TermWindow { let pos_y = (self.dimensions.pixel_height as f32 / -2.) + params.top_pixel_y; let gl_x = self.dimensions.pixel_width as f32 / -2.; + let (_bidi_enabled, bidi_direction) = params.line.bidi_info(); + let direction = bidi_direction.direction(); + let start = Instant::now(); let cursor_idx = if params.pane.is_some() @@ -75,25 +80,42 @@ impl crate::TermWindow { None }; + let mut composing_text_width = 0; + let mut composing_selections = vec![]; + // 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) - } else { - None + if cursor_idx.is_some() { + // Do we need to shape immediately, or can we use the pre-shaped data? + if let DeadKeyStatus::Composing(Composing { text, attr }) = &self.dead_key_status { + composing_text_width = unicode_column_width(text, None); + + if let Some(attr) = attr { + // convert text and attr to selections + let mut selection = 0usize..0; + // iterate over byte end of each character in text + for (i, end) in text + .char_indices() + .map(|(offset, _)| offset) + .chain([text.len()]) + .skip(1) + .take(attr.len()) + .enumerate() + { + // update end to unicode width + let end = unicode_column_width(&text[..end], None); + if attr[i].contains(ComposingAttribute::SELECTED) { + selection.end = end; + } + // add non-empty selection and prepare next selection + if i + 1 == attr.len() || !attr[i].contains(ComposingAttribute::SELECTED) { + if !selection.is_empty() { + composing_selections.push(selection); + } + selection = end..end; + } + } + } } - } else { - None - }; - - let mut composition_width = 0; - - let (_bidi_enabled, bidi_direction) = params.line.bidi_info(); - let direction = bidi_direction.direction(); - - // 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); } let cursor_cell = if params.stable_line_idx == Some(params.cursor.y) { @@ -102,8 +124,8 @@ impl crate::TermWindow { None }; - let cursor_range = if composition_width > 0 { - params.cursor.x..params.cursor.x + composition_width + let cursor_range = if composing_text_width > 0 { + params.cursor.x..params.cursor.x + composing_text_width } else if params.stable_line_idx == Some(params.cursor.y) { params.cursor.x..params.cursor.x + cursor_cell.as_ref().map(|c| c.width()).unwrap_or(1) } else { @@ -419,6 +441,36 @@ impl crate::TermWindow { quad.set_fg_color(cursor_border_color); quad.set_alt_color_and_mix_value(cursor_border_color_alt, cursor_border_mix); + + for selection in &composing_selections { + let selection = params.cursor.x + selection.start - cursor_range.start + ..params.cursor.x + selection.end - cursor_range.start; + if !selection.is_empty() { + let mut quad = layers.allocate(0)?; + quad.set_position( + pos_x + selection.start as f32 * cell_width, + pos_y, + pos_x + selection.end 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, + (selection.end - selection.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 29c670b32de0..7597ac78c963 100644 --- a/window/src/lib.rs +++ b/window/src/lib.rs @@ -151,7 +151,23 @@ pub enum DeadKeyStatus { None, /// Holding until composition is done; the string is the uncommitted /// composition text to show as a placeholder - Composing(String), + Composing(Composing), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct Composing { + /// Holding composing text + pub text: String, + /// Holding composing attribute of each unicode character in composing text + pub attr: Option>, +} + +bitflags! { + #[derive(Default)] + pub struct ComposingAttribute: u8 { + const NONE = 0; + const SELECTED = 1; + } } #[derive(Debug)] diff --git a/window/src/os/macos/window.rs b/window/src/os/macos/window.rs index 898a203470e6..78c77ae666a3 100644 --- a/window/src/os/macos/window.rs +++ b/window/src/os/macos/window.rs @@ -8,10 +8,11 @@ use crate::connection::ConnectionOps; use crate::os::macos::menu::{MenuItem, RepresentedItem}; use crate::parameters::{Border, Parameters, TitleBar}; use crate::{ - Clipboard, Connection, DeadKeyStatus, Dimensions, Handled, KeyCode, KeyEvent, Modifiers, - MouseButtons, MouseCursor, MouseEvent, MouseEventKind, MousePress, Point, RawKeyEvent, Rect, - RequestedWindowGeometry, ResizeIncrement, ResolvedGeometry, ScreenPoint, Size, ULength, - WindowDecorations, WindowEvent, WindowEventSender, WindowOps, WindowState, + Clipboard, Composing, ComposingAttribute, Connection, DeadKeyStatus, Dimensions, Handled, + KeyCode, KeyEvent, Modifiers, MouseButtons, MouseCursor, MouseEvent, MouseEventKind, + MousePress, Point, RawKeyEvent, Rect, RequestedWindowGeometry, ResizeIncrement, + ResolvedGeometry, ScreenPoint, Size, ULength, WindowDecorations, WindowEvent, + WindowEventSender, WindowOps, WindowState, }; use anyhow::{anyhow, bail, ensure}; use async_trait::async_trait; @@ -500,7 +501,7 @@ impl Window { ime_state: ImeDisposition::None, ime_last_event: None, live_resizing: false, - ime_text: String::new(), + ime_composing: Default::default(), })); let window: id = msg_send![get_window_class(), alloc]; @@ -1440,7 +1441,7 @@ struct Inner { /// Whether we're in live resize live_resizing: bool, - ime_text: String, + ime_composing: Composing, } #[repr(C)] @@ -1822,7 +1823,7 @@ impl WindowView { extern "C" fn has_marked_text(this: &mut Object, _sel: Sel) -> BOOL { if let Some(myself) = Self::get_this(this) { let inner = myself.inner.borrow(); - if inner.ime_text.is_empty() { + if inner.ime_composing.text.is_empty() { NO } else { YES @@ -1835,11 +1836,11 @@ impl WindowView { extern "C" fn marked_range(this: &mut Object, _sel: Sel) -> NSRange { if let Some(myself) = Self::get_this(this) { let inner = myself.inner.borrow(); - log::trace!("marked_range {:?}", inner.ime_text); - if inner.ime_text.is_empty() { + log::trace!("marked_range {:?}", inner.ime_composing); + if inner.ime_composing.text.is_empty() { NSRange::new(NSNotFound as _, 0) } else { - NSRange::new(0, inner.ime_text.len() as u64) + NSRange::new(0, inner.ime_composing.text.len() as u64) } } else { NSRange::new(NSNotFound as _, 0) @@ -1879,7 +1880,7 @@ impl WindowView { raw: None, }; - inner.ime_text.clear(); + inner.ime_composing = Default::default(); inner .events .dispatch(WindowEvent::AdviseDeadKeyStatus(DeadKeyStatus::None)); @@ -1905,7 +1906,23 @@ impl WindowView { ); if let Some(myself) = Self::get_this(this) { let mut inner = myself.inner.borrow_mut(); - inner.ime_text = s.to_string(); + + let text = s.to_string(); + let attr = Some( + text.chars() + .scan(0, |i, c| (Some(*i), *i += c.len_utf16()).0) + .map(|i| { + let mut attr = ComposingAttribute::NONE; + if selected_range.0.location <= (i as u64) + && (i as u64) < selected_range.0.location + selected_range.0.length + { + attr |= ComposingAttribute::SELECTED; + } + attr + }) + .collect(), + ); + inner.ime_composing = Composing { text, attr }; /* let key_is_down = inner.key_is_down.take().unwrap_or(true); @@ -1935,7 +1952,7 @@ impl WindowView { // FIXME: docs say to insert the text here, // but iterm doesn't... and we've never seen // this get called so far? - inner.ime_text.clear(); + inner.ime_composing = Default::default(); inner.ime_last_event.take(); inner.ime_state = ImeDisposition::Acted; } @@ -2432,7 +2449,10 @@ 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 { + text: composing, + attr: None, + }), )); return; @@ -2513,7 +2533,7 @@ impl WindowView { let mut inner = myself.inner.borrow_mut(); inner.key_is_down.replace(key_is_down); inner.ime_state = ImeDisposition::None; - inner.ime_text.clear(); + inner.ime_composing = Default::default(); } unsafe { @@ -2541,7 +2561,7 @@ 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_composing.clone()) } else { DeadKeyStatus::None }; @@ -2568,10 +2588,10 @@ impl WindowView { return; } } - let status = if inner.ime_text.is_empty() { + let status = if inner.ime_composing.text.is_empty() { DeadKeyStatus::None } else { - DeadKeyStatus::Composing(inner.ime_text.clone()) + DeadKeyStatus::Composing(inner.ime_composing.clone()) }; inner .events diff --git a/window/src/os/wayland/inputhandler.rs b/window/src/os/wayland/inputhandler.rs index 357d941f1747..32e0d88b9825 100644 --- a/window/src/os/wayland/inputhandler.rs +++ b/window/src/os/wayland/inputhandler.rs @@ -16,7 +16,7 @@ use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::{ }; use wezterm_input_types::{KeyCode, KeyEvent, KeyboardLedStatus, Modifiers}; -use crate::{DeadKeyStatus, WindowEvent}; +use crate::{Composing, DeadKeyStatus, WindowEvent}; use super::state::WaylandState; @@ -190,7 +190,7 @@ impl Dispatch for TextInputState { })); } let status = if let Some(text) = pending_state.pre_edit.take() { - DeadKeyStatus::Composing(text) + DeadKeyStatus::Composing(Composing { text, attr: None }) } else { DeadKeyStatus::None }; diff --git a/window/src/os/windows/window.rs b/window/src/os/windows/window.rs index bbeffb8459ae..d76dec798ec5 100644 --- a/window/src/os/windows/window.rs +++ b/window/src/os/windows/window.rs @@ -2,10 +2,10 @@ use super::*; use crate::connection::ConnectionOps; use crate::parameters::{self, Parameters}; use crate::{ - Appearance, Clipboard, DeadKeyStatus, Dimensions, Handled, KeyCode, KeyEvent, Modifiers, - MouseButtons, MouseCursor, MouseEvent, MouseEventKind, MousePress, Point, RawKeyEvent, Rect, - RequestedWindowGeometry, ResolvedGeometry, ScreenPoint, ScreenRect, ULength, WindowDecorations, - WindowEvent, WindowEventSender, WindowOps, WindowState, + Appearance, Clipboard, Composing, ComposingAttribute, DeadKeyStatus, Dimensions, Handled, + KeyCode, KeyEvent, Modifiers, MouseButtons, MouseCursor, MouseEvent, MouseEventKind, + MousePress, Point, RawKeyEvent, Rect, RequestedWindowGeometry, ResolvedGeometry, ScreenPoint, + ScreenRect, ULength, WindowDecorations, WindowEvent, WindowEventSender, WindowOps, WindowState, }; use anyhow::{bail, Context}; use async_trait::async_trait; @@ -53,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)] @@ -2022,26 +2024,28 @@ 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() + /// Get Vec or Vec corresponding to a IME composition string value + 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,12 +2123,27 @@ 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) { - inner - .events - .dispatch(WindowEvent::AdviseDeadKeyStatus(DeadKeyStatus::Composing( - composing, - ))); + if let Ok(compstr) = imc.get_raw::(GCS_COMPSTR) { + if let Ok(text) = OsString::from_wide(&compstr[..]).into_string() { + let attr = imc.get_raw::(GCS_COMPATTR).ok().map(|compattr| { + text.chars() + .scan(0, |i, c| (Some(*i), *i += c.len_utf16()).0) + .take_while(|&i| i < compattr.len()) + .map(|i| { + let mut attr = ComposingAttribute::NONE; + if compattr[i] & ATTR_TARGET_CONVERTED != 0 { + attr |= ComposingAttribute::SELECTED; + } + attr + }) + .collect() + }); + inner + .events + .dispatch(WindowEvent::AdviseDeadKeyStatus(DeadKeyStatus::Composing( + Composing { text, attr }, + ))) + } } // We will show the composing string ourselves. // Suppress the default composition display. @@ -2744,7 +2763,10 @@ unsafe fn key(hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM) -> Option { + FeedResult::Composing(text) => { log::trace!( "process_key_event: RawKeyEvent FeedResult::Composing: {:?}", - composition + text ); events.dispatch(WindowEvent::AdviseDeadKeyStatus(DeadKeyStatus::Composing( - composition, + Composing { text, attr: None }, ))); return None; }