2026-05-27 13:16:16 -03:00
|
|
|
/*
|
|
|
|
|
* Copyright (c) 2026-present, the Ladybird developers.
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <UI/Qt/WindowControlButton.h>
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
#include <AK/Assertions.h>
|
|
|
|
|
#include <AK/Platform.h>
|
|
|
|
|
#if defined(AK_OS_MACOS)
|
|
|
|
|
# include <UI/Qt/MacWindow.h>
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#include <QBoxLayout>
|
|
|
|
|
#include <QCursor>
|
2026-05-27 13:16:16 -03:00
|
|
|
#include <QEnterEvent>
|
|
|
|
|
#include <QMouseEvent>
|
2026-06-02 12:45:44 -03:00
|
|
|
#include <QPainter>
|
2026-05-27 13:16:16 -03:00
|
|
|
#include <QStyle>
|
2026-06-02 12:45:44 -03:00
|
|
|
#include <QWidget>
|
2026-05-27 13:16:16 -03:00
|
|
|
|
|
|
|
|
namespace Ladybird {
|
|
|
|
|
|
|
|
|
|
static constexpr auto pressed_outside_property = "pressedOutside";
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
#if defined(AK_OS_MACOS)
|
|
|
|
|
static void update_window_control_group(QWidget* widget)
|
|
|
|
|
{
|
|
|
|
|
if (!widget)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
for (auto* child : widget->children()) {
|
|
|
|
|
if (auto* button = dynamic_cast<WindowControlButton*>(child))
|
|
|
|
|
button->update();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void window_control_group_hover_changed(QWidget* widget)
|
|
|
|
|
{
|
|
|
|
|
update_window_control_group(widget);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class WindowControlGroupEventFilter final : public QObject {
|
|
|
|
|
public:
|
|
|
|
|
using QObject::QObject;
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
virtual bool eventFilter(QObject* watched, QEvent* event) override
|
|
|
|
|
{
|
|
|
|
|
auto* widget = qobject_cast<QWidget*>(watched);
|
|
|
|
|
if (!widget)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
if (event->type() == QEvent::Show || event->type() == QEvent::Move || event->type() == QEvent::Resize)
|
|
|
|
|
install_always_active_window_control_hover_tracking(*widget, window_control_group_hover_changed);
|
|
|
|
|
if (event->type() == QEvent::Enter || event->type() == QEvent::Leave)
|
|
|
|
|
update_window_control_group(widget);
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static bool window_control_group_hovered(QWidget const* widget)
|
|
|
|
|
{
|
|
|
|
|
if (!widget)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
auto position = widget->mapFromGlobal(QCursor::pos());
|
|
|
|
|
return widget->rect().contains(position);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-06-02 15:22:48 -03:00
|
|
|
static QSize default_window_control_button_size(QSize non_macos_size)
|
2026-06-02 12:45:44 -03:00
|
|
|
{
|
|
|
|
|
#if defined(AK_OS_MACOS)
|
|
|
|
|
Q_UNUSED(non_macos_size);
|
|
|
|
|
return { 16, 38 };
|
|
|
|
|
#else
|
|
|
|
|
return non_macos_size;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 15:22:48 -03:00
|
|
|
static int default_window_control_button_spacing()
|
2026-06-02 12:45:44 -03:00
|
|
|
{
|
|
|
|
|
#if defined(AK_OS_MACOS)
|
|
|
|
|
return 7;
|
|
|
|
|
#else
|
|
|
|
|
return 0;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static char const* object_name_for_type(WindowControlButtonType type)
|
|
|
|
|
{
|
|
|
|
|
switch (type) {
|
|
|
|
|
case WindowControlButtonType::Minimize:
|
|
|
|
|
case WindowControlButtonType::Maximize:
|
|
|
|
|
return "LadybirdWindowButton";
|
|
|
|
|
case WindowControlButtonType::Close:
|
|
|
|
|
return "LadybirdCloseWindowButton";
|
|
|
|
|
}
|
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WindowControlButton::WindowControlButton(WindowControlButtonType type, QString const& tool_tip, QSize icon_size, QSize button_size, QWidget* parent)
|
2026-05-27 13:16:16 -03:00
|
|
|
: QToolButton(parent)
|
2026-06-02 12:45:44 -03:00
|
|
|
, m_type(type)
|
2026-05-27 13:16:16 -03:00
|
|
|
{
|
2026-06-02 12:45:44 -03:00
|
|
|
setObjectName(object_name_for_type(type));
|
2026-05-27 13:16:16 -03:00
|
|
|
setToolTip(tool_tip);
|
|
|
|
|
setIconSize(icon_size);
|
|
|
|
|
setFixedSize(button_size);
|
|
|
|
|
setFocusPolicy(Qt::NoFocus);
|
|
|
|
|
setProperty(pressed_outside_property, false);
|
2026-06-02 12:45:44 -03:00
|
|
|
|
|
|
|
|
#if defined(AK_OS_MACOS)
|
|
|
|
|
setStyleSheet(QString {
|
|
|
|
|
"min-width: %1px; max-width: %1px; "
|
|
|
|
|
"min-height: %2px; max-height: %2px; "
|
|
|
|
|
"background: transparent; border: 0; padding: 0;" }
|
|
|
|
|
.arg(button_size.width())
|
|
|
|
|
.arg(button_size.height()));
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void add_window_control_buttons_to_layout(QBoxLayout& layout, QToolButton& minimize_button, QToolButton& maximize_button, QToolButton& close_button)
|
|
|
|
|
{
|
|
|
|
|
#if defined(AK_OS_MACOS)
|
|
|
|
|
layout.addWidget(&close_button);
|
|
|
|
|
layout.addWidget(&minimize_button);
|
|
|
|
|
layout.addWidget(&maximize_button);
|
|
|
|
|
#else
|
|
|
|
|
layout.addWidget(&minimize_button);
|
|
|
|
|
layout.addWidget(&maximize_button);
|
|
|
|
|
layout.addWidget(&close_button);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WindowControlButtons create_window_control_buttons(QWidget& parent, char const* object_name, QSize icon_size, QSize non_macos_button_size)
|
|
|
|
|
{
|
|
|
|
|
auto* container = new QWidget(&parent);
|
|
|
|
|
container->setObjectName(object_name);
|
|
|
|
|
|
|
|
|
|
auto* layout = new QHBoxLayout(container);
|
2026-06-02 15:22:48 -03:00
|
|
|
layout->setSpacing(default_window_control_button_spacing());
|
2026-06-02 12:45:44 -03:00
|
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
|
2026-06-02 15:22:48 -03:00
|
|
|
auto const button_size = default_window_control_button_size(non_macos_button_size);
|
2026-06-02 12:45:44 -03:00
|
|
|
auto* minimize = new WindowControlButton(WindowControlButtonType::Minimize, "Minimize", icon_size, button_size, container);
|
|
|
|
|
auto* maximize = new WindowControlButton(WindowControlButtonType::Maximize, "Maximize", icon_size, button_size, container);
|
|
|
|
|
auto* close = new WindowControlButton(WindowControlButtonType::Close, "Close", icon_size, button_size, container);
|
|
|
|
|
add_window_control_buttons_to_layout(*layout, *minimize, *maximize, *close);
|
|
|
|
|
|
|
|
|
|
#if defined(AK_OS_MACOS)
|
|
|
|
|
auto* event_filter = new WindowControlGroupEventFilter(container);
|
|
|
|
|
container->installEventFilter(event_filter);
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
return { container, minimize, maximize, close };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WindowControlButton::paintEvent(QPaintEvent* event)
|
|
|
|
|
{
|
|
|
|
|
#if !defined(AK_OS_MACOS)
|
|
|
|
|
QToolButton::paintEvent(event);
|
|
|
|
|
#else
|
|
|
|
|
Q_UNUSED(event);
|
|
|
|
|
|
|
|
|
|
static constexpr qreal diameter = 13.0;
|
|
|
|
|
auto bounds = QRectF(0, 0, diameter, diameter);
|
|
|
|
|
bounds.moveCenter(rect().center());
|
|
|
|
|
|
|
|
|
|
auto hovered = !property(pressed_outside_property).toBool() && window_control_group_hovered(parentWidget());
|
|
|
|
|
auto active = window() && window()->isActiveWindow();
|
|
|
|
|
auto colorized = active || hovered;
|
|
|
|
|
QColor fill;
|
|
|
|
|
QColor symbol;
|
|
|
|
|
switch (m_type) {
|
|
|
|
|
case WindowControlButtonType::Close:
|
|
|
|
|
fill = colorized ? QColor(255, 95, 86) : QColor(198, 198, 198);
|
|
|
|
|
symbol = QColor(128, 32, 29);
|
|
|
|
|
break;
|
|
|
|
|
case WindowControlButtonType::Minimize:
|
|
|
|
|
fill = colorized ? QColor(255, 189, 46) : QColor(198, 198, 198);
|
|
|
|
|
symbol = QColor(153, 101, 13);
|
|
|
|
|
break;
|
|
|
|
|
case WindowControlButtonType::Maximize:
|
|
|
|
|
fill = colorized ? QColor(39, 201, 63) : QColor(198, 198, 198);
|
|
|
|
|
symbol = QColor(25, 99, 30);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isDown() && !property(pressed_outside_property).toBool())
|
|
|
|
|
fill = fill.darker(112);
|
|
|
|
|
|
|
|
|
|
QPainter painter(this);
|
|
|
|
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
|
painter.setPen(QPen(fill.darker(active ? 118 : 106), 0.5));
|
|
|
|
|
painter.setBrush(fill);
|
|
|
|
|
painter.drawEllipse(bounds);
|
|
|
|
|
|
|
|
|
|
if (!hovered)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
painter.setPen(QPen(symbol, 1.2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
|
|
|
|
|
auto center = bounds.center();
|
|
|
|
|
switch (m_type) {
|
|
|
|
|
case WindowControlButtonType::Close:
|
|
|
|
|
painter.drawLine(center + QPointF(-2.8, -2.8), center + QPointF(2.8, 2.8));
|
|
|
|
|
painter.drawLine(center + QPointF(2.8, -2.8), center + QPointF(-2.8, 2.8));
|
|
|
|
|
break;
|
|
|
|
|
case WindowControlButtonType::Minimize:
|
|
|
|
|
painter.drawLine(center + QPointF(-3.2, 0), center + QPointF(3.2, 0));
|
|
|
|
|
break;
|
|
|
|
|
case WindowControlButtonType::Maximize:
|
|
|
|
|
painter.drawLine(center + QPointF(-3.0, 0), center + QPointF(3.0, 0));
|
|
|
|
|
painter.drawLine(center + QPointF(0, -3.0), center + QPointF(0, 3.0));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2026-05-27 13:16:16 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WindowControlButton::enterEvent(QEnterEvent* event)
|
|
|
|
|
{
|
|
|
|
|
QToolButton::enterEvent(event);
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
#if defined(AK_OS_MACOS)
|
|
|
|
|
update_window_control_group(parentWidget());
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-05-27 13:16:16 -03:00
|
|
|
if (!m_tracking_press)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
setDown(true);
|
|
|
|
|
set_pressed_outside(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WindowControlButton::leaveEvent(QEvent* event)
|
|
|
|
|
{
|
|
|
|
|
QToolButton::leaveEvent(event);
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
#if defined(AK_OS_MACOS)
|
|
|
|
|
update_window_control_group(parentWidget());
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-05-27 13:16:16 -03:00
|
|
|
if (!m_tracking_press)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
setDown(false);
|
|
|
|
|
set_pressed_outside(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WindowControlButton::mouseMoveEvent(QMouseEvent* event)
|
|
|
|
|
{
|
|
|
|
|
QToolButton::mouseMoveEvent(event);
|
|
|
|
|
|
|
|
|
|
if (m_tracking_press)
|
|
|
|
|
update_pressed_position(event->position().toPoint());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WindowControlButton::mouseDoubleClickEvent(QMouseEvent* event)
|
|
|
|
|
{
|
|
|
|
|
QToolButton::mouseDoubleClickEvent(event);
|
|
|
|
|
|
|
|
|
|
reset_transient_state();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WindowControlButton::mousePressEvent(QMouseEvent* event)
|
|
|
|
|
{
|
|
|
|
|
if (event->button() == Qt::LeftButton) {
|
|
|
|
|
m_tracking_press = true;
|
|
|
|
|
set_pressed_outside(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QToolButton::mousePressEvent(event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WindowControlButton::mouseReleaseEvent(QMouseEvent* event)
|
|
|
|
|
{
|
|
|
|
|
QToolButton::mouseReleaseEvent(event);
|
|
|
|
|
|
|
|
|
|
reset_transient_state();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WindowControlButton::reset_transient_state()
|
|
|
|
|
{
|
|
|
|
|
m_tracking_press = false;
|
|
|
|
|
setDown(false);
|
|
|
|
|
set_pressed_outside(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WindowControlButton::set_pressed_outside(bool pressed_outside)
|
|
|
|
|
{
|
|
|
|
|
if (property(pressed_outside_property).toBool() == pressed_outside)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
setProperty(pressed_outside_property, pressed_outside);
|
|
|
|
|
style()->unpolish(this);
|
|
|
|
|
style()->polish(this);
|
|
|
|
|
update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WindowControlButton::update_pressed_position(QPoint const& position)
|
|
|
|
|
{
|
|
|
|
|
auto is_inside = rect().contains(position);
|
|
|
|
|
setDown(is_inside);
|
|
|
|
|
set_pressed_outside(!is_inside);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|