Coverage Report

Created: 2026-06-07 07:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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()() const
Unexecuted instantiation: Page.cpp:Web::spin_event_loop_until_dialog_closed<bool>(Web::PageClient&, AK::Optional<bool>&, AK::SourceLocation)::{lambda()#1}::operator()() const
Unexecuted 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()() const
Unexecuted instantiation: Page.cpp:Web::spin_event_loop_until_dialog_closed<bool>(Web::PageClient&, AK::Optional<bool>&, AK::SourceLocation)::{lambda()#2}::operator()() const
Unexecuted 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
}