diff --git a/Base/res/html/misc/select.html b/Base/res/html/misc/select.html new file mode 100644 index 0000000000..2618a21f75 --- /dev/null +++ b/Base/res/html/misc/select.html @@ -0,0 +1,61 @@ + + + + + + Select showcase + + +

Basic select:

+

+ + Value: ? +

+ +

Basic select with separators:

+

+ + Value: ? +

+ +

Basic select with option groups and separators:

+

+ + Value: ? +

+ + diff --git a/Ladybird/AppKit/UI/LadybirdWebView.h b/Ladybird/AppKit/UI/LadybirdWebView.h index d1f35bce95..6ec20458bb 100644 --- a/Ladybird/AppKit/UI/LadybirdWebView.h +++ b/Ladybird/AppKit/UI/LadybirdWebView.h @@ -36,7 +36,7 @@ @end -@interface LadybirdWebView : NSClipView +@interface LadybirdWebView : NSClipView - (instancetype)init:(id)observer; diff --git a/Ladybird/AppKit/UI/LadybirdWebView.mm b/Ladybird/AppKit/UI/LadybirdWebView.mm index 3f524e10cf..06f33fbc36 100644 --- a/Ladybird/AppKit/UI/LadybirdWebView.mm +++ b/Ladybird/AppKit/UI/LadybirdWebView.mm @@ -62,6 +62,7 @@ struct HideCursor { @property (nonatomic, strong) NSMenu* image_context_menu; @property (nonatomic, strong) NSMenu* audio_context_menu; @property (nonatomic, strong) NSMenu* video_context_menu; +@property (nonatomic, strong) NSMenu* select_dropdown; @property (nonatomic, strong) NSTextField* status_label; @property (nonatomic, strong) NSAlert* dialog; @@ -608,6 +609,21 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_ [panel makeKeyAndOrderFront:nil]; }; + self.select_dropdown = [[NSMenu alloc] initWithTitle:@"Select Dropdown"]; + [self.select_dropdown setDelegate:self]; + + m_web_view_bridge->on_request_select_dropdown = [self](Gfx::IntPoint content_position, i32 minimum_width, Vector items) { + [self.select_dropdown removeAllItems]; + self.select_dropdown.minimumWidth = minimum_width; + for (auto const& item : items) { + [self selectDropdownAdd:self.select_dropdown + item:item]; + } + + auto* event = Ladybird::create_context_menu_mouse_event(self, content_position); + [NSMenu popUpContextMenu:self.select_dropdown withEvent:event forView:self]; + }; + m_web_view_bridge->on_get_all_cookies = [](auto const& url) { auto* delegate = (ApplicationDelegate*)[NSApp delegate]; return [delegate cookieJar].get_all_cookies(url); @@ -703,6 +719,48 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_ }; } +- (void)selectDropdownAdd:(NSMenu*)menu item:(Web::HTML::SelectItem const&)item +{ + if (item.type == Web::HTML::SelectItem::Type::OptionGroup) { + NSMenuItem* subtitle = [[NSMenuItem alloc] + initWithTitle:Ladybird::string_to_ns_string(item.label.value_or(""_string)) + action:nil + keyEquivalent:@""]; + subtitle.enabled = false; + [menu addItem:subtitle]; + + for (auto const& item : *item.items) { + [self selectDropdownAdd:menu + item:item]; + } + } + if (item.type == Web::HTML::SelectItem::Type::Option) { + NSMenuItem* menuItem = [[NSMenuItem alloc] + initWithTitle:Ladybird::string_to_ns_string(item.label.value_or(""_string)) + action:@selector(selectDropdownAction:) + keyEquivalent:@""]; + [menuItem setRepresentedObject:Ladybird::string_to_ns_string(item.value.value_or(""_string))]; + [menuItem setEnabled:YES]; + [menuItem setState:item.selected ? NSControlStateValueOn : NSControlStateValueOff]; + [menu addItem:menuItem]; + } + if (item.type == Web::HTML::SelectItem::Type::Separator) { + [menu addItem:[NSMenuItem separatorItem]]; + } +} + +- (void)selectDropdownAction:(NSMenuItem*)menuItem +{ + auto value = Ladybird::ns_string_to_string([menuItem representedObject]); + m_web_view_bridge->select_dropdown_closed(value); +} + +- (void)menuDidClose:(NSMenu*)menu +{ + if (!menu.highlightedItem) + m_web_view_bridge->select_dropdown_closed({}); +} + - (void)colorPickerClosed:(NSNotification*)notification { m_web_view_bridge->color_picker_closed(Ladybird::ns_color_to_gfx_color([[NSColorPanel sharedColorPanel] color])); diff --git a/Ladybird/Qt/Tab.cpp b/Ladybird/Qt/Tab.cpp index 2b962b65c6..b36e85c86e 100644 --- a/Ladybird/Qt/Tab.cpp +++ b/Ladybird/Qt/Tab.cpp @@ -226,6 +226,22 @@ Tab::Tab(BrowserWindow* window, WebContentOptions const& web_content_options, St m_dialog = nullptr; }; + m_select_dropdown = new QMenu("Select Dropdown", this); + QObject::connect(m_select_dropdown, &QMenu::aboutToHide, this, [this]() { + if (!m_select_dropdown->activeAction()) + view().select_dropdown_closed({}); + }); + + view().on_request_select_dropdown = [this](Gfx::IntPoint content_position, i32 minimum_width, Vector items) { + m_select_dropdown->clear(); + m_select_dropdown->setMinimumWidth(minimum_width); + for (auto const& item : items) { + select_dropdown_add_item(m_select_dropdown, item); + } + + m_select_dropdown->exec(mapToGlobal(QPoint(content_position.x(), content_position.y()))); + }; + QObject::connect(focus_location_editor_action, &QAction::triggered, this, &Tab::focus_location_editor); view().on_received_source = [this](auto const& url, auto const& source) { @@ -569,6 +585,37 @@ Tab::~Tab() close_sub_widgets(); } +void Tab::select_dropdown_add_item(QMenu* menu, Web::HTML::SelectItem const& item) +{ + if (item.type == Web::HTML::SelectItem::Type::OptionGroup) { + QAction* subtitle = new QAction(qstring_from_ak_string(item.label.value_or(""_string)), this); + subtitle->setDisabled(true); + menu->addAction(subtitle); + + for (auto const& item : *item.items) { + select_dropdown_add_item(menu, item); + } + } + if (item.type == Web::HTML::SelectItem::Type::Option) { + QAction* action = new QAction(qstring_from_ak_string(item.label.value_or(""_string)), this); + action->setCheckable(true); + action->setChecked(item.selected); + action->setData(QVariant(qstring_from_ak_string(item.value.value_or(""_string)))); + QObject::connect(action, &QAction::triggered, this, &Tab::select_dropdown_action); + menu->addAction(action); + } + if (item.type == Web::HTML::SelectItem::Type::Separator) { + menu->addSeparator(); + } +} + +void Tab::select_dropdown_action() +{ + QAction* action = qobject_cast(sender()); + auto value = action->data().value(); + view().select_dropdown_closed(ak_string_from_qstring(value)); +} + void Tab::update_reset_zoom_button() { auto zoom_level = view().zoom_level(); diff --git a/Ladybird/Qt/Tab.h b/Ladybird/Qt/Tab.h index d02b45b88e..50bbf11f27 100644 --- a/Ladybird/Qt/Tab.h +++ b/Ladybird/Qt/Tab.h @@ -54,12 +54,15 @@ public: public slots: void focus_location_editor(); void location_edit_return_pressed(); + void select_dropdown_action(); signals: void title_changed(int id, QString); void favicon_changed(int id, QIcon); private: + void select_dropdown_add_item(QMenu* menu, Web::HTML::SelectItem const& item); + virtual void resizeEvent(QResizeEvent*) override; virtual bool event(QEvent*) override; @@ -106,6 +109,8 @@ private: QAction* m_media_context_menu_loop_action { nullptr }; URL m_media_context_menu_url; + QMenu* m_select_dropdown { nullptr }; + int tab_index(); bool m_is_history_navigation { false }; diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn index 37fce57faa..08bda3326c 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn @@ -140,6 +140,7 @@ source_set("HTML") { "PotentialCORSRequest.cpp", "PromiseRejectionEvent.cpp", "RemoteBrowsingContext.cpp", + "SelectItem.cpp", "SessionHistoryEntry.cpp", "SharedImageRequest.cpp", "SourceSet.cpp", diff --git a/Tests/LibWeb/Text/expected/select.txt b/Tests/LibWeb/Text/expected/select.txt new file mode 100644 index 0000000000..97a482e775 --- /dev/null +++ b/Tests/LibWeb/Text/expected/select.txt @@ -0,0 +1,5 @@ +1. "one" +2. 0 +3. "two" +4. 3 +5. "three" diff --git a/Tests/LibWeb/Text/input/select.html b/Tests/LibWeb/Text/input/select.html new file mode 100644 index 0000000000..8381c66bf0 --- /dev/null +++ b/Tests/LibWeb/Text/input/select.html @@ -0,0 +1,67 @@ + + diff --git a/Userland/Applications/Browser/Tab.cpp b/Userland/Applications/Browser/Tab.cpp index 87ae4994df..b10df27305 100644 --- a/Userland/Applications/Browser/Tab.cpp +++ b/Userland/Applications/Browser/Tab.cpp @@ -578,6 +578,23 @@ Tab::Tab(BrowserWindow& window) m_dialog = nullptr; }; + m_select_dropdown = GUI::Menu::construct(); + m_select_dropdown->on_visibility_change = [this](bool visible) { + if (!visible && !m_select_dropdown_closed_by_action) + view().select_dropdown_closed({}); + }; + + view().on_request_select_dropdown = [this](Gfx::IntPoint content_position, i32, Vector items) { + m_select_dropdown_closed_by_action = false; + m_select_dropdown->remove_all_actions(); + // FIXME: Set menu minimum width + for (auto const& item : items) { + select_dropdown_add_item(*m_select_dropdown, item); + } + + m_select_dropdown->popup(view().screen_relative_rect().location().translated(content_position)); + }; + view().on_received_source = [this](auto& url, auto& source) { view_source(url, source); }; @@ -699,6 +716,31 @@ Tab::Tab(BrowserWindow& window) }; } +void Tab::select_dropdown_add_item(GUI::Menu& menu, Web::HTML::SelectItem const& item) +{ + if (item.type == Web::HTML::SelectItem::Type::OptionGroup) { + auto subtitle = GUI::Action::create(MUST(DeprecatedString::from_utf8(item.label.value_or(""_string))), nullptr); + subtitle->set_enabled(false); + menu.add_action(subtitle); + + for (auto const& item : *item.items) { + select_dropdown_add_item(menu, item); + } + } + if (item.type == Web::HTML::SelectItem::Type::Option) { + auto action = GUI::Action::create(MUST(DeprecatedString::from_utf8(item.label.value_or(""_string))), [this, item](GUI::Action&) { + m_select_dropdown_closed_by_action = true; + view().select_dropdown_closed(item.value.value_or(""_string)); + }); + action->set_checkable(true); + action->set_checked(item.selected); + menu.add_action(action); + } + if (item.type == Web::HTML::SelectItem::Type::Separator) { + menu.add_separator(); + } +} + void Tab::update_reset_zoom_button() { auto zoom_level = view().zoom_level(); diff --git a/Userland/Applications/Browser/Tab.h b/Userland/Applications/Browser/Tab.h index 7eccbd432e..01edfd5476 100644 --- a/Userland/Applications/Browser/Tab.h +++ b/Userland/Applications/Browser/Tab.h @@ -115,6 +115,8 @@ private: void update_status(Optional text_override = {}, i32 count_waiting = 0); void close_sub_widgets(); + void select_dropdown_add_item(GUI::Menu& menu, Web::HTML::SelectItem const& item); + WebView::History m_history; RefPtr m_web_content_view; @@ -155,6 +157,9 @@ private: RefPtr m_go_back_context_menu; RefPtr m_go_forward_context_menu; + RefPtr m_select_dropdown; + bool m_select_dropdown_closed_by_action { false }; + DeprecatedString m_title; RefPtr m_icon; diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 89ee2f2a1b..3722f2adb0 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -384,6 +384,7 @@ set(SOURCES HTML/Scripting/TemporaryExecutionContext.cpp HTML/Scripting/WindowEnvironmentSettingsObject.cpp HTML/Scripting/WorkerEnvironmentSettingsObject.cpp + HTML/SelectItem.cpp HTML/SessionHistoryEntry.cpp HTML/SharedImageRequest.cpp HTML/SourceSet.cpp diff --git a/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.cpp index 82090fadf5..8a4cabaae1 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.cpp @@ -1,15 +1,27 @@ /* * Copyright (c) 2020, the SerenityOS developers. * Copyright (c) 2021-2022, Andreas Kling + * Copyright (c) 2023, Bastiaan van der Plaat * * SPDX-License-Identifier: BSD-2-Clause */ #include +#include +#include +#include +#include +#include +#include #include +#include #include #include #include +#include +#include +#include +#include namespace Web::HTML { @@ -32,6 +44,17 @@ void HTMLSelectElement::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_options); + visitor.visit(m_inner_text_element); +} + +JS::GCPtr HTMLSelectElement::create_layout_node(NonnullRefPtr style) +{ + // AD-HOC: We rewrite `display: inline` to `display: inline-block`. + // This is required for the internal shadow tree to work correctly in layout. + if (style->display().is_inline_outside() && style->display().is_flow_inside()) + style->set_property(CSS::PropertyID::Display, CSS::DisplayStyleValue::create(CSS::Display::from_short(CSS::Display::Short::InlineBlock))); + + return Element::create_layout_node_for_display_type(document(), style->display(), style, this); } // https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-options @@ -177,6 +200,39 @@ Optional HTMLSelectElement::default_role() const return ARIA::Role::combobox; } +String HTMLSelectElement::value() const +{ + for (auto const& option_element : list_of_options()) + if (option_element->selected()) + return option_element->value(); + return ""_string; +} + +WebIDL::ExceptionOr HTMLSelectElement::set_value(String const& value) +{ + for (auto const& option_element : list_of_options()) + option_element->set_selected(option_element->value() == value); + update_inner_text_element(); + document().invalidate_layout(); + + // When the user agent is to send select update notifications, queue an element task on the user interaction task source given the select element to run these steps: + queue_an_element_task(HTML::Task::Source::UserInteraction, [this] { + // FIXME: 1. Set the select element's user interacted to true. + + // 2. Fire an event named input at the select element, with the bubbles and composed attributes initialized to true. + auto input_event = DOM::Event::create(realm(), HTML::EventNames::input); + input_event->set_bubbles(true); + input_event->set_composed(true); + dispatch_event(input_event); + + // 3. Fire an event named change at the select element, with the bubbles attribute initialized to true. + auto change_event = DOM::Event::create(realm(), HTML::EventNames::change); + change_event->set_bubbles(true); + dispatch_event(*change_event); + }); + return {}; +} + void HTMLSelectElement::set_is_open(bool open) { if (open == m_is_open) @@ -186,4 +242,139 @@ void HTMLSelectElement::set_is_open(bool open) invalidate_style(); } +bool HTMLSelectElement::has_activation_behavior() const +{ + return true; +} + +static Optional strip_newlines(Optional string) +{ + // FIXME: Move this to a more general function + if (!string.has_value()) + return {}; + + StringBuilder builder; + for (auto c : string.value().bytes_as_string_view()) { + if (c == '\r' || c == '\n') { + builder.append(' '); + } else { + builder.append(c); + } + } + return MUST(Infra::strip_and_collapse_whitespace(MUST(builder.to_string()))); +} + +void HTMLSelectElement::activation_behavior(DOM::Event const&) +{ + // Populate select items + Vector items; + for (auto const& child : children_as_vector()) { + if (is(*child)) { + auto& opt_group_element = verify_cast(*child); + Vector opt_group_items; + for (auto const& child : opt_group_element.children_as_vector()) { + if (is(*child)) { + auto& option_element = verify_cast(*child); + auto option_value = option_element.value(); + opt_group_items.append(SelectItem { SelectItem::Type::Option, strip_newlines(option_element.text_content()), option_value, {}, option_element.selected() }); + } + if (is(*child)) { + opt_group_items.append(SelectItem { SelectItem::Type::Separator }); + } + } + items.append(SelectItem { SelectItem::Type::OptionGroup, opt_group_element.get_attribute(AttributeNames::label), {}, opt_group_items }); + } + + if (is(*child)) { + auto& option_element = verify_cast(*child); + auto option_value = option_element.value(); + items.append(SelectItem { SelectItem::Type::Option, strip_newlines(option_element.text_content()), option_value, {}, option_element.selected() }); + } + if (is(*child)) { + items.append(SelectItem { SelectItem::Type::Separator }); + } + } + + // Request select dropdown + auto weak_element = make_weak_ptr(); + auto rect = get_bounding_client_rect(); + document().browsing_context()->top_level_browsing_context()->page().did_request_select_dropdown(weak_element, Gfx::IntPoint { rect->x(), rect->y() }, rect->width(), items); + set_is_open(true); +} + +void HTMLSelectElement::did_select_value(Optional value) +{ + set_is_open(false); + if (value.has_value()) { + MUST(set_value(*value)); + } +} + +void HTMLSelectElement::form_associated_element_was_inserted() +{ + create_shadow_tree_if_needed(); + + // Wait until children are ready + queue_an_element_task(HTML::Task::Source::Microtask, [this] { + // Select first option when no other option is selected + if (selected_index() == -1) { + auto options = list_of_options(); + if (options.size() > 0) { + options.at(0)->set_selected(true); + update_inner_text_element(); + document().invalidate_layout(); + } + } + }); +} + +void HTMLSelectElement::form_associated_element_was_removed(DOM::Node*) +{ + set_shadow_root(nullptr); +} + +void HTMLSelectElement::create_shadow_tree_if_needed() +{ + if (shadow_root_internal()) + return; + + auto shadow_root = heap().allocate(realm(), document(), *this, Bindings::ShadowRootMode::Closed); + set_shadow_root(shadow_root); + + auto border = DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML).release_value_but_fixme_should_propagate_errors(); + MUST(border->set_attribute(HTML::AttributeNames::style, R"~~~( + display: flex; + justify-content: center; + height: 100%; + padding: 4px; + border: 1px solid ButtonBorder; + background-color: ButtonFace; +)~~~"_string)); + MUST(shadow_root->append_child(border)); + + m_inner_text_element = DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML).release_value_but_fixme_should_propagate_errors(); + MUST(m_inner_text_element->set_attribute(HTML::AttributeNames::style, R"~~~( + flex: 1; +)~~~"_string)); + MUST(border->append_child(*m_inner_text_element)); + + // FIXME: Find better way to add chevron icon + auto chevron_icon_element = DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML).release_value_but_fixme_should_propagate_errors(); + MUST(chevron_icon_element->set_inner_html(""sv)); + MUST(border->append_child(*chevron_icon_element)); + + update_inner_text_element(); +} + +void HTMLSelectElement::update_inner_text_element() +{ + // Update inner text element to text content of selected option + for (auto const& option_element : list_of_options()) { + if (option_element->selected()) { + m_inner_text_element->set_text_content(strip_newlines(option_element->text_content())); + return; + } + } +} + } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.h b/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.h index a12d02d852..d57914783a 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.h @@ -2,6 +2,7 @@ * Copyright (c) 2020, the SerenityOS developers. * Copyright (c) 2021-2022, Andreas Kling * Copyright (c) 2022, Luke Wilde + * Copyright (c) 2023, Bastiaan van der Plaat * * SPDX-License-Identifier: BSD-2-Clause */ @@ -24,6 +25,8 @@ class HTMLSelectElement final public: virtual ~HTMLSelectElement() override; + virtual JS::GCPtr create_layout_node(NonnullRefPtr) override; + JS::GCPtr const& options(); size_t length(); @@ -34,6 +37,9 @@ public: int selected_index() const; void set_selected_index(int); + virtual String value() const override; + WebIDL::ExceptionOr set_value(String const&); + bool is_open() const { return m_is_open; } void set_is_open(bool); @@ -66,6 +72,14 @@ public: virtual Optional default_role() const override; + virtual bool has_activation_behavior() const override; + virtual void activation_behavior(DOM::Event const&) override; + + virtual void form_associated_element_was_inserted() override; + virtual void form_associated_element_was_removed(DOM::Node*) override; + + void did_select_value(Optional value); + private: HTMLSelectElement(DOM::Document&, DOM::QualifiedName); @@ -75,8 +89,12 @@ private: // ^DOM::Element virtual i32 default_tab_index_value() const override; + void create_shadow_tree_if_needed(); + void update_inner_text_element(); + JS::GCPtr m_options; bool m_is_open { false }; + JS::GCPtr m_inner_text_element; }; } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.idl b/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.idl index 6ad36f3bc7..e7b9c7c00d 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.idl +++ b/Userland/Libraries/LibWeb/HTML/HTMLSelectElement.idl @@ -31,7 +31,7 @@ interface HTMLSelectElement : HTMLElement { // FIXME: [SameObject] readonly attribute HTMLCollection selectedOptions; attribute long selectedIndex; - // FIXME: attribute DOMString value; + attribute DOMString value; // FIXME: readonly attribute boolean willValidate; // FIXME: readonly attribute ValidityState validity; diff --git a/Userland/Libraries/LibWeb/HTML/SelectItem.cpp b/Userland/Libraries/LibWeb/HTML/SelectItem.cpp new file mode 100644 index 0000000000..3638a66a62 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/SelectItem.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023, Bastiaan van der Plaat + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "SelectItem.h" +#include +#include + +template<> +ErrorOr IPC::encode(Encoder& encoder, Web::HTML::SelectItem const& select_item) +{ + TRY(encoder.encode(select_item.type)); + TRY(encoder.encode(select_item.label)); + TRY(encoder.encode(select_item.value)); + TRY(encoder.encode(select_item.items)); + TRY(encoder.encode(select_item.selected)); + return {}; +} + +template<> +ErrorOr IPC::decode(Decoder& decoder) +{ + auto type = TRY(decoder.decode()); + auto label = TRY(decoder.decode>()); + auto value = TRY(decoder.decode>()); + auto items = TRY(decoder.decode>>()); + auto selected = TRY(decoder.decode()); + return Web::HTML::SelectItem { type, move(label), move(value), move(items), selected }; +} diff --git a/Userland/Libraries/LibWeb/HTML/SelectItem.h b/Userland/Libraries/LibWeb/HTML/SelectItem.h new file mode 100644 index 0000000000..c342f39677 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/SelectItem.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023, Bastiaan van der Plaat + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::HTML { + +struct SelectItem { + enum class Type { + OptionGroup, + Option, + Separator, + }; + + Type type; + Optional label = {}; + Optional value = {}; + Optional> items = {}; + bool selected = false; +}; + +} + +namespace IPC { + +template<> +ErrorOr encode(Encoder&, Web::HTML::SelectItem const&); + +template<> +ErrorOr decode(Decoder&); + +} diff --git a/Userland/Libraries/LibWeb/Page/Page.cpp b/Userland/Libraries/LibWeb/Page/Page.cpp index 51a4824293..e72182e005 100644 --- a/Userland/Libraries/LibWeb/Page/Page.cpp +++ b/Userland/Libraries/LibWeb/Page/Page.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -323,7 +324,30 @@ void Page::color_picker_closed(Optional picked_color) m_pending_non_blocking_dialog = PendingNonBlockingDialog::None; if (m_pending_non_blocking_dialog_target) { - m_pending_non_blocking_dialog_target->did_pick_color(picked_color); + auto& input_element = verify_cast(*m_pending_non_blocking_dialog_target); + input_element.did_pick_color(move(picked_color)); + m_pending_non_blocking_dialog_target.clear(); + } + } +} + +void Page::did_request_select_dropdown(WeakPtr target, Gfx::IntPoint content_position, i32 minimum_width, Vector items) +{ + if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::None) { + m_pending_non_blocking_dialog = PendingNonBlockingDialog::Select; + m_pending_non_blocking_dialog_target = move(target); + m_client->page_did_request_select_dropdown(content_position, minimum_width, move(items)); + } +} + +void Page::select_dropdown_closed(Optional value) +{ + if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::Select) { + m_pending_non_blocking_dialog = PendingNonBlockingDialog::None; + + if (m_pending_non_blocking_dialog_target) { + auto& select_element = verify_cast(*m_pending_non_blocking_dialog_target); + select_element.did_select_value(move(value)); m_pending_non_blocking_dialog_target.clear(); } } diff --git a/Userland/Libraries/LibWeb/Page/Page.h b/Userland/Libraries/LibWeb/Page/Page.h index b9845cdbfb..767024107e 100644 --- a/Userland/Libraries/LibWeb/Page/Page.h +++ b/Userland/Libraries/LibWeb/Page/Page.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -128,9 +129,13 @@ public: void did_request_color_picker(WeakPtr target, Color current_color); void color_picker_closed(Optional picked_color); + void did_request_select_dropdown(WeakPtr target, Gfx::IntPoint content_position, i32 minimum_width, Vector items); + void select_dropdown_closed(Optional value); + enum class PendingNonBlockingDialog { None, ColorPicker, + Select, }; struct MediaContextMenu { @@ -185,7 +190,7 @@ private: Optional> m_pending_prompt_response; PendingNonBlockingDialog m_pending_non_blocking_dialog { PendingNonBlockingDialog::None }; - WeakPtr m_pending_non_blocking_dialog_target; + WeakPtr m_pending_non_blocking_dialog_target; Optional m_media_context_menu_element_id; @@ -272,6 +277,7 @@ public: // https://html.spec.whatwg.org/multipage/input.html#show-the-picker,-if-applicable virtual void page_did_request_file_picker(WeakPtr, [[maybe_unused]] bool multiple) {}; virtual void page_did_request_color_picker([[maybe_unused]] Color current_color) {}; + virtual void page_did_request_select_dropdown([[maybe_unused]] Gfx::IntPoint content_position, [[maybe_unused]] i32 minimum_width, [[maybe_unused]] Vector items) {}; virtual void page_did_finish_text_test() {}; diff --git a/Userland/Libraries/LibWebView/ViewImplementation.cpp b/Userland/Libraries/LibWebView/ViewImplementation.cpp index 840114b854..b0b0469437 100644 --- a/Userland/Libraries/LibWebView/ViewImplementation.cpp +++ b/Userland/Libraries/LibWebView/ViewImplementation.cpp @@ -266,6 +266,11 @@ void ViewImplementation::color_picker_closed(Optional picked_color) client().async_color_picker_closed(picked_color); } +void ViewImplementation::select_dropdown_closed(Optional value) +{ + client().async_select_dropdown_closed(value); +} + void ViewImplementation::toggle_media_play_state() { client().async_toggle_media_play_state(); diff --git a/Userland/Libraries/LibWebView/ViewImplementation.h b/Userland/Libraries/LibWebView/ViewImplementation.h index 4f9d5770a6..82b995cb41 100644 --- a/Userland/Libraries/LibWebView/ViewImplementation.h +++ b/Userland/Libraries/LibWebView/ViewImplementation.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -83,6 +84,7 @@ public: void confirm_closed(bool accepted); void prompt_closed(Optional response); void color_picker_closed(Optional picked_color); + void select_dropdown_closed(Optional value); void toggle_media_play_state(); void toggle_media_mute_state(); @@ -154,6 +156,7 @@ public: Function on_minimize_window; Function on_fullscreen_window; Function on_request_color_picker; + Function items)> on_request_select_dropdown; Function on_finish_handling_input_event; Function on_text_test_finish; Function on_theme_color_change; diff --git a/Userland/Libraries/LibWebView/WebContentClient.cpp b/Userland/Libraries/LibWebView/WebContentClient.cpp index 8cee533bec..277593d2cd 100644 --- a/Userland/Libraries/LibWebView/WebContentClient.cpp +++ b/Userland/Libraries/LibWebView/WebContentClient.cpp @@ -384,6 +384,12 @@ void WebContentClient::did_request_color_picker(Color const& current_color) m_view.on_request_color_picker(current_color); } +void WebContentClient::did_request_select_dropdown(Gfx::IntPoint content_position, i32 minimum_width, Vector const& items) +{ + if (m_view.on_request_select_dropdown) + m_view.on_request_select_dropdown(content_position, minimum_width, items); +} + void WebContentClient::did_finish_handling_input_event(bool event_was_accepted) { if (m_view.on_finish_handling_input_event) diff --git a/Userland/Libraries/LibWebView/WebContentClient.h b/Userland/Libraries/LibWebView/WebContentClient.h index ecfb9e503b..96fae8ced1 100644 --- a/Userland/Libraries/LibWebView/WebContentClient.h +++ b/Userland/Libraries/LibWebView/WebContentClient.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -82,6 +83,7 @@ private: virtual Messages::WebContentClient::DidRequestFullscreenWindowResponse did_request_fullscreen_window() override; virtual void did_request_file(DeprecatedString const& path, i32) override; virtual void did_request_color_picker(Color const& current_color) override; + virtual void did_request_select_dropdown(Gfx::IntPoint content_position, i32 minimum_width, Vector const& items) override; virtual void did_finish_handling_input_event(bool event_was_accepted) override; virtual void did_finish_text_test() override; virtual void did_change_theme_color(Gfx::Color color) override; diff --git a/Userland/Services/WebContent/ConnectionFromClient.cpp b/Userland/Services/WebContent/ConnectionFromClient.cpp index 4fcafa7bad..e69c6e9844 100644 --- a/Userland/Services/WebContent/ConnectionFromClient.cpp +++ b/Userland/Services/WebContent/ConnectionFromClient.cpp @@ -1052,6 +1052,11 @@ void ConnectionFromClient::color_picker_closed(Optional const& picked_col page().page().color_picker_closed(picked_color); } +void ConnectionFromClient::select_dropdown_closed(Optional const& value) +{ + page().page().select_dropdown_closed(value); +} + void ConnectionFromClient::toggle_media_play_state() { page().page().toggle_media_play_state().release_value_but_fixme_should_propagate_errors(); diff --git a/Userland/Services/WebContent/ConnectionFromClient.h b/Userland/Services/WebContent/ConnectionFromClient.h index cf2fc794ad..7e85e79114 100644 --- a/Userland/Services/WebContent/ConnectionFromClient.h +++ b/Userland/Services/WebContent/ConnectionFromClient.h @@ -110,6 +110,7 @@ private: virtual void confirm_closed(bool accepted) override; virtual void prompt_closed(Optional const& response) override; virtual void color_picker_closed(Optional const& picked_color) override; + virtual void select_dropdown_closed(Optional const& value) override; virtual void toggle_media_play_state() override; virtual void toggle_media_mute_state() override; diff --git a/Userland/Services/WebContent/PageClient.cpp b/Userland/Services/WebContent/PageClient.cpp index b97253943e..7c7ba7c099 100644 --- a/Userland/Services/WebContent/PageClient.cpp +++ b/Userland/Services/WebContent/PageClient.cpp @@ -404,6 +404,11 @@ void PageClient::color_picker_closed(Optional picked_color) page().color_picker_closed(picked_color); } +void PageClient::select_dropdown_closed(Optional value) +{ + page().select_dropdown_closed(value); +} + Web::WebIDL::ExceptionOr PageClient::toggle_media_play_state() { return page().toggle_media_play_state(); @@ -504,6 +509,11 @@ void PageClient::page_did_request_color_picker(Color current_color) client().async_did_request_color_picker(current_color); } +void PageClient::page_did_request_select_dropdown(Gfx::IntPoint content_position, i32 minimum_width, Vector items) +{ + client().async_did_request_select_dropdown(content_position, minimum_width, items); +} + void PageClient::page_did_change_theme_color(Gfx::Color color) { client().async_did_change_theme_color(color); diff --git a/Userland/Services/WebContent/PageClient.h b/Userland/Services/WebContent/PageClient.h index 576a9a5429..61f320986d 100644 --- a/Userland/Services/WebContent/PageClient.h +++ b/Userland/Services/WebContent/PageClient.h @@ -53,6 +53,7 @@ public: void confirm_closed(bool accepted); void prompt_closed(Optional response); void color_picker_closed(Optional picked_color); + void select_dropdown_closed(Optional value); [[nodiscard]] Gfx::Color background_color() const; @@ -118,6 +119,7 @@ private: virtual void page_did_close_browsing_context(Web::HTML::BrowsingContext const&) override; virtual void request_file(Web::FileRequest) override; virtual void page_did_request_color_picker(Color current_color) override; + virtual void page_did_request_select_dropdown(Gfx::IntPoint content_position, i32 minimum_width, Vector items) override; virtual void page_did_finish_text_test() override; virtual void page_did_change_theme_color(Gfx::Color color) override; virtual void page_did_insert_clipboard_entry(String data, String presentation_style, String mime_type) override; diff --git a/Userland/Services/WebContent/WebContentClient.ipc b/Userland/Services/WebContent/WebContentClient.ipc index b4fd9a79e7..7fb17ee9b9 100644 --- a/Userland/Services/WebContent/WebContentClient.ipc +++ b/Userland/Services/WebContent/WebContentClient.ipc @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -62,6 +63,7 @@ endpoint WebContentClient did_request_fullscreen_window() => (Gfx::IntRect window_rect) did_request_file(DeprecatedString path, i32 request_id) =| did_request_color_picker(Color current_color) =| + did_request_select_dropdown(Gfx::IntPoint content_position, i32 minimum_width, Vector items) =| did_finish_handling_input_event(bool event_was_accepted) =| did_change_theme_color(Gfx::Color color) =| did_insert_clipboard_entry(String data, String presentation_style, String mime_type) =| diff --git a/Userland/Services/WebContent/WebContentServer.ipc b/Userland/Services/WebContent/WebContentServer.ipc index 5926171255..d46222fa11 100644 --- a/Userland/Services/WebContent/WebContentServer.ipc +++ b/Userland/Services/WebContent/WebContentServer.ipc @@ -91,6 +91,7 @@ endpoint WebContentServer confirm_closed(bool accepted) =| prompt_closed(Optional response) =| color_picker_closed(Optional picked_color) =| + select_dropdown_closed(Optional value) =| toggle_media_play_state() =| toggle_media_mute_state() =|