diff options
| author | 2025-02-03 02:33:40 +0100 | |
|---|---|---|
| committer | 2025-02-03 02:33:40 +0100 | |
| commit | c83809adb907498ba2a573ec9fb50936601ac8fc (patch) | |
| tree | 5009581a507014cda6850f0780bd315108d56bdd | |
| parent | 3a35fd6249eeb324379d3a14b020ccc48ec16fb4 (diff) | |
| download | iced-c83809adb907498ba2a573ec9fb50936601ac8fc.tar.gz iced-c83809adb907498ba2a573ec9fb50936601ac8fc.tar.bz2 iced-c83809adb907498ba2a573ec9fb50936601ac8fc.zip | |
Implement basic IME selection in `Preedit` overlay
| -rw-r--r-- | core/src/input_method.rs | 47 | ||||
| -rw-r--r-- | core/src/text.rs | 17 | ||||
| -rw-r--r-- | widget/src/text_editor.rs | 27 | ||||
| -rw-r--r-- | widget/src/text_input.rs | 13 | ||||
| -rw-r--r-- | winit/src/program/window_manager.rs | 120 | 
5 files changed, 181 insertions, 43 deletions
| diff --git a/core/src/input_method.rs b/core/src/input_method.rs index b25f29aa..f10a1c3b 100644 --- a/core/src/input_method.rs +++ b/core/src/input_method.rs @@ -23,10 +23,50 @@ pub enum InputMethod<T = String> {          /// Ideally, your widget will show pre-edits on-the-spot; but, since that can          /// be tricky, you can instead provide the current pre-edit here and the          /// runtime will display it as an overlay (i.e. "Over-the-spot IME"). -        preedit: Option<T>, +        preedit: Option<Preedit<T>>,      },  } +/// The pre-edit of an [`InputMethod`]. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Preedit<T = String> { +    /// The current content. +    pub content: T, +    /// The selected range of the content. +    pub selection: Option<Range<usize>>, +} + +impl<T> Preedit<T> { +    /// Creates a new empty [`Preedit`]. +    pub fn new() -> Self +    where +        T: Default, +    { +        Self::default() +    } + +    /// Turns a [`Preedit`] into its owned version. +    pub fn to_owned(&self) -> Preedit +    where +        T: AsRef<str>, +    { +        Preedit { +            content: self.content.as_ref().to_owned(), +            selection: self.selection.clone(), +        } +    } +} + +impl Preedit { +    /// Borrows the contents of a [`Preedit`]. +    pub fn as_ref(&self) -> Preedit<&str> { +        Preedit { +            content: &self.content, +            selection: self.selection.clone(), +        } +    } +} +  /// The purpose of an [`InputMethod`].  #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]  pub enum Purpose { @@ -84,10 +124,7 @@ impl InputMethod {                  *self = Self::Open {                      position: *position,                      purpose: *purpose, -                    preedit: preedit -                        .as_ref() -                        .map(AsRef::as_ref) -                        .map(str::to_owned), +                    preedit: preedit.as_ref().map(Preedit::to_owned),                  };              }              InputMethod::Allowed diff --git a/core/src/text.rs b/core/src/text.rs index c144fd24..8dde9e21 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -270,6 +270,23 @@ pub struct Span<'a, Link = (), Font = crate::Font> {      pub strikethrough: bool,  } +impl<Link, Font> Default for Span<'_, Link, Font> { +    fn default() -> Self { +        Self { +            text: Cow::default(), +            size: None, +            line_height: None, +            font: None, +            color: None, +            link: None, +            highlight: None, +            padding: Padding::default(), +            underline: false, +            strikethrough: false, +        } +    } +} +  /// A text highlight.  #[derive(Debug, Clone, Copy, PartialEq)]  pub struct Highlight { diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 72e15c28..26d05ccd 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -55,6 +55,7 @@ use std::borrow::Cow;  use std::cell::RefCell;  use std::fmt;  use std::ops::DerefMut; +use std::ops::Range;  use std::sync::Arc;  pub use text::editor::{Action, Edit, Line, LineEnding, Motion}; @@ -365,7 +366,7 @@ where          InputMethod::Open {              position,              purpose: input_method::Purpose::Normal, -            preedit: Some(preedit), +            preedit: Some(preedit.as_ref()),          }      }  } @@ -496,7 +497,7 @@ where  #[derive(Debug)]  pub struct State<Highlighter: text::Highlighter> {      focus: Option<Focus>, -    preedit: Option<String>, +    preedit: Option<input_method::Preedit>,      last_click: Option<mouse::Click>,      drag_click: Option<mouse::click::Kind>,      partial_scroll: f32, @@ -751,11 +752,15 @@ where                  }                  Update::InputMethod(update) => match update {                      Ime::Toggle(is_open) => { -                        state.preedit = is_open.then(String::new); +                        state.preedit = +                            is_open.then(input_method::Preedit::new);                      } -                    Ime::Preedit(text) => { +                    Ime::Preedit { content, selection } => {                          if state.focus.is_some() { -                            state.preedit = Some(text); +                            state.preedit = Some(input_method::Preedit { +                                content, +                                selection, +                            });                          }                      }                      Ime::Commit(text) => { @@ -1202,7 +1207,10 @@ enum Update<Message> {  enum Ime {      Toggle(bool), -    Preedit(String), +    Preedit { +        content: String, +        selection: Option<Range<usize>>, +    },      Commit(String),  } @@ -1272,8 +1280,11 @@ impl<Message> Update<Message> {                          input_method::Event::Opened                      ))))                  } -                input_method::Event::Preedit(content, _range) => { -                    Some(Update::InputMethod(Ime::Preedit(content))) +                input_method::Event::Preedit(content, selection) => { +                    Some(Update::InputMethod(Ime::Preedit { +                        content, +                        selection, +                    }))                  }                  input_method::Event::Commit(content) => {                      Some(Update::InputMethod(Ime::Commit(content))) diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index a1a1d3b5..4c9e46c1 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -440,7 +440,7 @@ where              } else {                  input_method::Purpose::Normal              }, -            preedit: Some(preedit), +            preedit: Some(preedit.as_ref()),          }      } @@ -1256,13 +1256,16 @@ where                      state.is_ime_open =                          matches!(event, input_method::Event::Opened) -                            .then(String::new); +                            .then(input_method::Preedit::new);                  } -                input_method::Event::Preedit(content, _range) => { +                input_method::Event::Preedit(content, selection) => {                      let state = state::<Renderer>(tree);                      if state.is_focused.is_some() { -                        state.is_ime_open = Some(content.to_owned()); +                        state.is_ime_open = Some(input_method::Preedit { +                            content: content.to_owned(), +                            selection: selection.clone(), +                        });                      }                  }                  input_method::Event::Commit(text) => { @@ -1514,7 +1517,7 @@ pub struct State<P: text::Paragraph> {      placeholder: paragraph::Plain<P>,      icon: paragraph::Plain<P>,      is_focused: Option<Focus>, -    is_ime_open: Option<String>, +    is_ime_open: Option<input_method::Preedit>,      is_dragging: bool,      is_pasting: Option<Value>,      last_click: Option<mouse::Click>, diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index 35a8d7dc..86cee973 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -1,5 +1,6 @@  use crate::conversion;  use crate::core::alignment; +use crate::core::input_method;  use crate::core::mouse;  use crate::core::renderer;  use crate::core::text; @@ -12,11 +13,13 @@ use crate::core::{  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; +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::sync::Arc; +  #[allow(missing_debug_implementations)]  pub struct WindowManager<P, C>  where @@ -226,16 +229,26 @@ where              self.raw.set_ime_purpose(conversion::ime_purpose(purpose)); -            if let Some(content) = preedit { -                if content.is_empty() { +            if let Some(preedit) = preedit { +                if preedit.content.is_empty() {                      self.preedit = None; -                } else if let Some(preedit) = &mut self.preedit { -                    preedit.update(position, &content, &self.renderer); +                } else if let Some(overlay) = &mut self.preedit { +                    overlay.update( +                        position, +                        &preedit, +                        self.state.background_color(), +                        &self.renderer, +                    );                  } else { -                    let mut preedit = Preedit::new(); -                    preedit.update(position, &content, &self.renderer); - -                    self.preedit = Some(preedit); +                    let mut overlay = Preedit::new(); +                    overlay.update( +                        position, +                        &preedit, +                        self.state.background_color(), +                        &self.renderer, +                    ); + +                    self.preedit = Some(overlay);                  }              }          } else { @@ -263,7 +276,8 @@ where      Renderer: text::Renderer,  {      position: Point, -    content: text::paragraph::Plain<Renderer::Paragraph>, +    content: Renderer::Paragraph, +    spans: Vec<text::Span<'static, (), Renderer::Font>>,  }  impl<Renderer> Preedit<Renderer> @@ -273,24 +287,67 @@ where      fn new() -> Self {          Self {              position: Point::ORIGIN, -            content: text::paragraph::Plain::default(), +            spans: Vec::new(), +            content: Renderer::Paragraph::default(),          }      } -    fn update(&mut self, position: Point, text: &str, renderer: &Renderer) { +    fn update( +        &mut self, +        position: Point, +        preedit: &input_method::Preedit, +        background: Color, +        renderer: &Renderer, +    ) {          self.position = position; -        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, -            shaping: text::Shaping::Advanced, -            wrapping: text::Wrapping::None, -        }); +        let spans = match &preedit.selection { +            Some(selection) => { +                vec![ +                    text::Span { +                        text: Cow::Borrowed( +                            &preedit.content[..selection.start], +                        ), +                        ..text::Span::default() +                    }, +                    text::Span { +                        text: Cow::Borrowed( +                            if selection.start == selection.end { +                                "\u{200A}" +                            } else { +                                &preedit.content[selection.start..selection.end] +                            }, +                        ), +                        color: Some(background), +                        ..text::Span::default() +                    }, +                    text::Span { +                        text: Cow::Borrowed(&preedit.content[selection.end..]), +                        ..text::Span::default() +                    }, +                ] +            } +            _ => vec![text::Span { +                text: Cow::Borrowed(&preedit.content), +                ..text::Span::default() +            }], +        }; + +        if spans != self.spans.as_slice() { +            use text::Paragraph as _; + +            self.content = Renderer::Paragraph::with_spans(Text { +                content: &spans, +                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, +                shaping: text::Shaping::Advanced, +                wrapping: text::Wrapping::None, +            }); +        }      }      fn draw( @@ -300,6 +357,8 @@ where          background: Color,          viewport: &Rectangle,      ) { +        use text::Paragraph as _; +          if self.content.min_width() < 1.0 {              return;          } @@ -329,7 +388,7 @@ where              );              renderer.fill_paragraph( -                self.content.raw(), +                &self.content,                  bounds.position(),                  color,                  bounds, @@ -347,6 +406,17 @@ where                  },                  color,              ); + +            for span_bounds in self.content.span_bounds(1) { +                renderer.fill_quad( +                    renderer::Quad { +                        bounds: span_bounds +                            + (bounds.position() - Point::ORIGIN), +                        ..Default::default() +                    }, +                    color, +                ); +            }          });      }  } | 
