/* * Copyright (c) 2022-2023, Andreas Kling * Copyright (c) 2022, Matthew Costa * Copyright (c) 2022, Filiph Sandström * Copyright (c) 2023, Linus Groh * Copyright (c) 2024-2026, Sam Atkins * Copyright (c) 2025, Simon Farre * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(AK_OS_MACOS) # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Ladybird { static constexpr auto AUDIO_STATE_BUTTON_POSITION = QTabBar::LeftSide; static constexpr auto TAB_CLOSE_BUTTON_POSITION = QTabBar::RightSide; static constexpr auto WINDOW_DRAG_REGION_PROPERTY = "LadybirdWindowDragRegion"; #if defined(AK_OS_MACOS) static constexpr qreal WINDOW_CORNER_RADIUS = 12.0; #endif static constexpr int WINDOW_RESIZE_BORDER_WIDTH = 6; static constexpr int WINDOW_RESIZE_CORNER_WIDTH = WINDOW_RESIZE_BORDER_WIDTH * 2; static bool should_use_screen_signal_for_dpi_changes() { #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) return QGuiApplication::platformName() != "wayland"; #else return true; #endif } static Optional display_id_for_screen(QScreen* screen) { if (!screen) return {}; // Qt does not expose a portable physical display identifier. The compositor only // needs a stable per-process grouping key for Qt-backed windows. static u64 next_display_id = 1; static HashMap display_ids; return display_ids.ensure(screen, [] { return next_display_id++; }); } static Vector recently_closed_urls_for_window(TabWidget const& tabs_container) { Vector urls; urls.ensure_capacity(tabs_container.count()); for (int index = 0; index < tabs_container.count(); ++index) urls.append(tabs_container.tab(index)->view().url()); return urls; } FullscreenMode::FullscreenMode(BrowserWindow* window, ExitFullscreenButton* exit_button) : QObject(window) , m_window(window) , m_exit_button(exit_button) { connect(m_exit_button, &QPushButton::clicked, this, [this]() { exit(ExitInitiatedBy::UI); }); } void FullscreenMode::exit(ExitInitiatedBy initiated_by) { if (is_api_fullscreen()) { qApp->removeEventFilter(this); if (initiated_by == ExitInitiatedBy::UI && m_window->tab_index(m_fullscreen_tab) != -1) { m_fullscreen_tab->view().exit_fullscreen(); } emit on_exit_fullscreen(); } m_fullscreen_tab = nullptr; } void FullscreenMode::enter(Tab* tab) { qApp->installEventFilter(this); m_fullscreen_tab = tab; m_window->enter_fullscreen(); } void FullscreenMode::entered_fullscreen() { m_debounce = true; m_exit_button->animate_show(); // Let button float in place 3 * time it takes to animate it in place QTimer::singleShot(button_animation_time() * 3, [this]() { m_debounce = false; }); } bool FullscreenMode::is_api_fullscreen() const { return m_fullscreen_tab; } bool FullscreenMode::debounce() const { return m_debounce; } void FullscreenMode::maybe_animate_show_exit_button(QPointF pos) { u64 const mouse_y = static_cast(pos.y()); u64 const threshold = static_cast(m_window->height() * 0.01); if (debounce()) { return; } // Display the button if the mouse is 1% from the top if (mouse_y <= threshold) { if (!m_exit_button->isVisible()) { m_debounce = true; m_exit_button->animate_show(); QTimer::singleShot(button_animation_time() * 3, [this]() { m_debounce = false; }); } } else if (mouse_y > (threshold * 10) && m_exit_button->isVisible()) { // if the button has floated in, we want to hide it when leaving the top 10% m_exit_button->hide(); } } bool FullscreenMode::eventFilter(QObject* obj, QEvent* event) { ASSERT(is_api_fullscreen()); if (event->type() == QEvent::MouseMove) { QMouseEvent* mouse_event = static_cast(event); maybe_animate_show_exit_button(m_window->mapFromGlobal(mouse_event->globalPosition().toPoint())); } return QObject::eventFilter(obj, event); } ExitFullscreenButton::ExitFullscreenButton(QWidget* parent) : QPushButton("Exit fullscreen", parent) { #if defined(AK_OS_MACOS) // The web content view is a native QRhiWidget on macOS, so this overlay must also be native to remain above it. setAttribute(Qt::WA_NativeWindow); #endif setStyleSheet("background-color:rgb(55, 99, 129); color: white; padding: 10px; border-radius: 5px;"); adjustSize(); hide(); m_widget_animation = new QPropertyAnimation(this, "pos", this); } void ExitFullscreenButton::animate_show() { if (isVisible()) return; show(); raise(); auto const container_size = screen() ? screen()->geometry().size() : (parentWidget() ? parentWidget()->size() : QSize {}); int const destination_x = (container_size.width() - width()) / 2; int const destination_y = static_cast(static_cast(container_size.height()) * 0.05); m_widget_animation->stop(); m_widget_animation->setDuration(FullscreenMode::button_animation_time()); m_widget_animation->setStartValue(QPoint(destination_x, -height())); m_widget_animation->setEndValue(QPoint(destination_x, destination_y)); m_widget_animation->setEasingCurve(QEasingCurve::OutBounce); m_widget_animation->start(); } static QIcon const& app_icon() { static QIcon icon; if (icon.isNull()) { QPixmap pixmap; pixmap.load(":/Icons/ladybird.png"); icon = QIcon(pixmap); } return icon; } BrowserWindow::BrowserWindow(Vector const& initial_urls, IsPopupWindow is_popup_window, Tab* parent_tab, Optional page_index) : m_tabs_container(new TabWidget(this)) , m_is_popup_window(is_popup_window) { auto& application = WebView::Application::the(); auto const& browser_options = WebView::Application::browser_options(); setWindowFlag(Qt::FramelessWindowHint, uses_client_side_decorations()); setAttribute(Qt::WA_OpaquePaintEvent); setWindowIcon(app_icon()); qApp->installEventFilter(this); update_window_corners(); update_appkit_window_resizability(); update_window_border(); update_tabs_display(); // Listen for DPI changes m_device_pixel_ratio = devicePixelRatio(); m_current_screen = screen(); m_display_id = display_id_for_screen(m_current_screen); if (m_current_screen) m_refresh_rate = m_current_screen->refreshRate(); if (should_use_screen_signal_for_dpi_changes()) { setAttribute(Qt::WA_NativeWindow); setAttribute(Qt::WA_DontCreateNativeAncestors); } connect_screen_signals(m_current_screen); connect_window_screen_changed_signal(); m_hamburger_menu = new QMenu(this); menuBar()->setObjectName("LadybirdMenuBar"); create_menu_bar_window_controls(); update_menu_bar_style(); update_menu_bar_visibility(); auto* file_menu = menuBar()->addMenu("&File"); m_new_tab_action = new QAction("New &Tab", this); m_new_tab_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::AddTab)); m_hamburger_menu->addAction(m_new_tab_action); file_menu->addAction(m_new_tab_action); m_new_window_action = new QAction("New &Window", this); m_new_window_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::New)); m_hamburger_menu->addAction(m_new_window_action); file_menu->addAction(m_new_window_action); m_reopen_recently_closed_tab_action = new QAction("&Reopen Recently Closed Tab", this); m_reopen_recently_closed_tab_action->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_T)); m_hamburger_menu->addAction(m_reopen_recently_closed_tab_action); file_menu->addAction(m_reopen_recently_closed_tab_action); update_reopen_recently_closed_action(); auto* close_current_tab_action = new QAction("&Close Current Tab", this); close_current_tab_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::Close)); m_hamburger_menu->addAction(close_current_tab_action); file_menu->addAction(close_current_tab_action); auto* open_file_action = new QAction("&Open File...", this); open_file_action->setShortcut(QKeySequence(QKeySequence::StandardKey::Open)); m_hamburger_menu->addAction(open_file_action); file_menu->addAction(open_file_action); m_hamburger_menu->addSeparator(); auto* edit_menu = m_hamburger_menu->addMenu("&Edit"); menuBar()->addMenu(edit_menu); edit_menu->addAction(create_application_action(*this, application.cut_selection_action(), IncludeActionIcon::No)); edit_menu->addAction(create_application_action(*this, application.copy_selection_action(), IncludeActionIcon::No)); edit_menu->addAction(create_application_action(*this, application.paste_action(), IncludeActionIcon::No)); edit_menu->addAction(create_application_action(*this, application.select_all_action(), IncludeActionIcon::No)); edit_menu->addSeparator(); m_find_in_page_action = new QAction("&Find in Page...", this); m_find_in_page_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::Find)); auto find_previous_shortcuts = QKeySequence::keyBindings(QKeySequence::StandardKey::FindPrevious); for (auto const& shortcut : find_previous_shortcuts) new QShortcut(shortcut, this, [this] { if (m_current_tab) m_current_tab->find_previous(); }); auto find_next_shortcuts = QKeySequence::keyBindings(QKeySequence::StandardKey::FindNext); for (auto const& shortcut : find_next_shortcuts) new QShortcut(shortcut, this, [this] { if (m_current_tab) m_current_tab->find_next(); }); edit_menu->addAction(m_find_in_page_action); QObject::connect(m_find_in_page_action, &QAction::triggered, this, &BrowserWindow::show_find_in_page); edit_menu->addSeparator(); edit_menu->addAction(create_application_action(*edit_menu, application.open_settings_page_action(), IncludeActionIcon::No)); auto* view_menu = m_hamburger_menu->addMenu("&View"); menuBar()->addMenu(view_menu); auto* open_next_tab_action = new QAction("Open &Next Tab", this); open_next_tab_action->setShortcuts({ QKeySequence(Qt::CTRL | Qt::Key_PageDown), QKeySequence(Qt::CTRL | Qt::Key_Tab), #if defined(AK_OS_MACOS) QKeySequence(Qt::META | Qt::Key_Tab), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_BracketRight), #endif }); view_menu->addAction(open_next_tab_action); QObject::connect(open_next_tab_action, &QAction::triggered, this, &BrowserWindow::open_next_tab); auto* open_previous_tab_action = new QAction("Open &Previous Tab", this); open_previous_tab_action->setShortcuts({ QKeySequence(Qt::CTRL | Qt::Key_PageUp), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab), #if defined(AK_OS_MACOS) QKeySequence(Qt::META | Qt::SHIFT | Qt::Key_Tab), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_BracketLeft), #endif }); view_menu->addAction(open_previous_tab_action); QObject::connect(open_previous_tab_action, &QAction::triggered, this, &BrowserWindow::open_previous_tab); view_menu->addSeparator(); view_menu->addMenu(create_application_menu(*view_menu, application.zoom_menu())); view_menu->addSeparator(); view_menu->addMenu(create_application_menu(*view_menu, application.color_scheme_menu())); view_menu->addMenu(create_application_menu(*view_menu, application.contrast_menu())); view_menu->addMenu(create_application_menu(*view_menu, application.motion_menu())); view_menu->addSeparator(); if (show_menubar_option_available()) view_menu->addAction(create_application_action(*view_menu, application.toggle_menu_bar_action(), IncludeActionIcon::No)); m_bookmarks_menu = Application::the().qt_bookmarks_menu(); if (!m_bookmarks_menu) m_bookmarks_menu = create_application_menu(*this, application.bookmarks_menu()); m_hamburger_menu->addMenu(m_bookmarks_menu); menuBar()->addMenu(m_bookmarks_menu); m_history_menu = create_application_menu(*this, application.history_menu()); m_hamburger_menu->addMenu(m_history_menu); menuBar()->addMenu(m_history_menu); auto* inspect_menu = create_application_menu(*m_hamburger_menu, application.inspect_menu()); m_hamburger_menu->addMenu(inspect_menu); menuBar()->addMenu(inspect_menu); auto* debug_menu = create_application_menu(*m_hamburger_menu, application.debug_menu()); m_hamburger_menu->addMenu(debug_menu); menuBar()->addMenu(debug_menu); auto* help_menu = m_hamburger_menu->addMenu("&Help"); menuBar()->addMenu(help_menu); help_menu->addAction(create_application_action(*help_menu, application.open_about_page_action(), IncludeActionIcon::No)); m_hamburger_menu->addSeparator(); file_menu->addSeparator(); auto* quit_action = new QAction("&Quit", this); quit_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::Quit)); m_hamburger_menu->addAction(quit_action); file_menu->addAction(quit_action); #if defined(AK_OS_MACOS) QObject::connect(quit_action, &QAction::triggered, this, [] { Application::the().quit(); }); #else QObject::connect(quit_action, &QAction::triggered, this, &QMainWindow::close); #endif QObject::connect(m_new_tab_action, &QAction::triggered, this, [] { Application::the().open_new_tab(); }); QObject::connect(m_new_window_action, &QAction::triggered, this, [] { Application::the().open_new_window(); }); QObject::connect(m_reopen_recently_closed_tab_action, &QAction::triggered, this, [] { Application::the().reopen_recently_closed_tab(); }); QObject::connect(open_file_action, &QAction::triggered, this, [] { Application::the().open_file(); }); m_exit_button = new ExitFullscreenButton { this }; m_fullscreen_mode = new FullscreenMode { this, m_exit_button }; connect(m_fullscreen_mode, &FullscreenMode::on_exit_fullscreen, this, &BrowserWindow::exit_fullscreen); connect(m_fullscreen_mode, &FullscreenMode::on_exit_fullscreen, m_exit_button, &ExitFullscreenButton::hide); QObject::connect(m_tabs_container, &TabWidget::current_tab_changed, this, [this](int index) { auto* tab = m_tabs_container->tab(index); if (tab) setWindowTitle(QString("%1 - Ladybird").arg(tab->title())); set_current_tab(tab); if (tab) { if (auto* focus_widget = tab->focusWidget(); focus_widget && tab->isAncestorOf(focus_widget)) focus_widget->setFocus(); else tab->view().setFocus(); } fullscreen_mode().exit(FullscreenMode::ExitInitiatedBy::UI); }); QObject::connect(m_tabs_container, &TabWidget::tab_close_requested, this, &BrowserWindow::request_to_close_tab); QObject::connect(close_current_tab_action, &QAction::triggered, this, &BrowserWindow::request_to_close_current_tab); for (int i = 0; i <= 7; ++i) { new QShortcut(QKeySequence(Qt::CTRL | static_cast(Qt::Key_1 + i)), this, [this, i] { if (m_tabs_container->count() <= 1) return; m_tabs_container->set_current_index(min(i, m_tabs_container->count() - 1)); }); } new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_9), this, [this] { if (m_tabs_container->count() <= 1) return; m_tabs_container->set_current_index(m_tabs_container->count() - 1); }); if (parent_tab) { new_child_tab(Web::HTML::ActivateTab::Yes, *parent_tab, AK::move(page_index)); } else { for (size_t i = 0; i < initial_urls.size(); ++i) { new_tab_from_url(initial_urls[i], (i == 0) ? Web::HTML::ActivateTab::Yes : Web::HTML::ActivateTab::No); } } m_tabs_container->set_new_tab_action(m_new_tab_action); auto* main_widget = new QWidget(this); auto* main_layout = new QVBoxLayout(main_widget); main_layout->setContentsMargins(0, 0, 0, 0); main_layout->setSpacing(0); main_layout->addWidget(m_tabs_container, 1); m_devtools_banner = new DevToolsBanner(main_widget); connect(m_devtools_banner, &DevToolsBanner::disable_requested, this, [] { MUST(WebView::Application::the().toggle_devtools_enabled()); }); m_devtools_banner->hide(); main_layout->addWidget(m_devtools_banner); setCentralWidget(main_widget); setContextMenuPolicy(Qt::PreventContextMenu); if (browser_options.devtools_port.has_value()) on_devtools_enabled(); } BrowserWindow::~BrowserWindow() { qApp->removeEventFilter(this); } void BrowserWindow::update_tabs_display() { auto const& settings = Application::settings().tab_settings(); m_tabs_container->set_vertical_tabs_enabled(settings.vertical_tabs_enabled); m_tabs_container->set_vertical_tabs_expanded(settings.vertical_tabs_expanded); m_tabs_container->set_vertical_tabs_expand_on_hover(settings.vertical_tabs_expand_on_hover); } void BrowserWindow::rebuild_bookmarks_menu() { if (m_bookmarks_menu != Application::the().qt_bookmarks_menu()) repopulate_application_menu(*m_bookmarks_menu, *this, Application::the().bookmarks_menu()); for_each_tab([](Tab& tab) { tab.bookmarks_bar().rebuild(); }); } void BrowserWindow::show_bookmarks_bar_changed() { auto show_bookmarks_bar = WebView::Application::settings().show_bookmarks_bar(); for_each_tab([&](Tab& tab) { tab.bookmarks_bar().setVisible(show_bookmarks_bar); }); } void BrowserWindow::on_devtools_enabled() { m_devtools_banner->set_port(WebView::Application::browser_options().devtools_port.value_or(0)); m_devtools_banner->show(); } void BrowserWindow::on_devtools_disabled() { m_devtools_banner->hide(); } Tab& BrowserWindow::new_tab_from_url(URL::URL const& url, Web::HTML::ActivateTab activate_tab) { auto& tab = create_new_tab(activate_tab); tab.navigate(url); return tab; } Tab& BrowserWindow::new_child_tab(Web::HTML::ActivateTab activate_tab, Tab& parent, Optional page_index) { return create_new_tab(activate_tab, parent, page_index); } Tab& BrowserWindow::create_new_tab(Web::HTML::ActivateTab activate_tab, Tab& parent, Optional page_index) { if (!page_index.has_value()) return create_new_tab(activate_tab); auto* tab = new Tab(this, parent.view().client(), page_index.value()); // FIXME: Merge with other overload if (m_current_tab == nullptr) { set_current_tab(tab); } m_tabs_container->add_tab(tab, "New Tab"); if (activate_tab == Web::HTML::ActivateTab::Yes) m_tabs_container->set_current_tab(tab); initialize_tab(tab); return *tab; } FullscreenMode& BrowserWindow::fullscreen_mode() { return *m_fullscreen_mode; } bool BrowserWindow::uses_client_side_decorations() { return !WebView::Application::settings().config_variable_as_bool(WebView::ConfigVariableID::UseServerSideWindowDecorations); } Tab& BrowserWindow::create_new_tab(Web::HTML::ActivateTab activate_tab) { auto* tab = new Tab(this); if (m_current_tab == nullptr) { set_current_tab(tab); } m_tabs_container->add_tab(tab, "New Tab"); if (activate_tab == Web::HTML::ActivateTab::Yes) m_tabs_container->set_current_tab(tab); initialize_tab(tab); return *tab; } void BrowserWindow::initialize_tab(Tab* tab) { QObject::connect(tab, &Tab::title_changed, this, &BrowserWindow::tab_title_changed); QObject::connect(tab, &Tab::favicon_changed, this, &BrowserWindow::tab_favicon_changed); QObject::connect(tab, &Tab::audio_play_state_changed, this, &BrowserWindow::tab_audio_play_state_changed); QObject::connect(&tab->view(), &WebContentView::urls_dropped, this, [this](auto& urls) { VERIFY(urls.size()); m_current_tab->navigate(ak_url_from_qurl(urls[0])); for (qsizetype i = 1; i < urls.size(); ++i) new_tab_from_url(ak_url_from_qurl(urls[i]), Web::HTML::ActivateTab::No); }); tab->view().on_new_web_view = [this, tab](auto activate_tab, Web::HTML::WebViewHints hints, Optional page_index) { if (hints.popup) { WindowConfiguration configuration { .x = hints.screen_x, .y = hints.screen_y, .width = hints.width, .height = hints.height, }; auto& window = Application::the().new_window({}, configuration, IsPopupWindow::Yes, tab, AK::move(page_index)); return window.current_tab()->view().handle(); } auto& new_tab = new_child_tab(activate_tab, *tab, page_index); return new_tab.view().handle(); }; initialize_tab_buttons(tab); } void BrowserWindow::uninitialize_tab(Tab* tab) { QObject::disconnect(tab, nullptr, this, nullptr); QObject::disconnect(&tab->view(), nullptr, this, nullptr); } void BrowserWindow::adopt_tab(Tab& tab, int index) { index = clamp(index, 0, m_tabs_container->count()); tab.set_window(*this); m_tabs_container->insert_tab(index, &tab, "New Tab"); initialize_tab(&tab); tab_title_changed(index, tab.title()); tab.view().set_device_pixel_ratio(m_device_pixel_ratio); tab.view().set_display_metadata(m_display_id, m_refresh_rate); m_tabs_container->set_current_tab(&tab); } void BrowserWindow::move_tab_to_window(int index, BrowserWindow& target_window, int target_index) { if (index < 0 || index >= m_tabs_container->count()) return; if (&target_window == this) { if (target_index > index) --target_index; target_index = clamp(target_index, 0, m_tabs_container->count() - 1); move_tab(index, target_index); return; } auto* tab = m_tabs_container->tab(index); uninitialize_tab(tab); m_tabs_container->take_tab(index); if (m_current_tab == tab) set_current_tab(m_tabs_container->count() > 0 ? m_tabs_container->tab(m_tabs_container->current_index()) : nullptr); target_window.adopt_tab(*tab, target_index); if (m_tabs_container->count() == 0) { m_should_record_closed_window_on_close = false; close(); } } void BrowserWindow::detach_tab_to_new_window(int index, QPoint global_position) { if (index < 0 || index >= m_tabs_container->count()) return; WindowConfiguration configuration { .x = Web::DevicePixels { global_position.x() - 160 }, .y = Web::DevicePixels { global_position.y() - 18 }, .width = Web::DevicePixels { width() }, .height = Web::DevicePixels { height() }, .maximized = isMaximized(), }; auto& window = Application::the().new_window({}, configuration); move_tab_to_window(index, window, 0); } void BrowserWindow::set_current_tab(Tab* tab) { if (tab == m_current_tab) return; if (m_current_tab) m_current_tab->view().set_system_visibility_state(Web::HTML::VisibilityState::Hidden); m_current_tab = tab; if (m_current_tab) m_current_tab->view().set_system_visibility_state(Web::HTML::VisibilityState::Visible); WebView::Application::the().update_bookmark_action_for_current_web_view(); } void BrowserWindow::activate_tab(int index) { m_tabs_container->set_current_index(index); } bool BrowserWindow::activate_tab_with_url(URL::URL const& url) { for (int index = 0; index < m_tabs_container->count(); ++index) { auto* tab = m_tabs_container->tab(index); if (tab && tab->view().url() == url) { m_tabs_container->set_current_index(index); return true; } } return false; } void BrowserWindow::definitely_close_tab(int index) { auto* tab = m_tabs_container->tab(index); auto url = tab->view().url(); m_tabs_container->remove_tab(index); Application::history_store().record_closed_tab(url); Application::the().update_reopen_recently_closed_actions(); tab->deleteLater(); if (m_tabs_container->count() == 0) { m_should_record_closed_window_on_close = false; close(); } } void BrowserWindow::update_reopen_recently_closed_action() { if (!m_reopen_recently_closed_tab_action) return; auto recently_closed_entry = Application::history_store().most_recently_closed_entry(); m_reopen_recently_closed_tab_action->setText("&Reopen Recently Closed Tab"); m_reopen_recently_closed_tab_action->setEnabled(recently_closed_entry.has_value()); } void BrowserWindow::move_tab(int old_index, int new_index) { m_tabs_container->tab_bar()->moveTab(old_index, new_index); } void BrowserWindow::open_file() { m_current_tab->open_file(); } void BrowserWindow::request_to_close_tab(int index) { auto* tab = m_tabs_container->tab(index); tab->request_close(); } void BrowserWindow::request_to_close_current_tab() { request_to_close_tab(m_tabs_container->current_index()); } int BrowserWindow::tab_index(Tab* tab) { return m_tabs_container->index_of(tab); } void BrowserWindow::device_pixel_ratio_changed(qreal dpi) { m_device_pixel_ratio = dpi; for_each_tab([this](auto& tab) { tab.view().set_device_pixel_ratio(m_device_pixel_ratio); }); } bool BrowserWindow::connect_window_screen_changed_signal() { auto* window = windowHandle(); if (!window) return false; if (m_window_screen_changed_signal_window == window) return true; disconnect_window_screen_changed_signal(); m_window_screen_changed_signal_window = window; QObject::connect(window, &QWindow::screenChanged, this, [this](QScreen* screen) { screen_changed(screen); }); screen_changed(window->screen()); return true; } void BrowserWindow::disconnect_window_screen_changed_signal() { if (!m_window_screen_changed_signal_window) return; QObject::disconnect(m_window_screen_changed_signal_window, &QWindow::screenChanged, this, nullptr); m_window_screen_changed_signal_window = nullptr; } void BrowserWindow::connect_screen_signals(QScreen* screen) { if (!screen) return; if (should_use_screen_signal_for_dpi_changes()) QObject::connect(screen, &QScreen::logicalDotsPerInchChanged, this, &BrowserWindow::device_pixel_ratio_changed); QObject::connect(screen, &QScreen::refreshRateChanged, this, &BrowserWindow::refresh_rate_changed); } void BrowserWindow::disconnect_screen_signals(QScreen* screen) { if (!screen) return; QObject::disconnect(screen, &QScreen::logicalDotsPerInchChanged, this, &BrowserWindow::device_pixel_ratio_changed); QObject::disconnect(screen, &QScreen::refreshRateChanged, this, &BrowserWindow::refresh_rate_changed); } void BrowserWindow::screen_changed(QScreen* screen) { if (m_current_screen != screen) { disconnect_screen_signals(m_current_screen); m_current_screen = screen; connect_screen_signals(m_current_screen); } if (m_device_pixel_ratio != devicePixelRatio()) device_pixel_ratio_changed(devicePixelRatio()); auto display_id = display_id_for_screen(m_current_screen); auto refresh_rate = m_current_screen ? m_current_screen->refreshRate() : m_refresh_rate; if (m_display_id != display_id || m_refresh_rate != refresh_rate) display_metadata_changed(display_id, refresh_rate); } void BrowserWindow::refresh_rate_changed(qreal refresh_rate) { display_metadata_changed(m_display_id, refresh_rate); } void BrowserWindow::display_metadata_changed(Optional display_id, qreal refresh_rate) { m_display_id = display_id; m_refresh_rate = refresh_rate; for_each_tab([this](auto& tab) { tab.view().set_display_metadata(m_display_id, m_refresh_rate); }); } void BrowserWindow::tab_title_changed(int index, QString const& title) { // NOTE: Qt uses ampersands for shortcut keys in tab titles, so we need to escape them. QString title_escaped = title; title_escaped.replace("&", "&&"); m_tabs_container->set_tab_text(index, title_escaped); m_tabs_container->set_tab_tooltip(index, title); if (m_tabs_container->current_index() == index) setWindowTitle(QString("%1 - Ladybird").arg(title)); } void BrowserWindow::tab_favicon_changed(int index, QIcon const& icon) { if (index < 0) return; m_tabs_container->set_tab_icon(index, icon); } void BrowserWindow::initialize_tab_buttons(Tab* tab) { auto index = m_tabs_container->index_of(tab); m_tabs_container->set_tab_icon(index, tab->tab_icon()); auto* close_button = new TabBarButton(create_chrome_icon(ChromeIcon::TabClose, palette())); close_button->setToolTip("Close Tab"); connect(close_button, &QPushButton::clicked, this, [this, tab]() { auto index = m_tabs_container->index_of(tab); request_to_close_tab(index); }); m_tabs_container->tab_bar()->setTabButton(index, AUDIO_STATE_BUTTON_POSITION, nullptr); m_tabs_container->tab_bar()->setTabButton(index, TAB_CLOSE_BUTTON_POSITION, close_button); m_tabs_container->update_tab_button_visibility(); } void BrowserWindow::update_tab_button_icons() { for (int index = 0; index < m_tabs_container->count(); ++index) { if (auto* button = m_tabs_container->tab_bar()->tabButton(index, TAB_CLOSE_BUTTON_POSITION)) { if (auto* tab_bar_button = qobject_cast(button)) tab_bar_button->setIcon(create_chrome_icon(ChromeIcon::TabClose, palette())); } if (auto* button = m_tabs_container->tab_bar()->tabButton(index, AUDIO_STATE_BUTTON_POSITION)) { if (auto* tab_bar_button = qobject_cast(button)) tab_bar_button->setIcon(icon_for_page_mute_state(*m_tabs_container->tab(index))); } } } void BrowserWindow::create_menu_bar_window_controls() { if (use_left_traffic_light_window_controls()) return; auto window_control_buttons = create_window_control_buttons(*menuBar(), "LadybirdMenuBarWindowControls", { 16, 16 }, { 40, 30 }); m_menu_bar_window_controls = window_control_buttons.container; m_menu_bar_minimize_window_button = window_control_buttons.minimize; m_menu_bar_maximize_window_button = window_control_buttons.maximize; m_menu_bar_close_window_button = window_control_buttons.close; menuBar()->setCornerWidget(m_menu_bar_window_controls, Qt::TopRightCorner); connect(m_menu_bar_minimize_window_button, &QToolButton::clicked, this, [this] { showMinimized(); }); connect(m_menu_bar_maximize_window_button, &QToolButton::clicked, this, [this] { toggle_window_maximized(); }); connect(m_menu_bar_close_window_button, &QToolButton::clicked, this, [this] { close(); }); update_menu_bar_window_control_icons(); } void BrowserWindow::update_menu_bar_style() { menuBar()->setStyleSheet(ChromeStyle::menu_bar_style_sheet(palette())); } void BrowserWindow::update_menu_bar_visibility() { auto show_menu_bar = show_menubar_option_available() && WebView::Application::settings().show_menu_bar(); menuBar()->setVisible(show_menu_bar); if (m_menu_bar_window_controls) m_menu_bar_window_controls->setVisible(show_menu_bar && uses_client_side_decorations()); m_tabs_container->set_window_controls_visible(!show_menu_bar && uses_client_side_decorations()); } void BrowserWindow::update_menu_bar_window_control_icons() { if (!m_menu_bar_minimize_window_button || !m_menu_bar_maximize_window_button || !m_menu_bar_close_window_button) return; auto is_maximized = this->isMaximized(); m_menu_bar_minimize_window_button->setIcon(create_chrome_icon(ChromeIcon::WindowMinimize, palette())); m_menu_bar_maximize_window_button->setIcon(create_chrome_icon(is_maximized ? ChromeIcon::WindowRestore : ChromeIcon::WindowMaximize, palette())); m_menu_bar_maximize_window_button->setToolTip(is_maximized ? "Restore" : "Maximize"); m_menu_bar_close_window_button->setIcon(create_chrome_icon(ChromeIcon::WindowClose, palette())); } void BrowserWindow::update_window_decoration_state() { clear_resize_cursor(); auto should_be_frameless = uses_client_side_decorations(); auto is_frameless = windowFlags().testFlag(Qt::FramelessWindowHint); if (is_frameless != should_be_frameless) { auto was_visible = isVisible(); auto was_fullscreen = isFullScreen(); auto was_maximized = isMaximized(); setWindowFlag(Qt::FramelessWindowHint, should_be_frameless); if (was_visible) { if (was_fullscreen) showFullScreen(); else if (was_maximized) showMaximized(); else show(); } } update_appkit_window_resizability(); update_menu_bar_visibility(); update_window_border(); } void BrowserWindow::toggle_window_maximized() { if (isMaximized()) showNormal(); else showMaximized(); update_menu_bar_window_control_icons(); } bool BrowserWindow::start_window_move() { auto* handle = windowHandle(); if (!handle) return false; #if defined(AK_OS_MACOS) if (start_appkit_window_drag(*this)) return true; #endif return handle->startSystemMove(); } void BrowserWindow::tab_audio_play_state_changed(int index, Web::HTML::AudioPlayState play_state) { auto* tab = m_tabs_container->tab(index); switch (play_state) { case Web::HTML::AudioPlayState::Paused: if (tab->view().page_mute_state() == Web::HTML::MuteState::Unmuted) { m_tabs_container->tab_bar()->setTabButton(index, AUDIO_STATE_BUTTON_POSITION, nullptr); m_tabs_container->update_tab_button_visibility(); } break; case Web::HTML::AudioPlayState::Playing: auto* button = new TabBarButton(icon_for_page_mute_state(*tab)); button->setToolTip(tool_tip_for_page_mute_state(*tab)); button->setObjectName("LadybirdAudioState"); connect(button, &QPushButton::clicked, this, [this, tab]() { tab->view().toggle_page_mute_state(); auto index = tab_index(tab); switch (tab->view().audio_play_state()) { case Web::HTML::AudioPlayState::Paused: m_tabs_container->tab_bar()->setTabButton(index, AUDIO_STATE_BUTTON_POSITION, nullptr); m_tabs_container->update_tab_button_visibility(); break; case Web::HTML::AudioPlayState::Playing: auto* button = m_tabs_container->tab_bar()->tabButton(index, AUDIO_STATE_BUTTON_POSITION); as(button)->setIcon(icon_for_page_mute_state(*tab)); button->setToolTip(tool_tip_for_page_mute_state(*tab)); break; } }); m_tabs_container->tab_bar()->setTabButton(index, AUDIO_STATE_BUTTON_POSITION, button); m_tabs_container->update_tab_button_visibility(); break; } } QIcon BrowserWindow::icon_for_page_mute_state(Tab& tab) const { switch (tab.view().page_mute_state()) { case Web::HTML::MuteState::Muted: return create_chrome_icon(ChromeIcon::VolumeMuted, palette()); case Web::HTML::MuteState::Unmuted: return create_chrome_icon(ChromeIcon::Volume, palette()); } VERIFY_NOT_REACHED(); } QString BrowserWindow::tool_tip_for_page_mute_state(Tab& tab) const { switch (tab.view().page_mute_state()) { case Web::HTML::MuteState::Muted: return "Unmute tab"; case Web::HTML::MuteState::Unmuted: return "Mute tab"; } VERIFY_NOT_REACHED(); } void BrowserWindow::open_next_tab() { if (m_tabs_container->count() <= 1) return; auto next_index = m_tabs_container->current_index() + 1; if (next_index >= m_tabs_container->count()) next_index = 0; m_tabs_container->set_current_index(next_index); } void BrowserWindow::open_previous_tab() { if (m_tabs_container->count() <= 1) return; auto next_index = m_tabs_container->current_index() - 1; if (next_index < 0) next_index = m_tabs_container->count() - 1; m_tabs_container->set_current_index(next_index); } void BrowserWindow::show_find_in_page() { if (!m_current_tab) return; m_current_tab->show_find_in_page(); } void BrowserWindow::set_window_rect(Optional x, Optional y, Optional width, Optional height) { x = x.value_or(0); y = y.value_or(0); if (!width.has_value() || width.value() == 0) width = 800; if (!height.has_value() || height.value() == 0) height = 600; setGeometry(x.value().value(), y.value().value(), width.value().value(), height.value().value()); } void BrowserWindow::enter_fullscreen() { m_tabs_container->set_tab_bar_visible(false); current_tab()->bookmarks_bar().setVisible(false); m_restore_to_maximized = isMaximized(); showFullScreen(); } void BrowserWindow::exit_fullscreen() { m_tabs_container->set_tab_bar_visible(true); current_tab()->bookmarks_bar().setVisible(WebView::Application::settings().show_bookmarks_bar()); if (m_restore_to_maximized) showMaximized(); else showNormal(); } bool BrowserWindow::event(QEvent* event) { #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) if (event->type() == QEvent::DevicePixelRatioChange) { if (m_device_pixel_ratio != devicePixelRatio()) device_pixel_ratio_changed(devicePixelRatio()); } #endif if (event->type() == QEvent::WinIdChange) connect_window_screen_changed_signal(); if (event->type() == QEvent::PlatformSurface) { auto* platform_surface_event = static_cast(event); if (platform_surface_event->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) { connect_window_screen_changed_signal(); #if defined(AK_OS_MACOS) QTimer::singleShot(0, this, [this] { update_window_corners(); update_appkit_window_resizability(); }); #endif } else if (platform_surface_event->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) { disconnect_window_screen_changed_signal(); } } if (event->type() == QEvent::ScreenChangeInternal) screen_changed(screen()); if (event->type() == QEvent::WindowActivate) { Application::the().set_active_window(*this); QTimer::singleShot(0, this, [this] { refresh_resize_cursor_at_current_position(true); }); QTimer::singleShot(50, this, [this] { refresh_resize_cursor_at_current_position(true); }); } else if (event->type() == QEvent::WindowDeactivate || event->type() == QEvent::Hide) { clear_resize_cursor(); } return QMainWindow::event(event); } bool BrowserWindow::eventFilter(QObject* object, QEvent* event) { auto* widget = as_if(object); if (!widget || widget->window() != this) return QMainWindow::eventFilter(object, event); if (!uses_client_side_decorations()) return QMainWindow::eventFilter(object, event); if (m_is_resizing_window) { if (event->type() == QEvent::MouseMove) { auto* mouse_event = static_cast(event); update_window_resize(mouse_event->globalPosition().toPoint()); return true; } if (event->type() == QEvent::MouseButtonRelease) { finish_window_resize(); return true; } } auto const is_button = qobject_cast(object) != nullptr; if (is_button && (event->type() == QEvent::Enter || event->type() == QEvent::MouseMove || event->type() == QEvent::Leave)) { clear_resize_cursor(); } else if (event->type() == QEvent::Enter) { update_resize_cursor(mapFromGlobal(QCursor::pos())); } else if (event->type() == QEvent::MouseMove) { auto* mouse_event = static_cast(event); update_resize_cursor(widget->mapTo(this, mouse_event->position().toPoint())); } else if (event->type() == QEvent::Leave) { auto position = mapFromGlobal(QCursor::pos()); if (rect().contains(position)) update_resize_cursor(position); else clear_resize_cursor(); } else if (event->type() != QEvent::MouseButtonPress && event->type() != QEvent::MouseButtonDblClick) { return QMainWindow::eventFilter(object, event); } if (event->type() != QEvent::MouseButtonPress && event->type() != QEvent::MouseButtonDblClick) return QMainWindow::eventFilter(object, event); if (is_button) return QMainWindow::eventFilter(object, event); auto* mouse_event = static_cast(event); if (mouse_event->button() != Qt::LeftButton) return QMainWindow::eventFilter(object, event); auto position = widget->mapTo(this, mouse_event->position().toPoint()); auto edges = resize_edges_for_position(position); if (event->type() == QEvent::MouseButtonPress && !isMaximized() && !isFullScreen()) { if (edges != Qt::Edges {}) { #if defined(AK_OS_MACOS) if (start_window_resize(edges, mouse_event->globalPosition().toPoint())) return true; #else auto* handle = windowHandle(); if (handle && handle->startSystemResize(edges)) return true; #endif } } auto is_empty_window_drag_region = widget->property(WINDOW_DRAG_REGION_PROPERTY).toBool() && widget->childAt(mouse_event->position().toPoint()) == nullptr; if (is_empty_window_drag_region && !isFullScreen()) { if (event->type() == QEvent::MouseButtonDblClick) { toggle_window_maximized(); return true; } if (start_window_move()) return true; } if (isFullScreen() || widget != menuBar() || menuBar()->actionAt(mouse_event->position().toPoint()) != nullptr) return QMainWindow::eventFilter(object, event); if (event->type() == QEvent::MouseButtonDblClick) { toggle_window_maximized(); return true; } if (start_window_move()) return true; return QMainWindow::eventFilter(object, event); } bool BrowserWindow::position_is_in_rounded_corner_cutout(QPoint const& position) const { #if defined(AK_OS_MACOS) auto should_use_rounded_corners = WebView::Application::settings().config_variable_as_bool(WebView::ConfigVariableID::UseRoundedWindowCorners); if (!should_use_rounded_corners || isFullScreen()) return false; auto const radius = WINDOW_CORNER_RADIUS; auto const x = static_cast(position.x()); auto const y = static_cast(position.y()); auto const right = static_cast(width()); auto const bottom = static_cast(height()); auto is_outside_corner_arc = [radius](qreal dx, qreal dy) { return dx * dx + dy * dy > radius * radius; }; auto in_cutout = false; if (x < radius && y < radius) in_cutout = is_outside_corner_arc(radius - x, radius - y); else if (x >= right - radius && y < radius) in_cutout = is_outside_corner_arc(x - (right - radius), radius - y); else if (x < radius && y >= bottom - radius) in_cutout = is_outside_corner_arc(radius - x, y - (bottom - radius)); else if (x >= right - radius && y >= bottom - radius) in_cutout = is_outside_corner_arc(x - (right - radius), y - (bottom - radius)); return in_cutout; #else (void)position; #endif return false; } Qt::Edges BrowserWindow::resize_edges_for_position(QPoint const& position) const { if (position_is_in_rounded_corner_cutout(position)) return {}; Qt::Edges edges; auto in_left_resize_edge = position.x() <= WINDOW_RESIZE_BORDER_WIDTH; auto in_right_resize_edge = position.x() >= width() - WINDOW_RESIZE_BORDER_WIDTH; auto in_top_resize_edge = position.y() <= WINDOW_RESIZE_BORDER_WIDTH; auto in_bottom_resize_edge = position.y() >= height() - WINDOW_RESIZE_BORDER_WIDTH; auto in_left_resize_corner = position.x() <= WINDOW_RESIZE_CORNER_WIDTH; auto in_right_resize_corner = position.x() >= width() - WINDOW_RESIZE_CORNER_WIDTH; auto in_top_resize_corner = position.y() <= WINDOW_RESIZE_CORNER_WIDTH; auto in_bottom_resize_corner = position.y() >= height() - WINDOW_RESIZE_CORNER_WIDTH; if (in_left_resize_edge || (in_left_resize_corner && (in_top_resize_corner || in_bottom_resize_corner))) edges |= Qt::LeftEdge; if (in_right_resize_edge || (in_right_resize_corner && (in_top_resize_corner || in_bottom_resize_corner))) edges |= Qt::RightEdge; if (in_top_resize_edge || (in_top_resize_corner && (in_left_resize_corner || in_right_resize_corner))) edges |= Qt::TopEdge; if (in_bottom_resize_edge || (in_bottom_resize_corner && (in_left_resize_corner || in_right_resize_corner))) edges |= Qt::BottomEdge; return edges; } Optional BrowserWindow::resize_cursor_for_edges(Qt::Edges edges) const { if (edges == Qt::Edges {}) return {}; if ((edges & Qt::TopEdge && edges & Qt::LeftEdge) || (edges & Qt::BottomEdge && edges & Qt::RightEdge)) return Qt::SizeFDiagCursor; if ((edges & Qt::TopEdge && edges & Qt::RightEdge) || (edges & Qt::BottomEdge && edges & Qt::LeftEdge)) return Qt::SizeBDiagCursor; if (edges & Qt::LeftEdge || edges & Qt::RightEdge) return Qt::SizeHorCursor; if (edges & Qt::TopEdge || edges & Qt::BottomEdge) return Qt::SizeVerCursor; return {}; } bool BrowserWindow::start_window_resize(Qt::Edges edges, QPoint const& global_position) { if (edges == Qt::Edges {} || isMaximized() || isFullScreen()) return false; m_is_resizing_window = true; m_resize_edges = edges; m_resize_start_global_position = global_position; m_resize_start_geometry = geometry(); grabMouse(); return true; } void BrowserWindow::update_window_resize(QPoint const& global_position) { if (!m_is_resizing_window) return; auto delta = global_position - m_resize_start_global_position; auto new_geometry = m_resize_start_geometry; if (m_resize_edges & Qt::LeftEdge) { auto new_width = qBound(minimumWidth(), m_resize_start_geometry.width() - delta.x(), maximumWidth()); new_geometry.setX(m_resize_start_geometry.right() - new_width + 1); } else if (m_resize_edges & Qt::RightEdge) { auto new_width = qBound(minimumWidth(), m_resize_start_geometry.width() + delta.x(), maximumWidth()); new_geometry.setWidth(new_width); } if (m_resize_edges & Qt::TopEdge) { auto new_height = qBound(minimumHeight(), m_resize_start_geometry.height() - delta.y(), maximumHeight()); new_geometry.setY(m_resize_start_geometry.bottom() - new_height + 1); } else if (m_resize_edges & Qt::BottomEdge) { auto new_height = qBound(minimumHeight(), m_resize_start_geometry.height() + delta.y(), maximumHeight()); new_geometry.setHeight(new_height); } setGeometry(new_geometry); } void BrowserWindow::finish_window_resize() { if (!m_is_resizing_window) return; m_is_resizing_window = false; m_resize_edges = {}; releaseMouse(); } void BrowserWindow::update_resize_cursor(QPoint const& position) { if (!uses_client_side_decorations() || isMaximized() || isFullScreen() || !rect().contains(position)) { clear_resize_cursor(); return; } auto cursor_shape = resize_cursor_for_edges(resize_edges_for_position(position)); if (!cursor_shape.has_value()) { clear_resize_cursor(); return; } if (m_resize_cursor_active) QApplication::changeOverrideCursor(*cursor_shape); else { QApplication::setOverrideCursor(*cursor_shape); m_resize_cursor_active = true; } } void BrowserWindow::refresh_resize_cursor_at_current_position(bool force_reapply) { auto position = mapFromGlobal(QCursor::pos()); auto* child = childAt(position); for (auto* object = child; object; object = qobject_cast(object->parent())) { if (qobject_cast(object)) { clear_resize_cursor(); return; } } if (force_reapply && m_resize_cursor_active) { QApplication::restoreOverrideCursor(); m_resize_cursor_active = false; } update_resize_cursor(position); } void BrowserWindow::clear_resize_cursor() { if (!m_resize_cursor_active) return; QApplication::restoreOverrideCursor(); m_resize_cursor_active = false; } void BrowserWindow::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); update_window_corners(); for_each_tab([&](auto& tab) { tab.view().set_window_size({ width(), height() }); }); } void BrowserWindow::changeEvent(QEvent* event) { if (event->type() == QEvent::PaletteChange) { update_menu_bar_style(); update_menu_bar_window_control_icons(); update_tab_button_icons(); } else if (event->type() == QEvent::WindowStateChange) { update_menu_bar_window_control_icons(); m_tabs_container->update_window_button_icons(); update_window_corners(); update_window_border(); QWindowStateChangeEvent* stateChangeEvent = static_cast(event); bool was_fullscreen = stateChangeEvent->oldState() & Qt::WindowFullScreen; bool is_fullscreen = windowState() & Qt::WindowFullScreen; if (is_fullscreen && !was_fullscreen) { m_fullscreen_mode->entered_fullscreen(); if (m_fullscreen_mode->is_api_fullscreen()) view().set_is_fullscreen(Web::ViewportIsFullscreen::Yes); } else if (!is_fullscreen && was_fullscreen) { view().set_is_fullscreen(Web::ViewportIsFullscreen::No); } } QWidget::changeEvent(event); } void BrowserWindow::show_menu_bar_changed() { update_menu_bar_visibility(); } void BrowserWindow::config_variable_changed(WebView::ConfigVariableID variable) { if (variable == WebView::ConfigVariableID::UseRoundedWindowCorners) update_window_corners(); else if (variable == WebView::ConfigVariableID::UseServerSideWindowDecorations) update_window_decoration_state(); } void BrowserWindow::update_window_corners() { #if defined(AK_OS_MACOS) auto should_use_rounded_corners = WebView::Application::settings().config_variable_as_bool(WebView::ConfigVariableID::UseRoundedWindowCorners); auto should_round_window = should_use_rounded_corners && !isFullScreen(); clearMask(); set_rounded_window_corners(*this, should_round_window, WINDOW_CORNER_RADIUS, ChromeStyle::chrome_background(palette())); #endif } void BrowserWindow::update_appkit_window_resizability() { #if defined(AK_OS_MACOS) set_appkit_window_resizable(*this, !uses_client_side_decorations()); #endif } bool BrowserWindow::should_draw_window_border() const { #if defined(AK_OS_MACOS) // macOS frameless windows already get rounded corners and a native shadow, so a painted border would clash. return false; #else return windowFlags().testFlag(Qt::FramelessWindowHint) && !isFullScreen() && !isMaximized(); #endif } void BrowserWindow::update_window_border() { auto border_width = should_draw_window_border() ? 1 : 0; setContentsMargins(border_width, border_width, border_width, border_width); update(); } void BrowserWindow::paintEvent(QPaintEvent* event) { QMainWindow::paintEvent(event); if (!should_draw_window_border()) return; QPainter painter(this); auto color = ChromeStyle::chrome_window_outline(palette()); auto frame = rect(); painter.fillRect(QRect(frame.left(), frame.top(), frame.width(), 1), color); painter.fillRect(QRect(frame.left(), frame.bottom(), frame.width(), 1), color); painter.fillRect(QRect(frame.left(), frame.top(), 1, frame.height()), color); painter.fillRect(QRect(frame.right(), frame.top(), 1, frame.height()), color); } void BrowserWindow::moveEvent(QMoveEvent* event) { QWidget::moveEvent(event); for_each_tab([&](auto& tab) { tab.view().set_window_position({ x(), y() }); }); } void BrowserWindow::wheelEvent(QWheelEvent* event) { if (!m_current_tab) return; if ((event->modifiers() & Qt::ControlModifier) != 0) { if (event->angleDelta().y() > 0) m_current_tab->view().zoom_in(); else if (event->angleDelta().y() < 0) m_current_tab->view().zoom_out(); } } void BrowserWindow::closeEvent(QCloseEvent* event) { clear_resize_cursor(); Optional> recently_closed_window_urls; size_t recently_closed_window_active_tab_index { 0 }; if (m_should_record_closed_window_on_close && m_tabs_container->count() > 0) { recently_closed_window_urls = recently_closed_urls_for_window(*m_tabs_container); recently_closed_window_active_tab_index = static_cast(m_tabs_container->current_index()); } if (m_is_popup_window == IsPopupWindow::No) { Settings::the()->set_last_position(pos()); Settings::the()->set_last_size(size()); Settings::the()->set_is_maximized(isMaximized()); } QObject::deleteLater(); QMainWindow::closeEvent(event); if (event->isAccepted() && recently_closed_window_urls.has_value()) { Application::history_store().record_closed_window(recently_closed_window_urls.release_value(), recently_closed_window_active_tab_index); Application::the().update_reopen_recently_closed_actions(); } } }