UI/Qt: Add interface for exiting fullscreen

Added an "exit fullscreen" button that displays when entering
fullscreen.

If the user switches tab while in fullscreen, ladybird will exit
fullscreen and restore the window to the previous state.

Exiting fullscreen can be done in one of two ways:

- Click the exit fullscreen button
- Press escape

The exit fullscreen button will disappear after a certain amount of time
after entering fullscreen. To make it appear again, move the mouse
cursor to the top of the screen.
This commit is contained in:
Simon Farre 2025-04-01 14:26:34 +02:00
parent d79098939d
commit 0cc3c78181
3 changed files with 209 additions and 3 deletions

View file

@ -4,10 +4,12 @@
* Copyright (c) 2022, Filiph Sandström <filiph.sandstrom@filfatstudios.com>
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2025, Simon Farre <simon.farre.cx@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/RefPtr.h>
#include <AK/TypeCasts.h>
#include <LibWeb/CSS/PreferredColorScheme.h>
#include <LibWeb/CSS/PreferredContrast.h>
@ -32,12 +34,133 @@
#include <QInputDialog>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QShortcut>
#include <QStatusBar>
#include <QTabBar>
#include <QTimer>
#include <QWidget>
#include <QWindow>
#include <qnamespace.h>
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<u64>(pos.y());
u64 const threshold = static_cast<u64>(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<QMouseEvent*>(event);
maybe_animate_show_exit_button(mouse_event->pos());
}
if (event->type() == QEvent::KeyPress) {
QKeyEvent* key = static_cast<QKeyEvent*>(event);
if (key->key() == Qt::Key_Escape)
exit();
}
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<int>(static_cast<float>(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;
@ -618,12 +741,19 @@ BrowserWindow::BrowserWindow(Vector<URL::URL> const& initial_urls, IsPopupWindow
(void)static_cast<Ladybird::Application*>(QApplication::instance())->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<Tab>(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::close_tab);
QObject::connect(close_current_tab_action, &QAction::triggered, this, &BrowserWindow::close_current_tab);
@ -762,6 +892,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);
@ -1161,6 +1296,7 @@ void BrowserWindow::enter_fullscreen()
{
m_tabs_container->tabBar()->hide();
m_tabs_container->cornerWidget()->hide();
m_restore_to_maximized = isMaximized();
showFullScreen();
}
@ -1168,7 +1304,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)
@ -1195,6 +1334,17 @@ void BrowserWindow::resizeEvent(QResizeEvent* event)
});
}
void BrowserWindow::changeEvent(QEvent* event)
{
if (event->type() == QEvent::WindowStateChange) {
QWindowStateChangeEvent* stateChangeEvent = static_cast<QWindowStateChangeEvent*>(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);

View file

@ -18,13 +18,62 @@
#include <QLineEdit>
#include <QMainWindow>
#include <QMenuBar>
#include <QPushButton>
#include <QTabBar>
#include <QTabWidget>
#include <QToolBar>
class QPropertyAnimation;
namespace Ladybird {
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:
int tab_count() { return m_tabs_container->count(); }
int tab_index(Tab*);
Tab& create_new_tab(Web::HTML::ActivateTab activate_tab);
FullscreenMode& fullscreen_mode();
QMenu& hamburger_menu()
{
@ -141,6 +191,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;
@ -212,6 +263,11 @@ private:
ByteString m_navigator_compatibility_mode {};
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 };
};
}

View file

@ -367,13 +367,13 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
view().on_fullscreen_window = [this]() {
BrowserWindow* window = static_cast<BrowserWindow*>(m_window);
m_toolbar->hide();
window->enter_fullscreen();
window->fullscreen_mode().enter(this);
view().did_update_window_rect();
};
view().on_exit_fullscreen_window = [this]() {
BrowserWindow* window = static_cast<BrowserWindow*>(m_window);
window->exit_fullscreen();
window->fullscreen_mode().exit();
m_toolbar->show();
view().did_update_window_rect();
};