Treat the toolbar, tab strip, and vertical sidebar as one browser chrome surface. Add an explicit layout policy for platform window controls and sidebar widths. This lets macOS use left-side traffic lights while other platforms keep right-side custom controls. Keep the macOS toolbar stable in vertical-tabs mode. Traffic lights and navigation controls stay in the top row, and the sidebar sits below it. Draw chrome/content separators explicitly so the content edge remains clear without adding seams inside the chrome surface. Retune tab, toolbar, and omnibox styling for the unified chrome. Tabs use card-like selected states, quiet inactive states, and matching horizontal and vertical geometry. Make hover-expanded vertical tabs draw as a chrome overlay so the web content does not relayout. Keep the overlay above native web views and route macOS window drags through AppKit events for reliable dragging from custom chrome.
310 lines
8.5 KiB
C++
310 lines
8.5 KiB
C++
/*
|
|
* Copyright (c) 2026-present, the Ladybird developers.
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <UI/Qt/WindowControlButton.h>
|
|
|
|
#include <AK/Assertions.h>
|
|
#include <AK/Platform.h>
|
|
#if defined(AK_OS_MACOS)
|
|
# include <UI/Qt/MacWindow.h>
|
|
#endif
|
|
|
|
#include <QBoxLayout>
|
|
#include <QCursor>
|
|
#include <QEnterEvent>
|
|
#include <QMouseEvent>
|
|
#include <QPainter>
|
|
#include <QStyle>
|
|
#include <QWidget>
|
|
|
|
namespace Ladybird {
|
|
|
|
static constexpr auto pressed_outside_property = "pressedOutside";
|
|
|
|
#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
|
|
|
|
QSize WindowControlButton::default_size(QSize non_macos_size)
|
|
{
|
|
#if defined(AK_OS_MACOS)
|
|
Q_UNUSED(non_macos_size);
|
|
return { 16, 38 };
|
|
#else
|
|
return non_macos_size;
|
|
#endif
|
|
}
|
|
|
|
int WindowControlButton::default_spacing()
|
|
{
|
|
#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)
|
|
: QToolButton(parent)
|
|
, m_type(type)
|
|
{
|
|
setObjectName(object_name_for_type(type));
|
|
setToolTip(tool_tip);
|
|
setIconSize(icon_size);
|
|
setFixedSize(button_size);
|
|
setFocusPolicy(Qt::NoFocus);
|
|
setProperty(pressed_outside_property, false);
|
|
|
|
#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);
|
|
layout->setSpacing(WindowControlButton::default_spacing());
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
auto const button_size = WindowControlButton::default_size(non_macos_button_size);
|
|
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
|
|
}
|
|
|
|
void WindowControlButton::enterEvent(QEnterEvent* event)
|
|
{
|
|
QToolButton::enterEvent(event);
|
|
|
|
#if defined(AK_OS_MACOS)
|
|
update_window_control_group(parentWidget());
|
|
#endif
|
|
|
|
if (!m_tracking_press)
|
|
return;
|
|
|
|
setDown(true);
|
|
set_pressed_outside(false);
|
|
}
|
|
|
|
void WindowControlButton::leaveEvent(QEvent* event)
|
|
{
|
|
QToolButton::leaveEvent(event);
|
|
|
|
#if defined(AK_OS_MACOS)
|
|
update_window_control_group(parentWidget());
|
|
#endif
|
|
|
|
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);
|
|
}
|
|
|
|
}
|