LibWeb: Allow Option-generated text insertion

Track whether a keydown should perform text insertion separately from
the delivered code point. Native frontends and text-oriented test paths
can now mark events that came from text input, while shortcut-style key
events keep Alt-modified default insertion suppressed.

This lets macOS Option-generated text such as Option+A and Option+Space
insert into editable controls without making plain Alt shortcuts insert
their base character. Add coverage for Alt text, Ctrl+Alt text,
separator text, Ctrl-only shortcuts, and Alt shortcuts.
This commit is contained in:
Andreas Kling 2026-06-03 12:26:44 +02:00 committed by Andreas Kling
parent 6eb1afbf4b
commit d9d8c5ce89
16 changed files with 107 additions and 31 deletions

View file

@ -245,7 +245,7 @@ void EventLoop::process_input_events() const
[&](KeyEvent const& key_event) {
switch (key_event.type) {
case KeyEvent::Type::KeyDown:
return page.handle_keydown(key_event.key, key_event.modifiers, key_event.code_point, key_event.repeat);
return page.handle_keydown(key_event.key, key_event.modifiers, key_event.code_point, key_event.repeat, key_event.should_insert_text);
case KeyEvent::Type::KeyUp:
return page.handle_keyup(key_event.key, key_event.modifiers, key_event.code_point, key_event.repeat);
}

View file

@ -294,9 +294,9 @@ void Internals::send_text(HTML::HTMLElement& target, String const& text, WebIDL:
for (auto code_point : text.code_points()) {
if (auto data = webdriver_key_to_key_code(code_point); data.has_value())
page.handle_keydown(data->key_code, modifiers | data->additional_modifiers, data->code_point_to_send, false);
page.handle_keydown(data->key_code, modifiers | data->additional_modifiers, data->code_point_to_send, false, data->code_point_to_send != 0);
else
page.handle_keydown(UIEvents::code_point_to_key_code(code_point), modifiers, code_point, false);
page.handle_keydown(UIEvents::code_point_to_key_code(code_point), modifiers, code_point, false, true);
}
}
@ -305,7 +305,7 @@ void Internals::send_key(HTML::HTMLElement& target, String const& key_name, WebI
auto key_code = UIEvents::key_code_from_string(key_name);
target.focus();
page().handle_keydown(key_code, modifiers, 0, false);
page().handle_keydown(key_code, modifiers, 0, false, false);
}
void Internals::paste(HTML::HTMLElement& target, Utf16String const& text)
@ -318,7 +318,7 @@ void Internals::paste(HTML::HTMLElement& target, Utf16String const& text)
void Internals::commit_text()
{
page().handle_keydown(UIEvents::Key_Return, 0, 0x0d, false);
page().handle_keydown(UIEvents::Key_Return, 0, 0x0d, false, true);
}
UIEvents::MouseButton Internals::button_from_unsigned_short(WebIDL::UnsignedShort button)

View file

@ -869,16 +869,24 @@ static constexpr bool is_enter_key_or_interoperable_enter_key_combo(UIEvents::Ke
return false;
}
static constexpr bool should_ignore_keydown_event(u32 code_point, u32 modifiers)
static bool should_ignore_keydown_event(u32 code_point, u32 modifiers, bool should_insert_text)
{
if (modifiers & (UIEvents::KeyModifier::Mod_Ctrl | UIEvents::KeyModifier::Mod_Alt | UIEvents::KeyModifier::Mod_Super))
if (code_point == 0 || code_point == 27)
return true;
// FIXME: There are probably also keys with non-zero code points that should be filtered out.
return code_point == 0 || code_point == 27;
if (modifiers & UIEvents::KeyModifier::Mod_Super)
return true;
if ((modifiers & UIEvents::KeyModifier::Mod_Ctrl) && !(modifiers & UIEvents::KeyModifier::Mod_Alt))
return true;
if ((modifiers & UIEvents::KeyModifier::Mod_Alt) && !should_insert_text)
return true;
return false;
}
EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u32 code_point, bool repeat)
EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u32 code_point, bool repeat, bool should_insert_text)
{
if (!m_navigable->active_document())
return EventResult::Dropped;
@ -1047,7 +1055,7 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u
}
// FIXME: Text editing shortcut keys (copy/paste etc.) should be handled here.
if (!should_ignore_keydown_event(code_point, modifiers)) {
if (!should_ignore_keydown_event(code_point, modifiers, should_insert_text)) {
FIRE(input_event(UIEvents::EventNames::beforeinput, UIEvents::InputTypes::insertText, m_navigable, code_point));
target->handle_insert(UIEvents::InputTypes::insertText, Utf16String::from_code_point(code_point));
return EventResult::Handled;

View file

@ -51,7 +51,7 @@ public:
void update_hover_after_scroll();
GC::Ptr<DOM::Node> target_node_for_mouse_position(CSSPixelPoint);
EventResult handle_keydown(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
EventResult handle_keydown(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat, bool should_insert_text);
EventResult handle_keyup(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
EventResult handle_drag_and_drop_event(DragEvent::Type, CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector<HTML::SelectedFile> files);

View file

@ -13,7 +13,7 @@ namespace Web {
KeyEvent KeyEvent::clone_without_browser_data() const
{
return { type, key, modifiers, code_point, repeat, nullptr };
return { type, key, modifiers, code_point, repeat, should_insert_text, nullptr };
}
MouseEvent MouseEvent::clone_without_browser_data() const
@ -36,6 +36,7 @@ ErrorOr<void> IPC::encode(Encoder& encoder, Web::KeyEvent const& event)
TRY(encoder.encode(event.modifiers));
TRY(encoder.encode(event.code_point));
TRY(encoder.encode(event.repeat));
TRY(encoder.encode(event.should_insert_text));
return {};
}
@ -47,8 +48,9 @@ ErrorOr<Web::KeyEvent> IPC::decode(Decoder& decoder)
auto modifiers = TRY(decoder.decode<Web::UIEvents::KeyModifier>());
auto code_point = TRY(decoder.decode<u32>());
auto repeat = TRY(decoder.decode<bool>());
auto should_insert_text = TRY(decoder.decode<bool>());
return Web::KeyEvent { type, key, modifiers, code_point, repeat, nullptr };
return Web::KeyEvent { type, key, modifiers, code_point, repeat, should_insert_text, nullptr };
}
template<>

View file

@ -36,6 +36,7 @@ struct WEB_API KeyEvent {
UIEvents::KeyModifier modifiers { UIEvents::KeyModifier::Mod_None };
u32 code_point { 0 };
bool repeat { false };
bool should_insert_text { false };
OwnPtr<BrowserInputData> browser_data;
};

View file

@ -351,9 +351,9 @@ EventResult Page::handle_pinch_event(DevicePixelPoint position, unsigned modifie
return top_level_traversable()->event_handler().handle_pinch_event(device_to_css_point(position), modifiers, scale);
}
EventResult Page::handle_keydown(UIEvents::KeyCode key, unsigned modifiers, u32 code_point, bool repeat)
EventResult Page::handle_keydown(UIEvents::KeyCode key, unsigned modifiers, u32 code_point, bool repeat, bool should_insert_text)
{
return focused_navigable().event_handler().handle_keydown(key, modifiers, code_point, repeat);
return focused_navigable().event_handler().handle_keydown(key, modifiers, code_point, repeat, should_insert_text);
}
EventResult Page::handle_keyup(UIEvents::KeyCode key, unsigned modifiers, u32 code_point, bool repeat)

View file

@ -125,7 +125,7 @@ public:
EventResult handle_drag_and_drop_event(DragEvent::Type, DevicePixelPoint, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector<HTML::SelectedFile> files);
EventResult handle_pinch_event(DevicePixelPoint point, unsigned modifiers, double scale);
EventResult handle_keydown(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
EventResult handle_keydown(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat, bool should_insert_text);
EventResult handle_keyup(UIEvents::KeyCode, unsigned modifiers, u32 code_point, bool repeat);
void handle_sdl_input_events();

View file

@ -922,6 +922,7 @@ static bool is_shifted_character(u32 code_point)
struct KeyEvent {
u32 code_point { 0 };
UIEvents::KeyModifier modifiers { UIEvents::KeyModifier::Mod_None };
bool should_insert_text { false };
};
static KeyEvent key_code_to_page_event(u32 code_point, UIEvents::KeyModifier modifiers, KeyCodeData const& code)
{
@ -962,7 +963,11 @@ static KeyEvent key_code_to_page_event(u32 code_point, UIEvents::KeyModifier mod
if (has_flag(modifiers, UIEvents::KeyModifier::Mod_Shift))
code_point = code.alternate_key.value_or(code_point);
return { code_point, modifiers };
auto should_insert_text = code_point != 0
&& code_point < 0xE000
&& !(modifiers & (UIEvents::KeyModifier::Mod_Ctrl | UIEvents::KeyModifier::Mod_Alt | UIEvents::KeyModifier::Mod_Super));
return { code_point, modifiers, should_insert_text };
}
// https://w3c.github.io/webdriver/#dfn-dispatch-a-keydown-action
@ -1018,7 +1023,7 @@ static ErrorOr<void, WebDriver::Error> dispatch_key_down_action(ActionObject::Ke
// keyboard in accordance with the requirements of [UI-EVENTS], and producing the following events, as appropriate,
// with the specified properties. This will always produce events including at least a keyDown event.
auto event = key_code_to_page_event(raw_key, modifiers, code);
browsing_context.page().handle_keydown(code.code, event.modifiers, event.code_point, repeat);
browsing_context.page().handle_keydown(code.code, event.modifiers, event.code_point, repeat, event.should_insert_text);
// 13. Return success with data null.
return {};

View file

@ -0,0 +1,16 @@
keydown key=å keyCodePoints=e5 altKey=true ctrlKey=false
beforeinput data=å dataCodePoints=e5 inputType=insertText
value after Alt: å
keydown key=@ keyCodePoints=40 altKey=true ctrlKey=true
beforeinput data=@ dataCodePoints=40 inputType=insertText
value after Ctrl+Alt: å@
keydown key=  keyCodePoints=a0 altKey=true ctrlKey=false
beforeinput data=  dataCodePoints=a0 inputType=insertText
value after Alt+Space: å@ 
value code points after Alt+Space: e5 40 a0
keydown key=f keyCodePoints=66 altKey=true ctrlKey=false
value after Alt+F shortcut: å@ 
value code points after Alt+F shortcut: e5 40 a0
keydown key=x keyCodePoints=78 altKey=false ctrlKey=true
value after Ctrl: å@ 
value code points after Ctrl: e5 40 a0

View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<meta charset="utf-8">
<input id="input" />
<script src="../include.js"></script>
<script>
test(() => {
let input = document.getElementById("input");
let codePoints = text => Array.from(text, character => character.codePointAt(0).toString(16)).join(" ");
input.addEventListener("keydown", e => {
println(`keydown key=${e.key} keyCodePoints=${codePoints(e.key)} altKey=${e.altKey} ctrlKey=${e.ctrlKey}`);
});
input.addEventListener("beforeinput", e => {
println(`beforeinput data=${e.data} dataCodePoints=${codePoints(e.data)} inputType=${e.inputType}`);
});
internals.sendText(input, "å", internals.MOD_ALT);
println(`value after Alt: ${input.value}`);
internals.sendText(input, "@", internals.MOD_ALT | internals.MOD_CTRL);
println(`value after Ctrl+Alt: ${input.value}`);
internals.sendText(input, "\u00A0", internals.MOD_ALT);
println(`value after Alt+Space: ${input.value}`);
println(`value code points after Alt+Space: ${codePoints(input.value)}`);
internals.sendKey(input, "F", internals.MOD_ALT);
println(`value after Alt+F shortcut: ${input.value}`);
println(`value code points after Alt+F shortcut: ${codePoints(input.value)}`);
internals.sendText(input, "x", internals.MOD_CTRL);
println(`value after Ctrl: ${input.value}`);
println(`value code points after Ctrl: ${codePoints(input.value)}`);
});
</script>

View file

@ -20,7 +20,7 @@ Web::MouseEvent ns_event_to_mouse_event(Web::MouseEvent::Type, NSEvent*, NSView*
Web::DragEvent ns_event_to_drag_event(Web::DragEvent::Type, id<NSDraggingInfo>, NSView*);
Vector<URL::URL> drag_event_url_list(Web::DragEvent const&);
Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type, NSEvent*);
Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type, NSEvent*, bool should_insert_text = false);
NSEvent* key_event_to_ns_event(Web::KeyEvent const&);
NSEvent* create_context_menu_mouse_event(NSView*, Gfx::IntPoint);

View file

@ -289,7 +289,7 @@ private:
CFTypeRef m_event { nullptr };
};
Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type type, NSEvent* event)
Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type type, NSEvent* event, bool should_insert_text)
{
auto modifiers = ns_modifiers_to_key_modifiers(event.modifierFlags);
auto key_code = ns_key_code_to_key_code(event.keyCode, modifiers);
@ -311,7 +311,7 @@ Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type type, NSEvent* event)
if (code_point >= 0xE000 && code_point <= 0xF8FF)
code_point = 0;
return { type, key_code, modifiers, code_point, repeat, make<KeyData>(event) };
return { type, key_code, modifiers, code_point, repeat, should_insert_text, make<KeyData>(event) };
}
NSEvent* key_event_to_ns_event(Web::KeyEvent const& event)

View file

@ -952,7 +952,7 @@ static Web::DevicePixelPoint node_picker_position_for(Ladybird::WebViewBridge co
};
}
- (void)handleCurrentKeyDownEvent
- (void)handleCurrentKeyDownEvent:(BOOL)shouldInsertText
{
if (!self.current_key_down_event)
return;
@ -962,7 +962,7 @@ static Web::DevicePixelPoint node_picker_position_for(Ladybird::WebViewBridge co
return;
}
auto key_event = Ladybird::ns_event_to_key_event(Web::KeyEvent::Type::KeyDown, self.current_key_down_event);
auto key_event = Ladybird::ns_event_to_key_event(Web::KeyEvent::Type::KeyDown, self.current_key_down_event, shouldInsertText);
m_web_view_bridge->enqueue_input_event(move(key_event));
self.current_key_down_event = nil;
@ -1396,12 +1396,12 @@ static Web::DevicePixelPoint node_picker_position_for(Ladybird::WebViewBridge co
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange
{
[self handleCurrentKeyDownEvent];
[self handleCurrentKeyDownEvent:YES];
}
- (void)doCommandBySelector:(SEL)selector
{
[self handleCurrentKeyDownEvent];
[self handleCurrentKeyDownEvent:NO];
}
- (BOOL)hasMarkedText

View file

@ -106,12 +106,19 @@ void WebContentView::enqueue_native_event(Web::MouseEvent::Type type, double x,
void WebContentView::enqueue_native_event(Web::KeyEvent::Type type, guint keyval, GdkModifierType state)
{
auto modifiers = gdk_modifier_to_web(state);
auto code_point = gdk_keyval_to_unicode(keyval);
auto should_insert_text = type == Web::KeyEvent::Type::KeyDown
&& code_point != 0
&& !(modifiers & (Web::UIEvents::KeyModifier::Mod_Ctrl | Web::UIEvents::KeyModifier::Mod_Alt | Web::UIEvents::KeyModifier::Mod_Super));
Web::KeyEvent event {
.type = type,
.key = gdk_keyval_to_web(keyval),
.modifiers = gdk_modifier_to_web(state),
.code_point = gdk_keyval_to_unicode(keyval),
.modifiers = modifiers,
.code_point = code_point,
.repeat = false,
.should_insert_text = should_insert_text,
.browser_data = {},
};
enqueue_input_event(move(event));

View file

@ -1116,19 +1116,20 @@ void WebContentView::enqueue_native_event(Web::KeyEvent::Type type, QKeyEvent co
auto text = event.text();
auto code_point = text.isEmpty() ? 0u : event.text()[0].unicode();
auto should_insert_text = type == Web::KeyEvent::Type::KeyDown && !text.isEmpty();
auto to_web_event = [&]() -> Web::KeyEvent {
if (event.key() == Qt::Key_Backtab) {
// Qt transforms Shift+Tab into a "Backtab", so we undo that transformation here.
return { type, Web::UIEvents::KeyCode::Key_Tab, Web::UIEvents::Mod_Shift, '\t', event.isAutoRepeat(), make<KeyData>(event) };
return { type, Web::UIEvents::KeyCode::Key_Tab, Web::UIEvents::Mod_Shift, '\t', event.isAutoRepeat(), false, make<KeyData>(event) };
}
if (event.key() == Qt::Key_Enter || event.key() == Qt::Key_Return) {
// This ensures consistent behavior between systems that treat Enter as '\n' and '\r\n'
return { type, Web::UIEvents::KeyCode::Key_Return, modifiers, '\n', event.isAutoRepeat(), make<KeyData>(event) };
return { type, Web::UIEvents::KeyCode::Key_Return, modifiers, '\n', event.isAutoRepeat(), should_insert_text, make<KeyData>(event) };
}
return { type, keycode, modifiers, code_point, event.isAutoRepeat(), make<KeyData>(event) };
return { type, keycode, modifiers, code_point, event.isAutoRepeat(), should_insert_text, make<KeyData>(event) };
};
enqueue_input_event(to_web_event());