Skip to content

web: Implement basic IME #19896

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Conversation

kjarosh
Copy link
Member

@kjarosh kjarosh commented Mar 23, 2025

This patch implements IME preediting and committing on web. It does not
implement moving the cursor and proper positioning yet.

@kjarosh kjarosh added A-web Area: Web & Extensions text Issues relating to text rendering/input input Issues relating to user input in Flash content T-compat Type: Compatibility with Flash Player ime Issues related to Input Method Editor labels Mar 23, 2025
@kjarosh kjarosh added the waiting-on-review Waiting on review from a Ruffle team member label Mar 23, 2025
@danielhjacobs
Copy link
Contributor

This isn't working on Android when I try to type in an EditText with the virtual keyboard.

@danielhjacobs
Copy link
Contributor

A virtual keyboard like the one on Android isComposing.

@danielhjacobs
Copy link
Contributor

To explain the current logic, which I guess may need comments.

  1. If you type a single character using the virtual keyboard, that character fires a keydown and then keyup event into the focused EditText.
  2. If you backspace or delete a single character using the virtual keyboard, that backspace/delete fires a keydown and then keyup event into the focused EditText.
  3. If you paste a string using the virtual keyboard, each character in that string in sequence fires a keydown and then keyup event into the focused EditText.

@danielhjacobs
Copy link
Contributor

I'm guessing the IME logic can be used to support this more properly, but landing this PR without IME support would regress the virtual keyboard.

@danielhjacobs
Copy link
Contributor

danielhjacobs commented Mar 24, 2025

Maybe we can do exactly this but also keydown and keyup the event.data character(s) on compositionend.

@kjarosh kjarosh force-pushed the web-basic-ime branch 2 times, most recently from a00d15d to 2af747a Compare March 29, 2025 21:07
@kjarosh
Copy link
Member Author

kjarosh commented Mar 29, 2025

@danielhjacobs can you check if the current code works properly? It does for me

@danielhjacobs
Copy link
Contributor

Tried with GBoard and it worked perfectly. With Samsung Keyboard it's unfortunately a different story, see recording.

Screen_Recording_20250329_171917_Chrome.online-video-cutter.com.mp4

@kjarosh
Copy link
Member Author

kjarosh commented Mar 29, 2025

It seems that this keyboard uses IME for inputting all text. Without implementing IME on web we cannot have both IME preview and IME input working :/ This PR breaks IME preview, but fixes IME input.

@kjarosh kjarosh changed the title web: Ignore IME composing events when inputting text web: Implement basic IME Mar 30, 2025
@kjarosh
Copy link
Member Author

kjarosh commented Mar 30, 2025

Okay, as IME on web is a mess, I've decided to implement basic IME mechanics (including preediting) on web.

@danielhjacobs @n0samu You can test it out here: https://kjarosh.github.io/ruffle/pr19896/

@kjarosh kjarosh added A-input Area: Input handling and removed A-web Area: Web & Extensions input Issues relating to user input in Flash content labels Mar 31, 2025
This patch implements IME preediting and committing on web. It does not
implement moving the cursor and proper positioning yet.
@danielhjacobs
Copy link
Contributor

As stated on Discord, but putting here for future people, this is now working with Samsung Keyboard.

@kjarosh
Copy link
Member Author

kjarosh commented Apr 1, 2025

@jmousy Could you check if it works properly? It's available at https://kjarosh.github.io/ruffle/pr19896/

@jmousy
Copy link
Contributor

jmousy commented Apr 2, 2025

This is a summary of the chat on Discord.
Tested across OS and keyboard software on the same text and flash file.

  • '*' is cursor
  • 'Default' means using a regular hardware keyboard.
  • This is the result when you type "가나다" (rkskek, ㄱ + ㅏ + ㄴ + ㅏ + ㄷ + ㅏ).
  • Tested with the following flash files: 흥해라편의점.zip
  • Windows: Windows 10 24H2 & Google Chrome 135
  • macOS: macOS 15.3.2 & Google Chrome 135
  • iOS: iPhone 13 Pro iOS 18.4 & Safari
  • Android: Samsung Galaxy Z Fold 6 & Android 14 & One UI 6.1.1 & Samsung Internet
  • Linux: Ubuntu 24.04.2 LTS & Firefox latest (with VMware virtual machine)
OS Keyboard Type Result Other Issue
iOS Default ㄱㅏㄴㅏㄷㅏ*
iOS GBoard The text you entered appears in the following order and then disappears: ㄱ-가-간-{empty}-낟-{empty}
Android Samsung Keyboard 가나다*
Android GBoard 가나다* If you try to type "123가나다", it will be entered as "123ㄱㅏ나다".
Windows Default 가나다* If you click on the screen after entering text "가나다", an extra "다" is entered: "가나다다"
macOS Default ㄱ가나다*ㅏㄷㅏㄴㅏ The cursor will be positioned after "다" not the end. Also, if you type only "ㄱ", it will output "ㄱㄱ".
Linux Default 가나다*

if (!event || event.isComposing || event.inputType === 'insertCompositionText') {
// Ignore composing events, we'll get the composed text at the end
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Unfortunately for Windows IME to work we may need this:

    const now = Date.now();
    
    // If input occurs immediately (within 5ms) after compositionend, ignore it (Windows IME case)
    // 5 is arbitrary, can be adjusted
    if (now - lastCompositionEndTime < 5) {
        return;
    }

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd rather go with implementing deleteContentBackward than adding such hacks, but that's out of scope of this PR, as it's not IME

Copy link
Contributor

@danielhjacobs danielhjacobs Apr 3, 2025

Choose a reason for hiding this comment

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

Fair, it could be this I imagine, right?

            if (event.inputType === "deleteContentBackward") {
              for (const eventType of ["keydown", "keyup"]) {
                this.element.dispatchEvent(
                    new KeyboardEvent(eventType, {
                        key: "Backspace",
                        bubbles: true,
                    }),
                );
              }
            }

And deleteContentForward would be the same but with the Delete key.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think it's that easy, as it might be more than one character to delete. We would have to make sure that it's triggered for one character only, by e.g. calling preventDefault in beforeinput for deleteContentBackward when there's more than one character.

Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure it's not the exact same as a backspace? The description certainly sounds that way:

delete the content directly before the caret position and this intention is not covered by another inputType or delete the selection with the selection collapsing to its start after the deletion

https://w3c.github.io/input-events/#interface-InputEvent-Attributes

Copy link
Contributor

Choose a reason for hiding this comment

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

Namely, "directly before the caret"

Copy link
Contributor

@danielhjacobs danielhjacobs Apr 3, 2025

Choose a reason for hiding this comment

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

I should note, even when I type the whole 가나다 on Windows, the insertText only contains 다 and it is fired after a single deleteContentBackward which occurs after compositionend, where compositionend has 가나다 as data

Copy link
Contributor

@danielhjacobs danielhjacobs Apr 3, 2025

Choose a reason for hiding this comment

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

That's why "If you click on the screen after entering text "가나다", an extra "다" is entered: "가나다다""

Copy link
Member Author

Choose a reason for hiding this comment

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

Namely, "directly before the caret"

You can have more than one character before the caret. And note we're talking about automated events, not events caused directly by user input. If you wanted to remove the whole text inputted by IME and type it again, which event would you use for that? You're not deleting a word, not a line, there's no other inputType to choose in this case.

You can also check Japanese IME, as it works a bit differently than Korean. I suspect Windows may delete more than one character there.

@@ -1265,7 +1278,24 @@ export class InnerPlayer {
);
}
}
input.value = "";
this.virtualKeyboard.value = "";
Copy link
Contributor

@danielhjacobs danielhjacobs Apr 3, 2025

Choose a reason for hiding this comment

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

Relates to advanced text input so probably needs to be handled in a follow-up, but I have discovered that by clearing the input value on every input, when you click a suggestion with Android GBoard, it does not fire the expected deleteContentBackward event(s) if the suggestion has different characters from the entered text, probably because it does not think it has to do so as there is no text in the text field.

Maybe we can store and clear the thing we want to type as a data-* attribute instead, and leave the input alone unless a new text area gets focused, or set its value to the actual text field content from the Rust side.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I've been thinking about this, but that's a lot of work: you need to sync not only the text but also the caret from both sides (Rust, web). Also there's the issue of multiline fields, text restrictions, position of the text field, available glyphs, etc.

@jmousy
Copy link
Contributor

jmousy commented Apr 4, 2025

Below is the output from the link below, as requested by Daniel Jacobs on Discord:
https://codepen.io/danieljacobs/pen/ByaMLpL

Input text: '가나다' (rkskek)

iOS 18.4 (default keyboard)
keydown - key: ㄱ, data: N/A, inputType: N/A, isComposing: N/A
keypress - key: ㄱ, data: N/A, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: ㄱ, inputType: insertText, isComposing: N/A
input - key: N/A, data: ㄱ, inputType: insertText, isComposing: N/A
keyup - key: ㄱ, data: N/A, inputType: N/A, isComposing: N/A
keydown - key: ㅏ, data: N/A, inputType: N/A, isComposing: N/A
keypress - key: ㅏ, data: N/A, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: ㅏ, inputType: insertText, isComposing: N/A
input - key: N/A, data: ㅏ, inputType: insertText, isComposing: N/A
keyup - key: ㅏ, data: N/A, inputType: N/A, isComposing: N/A
keydown - key: ㄴ, data: N/A, inputType: N/A, isComposing: N/A
keypress - key: ㄴ, data: N/A, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: ㄴ, inputType: insertText, isComposing: N/A
input - key: N/A, data: ㄴ, inputType: insertText, isComposing: N/A
keyup - key: ㄴ, data: N/A, inputType: N/A, isComposing: N/A
keydown - key: ㅏ, data: N/A, inputType: N/A, isComposing: N/A
keypress - key: ㅏ, data: N/A, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: ㅏ, inputType: insertText, isComposing: N/A
input - key: N/A, data: ㅏ, inputType: insertText, isComposing: N/A
keyup - key: ㅏ, data: N/A, inputType: N/A, isComposing: N/A
keydown - key: ㄷ, data: N/A, inputType: N/A, isComposing: N/A
keypress - key: ㄷ, data: N/A, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: ㄷ, inputType: insertText, isComposing: N/A
input - key: N/A, data: ㄷ, inputType: insertText, isComposing: N/A
keyup - key: ㄷ, data: N/A, inputType: N/A, isComposing: N/A
keydown - key: ㅏ, data: N/A, inputType: N/A, isComposing: N/A
keypress - key: ㅏ, data: N/A, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: ㅏ, inputType: insertText, isComposing: N/A
input - key: N/A, data: ㅏ, inputType: insertText, isComposing: N/A
keyup - key: ㅏ, data: N/A, inputType: N/A, isComposing: N/A
Android 14 (samsung keyboard)
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: N/A
compositionstart - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
compositionupdate - key: N/A, data: ㄱ, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: ㄱ, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: ㄱ, inputType: insertCompositionText, isComposing: true
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 가, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 간, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 간, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 간, inputType: insertCompositionText, isComposing: true
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 가, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
compositionend - key: N/A, data: 가, inputType: N/A, isComposing: N/A
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: N/A
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: N/A
compositionstart - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
compositionupdate - key: N/A, data: 나, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 낟, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 낟, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 낟, inputType: insertCompositionText, isComposing: true
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 나, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
compositionend - key: N/A, data: 나, inputType: N/A, isComposing: N/A
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: N/A
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: N/A
compositionstart - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
compositionupdate - key: N/A, data: 다, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 다, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 다, inputType: insertCompositionText, isComposing: true
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
compositionend - key: N/A, data: 다, inputType: N/A, isComposing: N/A
Android (GBoard)
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: N/A
compositionstart - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
compositionupdate - key: N/A, data: ㄱ, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: ㄱ, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: ㄱ, inputType: insertCompositionText, isComposing: true
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 가, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 간, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 간, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 간, inputType: insertCompositionText, isComposing: true
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 가나, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 가나, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 가나, inputType: insertCompositionText, isComposing: true
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 가낟, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 가낟, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 가낟, inputType: insertCompositionText, isComposing: true
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
keydown - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 가나다, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 가나다, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 가나다, inputType: insertCompositionText, isComposing: true
keyup - key: Unidentified, data: N/A, inputType: N/A, isComposing: true
compositionend - key: N/A, data: 가나다, inputType: N/A, isComposing: N/A
Windows 11
keydown - key: Process, data: N/A, inputType: N/A, isComposing: N/A
compositionstart - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
compositionupdate - key: N/A, data: ㄱ, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: ㄱ, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: ㄱ, inputType: insertCompositionText, isComposing: true
keyup - key: Process, data: N/A, inputType: N/A, isComposing: true
keyup - key: r, data: N/A, inputType: N/A, isComposing: true
keydown - key: Process, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 가, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
keyup - key: Process, data: N/A, inputType: N/A, isComposing: true
keyup - key: k, data: N/A, inputType: N/A, isComposing: true
keydown - key: Process, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 간, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 간, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 간, inputType: insertCompositionText, isComposing: true
keyup - key: Process, data: N/A, inputType: N/A, isComposing: true
keyup - key: s, data: N/A, inputType: N/A, isComposing: true
keydown - key: Process, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 가, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
compositionend - key: N/A, data: 가, inputType: N/A, isComposing: N/A
compositionstart - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
compositionupdate - key: N/A, data: 나, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
keyup - key: Process, data: N/A, inputType: N/A, isComposing: true
keyup - key: k, data: N/A, inputType: N/A, isComposing: true
keydown - key: Process, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 낟, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 낟, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 낟, inputType: insertCompositionText, isComposing: true
keyup - key: Process, data: N/A, inputType: N/A, isComposing: true
keyup - key: e, data: N/A, inputType: N/A, isComposing: true
keydown - key: Process, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 나, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
compositionend - key: N/A, data: 나, inputType: N/A, isComposing: N/A
compositionstart - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
compositionupdate - key: N/A, data: 다, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 다, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 다, inputType: insertCompositionText, isComposing: true
keyup - key: Process, data: N/A, inputType: N/A, isComposing: true
keyup - key: k, data: N/A, inputType: N/A, isComposing: true
compositionend - key: N/A, data: 다, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 다, inputType: insertText, isComposing: N/A
input - key: N/A, data: 다, inputType: insertText, isComposing: N/A
Ubuntu 24.04
keydown - key: Process, data: N/A, inputType: N/A, isComposing: N/A
compositionstart - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
compositionupdate - key: N/A, data: ㄱ, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: ㄱ, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: ㄱ, inputType: insertCompositionText, isComposing: true
keyup - key: r, data: N/A, inputType: N/A, isComposing: true
keydown - key: Process, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 가, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
keyup - key: k, data: N/A, inputType: N/A, isComposing: true
keydown - key: Process, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 간, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 간, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 간, inputType: insertCompositionText, isComposing: true
keyup - key: s, data: N/A, inputType: N/A, isComposing: true
keydown - key: Process, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: N/A, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: N/A, inputType: insertCompositionText, isComposing: true
compositionend - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
input - key: N/A, data: N/A, inputType: insertCompositionText, isComposing: N/A
beforeinput - key: N/A, data: 가, inputType: insertText, isComposing: N/A
input - key: N/A, data: 가, inputType: insertText, isComposing: N/A
compositionstart - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
compositionupdate - key: N/A, data: 나, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
keyup - key: k, data: N/A, inputType: N/A, isComposing: true
keydown - key: Process, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 낟, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 낟, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 낟, inputType: insertCompositionText, isComposing: true
keyup - key: e, data: N/A, inputType: N/A, isComposing: true
keydown - key: Process, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: N/A, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: N/A, inputType: insertCompositionText, isComposing: true
compositionend - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
input - key: N/A, data: N/A, inputType: insertCompositionText, isComposing: N/A
beforeinput - key: N/A, data: 나, inputType: insertText, isComposing: N/A
input - key: N/A, data: 나, inputType: insertText, isComposing: N/A
compositionstart - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
compositionupdate - key: N/A, data: 다, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 다, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 다, inputType: insertCompositionText, isComposing: true
keyup - key: k, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: N/A, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: N/A, inputType: insertCompositionText, isComposing: true
compositionupdate - key: N/A, data: 다, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 다, inputType: insertCompositionText, isComposing: true
compositionend - key: N/A, data: 다, inputType: N/A, isComposing: N/A
input - key: N/A, data: 다, inputType: insertCompositionText, isComposing: N/A
macOS 15.4
keydown - key: ㄱ, data: N/A, inputType: N/A, isComposing: N/A
compositionstart - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
compositionupdate - key: N/A, data: ㄱ, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: ㄱ, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: ㄱ, inputType: insertCompositionText, isComposing: true
keyup - key: ㄱ, data: N/A, inputType: N/A, isComposing: true
keydown - key: ㅏ, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 가, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
keyup - key: ㅏ, data: N/A, inputType: N/A, isComposing: true
keydown - key: ㄴ, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 간, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 간, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 간, inputType: insertCompositionText, isComposing: true
keyup - key: ㄴ, data: N/A, inputType: N/A, isComposing: true
keydown - key: ㅏ, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 가, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 가, inputType: insertCompositionText, isComposing: true
compositionend - key: N/A, data: 가, inputType: N/A, isComposing: N/A
compositionstart - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
compositionupdate - key: N/A, data: 나, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
keyup - key: ㅏ, data: N/A, inputType: N/A, isComposing: true
keydown - key: ㄷ, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 낟, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 낟, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 낟, inputType: insertCompositionText, isComposing: true
keyup - key: ㄷ, data: N/A, inputType: N/A, isComposing: true
keydown - key: ㅏ, data: N/A, inputType: N/A, isComposing: true
compositionupdate - key: N/A, data: 나, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 나, inputType: insertCompositionText, isComposing: true
compositionend - key: N/A, data: 나, inputType: N/A, isComposing: N/A
compositionstart - key: N/A, data: N/A, inputType: N/A, isComposing: N/A
compositionupdate - key: N/A, data: 다, inputType: N/A, isComposing: N/A
beforeinput - key: N/A, data: 다, inputType: insertCompositionText, isComposing: true
input - key: N/A, data: 다, inputType: insertCompositionText, isComposing: true
keyup - key: ㅏ, data: N/A, inputType: N/A, isComposing: true
compositionend - key: N/A, data: 다, inputType: N/A, isComposing: N/A

@danielhjacobs
Copy link
Contributor

danielhjacobs commented Apr 4, 2025

Explanation of issues:

iOS default keyboard Because we clear the input for each character typed, the characters do not combine, as the only input event that fires is an insertText for each individual character (all six) and there are no composition events or advanced text events. We'd need to properly handle the advanced text events anyway.
iOS GBoard Unknown issue as I don't know what events it fires. Probably relates at least partially to clearing the input for each character typed.
Android Samsung Keyboard No issue, every input event occurs during composition and the data at compositionend is correct, containing all the entered text.
Android GBoard For the initially mentioned input, all input events happen during composition and the data at compositionend is correct. I'd need to see the events that fire when entering 1 + 2 + 3 + ㄱ + ㅏ + ㄴ + ㅏ + ㄷ + ㅏ to know the issue there.
Windows default keyboard Because we clear the input for each character typed, the deleteContentBackward event that is supposed to fire before a final input event duplicates the last combined character does not fire. Even if it did, we don't handle deleteContentBackward.
macOS default keyboard Unsure, based on the listed events I would expect everything to work correctly. If you ignore the order, macOS seems to be typing everything twice.
Linux default keyboard No issue, there is a non-composing insertText input event containing 가 that occurs following a compositionend event with no data, a non-composing insertText input event containing 나 that occurs following a compositionend event with no data, and a compositionend event at the end containing 다 as data.

Copy link
Contributor

@danielhjacobs danielhjacobs left a comment

Choose a reason for hiding this comment

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

JS changes approved. Functionality approved as it's a definite improvement despite not being perfect. Rust changes seem fine, I'm not confident enough to approve them myself though.

@danielhjacobs
Copy link
Contributor

I'm about 99.9% sure we need to stop clearing the hidden input for each character typed in the future.

@danielhjacobs
Copy link
Contributor

danielhjacobs commented Apr 24, 2025

I have a theory for what Mac is doing wrong, and if I'm right I wonder if this would help:

this.virtualKeyboard.addEventListener("keydown", this.ignoreComposingKeyEvents.bind(this));
this.virtualKeyboard.addEventListener("keyup", this.ignoreComposingKeyEvents.bind(this));
ignoreComposingKeyEvents(event: KeyboardEvent) {
    if (event.isComposing) {
        event.preventDefault();
        event.stopPropagation();
        return;
    }
}

My theory is maybe Mac attempts to fire events for composing key events, causing the keyup/keydown to write text too.

@kjarosh
Copy link
Member Author

kjarosh commented Apr 24, 2025

I'm about 99.9% sure we need to stop clearing the hidden input for each character typed in the future.

Me too. Moreover, I'm 97% sure we should just synchronize the text field with the HTML input and issue Flash events based on changes.

But that's a separate issue from IME, as IME in Flash behaves differently to normal input (and it gets underlined). Even with full text synchronization we should translate IME events to Flash.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-input Area: Input handling ime Issues related to Input Method Editor newsworthy T-compat Type: Compatibility with Flash Player text Issues relating to text rendering/input waiting-on-review Waiting on review from a Ruffle team member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants