Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support astral plane characters on Windows #174

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions druid-shell/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,10 @@ struct WndState {
/// The `char` of the last `WM_CHAR` event, if there has not already been
/// a `WM_KEYUP` event.
stashed_char: Option<char>,
//TODO: track surrogate orphan
/// Windows sends astral plane characters through as two WM_CHAR events,
/// using surrogate pairs (two UTF-16 code units). We stash the high so we
/// can reassemble it with the low when it comes.
stashed_high_surrogate: Option<u32>,
}

/// A structure that owns resources for the `WinCtx` (so it lasts long enough).
Expand Down Expand Up @@ -509,10 +512,33 @@ impl WndProc for MyWndProc {
WM_CHAR => {
if let Ok(mut s) = self.state.try_borrow_mut() {
let s = s.as_mut().unwrap();
//FIXME: this can receive lone surrogate pairs?
let key_code = s.stashed_key_code;
// WM_CHAR wparam is a UTF-16 code unit. Astral plane
// characters are sent as surrogate pairs in two events;
// so high surrogates we stash until the next WM_CHAR
// message comes through with the low surrogate, then we
// decode it at that time. Because Rust's char type
// represents Unicode scalar values, we can't represent
// lone surrogates; mismatched surrogates would indicate
// a buggy user IME, and there's really nothing we can do
// about it, so we discard such things, silently for now.
let code_unit = wparam as u32;
let scalar = if 0xD800 <= code_unit && code_unit <= 0xDC00 {
s.stashed_high_surrogate = Some(code_unit);
return None;
} else if 0xDC00 <= code_unit && code_unit <= 0xDFFF {
if let Some(high_surrogate) = s.stashed_high_surrogate.take() {
// std::char::decode_utf16 could do this too, but this is lighter.
0x10000 + ((high_surrogate - 0xD800) << 10) + (code_unit - 0xDC00)
} else {
return None;
}
} else {
s.stashed_high_surrogate.take();
code_unit
};

s.stashed_char = std::char::from_u32(wparam as u32);
s.stashed_char = std::char::from_u32(scalar);
let text = match s.stashed_char {
Some(c) => c,
None => {
Expand Down Expand Up @@ -860,6 +886,7 @@ impl WindowBuilder {
dpi,
stashed_key_code: KeyCode::Unknown(0.into()),
stashed_char: None,
stashed_high_surrogate: None,
};
win.wndproc.connect(&handle, state);
mem::drop(win);
Expand Down
9 changes: 5 additions & 4 deletions src/widget/textbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,12 +431,13 @@ impl Widget<String> for TextBoxRaw {
self.reset_cursor_blink(ctx);
}
// Actual typing
k_e if k_e.key_code.is_printable() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with removing this is that control characters get inserted into the string. I agree with you that we shouldn't be looking at the keycode (that's not really a source of truth), but should have an API indicating whether the keyboard event has printable text.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what situations do control characters come through this path? I haven’t tried much, but nothing immediately sprang to mind.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It happens when you press, say, control-A, but the command isn't interpreted.

k_e => {
let incoming_text = k_e.text().unwrap_or("");
self.insert(data, incoming_text);
self.reset_cursor_blink(ctx);
if !incoming_text.is_empty() {
self.insert(data, incoming_text);
self.reset_cursor_blink(ctx);
}
}
_ => {}
}
self.update_hscroll(ctx.text(), env, data);
ctx.invalidate();
Expand Down