/src/serenity/Userland/Libraries/LibWeb/Page/Page.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org> |
4 | | * Copyright (c) 2024, Tim Ledbetter <timledbetter@gmail.com> |
5 | | * |
6 | | * SPDX-License-Identifier: BSD-2-Clause |
7 | | */ |
8 | | |
9 | | #include <AK/ScopeGuard.h> |
10 | | #include <AK/SourceLocation.h> |
11 | | #include <LibIPC/Decoder.h> |
12 | | #include <LibIPC/Encoder.h> |
13 | | #include <LibWeb/CSS/StyleComputer.h> |
14 | | #include <LibWeb/DOM/Document.h> |
15 | | #include <LibWeb/DOM/Range.h> |
16 | | #include <LibWeb/HTML/BrowsingContext.h> |
17 | | #include <LibWeb/HTML/EventLoop/EventLoop.h> |
18 | | #include <LibWeb/HTML/HTMLInputElement.h> |
19 | | #include <LibWeb/HTML/HTMLMediaElement.h> |
20 | | #include <LibWeb/HTML/HTMLSelectElement.h> |
21 | | #include <LibWeb/HTML/Scripting/Environments.h> |
22 | | #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h> |
23 | | #include <LibWeb/HTML/SelectedFile.h> |
24 | | #include <LibWeb/HTML/TraversableNavigable.h> |
25 | | #include <LibWeb/HTML/Window.h> |
26 | | #include <LibWeb/Page/Page.h> |
27 | | #include <LibWeb/Platform/EventLoopPlugin.h> |
28 | | #include <LibWeb/Selection/Selection.h> |
29 | | |
30 | | namespace Web { |
31 | | |
32 | | JS_DEFINE_ALLOCATOR(Page); |
33 | | |
34 | | JS::NonnullGCPtr<Page> Page::create(JS::VM& vm, JS::NonnullGCPtr<PageClient> page_client) |
35 | 0 | { |
36 | 0 | return vm.heap().allocate_without_realm<Page>(page_client); |
37 | 0 | } |
38 | | |
39 | | Page::Page(JS::NonnullGCPtr<PageClient> client) |
40 | 0 | : m_client(client) |
41 | 0 | { |
42 | 0 | } |
43 | | |
44 | 0 | Page::~Page() = default; |
45 | | |
46 | | void Page::visit_edges(JS::Cell::Visitor& visitor) |
47 | 0 | { |
48 | 0 | Base::visit_edges(visitor); |
49 | 0 | visitor.visit(m_top_level_traversable); |
50 | 0 | visitor.visit(m_client); |
51 | 0 | visitor.visit(m_on_pending_dialog_closed); |
52 | 0 | } |
53 | | |
54 | | HTML::Navigable& Page::focused_navigable() |
55 | 0 | { |
56 | 0 | if (m_focused_navigable) |
57 | 0 | return *m_focused_navigable; |
58 | 0 | return top_level_traversable(); |
59 | 0 | } |
60 | | |
61 | | void Page::set_focused_navigable(Badge<EventHandler>, HTML::Navigable& navigable) |
62 | 0 | { |
63 | 0 | m_focused_navigable = navigable; |
64 | 0 | } |
65 | | |
66 | | void Page::load(URL::URL const& url) |
67 | 0 | { |
68 | 0 | (void)top_level_traversable()->navigate({ .url = url, .source_document = *top_level_traversable()->active_document(), .user_involvement = HTML::UserNavigationInvolvement::BrowserUI }); |
69 | 0 | } |
70 | | |
71 | | void Page::load_html(StringView html) |
72 | 0 | { |
73 | | // FIXME: #23909 Figure out why GC threshold does not stay low when repeatedly loading html from the WebView |
74 | 0 | heap().collect_garbage(); |
75 | |
|
76 | 0 | (void)top_level_traversable()->navigate({ .url = "about:srcdoc"sv, |
77 | 0 | .source_document = *top_level_traversable()->active_document(), |
78 | 0 | .document_resource = String::from_utf8(html).release_value_but_fixme_should_propagate_errors(), |
79 | 0 | .user_involvement = HTML::UserNavigationInvolvement::BrowserUI }); |
80 | 0 | } |
81 | | |
82 | | void Page::reload() |
83 | 0 | { |
84 | 0 | top_level_traversable()->reload(); |
85 | 0 | } |
86 | | |
87 | | void Page::traverse_the_history_by_delta(int delta) |
88 | 0 | { |
89 | 0 | top_level_traversable()->traverse_the_history_by_delta(delta); |
90 | 0 | } |
91 | | |
92 | | Gfx::Palette Page::palette() const |
93 | 0 | { |
94 | 0 | return m_client->palette(); |
95 | 0 | } |
96 | | |
97 | | // https://w3c.github.io/csswg-drafts/cssom-view-1/#web-exposed-screen-area |
98 | | CSSPixelRect Page::web_exposed_screen_area() const |
99 | 0 | { |
100 | 0 | auto device_pixel_rect = m_client->screen_rect(); |
101 | 0 | auto scale = client().device_pixels_per_css_pixel(); |
102 | 0 | return { |
103 | 0 | device_pixel_rect.x().value() / scale, |
104 | 0 | device_pixel_rect.y().value() / scale, |
105 | 0 | device_pixel_rect.width().value() / scale, |
106 | 0 | device_pixel_rect.height().value() / scale |
107 | 0 | }; |
108 | 0 | } |
109 | | |
110 | | CSS::PreferredColorScheme Page::preferred_color_scheme() const |
111 | 0 | { |
112 | 0 | return m_client->preferred_color_scheme(); |
113 | 0 | } |
114 | | |
115 | | CSS::PreferredContrast Page::preferred_contrast() const |
116 | 0 | { |
117 | 0 | return m_client->preferred_contrast(); |
118 | 0 | } |
119 | | |
120 | | CSS::PreferredMotion Page::preferred_motion() const |
121 | 0 | { |
122 | 0 | return m_client->preferred_motion(); |
123 | 0 | } |
124 | | |
125 | | CSSPixelPoint Page::device_to_css_point(DevicePixelPoint point) const |
126 | 0 | { |
127 | 0 | return { |
128 | 0 | point.x().value() / client().device_pixels_per_css_pixel(), |
129 | 0 | point.y().value() / client().device_pixels_per_css_pixel(), |
130 | 0 | }; |
131 | 0 | } |
132 | | |
133 | | DevicePixelPoint Page::css_to_device_point(CSSPixelPoint point) const |
134 | 0 | { |
135 | 0 | return { |
136 | 0 | point.x() * client().device_pixels_per_css_pixel(), |
137 | 0 | point.y() * client().device_pixels_per_css_pixel(), |
138 | 0 | }; |
139 | 0 | } |
140 | | |
141 | | DevicePixelRect Page::css_to_device_rect(CSSPixelRect rect) const |
142 | 0 | { |
143 | 0 | return { |
144 | 0 | rect.location().to_type<double>() * client().device_pixels_per_css_pixel(), |
145 | 0 | rect.size().to_type<double>() * client().device_pixels_per_css_pixel(), |
146 | 0 | }; |
147 | 0 | } |
148 | | |
149 | | CSSPixelRect Page::device_to_css_rect(DevicePixelRect rect) const |
150 | 0 | { |
151 | 0 | auto scale = client().device_pixels_per_css_pixel(); |
152 | 0 | return { |
153 | 0 | CSSPixels::nearest_value_for(rect.x().value() / scale), |
154 | 0 | CSSPixels::nearest_value_for(rect.y().value() / scale), |
155 | 0 | CSSPixels::floored_value_for(rect.width().value() / scale), |
156 | 0 | CSSPixels::floored_value_for(rect.height().value() / scale), |
157 | 0 | }; |
158 | 0 | } |
159 | | |
160 | | CSSPixelSize Page::device_to_css_size(DevicePixelSize size) const |
161 | 0 | { |
162 | 0 | auto scale = client().device_pixels_per_css_pixel(); |
163 | 0 | return { |
164 | 0 | CSSPixels::floored_value_for(size.width().value() / scale), |
165 | 0 | CSSPixels::floored_value_for(size.height().value() / scale), |
166 | 0 | }; |
167 | 0 | } |
168 | | |
169 | | DevicePixelRect Page::enclosing_device_rect(CSSPixelRect rect) const |
170 | 0 | { |
171 | 0 | auto scale = client().device_pixels_per_css_pixel(); |
172 | 0 | return DevicePixelRect( |
173 | 0 | floor(rect.x().to_double() * scale), |
174 | 0 | floor(rect.y().to_double() * scale), |
175 | 0 | ceil(rect.width().to_double() * scale), |
176 | 0 | ceil(rect.height().to_double() * scale)); |
177 | 0 | } |
178 | | |
179 | | DevicePixelRect Page::rounded_device_rect(CSSPixelRect rect) const |
180 | 0 | { |
181 | 0 | auto scale = client().device_pixels_per_css_pixel(); |
182 | 0 | return { |
183 | 0 | roundf(rect.x().to_double() * scale), |
184 | 0 | roundf(rect.y().to_double() * scale), |
185 | 0 | roundf(rect.width().to_double() * scale), |
186 | 0 | roundf(rect.height().to_double() * scale) |
187 | 0 | }; |
188 | 0 | } |
189 | | |
190 | | EventResult Page::handle_mouseup(DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers) |
191 | 0 | { |
192 | 0 | return top_level_traversable()->event_handler().handle_mouseup(device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers); |
193 | 0 | } |
194 | | |
195 | | EventResult Page::handle_mousedown(DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers) |
196 | 0 | { |
197 | 0 | return top_level_traversable()->event_handler().handle_mousedown(device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers); |
198 | 0 | } |
199 | | |
200 | | EventResult Page::handle_mousemove(DevicePixelPoint position, DevicePixelPoint screen_position, unsigned buttons, unsigned modifiers) |
201 | 0 | { |
202 | 0 | return top_level_traversable()->event_handler().handle_mousemove(device_to_css_point(position), device_to_css_point(screen_position), buttons, modifiers); |
203 | 0 | } |
204 | | |
205 | | EventResult Page::handle_mousewheel(DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, DevicePixels wheel_delta_x, DevicePixels wheel_delta_y) |
206 | 0 | { |
207 | 0 | return top_level_traversable()->event_handler().handle_mousewheel(device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers, wheel_delta_x.value(), wheel_delta_y.value()); |
208 | 0 | } |
209 | | |
210 | | EventResult Page::handle_doubleclick(DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers) |
211 | 0 | { |
212 | 0 | return top_level_traversable()->event_handler().handle_doubleclick(device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers); |
213 | 0 | } |
214 | | |
215 | | EventResult Page::handle_drag_and_drop_event(DragEvent::Type type, DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector<HTML::SelectedFile> files) |
216 | 0 | { |
217 | 0 | return top_level_traversable()->event_handler().handle_drag_and_drop_event(type, device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers, move(files)); |
218 | 0 | } |
219 | | |
220 | | EventResult Page::handle_keydown(UIEvents::KeyCode key, unsigned modifiers, u32 code_point) |
221 | 0 | { |
222 | 0 | return focused_navigable().event_handler().handle_keydown(key, modifiers, code_point); |
223 | 0 | } |
224 | | |
225 | | EventResult Page::handle_keyup(UIEvents::KeyCode key, unsigned modifiers, u32 code_point) |
226 | 0 | { |
227 | 0 | return focused_navigable().event_handler().handle_keyup(key, modifiers, code_point); |
228 | 0 | } |
229 | | |
230 | | void Page::set_top_level_traversable(JS::NonnullGCPtr<HTML::TraversableNavigable> navigable) |
231 | 0 | { |
232 | 0 | VERIFY(!m_top_level_traversable); // Replacement is not allowed! |
233 | 0 | VERIFY(&navigable->page() == this); |
234 | 0 | m_top_level_traversable = navigable; |
235 | 0 | } |
236 | | |
237 | | bool Page::top_level_traversable_is_initialized() const |
238 | 0 | { |
239 | 0 | return m_top_level_traversable; |
240 | 0 | } |
241 | | |
242 | | HTML::BrowsingContext& Page::top_level_browsing_context() |
243 | 0 | { |
244 | 0 | return *m_top_level_traversable->active_browsing_context(); |
245 | 0 | } |
246 | | |
247 | | HTML::BrowsingContext const& Page::top_level_browsing_context() const |
248 | 0 | { |
249 | 0 | return *m_top_level_traversable->active_browsing_context(); |
250 | 0 | } |
251 | | |
252 | | JS::NonnullGCPtr<HTML::TraversableNavigable> Page::top_level_traversable() const |
253 | 0 | { |
254 | 0 | return *m_top_level_traversable; |
255 | 0 | } |
256 | | |
257 | | template<typename ResponseType> |
258 | | static ResponseType spin_event_loop_until_dialog_closed(PageClient& client, Optional<ResponseType>& response, SourceLocation location = SourceLocation::current()) |
259 | 0 | { |
260 | 0 | auto& event_loop = Web::HTML::current_settings_object().responsible_event_loop(); |
261 | |
|
262 | 0 | ScopeGuard guard { [&] { event_loop.set_execution_paused(false); } };Unexecuted instantiation: Page.cpp:Web::spin_event_loop_until_dialog_closed<AK::Empty>(Web::PageClient&, AK::Optional<AK::Empty>&, AK::SourceLocation)::{lambda()#1}::operator()() constUnexecuted instantiation: Page.cpp:Web::spin_event_loop_until_dialog_closed<bool>(Web::PageClient&, AK::Optional<bool>&, AK::SourceLocation)::{lambda()#1}::operator()() constUnexecuted instantiation: Page.cpp:Web::spin_event_loop_until_dialog_closed<AK::Optional<AK::String> >(Web::PageClient&, AK::Optional<AK::Optional<AK::String> >&, AK::SourceLocation)::{lambda()#1}::operator()() const |
263 | 0 | event_loop.set_execution_paused(true); |
264 | |
|
265 | 0 | Web::Platform::EventLoopPlugin::the().spin_until([&]() { |
266 | 0 | return response.has_value() || !client.is_connection_open(); |
267 | 0 | }); Unexecuted instantiation: Page.cpp:Web::spin_event_loop_until_dialog_closed<AK::Empty>(Web::PageClient&, AK::Optional<AK::Empty>&, AK::SourceLocation)::{lambda()#2}::operator()() constUnexecuted instantiation: Page.cpp:Web::spin_event_loop_until_dialog_closed<bool>(Web::PageClient&, AK::Optional<bool>&, AK::SourceLocation)::{lambda()#2}::operator()() constUnexecuted instantiation: Page.cpp:Web::spin_event_loop_until_dialog_closed<AK::Optional<AK::String> >(Web::PageClient&, AK::Optional<AK::Optional<AK::String> >&, AK::SourceLocation)::{lambda()#2}::operator()() const |
268 | |
|
269 | 0 | if (!client.is_connection_open()) { |
270 | 0 | dbgln("WebContent client disconnected during {}. Exiting peacefully.", location.function_name()); |
271 | 0 | exit(0); |
272 | 0 | } |
273 | | |
274 | 0 | return response.release_value(); |
275 | 0 | } Unexecuted instantiation: Page.cpp:AK::Empty Web::spin_event_loop_until_dialog_closed<AK::Empty>(Web::PageClient&, AK::Optional<AK::Empty>&, AK::SourceLocation) Unexecuted instantiation: Page.cpp:bool Web::spin_event_loop_until_dialog_closed<bool>(Web::PageClient&, AK::Optional<bool>&, AK::SourceLocation) Unexecuted instantiation: Page.cpp:AK::Optional<AK::String> Web::spin_event_loop_until_dialog_closed<AK::Optional<AK::String> >(Web::PageClient&, AK::Optional<AK::Optional<AK::String> >&, AK::SourceLocation) |
276 | | |
277 | | void Page::did_request_alert(String const& message) |
278 | 0 | { |
279 | 0 | m_pending_dialog = PendingDialog::Alert; |
280 | 0 | m_client->page_did_request_alert(message); |
281 | |
|
282 | 0 | if (!message.is_empty()) |
283 | 0 | m_pending_dialog_text = message; |
284 | |
|
285 | 0 | spin_event_loop_until_dialog_closed(*m_client, m_pending_alert_response); |
286 | 0 | } |
287 | | |
288 | | void Page::alert_closed() |
289 | 0 | { |
290 | 0 | if (m_pending_dialog == PendingDialog::Alert) { |
291 | 0 | m_pending_alert_response = Empty {}; |
292 | 0 | on_pending_dialog_closed(); |
293 | 0 | } |
294 | 0 | } |
295 | | |
296 | | bool Page::did_request_confirm(String const& message) |
297 | 0 | { |
298 | 0 | m_pending_dialog = PendingDialog::Confirm; |
299 | 0 | m_client->page_did_request_confirm(message); |
300 | |
|
301 | 0 | if (!message.is_empty()) |
302 | 0 | m_pending_dialog_text = message; |
303 | |
|
304 | 0 | return spin_event_loop_until_dialog_closed(*m_client, m_pending_confirm_response); |
305 | 0 | } |
306 | | |
307 | | void Page::confirm_closed(bool accepted) |
308 | 0 | { |
309 | 0 | if (m_pending_dialog == PendingDialog::Confirm) { |
310 | 0 | m_pending_confirm_response = accepted; |
311 | 0 | on_pending_dialog_closed(); |
312 | 0 | } |
313 | 0 | } |
314 | | |
315 | | Optional<String> Page::did_request_prompt(String const& message, String const& default_) |
316 | 0 | { |
317 | 0 | m_pending_dialog = PendingDialog::Prompt; |
318 | 0 | m_client->page_did_request_prompt(message, default_); |
319 | |
|
320 | 0 | if (!message.is_empty()) |
321 | 0 | m_pending_dialog_text = message; |
322 | |
|
323 | 0 | return spin_event_loop_until_dialog_closed(*m_client, m_pending_prompt_response); |
324 | 0 | } |
325 | | |
326 | | void Page::prompt_closed(Optional<String> response) |
327 | 0 | { |
328 | 0 | if (m_pending_dialog == PendingDialog::Prompt) { |
329 | 0 | m_pending_prompt_response = move(response); |
330 | 0 | on_pending_dialog_closed(); |
331 | 0 | } |
332 | 0 | } |
333 | | |
334 | | void Page::dismiss_dialog(JS::GCPtr<JS::HeapFunction<void()>> on_dialog_closed) |
335 | 0 | { |
336 | 0 | m_on_pending_dialog_closed = on_dialog_closed; |
337 | |
|
338 | 0 | switch (m_pending_dialog) { |
339 | 0 | case PendingDialog::None: |
340 | 0 | break; |
341 | 0 | case PendingDialog::Alert: |
342 | 0 | m_client->page_did_request_accept_dialog(); |
343 | 0 | break; |
344 | 0 | case PendingDialog::Confirm: |
345 | 0 | case PendingDialog::Prompt: |
346 | 0 | m_client->page_did_request_dismiss_dialog(); |
347 | 0 | break; |
348 | 0 | } |
349 | 0 | } |
350 | | |
351 | | void Page::accept_dialog(JS::GCPtr<JS::HeapFunction<void()>> on_dialog_closed) |
352 | 0 | { |
353 | 0 | m_on_pending_dialog_closed = on_dialog_closed; |
354 | |
|
355 | 0 | switch (m_pending_dialog) { |
356 | 0 | case PendingDialog::None: |
357 | 0 | break; |
358 | 0 | case PendingDialog::Alert: |
359 | 0 | case PendingDialog::Confirm: |
360 | 0 | case PendingDialog::Prompt: |
361 | 0 | m_client->page_did_request_accept_dialog(); |
362 | 0 | break; |
363 | 0 | } |
364 | 0 | } |
365 | | |
366 | | void Page::on_pending_dialog_closed() |
367 | 0 | { |
368 | 0 | m_pending_dialog = PendingDialog::None; |
369 | 0 | m_pending_dialog_text.clear(); |
370 | |
|
371 | 0 | if (m_on_pending_dialog_closed) { |
372 | 0 | m_on_pending_dialog_closed->function()(); |
373 | 0 | m_on_pending_dialog_closed = nullptr; |
374 | 0 | } |
375 | 0 | } |
376 | | |
377 | | void Page::did_request_color_picker(WeakPtr<HTML::HTMLInputElement> target, Color current_color) |
378 | 0 | { |
379 | 0 | if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::None) { |
380 | 0 | m_pending_non_blocking_dialog = PendingNonBlockingDialog::ColorPicker; |
381 | 0 | m_pending_non_blocking_dialog_target = move(target); |
382 | |
|
383 | 0 | m_client->page_did_request_color_picker(current_color); |
384 | 0 | } |
385 | 0 | } |
386 | | |
387 | | void Page::color_picker_update(Optional<Color> picked_color, HTML::ColorPickerUpdateState state) |
388 | 0 | { |
389 | 0 | if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::ColorPicker) { |
390 | 0 | if (state == HTML::ColorPickerUpdateState::Closed) |
391 | 0 | m_pending_non_blocking_dialog = PendingNonBlockingDialog::None; |
392 | |
|
393 | 0 | if (m_pending_non_blocking_dialog_target) { |
394 | 0 | auto& input_element = verify_cast<HTML::HTMLInputElement>(*m_pending_non_blocking_dialog_target); |
395 | 0 | input_element.did_pick_color(move(picked_color), state); |
396 | 0 | if (state == HTML::ColorPickerUpdateState::Closed) |
397 | 0 | m_pending_non_blocking_dialog_target.clear(); |
398 | 0 | } |
399 | 0 | } |
400 | 0 | } |
401 | | |
402 | | void Page::did_request_file_picker(WeakPtr<HTML::HTMLInputElement> target, HTML::FileFilter accepted_file_types, HTML::AllowMultipleFiles allow_multiple_files) |
403 | 0 | { |
404 | 0 | if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::None) { |
405 | 0 | m_pending_non_blocking_dialog = PendingNonBlockingDialog::FilePicker; |
406 | 0 | m_pending_non_blocking_dialog_target = move(target); |
407 | |
|
408 | 0 | m_client->page_did_request_file_picker(move(accepted_file_types), allow_multiple_files); |
409 | 0 | } |
410 | 0 | } |
411 | | |
412 | | void Page::file_picker_closed(Span<HTML::SelectedFile> selected_files) |
413 | 0 | { |
414 | 0 | if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::FilePicker) { |
415 | 0 | m_pending_non_blocking_dialog = PendingNonBlockingDialog::None; |
416 | |
|
417 | 0 | if (m_pending_non_blocking_dialog_target) { |
418 | 0 | auto& input_element = verify_cast<HTML::HTMLInputElement>(*m_pending_non_blocking_dialog_target); |
419 | 0 | input_element.did_select_files(selected_files); |
420 | |
|
421 | 0 | m_pending_non_blocking_dialog_target.clear(); |
422 | 0 | } |
423 | 0 | } |
424 | 0 | } |
425 | | |
426 | | void Page::did_request_select_dropdown(WeakPtr<HTML::HTMLSelectElement> target, Web::CSSPixelPoint content_position, Web::CSSPixels minimum_width, Vector<Web::HTML::SelectItem> items) |
427 | 0 | { |
428 | 0 | if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::None) { |
429 | 0 | m_pending_non_blocking_dialog = PendingNonBlockingDialog::Select; |
430 | 0 | m_pending_non_blocking_dialog_target = move(target); |
431 | 0 | m_client->page_did_request_select_dropdown(content_position, minimum_width, move(items)); |
432 | 0 | } |
433 | 0 | } |
434 | | |
435 | | void Page::select_dropdown_closed(Optional<u32> const& selected_item_id) |
436 | 0 | { |
437 | 0 | if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::Select) { |
438 | 0 | m_pending_non_blocking_dialog = PendingNonBlockingDialog::None; |
439 | |
|
440 | 0 | if (m_pending_non_blocking_dialog_target) { |
441 | 0 | auto& select_element = verify_cast<HTML::HTMLSelectElement>(*m_pending_non_blocking_dialog_target); |
442 | 0 | select_element.did_select_item(selected_item_id); |
443 | 0 | m_pending_non_blocking_dialog_target.clear(); |
444 | 0 | } |
445 | 0 | } |
446 | 0 | } |
447 | | |
448 | | void Page::register_media_element(Badge<HTML::HTMLMediaElement>, int media_id) |
449 | 0 | { |
450 | 0 | m_media_elements.append(media_id); |
451 | 0 | } |
452 | | |
453 | | void Page::unregister_media_element(Badge<HTML::HTMLMediaElement>, int media_id) |
454 | 0 | { |
455 | 0 | m_media_elements.remove_all_matching([&](auto candidate_id) { |
456 | 0 | return candidate_id == media_id; |
457 | 0 | }); |
458 | 0 | } |
459 | | |
460 | | void Page::did_request_media_context_menu(i32 media_id, CSSPixelPoint position, ByteString const& target, unsigned modifiers, MediaContextMenu menu) |
461 | 0 | { |
462 | 0 | m_media_context_menu_element_id = media_id; |
463 | 0 | client().page_did_request_media_context_menu(position, target, modifiers, move(menu)); |
464 | 0 | } |
465 | | |
466 | | WebIDL::ExceptionOr<void> Page::toggle_media_play_state() |
467 | 0 | { |
468 | 0 | auto media_element = media_context_menu_element(); |
469 | 0 | if (!media_element) |
470 | 0 | return {}; |
471 | | |
472 | | // AD-HOC: An execution context is required for Promise creation hooks. |
473 | 0 | HTML::TemporaryExecutionContext execution_context { media_element->document().relevant_settings_object() }; |
474 | |
|
475 | 0 | if (media_element->potentially_playing()) |
476 | 0 | TRY(media_element->pause()); |
477 | 0 | else |
478 | 0 | TRY(media_element->play()); |
479 | |
|
480 | 0 | return {}; |
481 | 0 | } |
482 | | |
483 | | void Page::toggle_media_mute_state() |
484 | 0 | { |
485 | 0 | auto media_element = media_context_menu_element(); |
486 | 0 | if (!media_element) |
487 | 0 | return; |
488 | | |
489 | | // AD-HOC: An execution context is required for Promise creation hooks. |
490 | 0 | HTML::TemporaryExecutionContext execution_context { media_element->document().relevant_settings_object() }; |
491 | |
|
492 | 0 | media_element->set_muted(!media_element->muted()); |
493 | 0 | } |
494 | | |
495 | | WebIDL::ExceptionOr<void> Page::toggle_media_loop_state() |
496 | 0 | { |
497 | 0 | auto media_element = media_context_menu_element(); |
498 | 0 | if (!media_element) |
499 | 0 | return {}; |
500 | | |
501 | | // AD-HOC: An execution context is required for Promise creation hooks. |
502 | 0 | HTML::TemporaryExecutionContext execution_context { media_element->document().relevant_settings_object() }; |
503 | |
|
504 | 0 | if (media_element->has_attribute(HTML::AttributeNames::loop)) |
505 | 0 | media_element->remove_attribute(HTML::AttributeNames::loop); |
506 | 0 | else |
507 | 0 | TRY(media_element->set_attribute(HTML::AttributeNames::loop, {})); |
508 | |
|
509 | 0 | return {}; |
510 | 0 | } |
511 | | |
512 | | WebIDL::ExceptionOr<void> Page::toggle_media_controls_state() |
513 | 0 | { |
514 | 0 | auto media_element = media_context_menu_element(); |
515 | 0 | if (!media_element) |
516 | 0 | return {}; |
517 | | |
518 | 0 | HTML::TemporaryExecutionContext execution_context { media_element->document().relevant_settings_object() }; |
519 | |
|
520 | 0 | if (media_element->has_attribute(HTML::AttributeNames::controls)) |
521 | 0 | media_element->remove_attribute(HTML::AttributeNames::controls); |
522 | 0 | else |
523 | 0 | TRY(media_element->set_attribute(HTML::AttributeNames::controls, {})); |
524 | |
|
525 | 0 | return {}; |
526 | 0 | } |
527 | | |
528 | | void Page::toggle_page_mute_state() |
529 | 0 | { |
530 | 0 | m_mute_state = HTML::invert_mute_state(m_mute_state); |
531 | |
|
532 | 0 | for (auto media_id : m_media_elements) { |
533 | 0 | if (auto* node = DOM::Node::from_unique_id(media_id)) { |
534 | 0 | auto& media_element = verify_cast<HTML::HTMLMediaElement>(*node); |
535 | 0 | media_element.page_mute_state_changed({}); |
536 | 0 | } |
537 | 0 | } |
538 | 0 | } |
539 | | |
540 | | JS::GCPtr<HTML::HTMLMediaElement> Page::media_context_menu_element() |
541 | 0 | { |
542 | 0 | if (!m_media_context_menu_element_id.has_value()) |
543 | 0 | return nullptr; |
544 | | |
545 | 0 | auto* dom_node = DOM::Node::from_unique_id(*m_media_context_menu_element_id); |
546 | 0 | if (dom_node == nullptr) |
547 | 0 | return nullptr; |
548 | | |
549 | 0 | if (!is<HTML::HTMLMediaElement>(dom_node)) |
550 | 0 | return nullptr; |
551 | | |
552 | 0 | return static_cast<HTML::HTMLMediaElement*>(dom_node); |
553 | 0 | } |
554 | | |
555 | | void Page::set_user_style(String source) |
556 | 0 | { |
557 | 0 | m_user_style_sheet_source = source; |
558 | 0 | if (top_level_traversable_is_initialized() && top_level_traversable()->active_document()) { |
559 | 0 | top_level_traversable()->active_document()->style_computer().invalidate_rule_cache(); |
560 | 0 | } |
561 | 0 | } |
562 | | |
563 | | Vector<JS::Handle<DOM::Document>> Page::documents_in_active_window() const |
564 | 0 | { |
565 | 0 | if (!top_level_traversable_is_initialized()) |
566 | 0 | return {}; |
567 | | |
568 | 0 | auto documents = HTML::main_thread_event_loop().documents_in_this_event_loop(); |
569 | 0 | for (ssize_t i = documents.size() - 1; i >= 0; --i) { |
570 | 0 | if (documents[i]->window() != top_level_traversable()->active_window()) |
571 | 0 | documents.remove(i); |
572 | 0 | } |
573 | |
|
574 | 0 | return documents; |
575 | 0 | } |
576 | | |
577 | | void Page::clear_selection() |
578 | 0 | { |
579 | 0 | for (auto const& document : documents_in_active_window()) { |
580 | 0 | auto selection = document->get_selection(); |
581 | 0 | if (!selection) |
582 | 0 | continue; |
583 | | |
584 | 0 | selection->remove_all_ranges(); |
585 | 0 | } |
586 | 0 | } |
587 | | |
588 | | Page::FindInPageResult Page::perform_find_in_page_query(FindInPageQuery const& query, Optional<SearchDirection> direction) |
589 | 0 | { |
590 | 0 | VERIFY(top_level_traversable_is_initialized()); |
591 | | |
592 | 0 | Vector<JS::Handle<DOM::Range>> all_matches; |
593 | |
|
594 | 0 | auto find_current_match_index = [this, &direction](auto& document, auto& matches) -> size_t { |
595 | | // Always return the first match if there is no active query. |
596 | 0 | if (!m_last_find_in_page_query.has_value()) |
597 | 0 | return 0; |
598 | | |
599 | 0 | auto selection = document.get_selection(); |
600 | 0 | if (!selection) |
601 | 0 | return 0; |
602 | | |
603 | 0 | auto range = selection->range(); |
604 | 0 | if (!range) |
605 | 0 | return 0; |
606 | | |
607 | 0 | for (size_t i = 0; i < matches.size(); ++i) { |
608 | 0 | auto boundary_comparison_or_error = matches[i]->compare_boundary_points(DOM::Range::HowToCompareBoundaryPoints::START_TO_START, *range); |
609 | 0 | if (!boundary_comparison_or_error.is_error() && boundary_comparison_or_error.value() >= 0) { |
610 | | // If the match occurs after the current selection then we don't need to increment the match index later on. |
611 | 0 | if (boundary_comparison_or_error.value() && direction == SearchDirection::Forward) |
612 | 0 | direction = {}; |
613 | |
|
614 | 0 | return i; |
615 | 0 | } |
616 | 0 | } |
617 | | |
618 | 0 | return 0; |
619 | 0 | }; |
620 | |
|
621 | 0 | for (auto document : documents_in_active_window()) { |
622 | 0 | auto matches = document->find_matching_text(query.string, query.case_sensitivity); |
623 | 0 | if (document == top_level_traversable()->active_document()) { |
624 | 0 | auto new_match_index = find_current_match_index(*document, matches); |
625 | 0 | m_find_in_page_match_index = new_match_index + all_matches.size(); |
626 | 0 | } |
627 | |
|
628 | 0 | all_matches.extend(move(matches)); |
629 | 0 | } |
630 | |
|
631 | 0 | if (auto active_document = top_level_traversable()->active_document()) { |
632 | 0 | if (m_last_find_in_page_url.serialize(URL::ExcludeFragment::Yes) != active_document->url().serialize(URL::ExcludeFragment::Yes)) { |
633 | 0 | m_last_find_in_page_url = top_level_traversable()->active_document()->url(); |
634 | 0 | m_find_in_page_match_index = 0; |
635 | 0 | } |
636 | 0 | } |
637 | |
|
638 | 0 | if (direction.has_value()) { |
639 | 0 | if (direction.value() == SearchDirection::Forward) { |
640 | 0 | if (m_find_in_page_match_index >= all_matches.size() - 1) { |
641 | 0 | if (query.wrap_around == WrapAround::No) |
642 | 0 | return {}; |
643 | 0 | m_find_in_page_match_index = 0; |
644 | 0 | } else { |
645 | 0 | m_find_in_page_match_index++; |
646 | 0 | } |
647 | 0 | } else { |
648 | 0 | if (m_find_in_page_match_index == 0) { |
649 | 0 | if (query.wrap_around == WrapAround::No) |
650 | 0 | return {}; |
651 | 0 | m_find_in_page_match_index = all_matches.size() - 1; |
652 | 0 | } else { |
653 | 0 | m_find_in_page_match_index--; |
654 | 0 | } |
655 | 0 | } |
656 | 0 | } |
657 | | |
658 | 0 | update_find_in_page_selection(all_matches); |
659 | |
|
660 | 0 | return Page::FindInPageResult { |
661 | 0 | .current_match_index = m_find_in_page_match_index, |
662 | 0 | .total_match_count = all_matches.size(), |
663 | 0 | }; |
664 | 0 | } |
665 | | |
666 | | Page::FindInPageResult Page::find_in_page(FindInPageQuery const& query) |
667 | 0 | { |
668 | 0 | if (!top_level_traversable_is_initialized()) |
669 | 0 | return {}; |
670 | | |
671 | 0 | if (query.string.is_empty()) { |
672 | 0 | m_last_find_in_page_query = {}; |
673 | 0 | clear_selection(); |
674 | 0 | return {}; |
675 | 0 | } |
676 | | |
677 | 0 | auto result = perform_find_in_page_query(query); |
678 | |
|
679 | 0 | m_last_find_in_page_query = query; |
680 | 0 | m_last_find_in_page_url = top_level_traversable()->active_document()->url(); |
681 | |
|
682 | 0 | return result; |
683 | 0 | } |
684 | | |
685 | | Page::FindInPageResult Page::find_in_page_next_match() |
686 | 0 | { |
687 | 0 | if (!(m_last_find_in_page_query.has_value() && top_level_traversable_is_initialized())) |
688 | 0 | return {}; |
689 | | |
690 | 0 | auto result = perform_find_in_page_query(*m_last_find_in_page_query, SearchDirection::Forward); |
691 | 0 | return result; |
692 | 0 | } |
693 | | |
694 | | Page::FindInPageResult Page::find_in_page_previous_match() |
695 | 0 | { |
696 | 0 | if (!(m_last_find_in_page_query.has_value() && top_level_traversable_is_initialized())) |
697 | 0 | return {}; |
698 | | |
699 | 0 | auto result = perform_find_in_page_query(*m_last_find_in_page_query, SearchDirection::Backward); |
700 | 0 | return result; |
701 | 0 | } |
702 | | |
703 | | void Page::update_find_in_page_selection(Vector<JS::Handle<DOM::Range>> matches) |
704 | 0 | { |
705 | 0 | clear_selection(); |
706 | |
|
707 | 0 | if (matches.is_empty()) |
708 | 0 | return; |
709 | | |
710 | 0 | auto current_range = matches[m_find_in_page_match_index]; |
711 | 0 | auto common_ancestor_container = current_range->common_ancestor_container(); |
712 | 0 | auto& document = common_ancestor_container->document(); |
713 | 0 | if (!document.window()) |
714 | 0 | return; |
715 | | |
716 | 0 | auto selection = document.get_selection(); |
717 | 0 | if (!selection) |
718 | 0 | return; |
719 | | |
720 | 0 | selection->add_range(*current_range); |
721 | |
|
722 | 0 | if (auto* element = common_ancestor_container->parent_element()) { |
723 | 0 | DOM::ScrollIntoViewOptions scroll_options; |
724 | 0 | scroll_options.block = Bindings::ScrollLogicalPosition::Nearest; |
725 | 0 | scroll_options.inline_ = Bindings::ScrollLogicalPosition::Nearest; |
726 | 0 | scroll_options.behavior = Bindings::ScrollBehavior::Instant; |
727 | 0 | (void)element->scroll_into_view(scroll_options); |
728 | 0 | } |
729 | 0 | } |
730 | | |
731 | | } |
732 | | |
733 | | template<> |
734 | | ErrorOr<void> IPC::encode(Encoder& encoder, Web::Page::MediaContextMenu const& menu) |
735 | 0 | { |
736 | 0 | TRY(encoder.encode(menu.media_url)); |
737 | 0 | TRY(encoder.encode(menu.is_video)); |
738 | 0 | TRY(encoder.encode(menu.is_playing)); |
739 | 0 | TRY(encoder.encode(menu.is_muted)); |
740 | 0 | TRY(encoder.encode(menu.has_user_agent_controls)); |
741 | 0 | TRY(encoder.encode(menu.is_looping)); |
742 | 0 | return {}; |
743 | 0 | } |
744 | | |
745 | | template<> |
746 | | ErrorOr<Web::Page::MediaContextMenu> IPC::decode(Decoder& decoder) |
747 | 0 | { |
748 | 0 | return Web::Page::MediaContextMenu { |
749 | 0 | .media_url = TRY(decoder.decode<URL::URL>()), |
750 | 0 | .is_video = TRY(decoder.decode<bool>()), |
751 | 0 | .is_playing = TRY(decoder.decode<bool>()), |
752 | 0 | .is_muted = TRY(decoder.decode<bool>()), |
753 | 0 | .has_user_agent_controls = TRY(decoder.decode<bool>()), |
754 | 0 | .is_looping = TRY(decoder.decode<bool>()), |
755 | 0 | }; |
756 | 0 | } |