1use super::binding::CTX_MUTEX;
2use crate::{Context, Id, sys};
3
4#[derive(Clone, Debug, Eq, PartialEq)]
6pub struct ImePreedit {
7 text: String,
8 cursor_range: Option<(usize, usize)>,
9 owner: ImeOwner,
10}
11
12impl ImePreedit {
13 pub fn text(&self) -> &str {
14 &self.text
15 }
16
17 pub fn cursor_range(&self) -> Option<(usize, usize)> {
20 self.cursor_range
21 }
22}
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
25pub enum ImePreeditError {
26 #[error("IME cursor offsets must be UTF-8 boundaries within the preedit text")]
27 InvalidCursorRange,
28 #[error("IME preedit text must not contain NUL")]
29 InteriorNul,
30}
31
32#[derive(Debug, Default)]
33pub(crate) struct ImeState {
34 pub(crate) preedit: Option<ImePreedit>,
35 presentation: Option<InputPresentation>,
36 session: ImeSession,
37}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40struct ImeOwner {
41 widget: sys::ImGuiID,
42 window: sys::ImGuiID,
43 viewport: sys::ImGuiID,
44}
45
46impl ImeOwner {
47 fn current(raw: &sys::ImGuiContext) -> Option<Self> {
48 if raw.ActiveId == 0
49 || raw.InputTextState.ID != raw.ActiveId
50 || raw.ActiveIdWindow.is_null()
51 || raw.InputTextState.Flags & sys::ImGuiInputTextFlags_ReadOnly != 0
52 {
53 return None;
54 }
55 let window = unsafe { &*raw.ActiveIdWindow };
56 if window.Viewport.is_null() {
57 return None;
58 }
59 Some(Self {
60 widget: raw.ActiveId,
61 window: window.ID,
62 viewport: unsafe { (*window.Viewport)._ImGuiViewport.ID },
63 })
64 }
65}
66
67#[derive(Clone, Copy, Debug, Default)]
68enum ImeSession {
69 #[default]
70 Idle,
71 Composing(ImeOwner),
72 Cancelled {
73 owner: ImeOwner,
74 reset_requested: bool,
75 },
76 Disabled,
77}
78
79impl ImeState {
80 pub(crate) fn cancel(&mut self) {
81 self.preedit = None;
82 if let ImeSession::Composing(owner) = self.session {
83 self.session = ImeSession::Cancelled {
84 owner,
85 reset_requested: true,
86 };
87 }
88 }
89
90 fn reconcile_owner(&mut self, current: Option<ImeOwner>) {
91 if let ImeSession::Composing(owner) = self.session
92 && current != Some(owner)
93 {
94 self.cancel();
95 }
96 }
97
98 pub(crate) fn session_viewport(&self) -> Option<Id> {
99 match self.session {
100 ImeSession::Composing(owner) | ImeSession::Cancelled { owner, .. } => {
101 Some(Id::from(owner.viewport))
102 }
103 ImeSession::Idle | ImeSession::Disabled => None,
104 }
105 }
106
107 pub(crate) fn take_reset_request(&mut self) -> Option<Id> {
108 if let ImeSession::Cancelled {
109 owner,
110 reset_requested,
111 } = &mut self.session
112 && std::mem::take(reset_requested)
113 {
114 Some(Id::from(owner.viewport))
115 } else {
116 None
117 }
118 }
119
120 pub(crate) fn reset_session(&mut self) {
121 self.preedit = None;
122 self.session = ImeSession::Idle;
123 }
124}
125
126#[derive(Clone, Copy, Debug)]
127struct InputPresentation {
128 frame: i32,
129 owner: sys::ImGuiID,
130 font: *mut sys::ImFont,
131 size: f32,
132 density: f32,
133}
134
135impl crate::Ui {
136 pub(crate) fn run_input_widget<R>(
138 &self,
139 label: *const std::ffi::c_char,
140 f: impl FnOnce() -> R,
141 ) -> R {
142 self.run_with_bound_context(|| {
143 let owner = unsafe { sys::igGetID_Str(label) };
145 let (font, size, density) = unsafe {
146 let font = (*self.ctx).Font;
147 (font, (*self.ctx).FontSize, (*font).CurrentRasterizerDensity)
148 };
149 let result = f();
150 unsafe {
151 let raw = &*self.ctx;
152 if raw.ActiveId == owner
153 && raw.InputTextState.ID == owner
154 && raw.LastItemData.ID == owner
155 {
156 self.ime.borrow_mut().presentation = Some(InputPresentation {
157 frame: raw.FrameCount,
158 owner: raw.ActiveId,
159 font,
160 size,
161 density,
162 });
163 }
164 self.ime
165 .borrow_mut()
166 .reconcile_owner(ImeOwner::current(raw));
167 }
168 result
169 })
170 }
171}
172
173impl Context {
174 pub fn set_ime_preedit(
186 &mut self,
187 text: &str,
188 cursor_range: Option<(usize, usize)>,
189 ) -> Result<(), ImePreeditError> {
190 let _guard = CTX_MUTEX.lock();
191 self.assert_current_context("Context::set_ime_preedit()");
192 if let Some((start, end)) = cursor_range
193 && (!text.is_char_boundary(start) || !text.is_char_boundary(end))
194 {
195 return Err(ImePreeditError::InvalidCursorRange);
196 }
197 if text.contains('\0') {
198 return Err(ImePreeditError::InteriorNul);
199 }
200 let raw = unsafe { &*self.as_raw() };
201 let owner = ImeOwner::current(raw);
202 let mut ime = self.state.ime.borrow_mut();
203 ime.reconcile_owner(owner);
204 if matches!(
205 ime.session,
206 ImeSession::Cancelled { .. } | ImeSession::Disabled
207 ) {
208 return Ok(());
209 }
210 if text.is_empty() {
211 ime.preedit = None;
212 return Ok(());
213 }
214 let Some(owner) = owner.filter(|owner| {
215 ime.presentation
216 .is_some_and(|p| p.owner == owner.widget && p.frame == raw.FrameCount)
217 }) else {
218 return Ok(());
219 };
220 ime.session = ImeSession::Composing(owner);
221 ime.preedit = Some(ImePreedit {
222 text: text.to_owned(),
223 cursor_range,
224 owner,
225 });
226 Ok(())
227 }
228
229 pub fn ime_preedit(&self) -> Option<ImePreedit> {
231 self.state.ime.borrow().preedit.clone()
232 }
233
234 pub fn has_ime_preedit(&self) -> bool {
236 self.state.ime.borrow().preedit.is_some()
237 }
238
239 pub fn has_ime_composition(&self) -> bool {
241 matches!(self.state.ime.borrow().session, ImeSession::Composing(_))
242 }
243
244 pub fn notify_ime_enabled(&mut self, enabled: bool) {
249 let mut ime = self.state.ime.borrow_mut();
250 if enabled {
251 ime.reset_session();
252 } else {
253 ime.cancel();
254 if let ImeSession::Cancelled {
255 reset_requested, ..
256 } = &mut ime.session
257 {
258 *reset_requested = false;
259 } else {
260 ime.session = ImeSession::Disabled;
261 }
262 }
263 }
264
265 pub fn commit_ime_text(&mut self, text: &str) -> bool {
271 let _guard = CTX_MUTEX.lock();
272 self.assert_current_context("Context::commit_ime_text()");
273 let raw = unsafe { &*self.as_raw() };
274 {
275 let mut ime = self.state.ime.borrow_mut();
276 ime.reconcile_owner(ImeOwner::current(raw));
277 if matches!(
278 ime.session,
279 ImeSession::Cancelled { .. } | ImeSession::Disabled
280 ) || !(raw.PlatformImeData.WantVisible || raw.PlatformImeData.WantTextInput)
281 {
282 return false;
283 }
284 ime.reset_session();
285 }
286 let mut forwarded = false;
287 for ch in text.chars().filter(|ch| !ch.is_control()) {
288 self.io_mut().add_input_character(ch);
289 forwarded = true;
290 }
291 forwarded
292 }
293
294 pub fn ime_session_viewport(&self) -> Option<Id> {
296 self.state.ime.borrow().session_viewport()
297 }
298
299 pub fn clear_ime_preedit(&mut self) {
304 self.state.ime.borrow_mut().cancel();
305 }
306
307 pub fn text_input_viewport(&self) -> Option<Id> {
309 let _guard = CTX_MUTEX.lock();
310 self.assert_current_context("Context::text_input_viewport()");
311 let raw = unsafe { &*self.as_raw() };
312 (raw.PlatformImeData.WantVisible || raw.PlatformImeData.WantTextInput)
313 .then(|| Id::from(raw.PlatformImeData.ViewportId))
314 }
315
316 pub(crate) fn render_ime_preedit(&mut self) {
317 let raw = unsafe { &*self.as_raw() };
318 {
319 let mut state = self.state.ime.borrow_mut();
320 let current = ImeOwner::current(raw).filter(|owner| {
323 raw.PlatformImeData.WantVisible
324 && state
325 .presentation
326 .is_some_and(|p| p.owner == owner.widget && p.frame == raw.FrameCount)
327 });
328 state.reconcile_owner(current);
329 }
330 let (preedit, presentation) = {
332 let state = self.state.ime.borrow();
333 let Some(preedit) = state.preedit.clone() else {
334 return;
335 };
336 (preedit, state.presentation)
337 };
338 let ime = raw.PlatformImeData;
340 if raw.IO.AppFocusLost
341 || ImeOwner::current(raw) != Some(preedit.owner)
342 || !ime.WantVisible
343 || !presentation
344 .is_some_and(|p| p.owner == preedit.owner.widget && p.frame == raw.FrameCount)
345 {
346 self.clear_ime_preedit();
347 return;
348 }
349 let viewport = unsafe { sys::igFindViewportByID(ime.ViewportId) };
350 let presentation = presentation.expect("validated current-frame input presentation");
351 let size = presentation.size;
352 if viewport.is_null()
353 || presentation.font.is_null()
354 || !presentation.density.is_finite()
355 || presentation.density <= 0.0
356 || !size.is_finite()
357 || size <= 0.0
358 || !ime.InputPos.x.is_finite()
359 || !ime.InputPos.y.is_finite()
360 {
361 return;
362 }
363 let password = raw.InputTextState.Flags & sys::ImGuiInputTextFlags_Password != 0;
364 let masked;
365 let (text, cursor) = if password {
366 masked = "*".repeat(preedit.text.chars().count());
367 (
368 masked.as_str(),
369 preedit.cursor_range.map(|(a, b)| {
370 (
371 preedit.text[..a].chars().count(),
372 preedit.text[..b].chars().count(),
373 )
374 }),
375 )
376 } else {
377 (preedit.text.as_str(), preedit.cursor_range)
378 };
379 unsafe {
381 let font = presentation.font;
382 let previous_density = (*font).CurrentRasterizerDensity;
383 (*font).CurrentRasterizerDensity = presentation.density;
384 draw_preedit(font, size, viewport, ime.InputPos, text, cursor);
385 (*font).CurrentRasterizerDensity = previous_density;
386 }
387 }
388}
389
390unsafe fn draw_preedit(
391 font: *mut sys::ImFont,
392 size: f32,
393 viewport: *mut sys::ImGuiViewport,
394 anchor: sys::ImVec2_c,
395 text: &str,
396 cursor: Option<(usize, usize)>,
397) {
398 unsafe {
400 let measure = |text: &str| {
401 sys::ImFont_CalcTextSizeA(
402 font,
403 size,
404 f32::MAX,
405 0.0,
406 text.as_ptr().cast(),
407 text.as_ptr().add(text.len()).cast(),
408 std::ptr::null_mut(),
409 )
410 };
411 let offset = |byte: usize| {
412 let prefix = &text[..byte];
413 let line = prefix.rsplit('\n').next().unwrap_or("");
414 sys::ImVec2_c {
415 x: measure(line).x,
416 y: size * prefix.bytes().filter(|&b| b == b'\n').count() as f32,
417 }
418 };
419 let extent = measure(text);
420 let vp = &*viewport;
421 if !extent.x.is_finite() || !extent.y.is_finite() || vp.Size.x <= 0.0 || vp.Size.y <= 0.0 {
422 return;
423 }
424 let padding = 3.0;
425 let width = (extent.x + padding * 2.0).min(vp.Size.x);
426 let height = (extent.y.max(size) + padding * 2.0).min(vp.Size.y);
427 let left = anchor.x.max(vp.Pos.x).min(vp.Pos.x + vp.Size.x - width);
428 let top = anchor.y.max(vp.Pos.y).min(vp.Pos.y + vp.Size.y - height);
429 let caret = cursor.map(|(_, end)| offset(end));
430 let scroll_x = caret.map_or(0.0, |caret| (caret.x + padding * 2.0 - width).max(0.0));
431 let scroll_y = caret.map_or(0.0, |caret| {
432 (caret.y + size + padding * 2.0 - height).max(0.0)
433 });
434 let origin = sys::ImVec2_c {
435 x: left + padding - scroll_x,
436 y: top + padding - scroll_y,
437 };
438 let min = sys::ImVec2_c { x: left, y: top };
439 let max = sys::ImVec2_c {
440 x: left + width,
441 y: top + height,
442 };
443 let draw = sys::igGetForegroundDrawList_ViewportPtr(viewport);
444 sys::ImDrawList_PushClipRect(draw, min, max, true);
445 sys::ImDrawList_AddRectFilled(
446 draw,
447 min,
448 max,
449 sys::igGetColorU32_Col(sys::ImGuiCol_PopupBg, 1.0),
450 0.0,
451 0,
452 );
453 if let Some((a, b)) = cursor {
454 let start = a.min(b);
455 let end = a.max(b);
456 let mut byte = 0;
457 for line in text.split_inclusive('\n') {
458 let line_end = byte + line.len();
459 if start != end && start < line_end && end > byte {
460 let from = offset(start.max(byte));
461 let to = offset(end.min(line_end - usize::from(line.ends_with('\n'))));
462 sys::ImDrawList_AddRectFilled(
463 draw,
464 sys::ImVec2_c {
465 x: origin.x + from.x,
466 y: origin.y + from.y,
467 },
468 sys::ImVec2_c {
469 x: origin.x + to.x.max(from.x + 1.0),
470 y: origin.y + from.y + size,
471 },
472 sys::igGetColorU32_Col(sys::ImGuiCol_TextSelectedBg, 1.0),
473 0.0,
474 0,
475 );
476 }
477 byte = line_end;
478 }
479 }
480 let color = sys::igGetColorU32_Col(sys::ImGuiCol_Text, 1.0);
481 sys::ImDrawList_AddText_FontPtr(
482 draw,
483 font,
484 size,
485 origin,
486 color,
487 text.as_ptr().cast(),
488 text.as_ptr().add(text.len()).cast(),
489 0.0,
490 std::ptr::null(),
491 );
492 sys::ImDrawList_AddLine(
493 draw,
494 sys::ImVec2_c {
495 x: origin.x,
496 y: origin.y + extent.y.max(size),
497 },
498 sys::ImVec2_c {
499 x: origin.x + extent.x,
500 y: origin.y + extent.y.max(size),
501 },
502 color,
503 1.0,
504 0,
505 );
506 if let Some(caret) = caret {
507 let position = sys::ImVec2_c {
508 x: origin.x + caret.x,
509 y: origin.y + caret.y,
510 };
511 sys::ImDrawList_AddLine(
512 draw,
513 position,
514 sys::ImVec2_c {
515 x: position.x,
516 y: position.y + size,
517 },
518 color,
519 1.0,
520 0,
521 );
522 (*sys::igGetCurrentContext()).PlatformImeData.InputPos = position;
523 }
524 sys::ImDrawList_PopClipRect(draw);
525 }
526}