diff --git a/UI/Qt/BrowserWindow.cpp b/UI/Qt/BrowserWindow.cpp index 175e173985..7ad6dac9fd 100644 --- a/UI/Qt/BrowserWindow.cpp +++ b/UI/Qt/BrowserWindow.cpp @@ -4,10 +4,12 @@ * Copyright (c) 2022, Filiph Sandström * Copyright (c) 2023, Linus Groh * Copyright (c) 2024-2025, Sam Atkins + * Copyright (c) 2025, Simon Farre * * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -25,14 +27,128 @@ #include #include #include +#include +#include #include #include #include +#include +#include #include +#include #include namespace Ladybird { +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(); + }); +} + +void FullscreenMode::exit() +{ + // If there's a document tree in fullscreen, exit fully on root document. + if (is_api_fullscreen()) { + qApp->removeEventFilter(this); + if (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(mouse_event->pos()); + } + + return QObject::eventFilter(obj, event); +} + +ExitFullscreenButton::ExitFullscreenButton(QWidget* parent) + : QPushButton("Exit fullscreen", parent) +{ + setStyleSheet("background-color:rgb(55, 99, 129); color: white; padding: 10px; border-radius: 5px;"); + adjustSize(); + hide(); + m_widget_animation = new QPropertyAnimation(this, "pos"); +} + +void ExitFullscreenButton::animate_show() +{ + if (isVisible()) + return; + + show(); + QScreen* current_screen = screen(); + QRect screen_geometry = current_screen->geometry(); + + int const destination_x = (screen_geometry.width() - width()) / 2; + int const destination_y = static_cast(static_cast(screen_geometry.height()) * 0.05); + + 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; @@ -230,12 +346,19 @@ BrowserWindow::BrowserWindow(Vector const& initial_urls, IsPopupWindow (void)Application::the().new_window({}); }); QObject::connect(open_file_action, &QAction::triggered, this, &BrowserWindow::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, &QTabWidget::currentChanged, [this](int index) { auto* tab = as(m_tabs_container->widget(index)); if (tab) setWindowTitle(QString("%1 - Ladybird").arg(tab->title())); set_current_tab(tab); + fullscreen_mode().exit(); }); QObject::connect(m_tabs_container, &QTabWidget::tabCloseRequested, this, &BrowserWindow::request_to_close_tab); QObject::connect(close_current_tab_action, &QAction::triggered, this, &BrowserWindow::request_to_close_current_tab); @@ -328,6 +451,11 @@ Tab& BrowserWindow::create_new_tab(Web::HTML::ActivateTab activate_tab, Tab& par return *tab; } +FullscreenMode& BrowserWindow::fullscreen_mode() +{ + return *m_fullscreen_mode; +} + Tab& BrowserWindow::create_new_tab(Web::HTML::ActivateTab activate_tab) { auto* tab = new Tab(this); @@ -581,6 +709,7 @@ void BrowserWindow::enter_fullscreen() { m_tabs_container->tabBar()->hide(); m_tabs_container->cornerWidget()->hide(); + m_restore_to_maximized = isMaximized(); showFullScreen(); } @@ -588,7 +717,10 @@ void BrowserWindow::exit_fullscreen() { m_tabs_container->tabBar()->show(); m_tabs_container->cornerWidget()->show(); - showNormal(); + if (m_restore_to_maximized) + showMaximized(); + else + showNormal(); } bool BrowserWindow::event(QEvent* event) @@ -615,6 +747,17 @@ void BrowserWindow::resizeEvent(QResizeEvent* event) }); } +void BrowserWindow::changeEvent(QEvent* event) +{ + if (event->type() == QEvent::WindowStateChange) { + QWindowStateChangeEvent* stateChangeEvent = static_cast(event); + if (windowState() & Qt::WindowFullScreen && !(stateChangeEvent->oldState() & Qt::WindowFullScreen)) { + m_fullscreen_mode->entered_fullscreen(); + } + } + QWidget::changeEvent(event); +} + void BrowserWindow::moveEvent(QMoveEvent* event) { QWidget::moveEvent(event); diff --git a/UI/Qt/BrowserWindow.h b/UI/Qt/BrowserWindow.h index 9dbdf70430..861838e3f7 100644 --- a/UI/Qt/BrowserWindow.h +++ b/UI/Qt/BrowserWindow.h @@ -15,14 +15,63 @@ #include #include #include +#include #include #include #include +class QPropertyAnimation; + namespace Ladybird { class Tab; class WebContentView; +class BrowserWindow; + +class ExitFullscreenButton : public QPushButton { + Q_OBJECT + +public: + ExitFullscreenButton(QWidget* parent = nullptr); + ~ExitFullscreenButton() override = default; + void animate_show(); + +private: + QPropertyAnimation* m_widget_animation; +}; + +// Handles Qt UI state related to when Ladybird has entered fullscreen, +// like displaying an exit button, listening for escape key presses and so on +class FullscreenMode : public QObject { + Q_OBJECT + +public: + static constexpr int button_animation_time() { return 750; } + explicit FullscreenMode(BrowserWindow* window, ExitFullscreenButton* exit_button); + + void exit(); + void enter(Tab* tab); + // Called after a window change event that has identifed the current window state to be fullscreen. + void entered_fullscreen(); + bool is_api_fullscreen() const; +signals: + void on_exit_fullscreen(); + +protected: + // FullscreenMode's eventFilter is responsible for things that need to happen when a document + // is in "fullscreen API fullscreen", like exiting when pushing escape, showing the exit button + virtual bool eventFilter(QObject* obj, QEvent* event) override; + +private: + bool debounce() const; + // Called when in fullscreen. Displays exit fullscreen button if mouse comes close to the top of the screen. + void maybe_animate_show_exit_button(QPointF pos); + BrowserWindow* m_window; + ExitFullscreenButton* m_exit_button; + // Never access this directly. First check m_window->tab_index(m_fullscreen_tab) != -1, to verify it's liveness. + Tab* m_fullscreen_tab { nullptr }; + bool m_debounce { false }; +}; class BrowserWindow : public QMainWindow { Q_OBJECT @@ -42,6 +91,7 @@ public: Tab& create_new_tab(Web::HTML::ActivateTab activate_tab); Tab* current_tab() const { return m_current_tab; } + FullscreenMode& fullscreen_mode(); QMenu& hamburger_menu() const { return *m_hamburger_menu; } @@ -80,6 +130,7 @@ protected: private: virtual bool event(QEvent*) override; virtual void resizeEvent(QResizeEvent*) override; + virtual void changeEvent(QEvent* event) override; virtual void moveEvent(QMoveEvent*) override; virtual void wheelEvent(QWheelEvent*) override; virtual void closeEvent(QCloseEvent*) override; @@ -122,6 +173,11 @@ private: QAction* m_find_in_page_action { nullptr }; IsPopupWindow m_is_popup_window { IsPopupWindow::No }; + + ExitFullscreenButton* m_exit_button { nullptr }; + FullscreenMode* m_fullscreen_mode { nullptr }; + // Determine if window should restore to maximized or normal, when exiting fullscreen. + bool m_restore_to_maximized { false }; }; } diff --git a/UI/Qt/Tab.cpp b/UI/Qt/Tab.cpp index 70c51306f0..8f980eb509 100644 --- a/UI/Qt/Tab.cpp +++ b/UI/Qt/Tab.cpp @@ -350,12 +350,12 @@ Tab::Tab(BrowserWindow* window, RefPtr parent_client, view().on_fullscreen_window = [this]() { m_toolbar->hide(); - m_window->enter_fullscreen(); + m_window->fullscreen_mode().enter(this); view().did_update_window_rect(); }; view().on_exit_fullscreen_window = [this]() { - m_window->exit_fullscreen(); + m_window->fullscreen_mode().exit(); m_toolbar->show(); view().did_update_window_rect(); };