From ae10adda74320e8098bfeb401f12a278e1e7b3e2 Mon Sep 17 00:00:00 2001
From: Héctor Ramón Jiménez <hector@hecrj.dev>
Date: Sun, 2 Feb 2025 20:45:29 +0100
Subject: Refactor and simplify `input_method` API

---
 winit/src/conversion.rs             |  36 +++++----
 winit/src/program.rs                | 156 ++----------------------------------
 winit/src/program/state.rs          |  12 +--
 winit/src/program/window_manager.rs | 145 ++++++++++++++++++++++++++++++++-
 4 files changed, 174 insertions(+), 175 deletions(-)

(limited to 'winit')

diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs
index c7f9aaaf..ab84afff 100644
--- a/winit/src/conversion.rs
+++ b/winit/src/conversion.rs
@@ -141,6 +141,7 @@ pub fn window_event(
     scale_factor: f64,
     modifiers: winit::keyboard::ModifiersState,
 ) -> Option<Event> {
+    use winit::event::Ime;
     use winit::event::WindowEvent;
 
     match event {
@@ -284,19 +285,15 @@ pub fn window_event(
                 self::modifiers(new_modifiers.state()),
             )))
         }
-        WindowEvent::Ime(ime) => {
-            use winit::event::Ime;
-            println!("ime event: {:?}", ime);
-            Some(Event::InputMethod(match ime {
-                Ime::Enabled => input_method::Event::Enabled,
-                Ime::Preedit(s, size) => input_method::Event::Preedit(
-                    s,
-                    size.map(|(start, end)| (start..end)),
-                ),
-                Ime::Commit(s) => input_method::Event::Commit(s),
-                Ime::Disabled => input_method::Event::Disabled,
-            }))
-        }
+        WindowEvent::Ime(event) => Some(Event::InputMethod(match event {
+            Ime::Enabled => input_method::Event::Opened,
+            Ime::Preedit(content, size) => input_method::Event::Preedit(
+                content,
+                size.map(|(start, end)| (start..end)),
+            ),
+            Ime::Commit(content) => input_method::Event::Commit(content),
+            Ime::Disabled => input_method::Event::Closed,
+        })),
         WindowEvent::Focused(focused) => Some(Event::Window(if focused {
             window::Event::Focused
         } else {
@@ -1174,7 +1171,7 @@ pub fn resize_direction(
     }
 }
 
-/// Converts some [`window::Icon`] into it's `winit` counterpart.
+/// Converts some [`window::Icon`] into its `winit` counterpart.
 ///
 /// Returns `None` if there is an error during the conversion.
 pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> {
@@ -1183,6 +1180,17 @@ pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> {
     winit::window::Icon::from_rgba(pixels, size.width, size.height).ok()
 }
 
+/// Convertions some [`input_method::Purpose`] to its `winit` counterpart.
+pub fn ime_purpose(
+    purpose: input_method::Purpose,
+) -> winit::window::ImePurpose {
+    match purpose {
+        input_method::Purpose::Normal => winit::window::ImePurpose::Normal,
+        input_method::Purpose::Secure => winit::window::ImePurpose::Password,
+        input_method::Purpose::Terminal => winit::window::ImePurpose::Terminal,
+    }
+}
+
 // See: https://en.wikipedia.org/wiki/Private_Use_Areas
 fn is_private_use(c: char) -> bool {
     ('\u{E000}'..='\u{F8FF}').contains(&c)
diff --git a/winit/src/program.rs b/winit/src/program.rs
index 688d6731..302bc6c3 100644
--- a/winit/src/program.rs
+++ b/winit/src/program.rs
@@ -3,8 +3,6 @@ mod state;
 mod window_manager;
 
 pub use state::State;
-use winit::dpi::LogicalPosition;
-use winit::dpi::LogicalSize;
 
 use crate::conversion;
 use crate::core;
@@ -581,8 +579,6 @@ async fn run_instance<P, C>(
     let mut clipboard = Clipboard::unconnected();
     let mut compositor_receiver: Option<oneshot::Receiver<_>> = None;
 
-    let mut preedit = Preedit::<P>::new();
-
     debug.startup_finished();
 
     loop {
@@ -878,29 +874,15 @@ async fn run_instance<P, C>(
 
                         if let user_interface::State::Updated {
                             redraw_request,
-                            caret_info,
+                            input_method,
                         } = ui_state
                         {
-                            match redraw_request {
-                                Some(window::RedrawRequest::NextFrame) => {
-                                    window.raw.request_redraw();
-                                    window.redraw_at = None;
-                                }
-                                Some(window::RedrawRequest::At(at)) => {
-                                    window.redraw_at = Some(at);
-                                }
-                                None => {}
-                            }
-
-                            if let Some(caret_info) = caret_info {
-                                update_input_method(
-                                    window,
-                                    &mut preedit,
-                                    &caret_info,
-                                );
-                            }
+                            window.request_redraw(redraw_request);
+                            window.request_input_method(input_method);
                         }
 
+                        window.draw_preedit();
+
                         debug.render_started();
                         match compositor.present(
                             &mut window.renderer,
@@ -1048,31 +1030,14 @@ async fn run_instance<P, C>(
                             match ui_state {
                                 user_interface::State::Updated {
                                     redraw_request: _redraw_request,
-                                    caret_info,
+                                    input_method,
                                 } => {
                                     #[cfg(not(
                                         feature = "unconditional-rendering"
                                     ))]
-                                    match _redraw_request {
-                                        Some(
-                                            window::RedrawRequest::NextFrame,
-                                        ) => {
-                                            window.raw.request_redraw();
-                                            window.redraw_at = None;
-                                        }
-                                        Some(window::RedrawRequest::At(at)) => {
-                                            window.redraw_at = Some(at);
-                                        }
-                                        None => {}
-                                    }
+                                    window.request_redraw(_redraw_request);
 
-                                    if let Some(caret_info) = caret_info {
-                                        update_input_method(
-                                            window,
-                                            &mut preedit,
-                                            &caret_info,
-                                        );
-                                    }
+                                    window.request_input_method(input_method);
                                 }
                                 user_interface::State::Outdated => {
                                     uis_stale = true;
@@ -1165,111 +1130,6 @@ async fn run_instance<P, C>(
     let _ = ManuallyDrop::into_inner(user_interfaces);
 }
 
-fn update_input_method<P, C>(
-    window: &mut crate::program::window_manager::Window<P, C>,
-    preedit: &mut Preedit<P>,
-    caret_info: &crate::core::CaretInfo,
-) where
-    P: Program,
-    C: Compositor<Renderer = P::Renderer> + 'static,
-{
-    window.raw.set_ime_allowed(caret_info.input_method_allowed);
-    window.raw.set_ime_cursor_area(
-        LogicalPosition::new(caret_info.position.x, caret_info.position.y),
-        LogicalSize::new(10, 10),
-    );
-
-    let text = window.state.preedit();
-    if !text.is_empty() {
-        preedit.update(text.as_str(), &window.renderer);
-        preedit.fill(
-            &mut window.renderer,
-            window.state.text_color(),
-            window.state.background_color(),
-            caret_info.position,
-        );
-    }
-}
-
-struct Preedit<P: Program> {
-    content: Option<<P::Renderer as core::text::Renderer>::Paragraph>,
-}
-
-impl<P: Program> Preedit<P> {
-    fn new() -> Self {
-        Self { content: None }
-    }
-
-    fn update(&mut self, text: &str, renderer: &P::Renderer) {
-        use core::text::Paragraph as _;
-        use core::text::Renderer as _;
-
-        self.content = Some(
-            <P::Renderer as core::text::Renderer>::Paragraph::with_text(
-                core::Text::<&str, <P::Renderer as core::text::Renderer>::Font> {
-                    content: text,
-                    bounds: Size::INFINITY,
-                    size: renderer.default_size(),
-                    line_height: core::text::LineHeight::default(),
-                    font: renderer.default_font(),
-                    horizontal_alignment: core::alignment::Horizontal::Left,
-                    vertical_alignment: core::alignment::Vertical::Top, //Bottom,
-                    shaping: core::text::Shaping::Advanced,
-                    wrapping: core::text::Wrapping::None,
-                },
-            ),
-        );
-    }
-
-    fn fill(
-        &self,
-        renderer: &mut P::Renderer,
-        fore_color: core::Color,
-        bg_color: core::Color,
-        caret_position: Point,
-    ) {
-        use core::text::Paragraph as _;
-        use core::text::Renderer as _;
-        use core::Renderer as _;
-
-        let Some(ref content) = self.content else {
-            return;
-        };
-        if content.min_width() < 1.0 {
-            return;
-        }
-
-        let top_left = Point::new(
-            caret_position.x,
-            caret_position.y - content.min_height(),
-        );
-        let bounds = core::Rectangle::new(top_left, content.min_bounds());
-        renderer.with_layer(bounds, |renderer| {
-            renderer.fill_quad(
-                core::renderer::Quad {
-                    bounds,
-                    ..Default::default()
-                },
-                core::Background::Color(bg_color),
-            );
-
-            let underline = 2.;
-            renderer.fill_quad(
-                core::renderer::Quad {
-                    bounds: bounds.shrink(core::Padding {
-                        top: bounds.height - underline,
-                        ..Default::default()
-                    }),
-                    ..Default::default()
-                },
-                core::Background::Color(fore_color),
-            );
-
-            renderer.fill_paragraph(content, top_left, fore_color, bounds);
-        });
-    }
-}
-
 /// Builds a window's [`UserInterface`] for the [`Program`].
 fn build_user_interface<'a, P: Program>(
     program: &'a P,
diff --git a/winit/src/program/state.rs b/winit/src/program/state.rs
index 6361d1ba..e883d04a 100644
--- a/winit/src/program/state.rs
+++ b/winit/src/program/state.rs
@@ -4,7 +4,7 @@ use crate::core::{Color, Size};
 use crate::graphics::Viewport;
 use crate::program::Program;
 
-use winit::event::{Ime, Touch, WindowEvent};
+use winit::event::{Touch, WindowEvent};
 use winit::window::Window;
 
 use std::fmt::{Debug, Formatter};
@@ -22,7 +22,6 @@ where
     modifiers: winit::keyboard::ModifiersState,
     theme: P::Theme,
     style: theme::Style,
-    preedit: String,
 }
 
 impl<P: Program> Debug for State<P>
@@ -74,7 +73,6 @@ where
             modifiers: winit::keyboard::ModifiersState::default(),
             theme,
             style,
-            preedit: String::default(),
         }
     }
 
@@ -138,11 +136,6 @@ where
         self.style.text_color
     }
 
-    /// TODO
-    pub fn preedit(&self) -> String {
-        self.preedit.clone()
-    }
-
     /// Processes the provided window event and updates the [`State`] accordingly.
     pub fn update(
         &mut self,
@@ -186,9 +179,6 @@ where
             WindowEvent::ModifiersChanged(new_modifiers) => {
                 self.modifiers = new_modifiers.state();
             }
-            WindowEvent::Ime(Ime::Preedit(text, _)) => {
-                self.preedit = text.clone();
-            }
             #[cfg(feature = "debug")]
             WindowEvent::KeyboardInput {
                 event:
diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs
index a3c991df..cd49a8b4 100644
--- a/winit/src/program/window_manager.rs
+++ b/winit/src/program/window_manager.rs
@@ -1,13 +1,20 @@
+use crate::conversion;
+use crate::core::alignment;
 use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::text;
 use crate::core::theme;
 use crate::core::time::Instant;
-use crate::core::window::Id;
-use crate::core::{Point, Size};
+use crate::core::window::{Id, RedrawRequest};
+use crate::core::{
+    Color, InputMethod, Padding, Point, Rectangle, Size, Text, Vector,
+};
 use crate::graphics::Compositor;
 use crate::program::{Program, State};
 
 use std::collections::BTreeMap;
 use std::sync::Arc;
+use winit::dpi::{LogicalPosition, LogicalSize};
 use winit::monitor::MonitorHandle;
 
 #[allow(missing_debug_implementations)]
@@ -65,6 +72,7 @@ where
                 renderer,
                 mouse_interaction: mouse::Interaction::None,
                 redraw_at: None,
+                preedit: None,
             },
         );
 
@@ -155,6 +163,7 @@ where
     pub surface: C::Surface,
     pub renderer: P::Renderer,
     pub redraw_at: Option<Instant>,
+    preedit: Option<Preedit<P::Renderer>>,
 }
 
 impl<P, C> Window<P, C>
@@ -179,4 +188,136 @@ where
 
         Size::new(size.width, size.height)
     }
+
+    pub fn request_redraw(&mut self, redraw_request: RedrawRequest) {
+        match redraw_request {
+            RedrawRequest::NextFrame => {
+                self.raw.request_redraw();
+                self.redraw_at = None;
+            }
+            RedrawRequest::At(at) => {
+                self.redraw_at = Some(at);
+            }
+            RedrawRequest::Wait => {}
+        }
+    }
+
+    pub fn request_input_method(&mut self, input_method: InputMethod) {
+        self.raw.set_ime_allowed(match input_method {
+            InputMethod::Disabled => false,
+            InputMethod::Allowed | InputMethod::Open { .. } => true,
+        });
+
+        if let InputMethod::Open {
+            position,
+            purpose,
+            preedit,
+        } = input_method
+        {
+            self.raw.set_ime_cursor_area(
+                LogicalPosition::new(position.x, position.y),
+                LogicalSize::new(10, 10),
+            );
+
+            self.raw.set_ime_purpose(conversion::ime_purpose(purpose));
+
+            if let Some(content) = preedit {
+                if let Some(preedit) = &mut self.preedit {
+                    preedit.update(&content, &self.renderer);
+                } else {
+                    let mut preedit = Preedit::new();
+                    preedit.update(&content, &self.renderer);
+
+                    self.preedit = Some(preedit);
+                }
+            }
+        } else {
+            self.preedit = None;
+        }
+    }
+
+    pub fn draw_preedit(&mut self) {
+        if let Some(preedit) = &self.preedit {
+            preedit.draw(
+                &mut self.renderer,
+                self.state.text_color(),
+                self.state.background_color(),
+            );
+        }
+    }
+}
+
+struct Preedit<Renderer>
+where
+    Renderer: text::Renderer,
+{
+    position: Point,
+    content: text::paragraph::Plain<Renderer::Paragraph>,
+}
+
+impl<Renderer> Preedit<Renderer>
+where
+    Renderer: text::Renderer,
+{
+    fn new() -> Self {
+        Self {
+            position: Point::ORIGIN,
+            content: text::paragraph::Plain::default(),
+        }
+    }
+
+    fn update(&mut self, text: &str, renderer: &Renderer) {
+        self.content.update(Text {
+            content: text,
+            bounds: Size::INFINITY,
+            size: renderer.default_size(),
+            line_height: text::LineHeight::default(),
+            font: renderer.default_font(),
+            horizontal_alignment: alignment::Horizontal::Left,
+            vertical_alignment: alignment::Vertical::Top, //Bottom,
+            shaping: text::Shaping::Advanced,
+            wrapping: text::Wrapping::None,
+        });
+    }
+
+    fn draw(&self, renderer: &mut Renderer, color: Color, background: Color) {
+        if self.content.min_width() < 1.0 {
+            return;
+        }
+
+        let top_left =
+            self.position - Vector::new(0.0, self.content.min_height());
+
+        let bounds = Rectangle::new(top_left, self.content.min_bounds());
+
+        renderer.with_layer(bounds, |renderer| {
+            renderer.fill_quad(
+                renderer::Quad {
+                    bounds,
+                    ..Default::default()
+                },
+                background,
+            );
+
+            renderer.fill_paragraph(
+                self.content.raw(),
+                top_left,
+                color,
+                bounds,
+            );
+
+            const UNDERLINE: f32 = 2.0;
+
+            renderer.fill_quad(
+                renderer::Quad {
+                    bounds: bounds.shrink(Padding {
+                        top: bounds.height - UNDERLINE,
+                        ..Default::default()
+                    }),
+                    ..Default::default()
+                },
+                color,
+            );
+        });
+    }
 }
-- 
cgit