From debb5690cefa0f067eac9498cc5827d094dadd11 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sun, 18 Feb 2024 21:12:14 -0500 Subject: [PATCH] LibWeb: Begin implementing the HTMLInputElement 'image' type state This implements enough to represent with its loaded source image (or fallback to its alt text, if applicable). This does not implement acquring coordinates from user-activated click events on the image. --- Tests/LibWeb/Layout/expected/input-image.txt | 17 +++ Tests/LibWeb/Layout/input/input-image.html | 3 + Tests/LibWeb/Text/expected/input-image.txt | 2 + Tests/LibWeb/Text/input/input-image.html | 31 ++++ Userland/Libraries/LibWeb/CSS/Default.css | 6 +- .../LibWeb/HTML/HTMLInputElement.cpp | 138 ++++++++++++++++++ .../Libraries/LibWeb/HTML/HTMLInputElement.h | 19 ++- 7 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 Tests/LibWeb/Layout/expected/input-image.txt create mode 100644 Tests/LibWeb/Layout/input/input-image.html create mode 100644 Tests/LibWeb/Text/expected/input-image.txt create mode 100644 Tests/LibWeb/Text/input/input-image.html diff --git a/Tests/LibWeb/Layout/expected/input-image.txt b/Tests/LibWeb/Layout/expected/input-image.txt new file mode 100644 index 0000000000..74263a5254 --- /dev/null +++ b/Tests/LibWeb/Layout/expected/input-image.txt @@ -0,0 +1,17 @@ +Viewport <#document> at (0,0) content-size 800x600 children: not-inline + BlockContainer at (0,0) content-size 800x600 [BFC] children: not-inline + BlockContainer at (8,8) content-size 784x120 children: not-inline + BlockContainer
at (8,8) content-size 784x120 children: inline + frag 0 from ImageBox start: 0, length: 0, rect: [8,8 120x120] baseline: 120 + TextNode <#text> + ImageBox at (8,8) content-size 120x120 inline-block children: not-inline + TextNode <#text> + BlockContainer <(anonymous)> at (8,144) content-size 784x0 children: inline + TextNode <#text> + +ViewportPaintable (Viewport<#document>) [0,0 800x600] + PaintableWithLines (BlockContainer) [0,0 800x600] + PaintableWithLines (BlockContainer) [8,8 784x120] overflow: [8,8 784x136] + PaintableWithLines (BlockContainer) [8,8 784x120] + ImagePaintable (ImageBox) [8,8 120x120] + PaintableWithLines (BlockContainer(anonymous)) [8,144 784x0] diff --git a/Tests/LibWeb/Layout/input/input-image.html b/Tests/LibWeb/Layout/input/input-image.html new file mode 100644 index 0000000000..34fb390aa5 --- /dev/null +++ b/Tests/LibWeb/Layout/input/input-image.html @@ -0,0 +1,3 @@ + + +
diff --git a/Tests/LibWeb/Text/expected/input-image.txt b/Tests/LibWeb/Text/expected/input-image.txt new file mode 100644 index 0000000000..e99c0ff6f4 --- /dev/null +++ b/Tests/LibWeb/Text/expected/input-image.txt @@ -0,0 +1,2 @@ +../../Layout/input/120.png loaded +file:///i-do-no-exist-i-swear.png failed diff --git a/Tests/LibWeb/Text/input/input-image.html b/Tests/LibWeb/Text/input/input-image.html new file mode 100644 index 0000000000..96b8ed1bb3 --- /dev/null +++ b/Tests/LibWeb/Text/input/input-image.html @@ -0,0 +1,31 @@ + + diff --git a/Userland/Libraries/LibWeb/CSS/Default.css b/Userland/Libraries/LibWeb/CSS/Default.css index e576de0ee2..d64b9e34a5 100644 --- a/Userland/Libraries/LibWeb/CSS/Default.css +++ b/Userland/Libraries/LibWeb/CSS/Default.css @@ -26,7 +26,7 @@ label { } /* FIXME: This is a temporary hack until we can render a native-looking frame for these. */ -input:not([type=submit], input[type=button], input[type=reset], input[type=color], input[type=checkbox], input[type=radio], input[type=range]), textarea { +input:not([type=submit], input[type=button], input[type=image], input[type=reset], input[type=color], input[type=checkbox], input[type=radio], input[type=range]), textarea { border: 1px solid ButtonBorder; min-height: 16px; width: attr(size ch, 20ch); @@ -55,6 +55,10 @@ button, input[type=submit], input[type=button], input[type=reset], select { cursor: default; } +input[type=image] { + cursor: pointer; +} + button:hover, input[type=submit]:hover, input[type=button]:hover, input[type=reset]:hover, select:hover { /* FIXME: There isn't a keyword for this, so this is a slightly lightened * version of our light ButtonFace color. Once we support `color-scheme: dark` diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp index 65195edb1e..f4a14bcbd7 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -17,20 +17,24 @@ #include #include #include +#include #include #include +#include #include #include #include #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -66,6 +70,7 @@ void HTMLInputElement::visit_edges(Cell::Visitor& visitor) visitor.visit(m_legacy_pre_activation_behavior_checked_element_in_group); visitor.visit(m_selected_files); visitor.visit(m_slider_thumb); + visitor.visit(m_image_request); } JS::GCPtr HTMLInputElement::create_layout_node(NonnullRefPtr style) @@ -76,6 +81,9 @@ JS::GCPtr HTMLInputElement::create_layout_node(NonnullRefPtr(document(), *this, move(style)); + if (type_state() == TypeAttributeState::ImageButton) + return heap().allocate_without_realm(document(), *this, move(style), *this); + if (type_state() == TypeAttributeState::Checkbox) return heap().allocate_without_realm(document(), *this, move(style)); @@ -285,6 +293,23 @@ WebIDL::ExceptionOr HTMLInputElement::run_input_activation_behavior(DOM::E } else if (type_state() == TypeAttributeState::FileUpload || type_state() == TypeAttributeState::Color) { show_the_picker_if_applicable(*this); } + // https://html.spec.whatwg.org/multipage/input.html#image-button-state-(type=image):input-activation-behavior + else if (type_state() == TypeAttributeState::ImageButton) { + // 1. If the element does not have a form owner, then return. + auto* form = this->form(); + if (!form) + return {}; + + // 2. If the element's node document is not fully active, then return. + if (!document().is_fully_active()) + return {}; + + // FIXME: 3. If the user activated the control while explicitly selecting a coordinate, then set the element's selected + // coordinate to that coordinate. + + // 4. Submit the element's form owner from the element with userInvolvement set to event's user navigation involvement. + TRY(form->submit_form(*this, { .user_involvement = user_navigation_involvement(event) })); + } return {}; } @@ -869,9 +894,77 @@ void HTMLInputElement::form_associated_element_attribute_changed(FlyString const m_placeholder_text_node->set_data(placeholder()); } else if (name == HTML::AttributeNames::readonly) { handle_readonly_attribute(value); + } else if (name == HTML::AttributeNames::src) { + handle_src_attribute(value.value_or({})).release_value_but_fixme_should_propagate_errors(); + } else if (name == HTML::AttributeNames::alt) { + if (layout_node() && type_state() == TypeAttributeState::ImageButton) + did_update_alt_text(verify_cast(*layout_node())); } } +// https://html.spec.whatwg.org/multipage/input.html#attr-input-src +WebIDL::ExceptionOr HTMLInputElement::handle_src_attribute(StringView value) +{ + auto& realm = this->realm(); + auto& vm = realm.vm(); + + if (type_state() != TypeAttributeState::ImageButton) + return {}; + + // 1. Let url be the result of encoding-parsing a URL given the src attribute's value, relative to the element's + // node document. + auto url = document().parse_url(value); + + // 2. If url is failure, then return. + if (!url.is_valid()) + return {}; + + // 3. Let request be a new request whose URL is url, client is the element's node document's relevant settings + // object, destination is "image", initiator type is "input", credentials mode is "include", and whose + // use-URL-credentials flag is set. + auto request = Fetch::Infrastructure::Request::create(vm); + request->set_url(move(url)); + request->set_client(&document().relevant_settings_object()); + request->set_destination(Fetch::Infrastructure::Request::Destination::Image); + request->set_initiator_type(Fetch::Infrastructure::Request::InitiatorType::Input); + request->set_credentials_mode(Fetch::Infrastructure::Request::CredentialsMode::Include); + request->set_use_url_credentials(true); + + // 4. Fetch request, with processResponseEndOfBody set to the following step given response response: + m_image_request = SharedImageRequest::get_or_create(realm, document().page(), request->url()); + m_image_request->add_callbacks( + [this, &realm]() { + // 1. If the download was successful and the image is available, queue an element task on the user interaction + // task source given the input element to fire an event named load at the input element. + queue_an_element_task(HTML::Task::Source::UserInteraction, [this, &realm]() { + dispatch_event(DOM::Event::create(realm, HTML::EventNames::load)); + }); + + m_load_event_delayer.clear(); + document().invalidate_layout(); + }, + [this, &realm]() { + // 2. Otherwise, if the fetching process fails without a response from the remote server, or completes but the + // image is not a valid or supported image, then queue an element task on the user interaction task source + // given the input element to fire an event named error on the input element. + queue_an_element_task(HTML::Task::Source::UserInteraction, [this, &realm]() { + dispatch_event(DOM::Event::create(realm, HTML::EventNames::error)); + }); + + m_load_event_delayer.clear(); + }); + + if (m_image_request->needs_fetching()) { + m_image_request->fetch_image(realm, request); + } + + // Fetching the image must delay the load event of the element's node document until the task that is queued by the + // networking task source once the resource has been fetched (defined below) has been run. + m_load_event_delayer.emplace(document()); + + return {}; +} + HTMLInputElement::TypeAttributeState HTMLInputElement::parse_type_attribute(StringView type) { #define __ENUMERATE_HTML_INPUT_TYPE_ATTRIBUTE(keyword, state) \ @@ -1161,6 +1254,51 @@ void HTMLInputElement::legacy_cancelled_activation_behavior_was_not_called() m_legacy_pre_activation_behavior_checked_element_in_group = nullptr; } +JS::GCPtr HTMLInputElement::image_data() const +{ + if (m_image_request) + return m_image_request->image_data(); + return nullptr; +} + +bool HTMLInputElement::is_image_available() const +{ + return image_data() != nullptr; +} + +Optional HTMLInputElement::intrinsic_width() const +{ + if (auto image_data = this->image_data()) + return image_data->intrinsic_width(); + return {}; +} + +Optional HTMLInputElement::intrinsic_height() const +{ + if (auto image_data = this->image_data()) + return image_data->intrinsic_height(); + return {}; +} + +Optional HTMLInputElement::intrinsic_aspect_ratio() const +{ + if (auto image_data = this->image_data()) + return image_data->intrinsic_aspect_ratio(); + return {}; +} + +RefPtr HTMLInputElement::current_image_bitmap(Gfx::IntSize size) const +{ + if (auto image_data = this->image_data()) + return image_data->bitmap(0, size); + return nullptr; +} + +void HTMLInputElement::set_visible_in_viewport(bool) +{ + // FIXME: Loosen grip on image data when it's not visible, e.g via volatile memory. +} + // https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex i32 HTMLInputElement::default_tab_index_value() const { diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h index f026cd8107..b85ed9d35c 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -8,10 +8,12 @@ #pragma once +#include #include #include #include #include +#include #include namespace Web::HTML { @@ -44,7 +46,8 @@ namespace Web::HTML { class HTMLInputElement final : public HTMLElement , public FormAssociatedElement - , public DOM::EditableTextNodeOwner { + , public DOM::EditableTextNodeOwner + , public Layout::ImageProvider { WEB_PLATFORM_OBJECT(HTMLInputElement, HTMLElement); JS_DECLARE_ALLOCATOR(HTMLInputElement); FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLInputElement) @@ -186,6 +189,14 @@ private: // ^DOM::Element virtual i32 default_tab_index_value() const override; + // ^Layout::ImageProvider + virtual bool is_image_available() const override; + virtual Optional intrinsic_width() const override; + virtual Optional intrinsic_height() const override; + virtual Optional intrinsic_aspect_ratio() const override; + virtual RefPtr current_image_bitmap(Gfx::IntSize = {}) const override; + virtual void set_visible_in_viewport(bool) override; + virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; @@ -212,6 +223,7 @@ private: void set_checked_within_group(); void handle_readonly_attribute(Optional const& value); + WebIDL::ExceptionOr handle_src_attribute(StringView value); // https://html.spec.whatwg.org/multipage/input.html#value-sanitization-algorithm String value_sanitization_algorithm(String const&) const; @@ -238,6 +250,11 @@ private: void update_slider_thumb_element(); JS::GCPtr m_slider_thumb; + JS::GCPtr image_data() const; + JS::GCPtr m_image_request; + + Optional m_load_event_delayer; + // https://html.spec.whatwg.org/multipage/input.html#dom-input-indeterminate bool m_indeterminate { false };